release: v1.8.0 - backtest engine with batch strategy comparison

- Add backtest section to README with CLI usage and run_all_strategies.py demo
- Update all version numbers to 1.8.0 (pyproject.toml, __init__.py, cli/__init__.py, docs/conf.py)
- Fix turtle_breakout strategy: TAQ returns 3 values (UP, MID, DOWN)
- Add run_all_strategies.py batch comparison script
- Update README intro to highlight backtest feature
- Add backtest to CLI command table and architecture tree

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
GitHub
2026-06-09 20:35:38 +08:00
co-authored by Claude Opus 4.8
parent 70c69c8a66
commit b5b5d0dc5b
10 changed files with 401 additions and 44 deletions
+3 -1
View File
@@ -76,7 +76,9 @@
"Bash(mypy src/easy_tdx/backtest/types.py)",
"Bash(mypy tests/unit/test_backtest_types.py)",
"Bash(mypy src/easy_tdx/backtest/types.py tests/unit/test_backtest_types.py)",
"Bash(mypy *)"
"Bash(mypy *)",
"Bash(git diff *)",
"Bash(echo \"exit: $?\")"
]
}
}
+133 -3
View File
@@ -14,13 +14,15 @@ easy-tdx 要做的事很简单:**把机构的数据锁砸开,扔到每个普
它是一个完全免费、无需注册、无需 API Key、纯开源的行**情核武器**。
一行命令,A股、港股、美股、期货——K线、报价、资金流向、板块轮动、分时明细、逐笔成交,**毫秒级拉满**。
**32个技术指标**MACD、KDJ、RSI、BOLL……连捉妖大师”和30日乖离率信号”都给你算好)开箱即用。
**32个技术指标**MACD、KDJ、RSI、BOLL……连捉妖大师”和30日乖离率信号”都给你算好)开箱即用。
**缠论分析**(笔、中枢、买卖点、背驰)一键出结果——你不再需要手画分型、猜线段。
**内置回测引擎**——写个策略文件,一行命令跑回测,9 个经典策略自带,批量对比哪个最赚钱一目了然。
装上就能跑。**Python API + CLI 双通道**,输出 JSON 天然喂给 AI AgentClaude Code、OpenClaw、Hermes 直接吃。
**你不懂 TCP 协议?不用。**
**你不会写量化框架?不用。**
**你不懂 TCP 协议?不用。**
**你不会写量化框架?不用。**
**你想回测验证策略?自带引擎,不用。**
**你不想给任何平台付一分钱?完全不用。**
`pip install easy-tdx`,30秒后——你屏幕上的数据,和机构看到的**是同一份**。
@@ -265,6 +267,120 @@ easy-tdx chanlun SZ 000001 --period 30MIN
`[✓]` 表示确认背驰。趋势背驰(上)代表上涨趋势可能结束,趋势背驰(下)代表下跌趋势可能结束。
### 回测引擎
内置向量回测引擎,加载 Python 策略文件即可跑回测。策略继承 `Strategy` 基类,在 `init()` 注册指标,在 `next()` 逐 bar 生成买卖信号,引擎完成订单模拟、持仓跟踪和绩效分析。
**单策略回测:**
```bash
easy-tdx backtest SZ 300308 --strategy-file strategies/expma_cross.py --count 2000 --cash 1000000 --adjust QFQ --table
```
输出示例:
```
=== 回测绩效概要 ===
总收益率: 1413.51%
年化收益: 40.85%
最大回撤: 76.75%
夏普比率: 0.88
胜率: 20.8%
交易次数: 24
```
**全策略批量对比:**
项目自带 `run_all_strategies.py`,一次跑完 `strategies/` 下所有策略并排名:
```bash
python -X utf8 run_all_strategies.py SZ 300308 --count 2000 --cash 1000000 --adjust QFQ
```
输出示例(以 SZ 300308 为例):
```
发现 9 个策略文件
标的: SZ 300308 | K线: 2000 | 资金: 1,000,000 | 复权: QFQ
================================================================================
>> 运行策略: bias_reversal ... 完成 (2.4s)
>> 运行策略: bollinger_breakout ... 完成 (0.6s)
>> 运行策略: expma_cross ... 完成 (0.6s)
>> 运行策略: kdj_golden ... 完成 (0.1s)
>> 运行策略: ma_cross ... 完成 (1.4s)
>> 运行策略: macd_cross ... 完成 (2.1s)
>> 运行策略: rsi_reversal ... 完成 (0.2s)
>> 运行策略: turtle_breakout ... 完成 (0.1s)
>> 运行策略: volume_price ... 完成 (6.3s)
================================================================================
[*] 策略绩效排名 (按总收益率降序)
================================================================================
排名 策略 总收益率 年化收益 最大回撤 夏普 胜率 交易次数 盈亏比
----------------------------------------------------------------------------------------------------
*1* 1 expma_cross 1413.51% 40.85% 76.75% 0.88 20.8% 24 6.45
*2* 2 ma_cross 1258.07% 38.94% 58.01% 0.87 38.2% 55 2.21
*3* 3 turtle_breakout 905.07% 33.76% 48.30% 0.83 75.0% 4 10.14
4 bias_reversal 504.94% 25.47% 42.25% 0.70 66.3% 95 2.08
5 macd_cross 387.67% 22.11% 61.08% 0.60 40.0% 85 2.20
6 volume_price 247.72% 17.01% 65.73% 0.50 43.3% 254 1.40
7 bollinger_breakout 169.65% 13.32% 49.71% 0.44 66.7% 24 1.93
8 rsi_reversal 95.89% 8.85% 56.51% 0.33 57.1% 7 2.48
9 kdj_golden 89.10% 8.36% 61.86% 0.32 66.7% 3 10.49
```
综合评分(夏普 × 0.4 + 收益/回撤 × 0.3 + 胜率 × 0.3):
```
*1* 1 turtle_breakout 23.04 0.83 0.70 75.0%
*2* 2 bias_reversal 20.35 0.70 0.60 66.3%
*3* 3 bollinger_breakout 20.26 0.44 0.27 66.7%
```
换一个标的再跑:
```bash
# 贵州茅台
python -X utf8 run_all_strategies.py SH 600519 --count 2000 --cash 1000000 --adjust QFQ
```
#### 自带策略示例
`strategies/` 目录下有 9 个开箱即用的策略文件,可直接用于 `--strategy-file`
| 文件 | 策略 | 类型 | 适合行情 |
|------|------|------|----------|
| `ma_cross.py` | 双均线交叉(MA5/MA20) | 趋势跟踪 | 单边趋势 |
| `expma_cross.py` | EMA12/EMA50 交叉 | 趋势跟踪 | 单边趋势(比 MA 更灵敏) |
| `macd_cross.py` | MACD 金叉死叉 | 趋势跟踪 | 中长线趋势 |
| `bollinger_breakout.py` | 布林带突破 | 震荡反转 | 横盘震荡 |
| `rsi_reversal.py` | RSI 超买超卖 | 反转 | 震荡市 |
| `kdj_golden.py` | KDJ 低位金叉/高位死叉 | 反转 | 短线震荡 |
| `turtle_breakout.py` | 海龟交易法(唐安奇通道) | 趋势突破 | 牛市启动 |
| `bias_reversal.py` | 乖离率反转 | 反转 | 震荡回归 |
| `volume_price.py` | 量价配合 | 综合判断 | 放量突破 |
编写自定义策略只需继承 `Strategy` 基类:
```python
from easy_tdx.backtest import Strategy
from easy_tdx import MyTT
class MyStrategy(Strategy):
def init(self):
self.ma = self.I(MyTT.MA, self.data.close, 10)
def next(self):
if self.data.close[0] > self.ma[self._bar_index]:
self.buy(size=0) # size=0 表示全仓
elif self.position["size"] > 0:
self.sell(size=0) # size=0 表示清仓
```
完整 API 参考:[docs/backtest_usage.md](docs/backtest_usage.md)
### 捉妖大师(重点)
捉妖大师是多周期涨幅共振指标,通过 20/60/120 日涨幅及指数平滑判断短中长线趋势是否同向,用于筛选趋势刚启动的强势股。
@@ -416,6 +532,7 @@ easy-tdx offline sync-all
| `symbol-info` | 个股特征快照 |
| `indicator` | 技术指标计算(32 个:MACD/KDJ/RSI/BOLL/DMI/ATR... |
| `indicator-list` | 列出可用技术指标 |
| `backtest` | 回测引擎(加载策略文件,输出绩效报告) |
| `f10` | F10 公司信息 |
| `fund-flow` | 历史资金流向 |
| `ex kline` | 扩展市场 K 线 |
@@ -935,6 +1052,7 @@ src/easy_tdx/
├── commands/ # 标准协议命令(无 IO)
├── codec/ # price / volume / datetime / frame / bitmap 编解码
├── chanlun/ # 缠论技术分析(K线合并/分型/笔/线段/中枢/买卖点/背驰)
├── backtest/ # 回测引擎(Strategy基类/向量化引擎/绩效分析)
├── models/ # 纯 dataclass,无业务逻辑
├── offline/ # 离线数据读写模块(读取 + 写入同步)
└── cli/ # easy-tdx CLIclick
@@ -963,6 +1081,18 @@ ruff format --check src/ tests/ # format check
## Changelog
### 1.8.0 (2026-06-09)
**回测引擎** — 内置向量回测引擎,支持自定义策略回测和全策略批量对比。
- 新增 `backtest` 子包:Strategy 基类、BacktestEngine、OrderSimulator、PortfolioTracker、PerformanceAnalyzer
- 新增 `easy-tdx backtest` CLI 命令,支持 `--strategy-file``--cash``--commission``--adjust` 等参数
- 绩效报告包含 19 项指标:总收益率、年化收益、最大回撤、夏普比率、索提诺、卡玛、胜率、盈亏比等
- 新增 `strategies/` 目录,包含 9 个开箱即用的策略示例(MA/EMA/MACD/BOLL/RSI/KDJ/BIAS/海龟/量价)
- 新增 `run_all_strategies.py` 批量对比脚本,一键跑完全部策略并按收益率和综合评分排名
- 自带策略在 SZ 300308 上 3 年回测:收益率最高 1413%expma_cross),综合最优 turtle_breakout
- 30+ 离线单元测试覆盖,零网络依赖
### 1.7.1 (2026-06-08)
**Bug 修复** — 修复缠论笔计算在持续下跌/上涨走势中因"分型陷阱"导致近期笔丢失的问题。
+1 -1
View File
@@ -4,7 +4,7 @@ copyright = "2025, Justin Gu"
author = "Justin Gu"
# The full version, including alpha/beta/rc tags
release = "1.7.1"
release = "1.8.0"
# -- Extensions ---------------------------------------------------------------
extensions = [
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "easy-tdx"
version = "1.7.1"
version = "1.8.0"
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
readme = "README.md"
requires-python = ">=3.10"
+231
View File
@@ -0,0 +1,231 @@
"""批量回测脚本:依次运行 strategies/ 目录下所有策略并比较结果。
用法::
python run_all_strategies.py SZ 300308 --count 2000 --cash 1000000 --adjust QFQ
输出每个策略的绩效指标,并按总收益率排名。
"""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
# 确保 easy_tdx 可导入
sys.path.insert(0, str(Path(__file__).parent / "src"))
import click
@click.command()
@click.argument("market")
@click.argument("code")
@click.option("--count", default=2000, type=int, help="K线数量")
@click.option("--cash", default=1000000.0, type=float, help="初始资金")
@click.option("--commission", default=0.0003, type=float, help="佣金率")
@click.option("--adjust", default="QFQ", help="复权: NONE/QFQ/HFQ")
@click.option("--period", default="DAILY", help="K线周期")
def run_all(
market: str,
code: str,
count: int,
cash: float,
commission: float,
adjust: str,
period: str,
) -> None:
"""批量运行 strategies/ 目录下所有策略并比较结果。"""
from easy_tdx.backtest.engine import BacktestEngine
from easy_tdx.backtest.strategy import Strategy
from easy_tdx.cli.parsers import parse_adjust, parse_market, parse_period
from easy_tdx.mac.client import MacClient
# 1. 发现策略文件
strategies_dir = Path(__file__).parent / "strategies"
strategy_files = sorted(strategies_dir.glob("*.py"))
if not strategy_files:
click.echo("未找到策略文件 (strategies/*.py)", err=True)
raise SystemExit(1)
click.echo(f"发现 {len(strategy_files)} 个策略文件")
click.echo(f"标的: {market} {code} | K线: {count} | 资金: {cash:,.0f} | 复权: {adjust}")
click.echo("=" * 80)
# 2. 获取数据(所有策略共享同一份数据)
mkt = parse_market(market)
click.echo("正在获取行情数据...")
client = MacClient.from_best_host()
client.connect()
try:
df = client.get_stock_kline(
mkt,
code,
period=parse_period(period),
start=0,
count=count,
adjust=parse_adjust(adjust),
)
finally:
client.close()
click.echo(f"获取到 {len(df)} 条K线数据")
click.echo("=" * 80)
# 3. 逐个运行策略
results: list[dict] = []
for sf in strategy_files:
strategy_name = sf.stem
click.echo(f"\n>> 运行策略: {strategy_name} ...", nl=False)
# 加载策略类
import importlib.util
spec = importlib.util.spec_from_file_location("strategy_module", sf)
if spec is None or spec.loader is None:
click.echo(" [加载失败]")
continue
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 查找 Strategy 子类
strategy_cls = None
for attr_name in dir(module):
obj = getattr(module, attr_name)
try:
if isinstance(obj, type) and issubclass(obj, Strategy) and obj is not Strategy:
strategy_cls = obj
break
except TypeError:
pass
if strategy_cls is None:
click.echo(" [未找到 Strategy 子类]")
continue
# 运行回测
t0 = time.perf_counter()
try:
engine = BacktestEngine(
strategy=strategy_cls,
cash=cash,
commission=commission,
)
result = engine.run(df)
elapsed = time.perf_counter() - t0
perf = result.performance
click.echo(f" 完成 ({elapsed:.1f}s)")
results.append({
"strategy": strategy_name,
"total_return": perf.get("total_return", 0),
"annual_return": perf.get("annual_return", 0),
"max_drawdown": perf.get("max_drawdown", 0),
"sharpe": perf.get("sharpe", 0),
"sortino": perf.get("sortino", 0),
"calmar": perf.get("calmar", 0),
"win_rate": perf.get("win_rate", 0),
"total_trades": perf.get("total_trades", 0),
"profit_factor": perf.get("profit_factor", 0),
"volatility": perf.get("volatility", 0),
})
except Exception as e:
elapsed = time.perf_counter() - t0
click.echo(f" 错误 ({elapsed:.1f}s): {e}")
results.append({
"strategy": strategy_name,
"error": str(e),
})
# 4. 输出排名
click.echo("\n" + "=" * 80)
click.echo("[*] 策略绩效排名 (按总收益率降序)")
click.echo("=" * 80)
# 过滤掉有错误的策略
valid = [r for r in results if "error" not in r]
errored = [r for r in results if "error" in r]
if not valid:
click.echo("所有策略均运行失败!")
for r in errored:
click.echo(f" {r['strategy']}: {r['error']}")
raise SystemExit(1)
# 按总收益率排序
valid.sort(key=lambda x: x["total_return"], reverse=True)
# 表头
click.echo(
f"{'排名':>4} {'策略':<22} {'总收益率':>10} {'年化收益':>10} "
f"{'最大回撤':>10} {'夏普':>8} {'胜率':>8} {'交易次数':>8} {'盈亏比':>8}"
)
click.echo("-" * 100)
for i, r in enumerate(valid, 1):
medal = " *1*" if i == 1 else " *2*" if i == 2 else " *3*" if i == 3 else " "
click.echo(
f"{medal}{i:>2} {r['strategy']:<22} "
f"{r['total_return']:>9.2%} "
f"{r['annual_return']:>9.2%} "
f"{r['max_drawdown']:>9.2%} "
f"{r['sharpe']:>8.2f} "
f"{r['win_rate']:>7.1%} "
f"{r['total_trades']:>8} "
f"{r['profit_factor']:>8.2f}"
)
# 最佳策略详细报告
best = valid[0]
click.echo("\n" + "=" * 80)
click.echo(f"[BEST] 最佳策略: {best['strategy']}")
click.echo("=" * 80)
click.echo(f" 总收益率: {best['total_return']:.2%}")
click.echo(f" 年化收益: {best['annual_return']:.2%}")
click.echo(f" 最大回撤: {best['max_drawdown']:.2%}")
click.echo(f" 夏普比率: {best['sharpe']:.2f}")
click.echo(f" 索提诺: {best['sortino']:.2f}")
click.echo(f" 卡玛比率: {best['calmar']:.2f}")
click.echo(f" 胜率: {best['win_rate']:.1%}")
click.echo(f" 交易次数: {best['total_trades']}")
click.echo(f" 盈亏比: {best['profit_factor']:.2f}")
click.echo(f" 年化波动: {best['volatility']:.4f}")
# 综合评分(综合夏普、收益率、回撤)
click.echo("\n" + "=" * 80)
click.echo("[*] 综合评分排名 (Sharpe*0.4 + Ret/DD*0.3 + WinRate*0.3)")
click.echo("=" * 80)
scored = []
for r in valid:
# 避免除以零
ret_dd_ratio = r["annual_return"] / r["max_drawdown"] if r["max_drawdown"] > 1e-6 else 999.0
score = r["sharpe"] * 0.4 + ret_dd_ratio * 0.3 + r["win_rate"] * 100 * 0.3
scored.append((r, score))
scored.sort(key=lambda x: x[1], reverse=True)
click.echo(
f"{'排名':>4} {'策略':<22} {'综合评分':>10} {'夏普':>8} {'收益/回撤':>10} {'胜率':>8}"
)
click.echo("-" * 70)
for i, (r, score) in enumerate(scored, 1):
ret_dd_ratio = r["annual_return"] / r["max_drawdown"] if r["max_drawdown"] > 1e-6 else 999.0
medal = " *1*" if i == 1 else " *2*" if i == 2 else " *3*" if i == 3 else " "
click.echo(
f"{medal}{i:>2} {r['strategy']:<22} {score:>10.2f} "
f"{r['sharpe']:>8.2f} {ret_dd_ratio:>10.2f} {r['win_rate']:>7.1%}"
)
# 报告错误
if errored:
click.echo("\n[!] 以下策略运行失败:")
for r in errored:
click.echo(f" {r['strategy']}: {r['error']}")
if __name__ == "__main__":
run_all()
+1 -1
View File
@@ -107,4 +107,4 @@ __all__ = [
"save_best_ex_host",
]
__version__ = "1.4.0"
__version__ = "1.8.0"
+26 -32
View File
@@ -46,17 +46,12 @@ class PortfolioTracker:
Args:
trades: 交易列表
"""
# 构建 datetime → Trade 映射
buy_map: dict[int, Trade] = {}
sell_map: dict[int, Trade] = {}
# 构建 datetime → [Trade] 映射(支持同 bar 多笔交易)
trade_map: dict[int, list[Trade]] = {}
for trade in trades:
if trade.rejected:
continue
if trade.direction == "BUY":
buy_map[trade.datetime] = trade
else:
sell_map[trade.datetime] = trade
trade_map.setdefault(trade.datetime, []).append(trade)
# 遍历每个 bar
for i in range(self._n):
@@ -68,33 +63,32 @@ class PortfolioTracker:
self._position[i] = self._position[i - 1]
self._avg_price[i] = self._avg_price[i - 1]
# 处理买入
if dt in buy_map:
trade = buy_map[dt]
cost = trade.size * trade.price + trade.commission + trade.slippage
self._cash[i] -= cost
# 处理该 bar 的所有交易
for trade in trade_map.get(dt, []):
if trade.direction == "BUY":
cost = trade.size * trade.price + trade.commission + trade.slippage
self._cash[i] -= cost
# 更新均价
if self._position[i] > 0:
total_cost = self._position[i] * self._avg_price[i] + trade.size * trade.price
self._position[i] += trade.size
self._avg_price[i] = total_cost / self._position[i]
else:
# 新开仓或从空仓开仓
self._position[i] = trade.size
self._avg_price[i] = trade.price
# 更新均价
if self._position[i] > 0:
prev_cost = self._position[i] * self._avg_price[i]
total_cost = prev_cost + trade.size * trade.price
self._position[i] += trade.size
self._avg_price[i] = total_cost / self._position[i]
else:
# 新开仓或从空仓开仓
self._position[i] = trade.size
self._avg_price[i] = trade.price
# 处理卖出
if dt in sell_map:
trade = sell_map[dt]
proceeds = trade.size * trade.price - trade.commission - trade.slippage
self._cash[i] += proceeds
self._position[i] -= trade.size
elif trade.direction == "SELL":
proceeds = trade.size * trade.price - trade.commission - trade.slippage
self._cash[i] += proceeds
self._position[i] -= trade.size
# 清空持仓时归零
if self._position[i] <= 0:
self._position[i] = 0.0
self._avg_price[i] = 0.0
# 清空持仓时归零
if self._position[i] <= 0:
self._position[i] = 0.0
self._avg_price[i] = 0.0
@property
def equity_curve(self) -> pd.DataFrame:
+3 -3
View File
@@ -19,7 +19,7 @@ class Signal:
"""策略产生的交易信号。
Attributes:
datetime: 信号时间(Unix timestamp 毫秒
datetime: 信号时间(YYYYMMDD 整数格式,如 20240101
direction: 交易方向
size: 交易数量(0 = 全仓/清仓)
price: 限价(None = 市价单)
@@ -43,7 +43,7 @@ class Trade:
"""已成交记录。
Attributes:
datetime: 成交时间(Unix timestamp 毫秒
datetime: 成交时间(YYYYMMDD 整数格式,如 20240101
direction: 交易方向
size: 成交数量
price: 成交价格
@@ -71,7 +71,7 @@ class Position:
"""持仓快照。
Attributes:
datetime: 快照时间(Unix timestamp 毫秒
datetime: 快照时间(YYYYMMDD 整数格式,如 20240101
size: 持仓数量(正=多头,负=空头,0=空仓)
avg_price: 平均持仓成本
market_value: 市值
+1 -1
View File
@@ -23,7 +23,7 @@ from ..backtest.cli import backtest
@click.group()
@click.version_option(version="1.7.0", prog_name="easy-tdx")
@click.version_option(version="1.8.0", prog_name="easy-tdx")
def cli() -> None:
"""easy-tdx -- 通达信行情数据 CLI(默认 JSON 输出,适合 Agent 使用)。
+1 -1
View File
@@ -16,7 +16,7 @@ class TurtleStrategy(Strategy):
"""海龟交易法(唐安奇通道突破)策略。"""
def init(self) -> None:
self.upper, self.lower = self.I(MyTT.TAQ, self.data.high, self.data.low, 20)
self.upper, _, self.lower = self.I(MyTT.TAQ, self.data.high, self.data.low, 20)
def next(self) -> None:
cur = self.data.close[0]