diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 35e19d1..20d405f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -17,7 +17,62 @@ "Bash(pip list *)", "Bash(uv run *)", "Bash(git stash *)", - "Bash(dir /s /b src\\\\xmtdx)" + "Bash(dir /s /b src\\\\xmtdx)", + "Bash(git remote *)", + "Bash(uv --version)", + "Bash(git rm *)", + "mcp__plugin_context7_context7__resolve-library-id", + "mcp__plugin_context7_context7__query-docs", + "Bash(grep -E \"\\\\.py$\")", + "Bash(awk '{added+=$1; deleted+=$2} END {print \"+\" added \" -\" deleted}')", + "Bash(find D:/python/easty_tdx/examples/1[1-7]* -name \"*.py\")", + "Bash(easy-tdx ping *)", + "Bash(easy-tdx version *)", + "Bash(easy-tdx kline *)", + "Bash(easy-tdx quote *)", + "Bash(easy-tdx tick *)", + "Bash(easy-tdx transaction *)", + "Bash(easy-tdx auction *)", + "Bash(easy-tdx quote-list *)", + "Bash(easy-tdx board-list *)", + "Bash(easy-tdx board-members *)", + "Bash(easy-tdx belong-board *)", + "Bash(easy-tdx capital-flow *)", + "Bash(echo \"EXIT CODE: $?\")", + "Bash(easy-tdx unusual *)", + "Bash(easy-tdx market-stat *)", + "Bash(easy-tdx server-info *)", + "Bash(easy-tdx symbol-info *)", + "Bash(easy-tdx ex *)", + "WebSearch", + "mcp__zread__search_doc", + "mcp__zread__read_file", + "Bash(git status *)", + "Bash(pip show *)", + "Bash(git tag *)", + "Bash(twine upload *)", + "Bash(pip index *)", + "Bash(curl -s https://pypi.org/pypi/easy-tdx/json)", + "Bash(curl -sI \"https://files.pythonhosted.org/packages/py3/e/easy-tdx/easy_tdx-1.2.0-py3-none-any.whl\")", + "Bash(gh run *)", + "Bash(easy-tdx *)", + "mcp__web-search-prime__web_search_prime", + "mcp__plugin_compound-engineering_context7__resolve-library-id", + "mcp__plugin_compound-engineering_context7__query-docs", + "Bash(pip uninstall *)", + "Bash(xargs grep -l \"Command\\\\|Request\\\\|Response\")", + "Bash(quotes/stock_xdxr_info.go)", + "Bash(mypy src/)", + "Bash(ruff format *)", + "mcp__plugin_episodic-memory_episodic-memory__search", + "Bash(echo \"FK/\")", + "Bash(git pull *)", + "mcp__playwright__browser_navigate", + "mcp__playwright__browser_snapshot", + "Bash(gh auth *)", + "Bash(rm -rf easy_tdx.wiki)", + "Bash(gh repo *)", + "Bash(gh api *)" ] } } diff --git a/.gitignore b/.gitignore index ae927ea..f065990 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ venv/ .omc/ uv.lock docs/_build/ +FK/ diff --git a/README.md b/README.md index a97c713..9a45f57 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ easy-tdx 要做的事很简单:**把机构的数据锁砸开,扔到每个普 随便用,随便改,随便分发。 **数据面前,人人平等。** +📖 **详细用法请查看 [GitHub Wiki](https://github.com/handsomejustin/easy_tdx/wiki)** + ## 安装 ```bash diff --git a/docs/superpowers/specs/2026-06-09-backtest-engine-design.md b/docs/superpowers/specs/2026-06-09-backtest-engine-design.md new file mode 100644 index 0000000..12eb020 --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-backtest-engine-design.md @@ -0,0 +1,424 @@ +# Backtest Engine Design — easy-tdx + +**Date:** 2026-06-09 +**Status:** Approved +**Module:** `easy_tdx.backtest` +**Priority:** P0 — 量化工具链全栈的第一块拼图 + +--- + +## 1. 目标 + +为 easy-tdx 新增自建回测引擎模块,让用户能基于 easy-tdx 获取的 K 线数据执行策略回测、查看绩效报告。 + +### 核心约束 + +- 纯计算模块,与 `chanlun` 同级,零网络依赖 +- 仅依赖 `pandas`/`numpy`(项目已有),不引入第三方回测库 +- 接收 easy-tdx 标准 DataFrame(`datetime, open, close, high, low, vol, amount`) +- 双模式策略定义:Python 类继承 + DSL 公式语法 +- v1 只实现向量化执行路径(日级策略),架构预留事件驱动扩展点 + +--- + +## 2. 架构 + +### 2.1 文件结构 + +``` +src/easy_tdx/backtest/ +├── __init__.py # 公开 API 导出 +├── strategy.py # Strategy 基类 + StrategyDataProxy + IndicatorProvider +├── dsl.py # DSL 解析器 + @dsl_strategy 装饰器 + 字符串 DSL 编译 +├── engine.py # BacktestEngine(向量化执行路径) +├── orders.py # OrderSimulator(撮合规则) +├── portfolio.py # PortfolioTracker(持仓/资金曲线) +├── performance.py # PerformanceAnalyzer(绩效指标计算) +├── types.py # Trade / Position / Signal / BacktestResult 数据类 +└── cli.py # CLI 集成(easy-tdx backtest ...) +``` + +### 2.2 模块交互流 + +``` +easy_tdx MacClient.get_stock_kline() → DataFrame + │ + ▼ + BacktestEngine(strategy, cash=100000) + │ + ┌───────────┼───────────┐ + ▼ ▼ ▼ + Strategy DSL Parser OrderSimulator + (Python类) (公式语法) (撮合规则) + │ │ │ + └─────┬─────┘ │ + ▼ │ + Signal (bool mask) │ + │ │ + ▼ ▼ + PortfolioTracker ←────┘ + │ + ▼ + PerformanceAnalyzer + │ + ▼ + BacktestResult + (绩效指标 + 资金曲线 + 交易记录) +``` + +--- + +## 3. 核心数据类型 + +### 3.1 Signal + +```python +@dataclass +class Signal: + datetime: int + direction: Literal["BUY", "SELL"] + size: float # 0 = 全仓/清仓 + price: float | None # None = 市价 + stop_loss: float | None + take_profit: float | None +``` + +### 3.2 Trade + +```python +@dataclass +class Trade: + datetime: int + direction: Literal["BUY", "SELL"] + size: float + price: float + commission: float + slippage: float + pnl: float # 仅平仓时计算 +``` + +### 3.3 Position + +```python +@dataclass +class Position: + datetime: int + size: float # 正=多头,负=空头,0=空仓 + avg_price: float + market_value: float + unrealized_pnl: float +``` + +### 3.4 BacktestResult + +```python +@dataclass +class BacktestResult: + performance: dict[str, float] + equity_curve: pd.DataFrame # datetime, cash, position_value, total, drawdown, drawdown_pct + trades: pd.DataFrame # datetime, direction, size, price, commission, pnl + positions: pd.DataFrame # datetime, size, avg_price, market_value, unrealized_pnl + config: dict +``` + +方法: +- `to_json() → str` +- `to_dict() → dict` +- `summary() → None`(打印概要) + +--- + +## 4. Strategy 基类 + +### 4.1 接口定义 + +```python +class Strategy(ABC): + def init(self) -> None: + """注册指标。策略初始化时调用一次。""" + pass + + def next(self) -> None: + """每根 K 线调用。在此生成买卖信号。""" + pass + + def I(self, func: Callable, *args, **kwargs) -> np.ndarray: + """注册指标函数。init() 后一次性计算,返回完整数组。""" + ... + + def buy(self, size: float = 0, price: float | None = None, + stop_loss: float | None = None, take_profit: float | None = None) -> None: + """买入。size=0 全仓。""" + ... + + def sell(self, size: float = 0, price: float | None = None, + stop_loss: float | None = None, take_profit: float | None = None) -> None: + """卖出。size=0 清仓。""" + ... + + @property + def data(self) -> StrategyDataProxy: ... + + @property + def position(self) -> Position: ... +``` + +### 4.2 StrategyDataProxy + +```python +class StrategyDataProxy: + """K 线数据代理。支持 .close[0](当前)、.close[-1](前一根)。""" + @property + def open(self) -> _SeriesAccessor: ... + @property + def close(self) -> _SeriesAccessor: ... + @property + def high(self) -> _SeriesAccessor: ... + @property + def low(self) -> _SeriesAccessor: ... + @property + def vol(self) -> _SeriesAccessor: ... + @property + def amount(self) -> _SeriesAccessor: ... + +class _SeriesAccessor: + """[0] 当前值、[-1] 前一根、切片。""" + def __getitem__(self, key: int) -> float: ... + def __len__(self) -> int: ... +``` + +### 4.3 Python 类策略示例 + +```python +class MACrossStrategy(Strategy): + def init(self): + self.ma5 = self.I(MA, self.data.close, 5) + self.ma20 = self.I(MA, self.data.close, 20) + + def next(self): + if crossover(self.ma5, self.ma20): + self.buy(size=100) + elif crossover(self.ma20, self.ma5): + self.sell(size=100) + +engine = BacktestEngine(strategy=MACrossStrategy, cash=100000) +result = engine.run(df) +``` + +--- + +## 5. DSL 策略定义 + +### 5.1 设计边界 + +| 能做 | 不做 | +|------|------| +| 指标交叉、比较、逻辑组合 | 循环、变量赋值、函数定义 | +| 内置常用函数(CROSS, ABOVE, BELOW, BETWEEN) | 自定义控制流 | +| 参数化(可调窗口期) | 图灵完备 | + +超出 DSL 能力的——直接用 Python 类。 + +### 5.2 两种 DSL 模式 + +**Python 装饰器模式**: + +```python +from easy_tdx.backtest import dsl_strategy + +@dsl_strategy +def dual_ma(df): + buy = CROSS(MA(df.close, 5), MA(df.close, 20)) + sell = CROSS(MA(df.close, 20), MA(df.close, 5)) + return buy, sell +``` + +**字符串模式**(CLI 用): + +```bash +easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --cash 100000 --table +``` + +### 5.3 内置 DSL 函数 + +复用 `MyTT.py` 已有实现: + +| 函数 | 签名 | 含义 | +|------|------|------| +| `MA(series, n)` | `(ndarray, int) → ndarray` | 简单移动平均 | +| `EMA(series, n)` | `(ndarray, int) → ndarray` | 指数移动平均 | +| `RSI(series, n)` | `(ndarray, int) → ndarray` | 相对强弱 | +| `BOLL(series, n, k)` | `(ndarray, int, float) → tuple` | 布林带 | +| `MACD(series, fast, slow, signal)` | `(ndarray, ...) → tuple` | MACD | +| `CROSS(a, b)` | `(ndarray, ndarray) → ndarray[bool]` | 上穿检测 | +| `REF(series, n)` | `(ndarray, int) → ndarray` | 前 n 期值 | +| `HHV(series, n)` | `(ndarray, int) → ndarray` | n 期最高 | +| `LLV(series, n)` | `(ndarray, int) → ndarray` | n 期最低 | +| `BETWEEN(x, a, b)` | `(ndarray, ...) → ndarray[bool]` | 区间判断 | +| `COUNT(cond, n)` | `(ndarray[bool], int) → ndarray` | n 期满足条件次数 | + +### 5.4 DSL 编译器 + +`DSLCompiler.compile(func)` 流程: +1. 调用 `func(mock_df)` 捕获 DSL 函数调用 +2. 记录 `(buy_mask, sell_mask)` 信号生成规则 +3. 动态生成 Strategy 子类 + +引擎侧优化:DSL 策略不逐 Bar 调用 `next()`,直接用 bool mask 一次性生成全部 Signal。 + +--- + +## 6. 引擎执行流 + +### 6.1 BacktestEngine 构造参数 + +```python +class BacktestEngine: + def __init__( + self, + strategy: type[Strategy] | Strategy, + cash: float = 100000.0, + commission: float = 0.0003, + min_commission: float = 5.0, + stamp_tax: float = 0.001, + slippage: float = 0.0, + execution: str = "next_open", # "next_open" | "next_close" | "this_close" | "worst" | "best" + position_mode: str = "full", # "full" | "fixed" | "percent" | "signal_only" + benchmark: pd.DataFrame | None = None, + ): + ... +``` + +### 6.2 四步执行管道 + +1. **信号生成**:DSL → bool mask;Python 类 → trace next() 生成 mask +2. **信号→订单**(OrderSimulator):根据 execution 规则确定成交价,根据仓位模式确定量 +3. **持仓追踪**(PortfolioTracker):逐 Bar 更新现金/持仓/市值/回撤 +4. **绩效分析**(PerformanceAnalyzer):从资金曲线计算全部指标 + +Step 2 是唯一需要逐行处理的步骤(仓位依赖前一 Bar 状态)。其余步骤全向量化。 + +### 6.3 OrderSimulator 成交价规则 + +| 模式 | 说明 | +|------|------| +| `next_open`(默认) | 下一根 K 线开盘价成交,最真实 | +| `next_close` | 下一根 K 线收盘价成交 | +| `this_close` | 当根 K 线收盘价成交(有未来函数风险,标注警告) | +| `worst` | 对投资者最差价格(买入取 high,卖出取 low) | +| `best` | 对投资者最优价格(买入取 low,卖出取 high) | + +### 6.4 仓位管理规则 + +| 模式 | 说明 | +|------|------| +| `full`(默认) | 买入用全部现金,卖出清仓 | +| `fixed` | 每次固定股数 | +| `percent` | 每次用总资产的 N% | +| `signal_only` | 只生成信号,不模拟仓位 | + +### 6.5 费用模型 + +- 佣金:`max(size * price * commission_rate, min_commission)`,买卖双向 +- 印花税:`size * price * stamp_tax_rate`,仅卖出 +- 滑点:`size * slippage_per_share` + +### 6.6 多策略批量回测 + +```python +# 多只股票 +results = engine.run_many({ + "SH600519": df_519, + "SZ000858": df_858, +}) +# → dict[str, BacktestResult] + +# 参数扫描 +results = engine.run_grid(df, params={ + "short": [5, 10, 15], + "long": [20, 30, 60], +}) +# → list[GridResult],支持 .sort_by("sharpe").to_table() +``` + +--- + +## 7. 绩效指标 + +| 指标 | key | 算法 | +|------|-----|------| +| 总收益率 | `total_return` | `(total[-1] / total[0]) - 1` | +| 年化收益率 | `annual_return` | `(1 + r) ** (252/n) - 1` | +| 最大回撤 | `max_drawdown` | `max((peak - total) / peak)` | +| 最大回撤天数 | `max_dd_duration` | 首次新高 - 回撤起点 | +| 夏普比率 | `sharpe` | `(mean(ret) - rf/252) / std(ret) * sqrt(252)` | +| 索提诺比率 | `sortino` | 分母只用负收益标准差 | +| 卡玛比率 | `calmar` | `annual_return / max_drawdown` | +| 总交易次数 | `total_trades` | `len(trades)` | +| 盈利/亏损次数 | `win_trades` / `lose_trades` | `trade_pnl > 0 / <= 0` | +| 胜率 | `win_rate` | `win_trades / total_trades` | +| 盈亏比 | `profit_factor` | `sum(win_pnl) / abs(sum(lose_pnl))` | +| 平均盈利/亏损 | `avg_win` / `avg_loss` | 盈利/亏损交易均值 | +| 最大单笔盈亏 | `max_win` / `max_loss` | 单笔极值 | +| 平均持仓天数 | `avg_holding_days` | 买入到卖出的 Bar 数均值 | +| 收益波动率 | `volatility` | `std(daily_ret) * sqrt(252)` | +| 基准超额收益 | `alpha` | 策略收益 - 基准收益(需 benchmark) | +| 信息比率 | `information_ratio` | 超额收益均值 / 跟踪误差(需 benchmark) | + +--- + +## 8. CLI 集成 + +### 8.1 命令 + +```bash +# DSL 字符串模式 +easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --cash 100000 --table + +# DSL 文件模式 +easy-tdx backtest SH 600519 --strategy-file my_strategy.py --cash 100000 + +# 参数化 +easy-tdx backtest SH 600519 --strategy "CROSS(MA({short}),MA({long}))" --params short=5,long=20 + +# 指定周期/复权 +easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --period 5MIN --adjust QFQ + +# 参数扫描 +easy-tdx backtest SH 600519 --strategy "CROSS(MA({short}),MA({long}))" \ + --grid short=5,10,15 --grid long=20,30,60 --sort-by sharpe --table + +# 输出 CSV +easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --output csv +``` + +### 8.2 输出格式 + +默认 JSON,`--table` 切换表格,`--output csv` 输出 CSV。与现有 CLI 行为一致。 + +--- + +## 9. 测试计划 + +``` +tests/unit/test_backtest_strategy.py # Strategy 基类 + 指标注入 +tests/unit/test_backtest_dsl.py # DSL 解析 + 编译 +tests/unit/test_backtest_engine.py # 引擎核心(信号→成交→持仓) +tests/unit/test_backtest_orders.py # 撮合规则(5 种 execution 模式) +tests/unit/test_backtest_portfolio.py # 持仓追踪 + 资金曲线 +tests/unit/test_backtest_performance.py # 绩效计算(手工验证已知结果) +tests/unit/test_backtest_cli.py # CLI 命令(click test runner) +``` + +全部离线测试,使用手工构造的 DataFrame fixture,零网络依赖。 + +--- + +## 10. 未来扩展点(v1 不实现,架构不堵死) + +- 事件驱动执行路径(支持日内策略、逐 tick 推演) +- 多品种组合回测(Portfolio 级别,同时持有多只股票) +- 风控模块(最大回撤止损、单笔止损、仓位上限) +- 实时模拟交易(Strategy 基类接口可直接迁移) +- 与缠论模块深度集成(策略可直接引用笔/中枢/买卖点信号) +- 可视化(K 线 + 买卖点标注 + 资金曲线) diff --git a/src/easy_tdx/MyTT.py b/src/easy_tdx/MyTT.py index 0cf8d98..3b129c9 100644 --- a/src/easy_tdx/MyTT.py +++ b/src/easy_tdx/MyTT.py @@ -321,4 +321,14 @@ def BIAS_SIGNAL(CLOSE, P=10, M=30): #乖离率信号指标:M日乖离 + 短/ X_LMA = MA(X, M) #长周期信号线 MA(X,M) return RD(X), RD(S_SMA), RD(X_LMA) +def FK(CLOSE): #FK趋势指标:快线EMA(2)与斜率外推慢线EMA(42)比较 + fast = EMA(CLOSE, 2) + slow = EMA(SLOPE(CLOSE, 21) * 20 + CLOSE, 42) + return fast > slow + +def OUTPERFORM_20D(CLOSE, INDEX_CLOSE): #20日相对强度:个股涨幅跑赢大盘返回1,否则返回0 + stock_ret = (CLOSE - REF(CLOSE, 20)) / REF(CLOSE, 20) + index_ret = (INDEX_CLOSE - REF(INDEX_CLOSE, 20)) / REF(INDEX_CLOSE, 20) + return IF(stock_ret > index_ret, 1, 0) + #望大家能提交更多指标和函数 https://github.com/mpquant/MyTT diff --git a/src/easy_tdx/codec/frame.py b/src/easy_tdx/codec/frame.py index 9f0833d..40f1ef9 100644 --- a/src/easy_tdx/codec/frame.py +++ b/src/easy_tdx/codec/frame.py @@ -1,12 +1,16 @@ """响应帧头解析与 zlib 解压。 -响应帧格式(16 字节固定头 + body): - struct " FrameHeader: """解析 16 字节响应帧头。""" - u0, u1, u2, zipsize, unzipsize = unpack_from( + magic, seq_id, method, zipsize, unzipsize = unpack_from( _HEADER_FMT, buf, 0, "frame header", ) - return FrameHeader(u0, u1, u2, zipsize, unzipsize) + return FrameHeader(magic, seq_id, method, zipsize, unzipsize) def decompress_body(header: FrameHeader, raw_body: bytes) -> bytes: diff --git a/src/easy_tdx/commands/security_list.py b/src/easy_tdx/commands/security_list.py index aeb8f75..2a569fc 100644 --- a/src/easy_tdx/commands/security_list.py +++ b/src/easy_tdx/commands/security_list.py @@ -39,10 +39,10 @@ class GetSecurityListCmd(BaseCommand[list[SecurityInfo]]): code_bytes, volunit, name_bytes, - _unknown1, # 4字节,含义未明 + _unknown1, # 4字节,排序/分组字段(非用户可见数据) decimal_point, pre_close_raw, - _unknown2, # 4字节,含义未明 + _unknown2, # 4字节,私有时间戳(非用户可见数据) ) = struct.unpack("<6sH8s4sBI4s", raw) code = code_bytes.decode("utf-8", errors="replace").rstrip("\x00") diff --git a/src/easy_tdx/commands/security_quotes.py b/src/easy_tdx/commands/security_quotes.py index 32ee25f..c628fb1 100644 --- a/src/easy_tdx/commands/security_quotes.py +++ b/src/easy_tdx/commands/security_quotes.py @@ -100,8 +100,8 @@ class GetSecurityQuotesCmd(BaseCommand[list[SecurityQuote]]): s_vol, pos = get_price(body, pos) b_vol, pos = get_price(body, pos) - unknown_2, pos = get_price(body, pos) - unknown_3, pos = get_price(body, pos) + unknown_2, pos = get_price(body, pos) # IndexOpenAmount(指数)/舍入残差(个股) + unknown_3, pos = get_price(body, pos) # StockOpenAmount(个股)/负值(指数) # 五档买盘 bid1_d, pos = get_price(body, pos) @@ -129,8 +129,8 @@ class GetSecurityQuotesCmd(BaseCommand[list[SecurityQuote]]): bv5, pos = get_price(body, pos) av5, pos = get_price(body, pos) - # 尾部:2字节 H + 4个 get_price + 2字节 h + 2字节 H - (unknown_4,) = unpack_from("