diff --git a/CHANGELOG.md b/CHANGELOG.md index be92e6b..019c919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ 本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。 +## [1.30.1] — 2026-09-03 + +**彻底移除 ZIG 之字转向指标与 `zig_breakout` 策略**——ZIG 是教科书级的**未来函数**:波峰/波谷拐点只有在**其后**的走势反向走满 X% 确认转向后才会**回溯标出**,也就是说序列里每个拐点的位置都用到了"当时不可能知道"的未来信息。把它当买卖信号回测,等于允许策略在波谷那一天精准买入、在见顶前一天精准卖出——收益必然严重虚高、参数寻优必然过拟合,回测结果与实盘表现脱节,**这正是本工具最要防的失真**。 + +### 移除 + +- **为什么必须删,而不是加警示**——v1.29 引入时已尝试用「硬止损 + 右侧突破确认进场」两层保护对冲前视偏差,但那只能**缓解**而非**消除**:`zig_breakout` 的建仓信号"ZIG 向上启动(波谷确认)"本质上仍是偷看未来的产物,建立其上的任何回测数字、寻优选参、AI 解读都自带系统性偏差。铁证就在本项目自己的黄金基线里:`zig_breakout` 在固定样本上胜率 **100%**、总收益 **+88.2%**、夏普 **1.73**——一组完美得不像真话的数字,恰恰是前视偏差的签名(实盘信号天然滞后于回测拐点,实盘表现只会大幅劣于回测)。回测工具的生命线是"数字可信":与其留着一个数字必然失真的指标靠 docstring 警告自律(挡不住复制粘贴),不如删掉——**宁可少一个指标,不给用户一个实盘必然打脸的完美陷阱**。 +- **移除范围(三端同步消失)**——① `MyTT.ZIG` 指标函数(`src/easy_tdx/MyTT.py`);② 内置策略 `zig_breakout`(`backtest/strategies/builtin.py` 注册表删除后,CLI 回测、Web API、WebUI 回测策略下拉与对比页自动不再出现);③ 参数寻优预设网格(`presets.py`)与「一键寻优所有策略」中的对应条目;④ 仓库根示例策略 `strategies/zig_breakout.py`(`--strategy-file` 入口);⑤ 单测 `test_mytt_zig.py` / `test_zig_strategy.py` 删除,向量化对拍测试的路径依赖白名单随之清空(其余全部 19 个内置策略均可向量化对拍),黄金基线 `backtest_metrics.json` 移除 zig_breakout 条目(其余策略零漂移);⑥ README / strategies/README / 架构文档中的相关说明。 +- **兼容性影响(有意为之)**——外部脚本 `from easy_tdx.MyTT import ZIG` 将抛 `ImportError`,引用 `zig_breakout` 的回测命令 / API 调用将报"策略不存在":**快速失败好过静默失真**。需要之字转向类形态分析的用户,可自行基于已确认 K 线实现不含未来函数的右侧确认版本,或改用海龟突破(`turtle_breakout`)/ 唐奇安通道(`donchian`)等天然右侧的策略。 + ## [1.29.2] — 2026-09-02 **可编辑安装失效时给出友好报错**([#58](https://github.com/handsomejustin/easy_tdx/discussions/58))——`pip install -e .` 会在 site-packages 写入 `_editable_impl_easy_tdx.pth`(内容为仓库 `src/` 绝对路径)。仓库目录被移动/重命名/重新 clone 或该文件丢失后,`easy-tdx` 只会抛出一句无法定位的 `No module named 'easy_tdx.cli'`:`easy_tdx` 本体因 site-packages 里的 `web/dist` 命名空间碎片仍可导入,报错极具误导性(实测复现,失效态 `easy_tdx.__path__` 只剩 site-packages 碎片路径)。 diff --git a/README.md b/README.md index 2c4c7a0..78dc202 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ easy-tdx 要做的事很简单:**把机构的数据锁砸开,扔到每个普 **这一步的意义**:取数自由(v1.0)→ 回测自由(v1.17)→ 防过拟合(v1.25)→ **认知自由(v1.29)**。前几步解决「数据与工具的不平等」,这一步解决「解读能力的不平等」——机构有研究员写晨报,你现在有自己的 AI 分析师。仍是 MIT 开源、零注册、零平台费,唯一成本是你自己模型的调用费(用 Ollama 本地模型连这个都是零)。 -**v1.29 同场加映**(借鉴社区 Fork [swimmingaaron/easy_tdx](https://github.com/swimmingaaron/easy_tdx) 的好想法,逐项甄别后移植):**ZIG 右侧突破回补策略**(MyTT 新增之字转向指标,波谷建仓带硬止损 + 见顶记前高 + 右侧突破回补,对冲未来函数的前视偏差);**交易时段感知刷新**(看板轮询休市自动暂停、三态状态栏、手动刷新不受限,不再半夜空转刷数据);**120 分钟 K 线**(MAC 原生两级降级获取)与**逐 bar 涨跌幅/振幅字段**(`pre_close`/`change_pct`/`amplitude_pct` 直接返回);**159 只核心龙头池**(东财全行业龙头名单 → `universe=core` 一键只扫龙头,配「龙头池」页面;名单仅为扫描范围,不构成任何推荐);全站**风险提示与免责声明**常驻。WebUI 工程加固:未知 API 路径返回 JSON 404 而非 HTML、入口页 `no-store` 防缓存旧版——更新即所见。 +**v1.29 同场加映**(借鉴社区 Fork [swimmingaaron/easy_tdx](https://github.com/swimmingaaron/easy_tdx) 的好想法,逐项甄别后移植):**交易时段感知刷新**(看板轮询休市自动暂停、三态状态栏、手动刷新不受限,不再半夜空转刷数据);**120 分钟 K 线**(MAC 原生两级降级获取)与**逐 bar 涨跌幅/振幅字段**(`pre_close`/`change_pct`/`amplitude_pct` 直接返回);**159 只核心龙头池**(东财全行业龙头名单 → `universe=core` 一键只扫龙头,配「龙头池」页面;名单仅为扫描范围,不构成任何推荐);全站**风险提示与免责声明**常驻。WebUI 工程加固:未知 API 路径返回 JSON 404 而非 HTML、入口页 `no-store` 防缓存旧版——更新即所见。 **期货持仓雷达:中金所成交持仓排名(v1.29.1 新增)**——每个交易日收盘后约 16:15,中金所官网公布各期货品种「成交量 / 持买单量(多单)/ 持卖单量(空单)」各前 20 名期货公司会员的排名,这是散户能免费看到的**最接近"主力动向"的公开数据**。本工具把它变成一键采集:CLI 一行命令(`easy-tdx ccpm IF --table`)、Web API(`GET /api/v1/ccpm/rank`)、WebUI「期货持仓排名」页(品种下拉 + 日期选择器 + 一键采集,合约页签自动标注主力合约,前 20 名合计多单/空单/净持仓概览,三组排名并排表格)。覆盖 IF 沪深300 / IH 上证50 / IC 中证500 / IM 中证1000 四大股指期货与 TS/TF/T/TL 2/5/10/30 年期国债期货;非交易日与未发布自动识别并可回溯最近交易日;每个交易日数据发布后不可变,按日落盘缓存(`~/.easy_tdx/cache/ccpm/`),历史二次查询零网络。页面附三段新手科普:「(代客)」是什么意思、多单/空单/加减仓怎么看、以及最重要的一条——**排名表看不出是套保还是投机,空单多 ≠ 看空市场**(股指期货空单大头常是机构套保盘)。 diff --git a/docs/architecture.html b/docs/architecture.html index 3572a9a..4b21e0b 100644 --- a/docs/architecture.html +++ b/docs/architecture.html @@ -194,8 +194,8 @@ const LAYERS = [ d:['DeepSeek · Qwen · 智谱 · Kimi · MiniMax…','OpenAI / Anthropic 兼容 + Ollama 本地'], tip:'LlmClient 用标准库 urllib 实现(零三方依赖):Provider 预设 + ~/.easy_tdx 配置文件与 WebUI 双向同步,为回测报告生成自然语言解读,对话历史落 SQLite。' }, { t:'示例策略库', s:'strategies/(仓库根目录)', - d:['~19 个 Strategy 子类','MA / MACD / 海龟 / ZIG / 捉妖 / 乖离 …','运行时动态加载进回测与扫描'], - tip:'仓库根 strategies/ 目录:ma_cross、macd_cross、turtle_breakout、zig_breakout、zhuoyao_momentum、bias_reversal 等 19 个示例策略文件,由 CLI(backtest --strategy-file、run-all --strategies-dir)与选股扫描在运行时动态 import;与包内 backtest/strategies/ 注册表相互独立。' } + d:['~16 个 Strategy 子类','MA / MACD / 海龟 / 捉妖 / 乖离 …','运行时动态加载进回测与扫描'], + tip:'仓库根 strategies/ 目录:ma_cross、macd_cross、turtle_breakout、zhuoyao_momentum、bias_reversal 等 16 个示例策略文件,由 CLI(backtest --strategy-file、run-all --strategies-dir)与选股扫描在运行时动态 import;与包内 backtest/strategies/ 注册表相互独立。' } ] ] }, diff --git a/pyproject.toml b/pyproject.toml index 115117a..4f9647c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "easy-tdx" -version = "1.29.2" +version = "1.30.1" description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步" readme = "README.md" requires-python = ">=3.10" diff --git a/src/easy_tdx/MyTT.py b/src/easy_tdx/MyTT.py index 9a77b9d..f33a9dc 100644 --- a/src/easy_tdx/MyTT.py +++ b/src/easy_tdx/MyTT.py @@ -571,117 +571,4 @@ def FSL(CLOSE, VOL, CAPITAL): # 分水岭指标:多空趋势强弱分界(SW return RD(SWL), RD(SWS) -def ZIG(S, X=35): # 之字转向指标(未来函数):S为价格序列,X为转向阈值百分比(如10表示10%) - """之字转向指标 (ZigZag) — 经典未来函数。 - - 当价格从前一个极值点反向变动超过 X% 时确立波峰/波谷拐点并转向, - 拐点之间线性插值,返回与 S 等长的拟合序列。 - - 注意:拐点只有在**其后**的走势确认了转向才会回溯标出,序列中波峰/ - 波谷位置含有未来信息。把 ZIG 拐点直接当买卖信号回测会严重高估收益 - (前视偏差);如需使用,必须配合右侧确认或止损保护(参见内置策略 - ``zig_breakout`` 的做法)。 - - Args: - S: 价格序列(通常为 CLOSE) - X: 转向阈值百分比。10 表示 10%;也可传小数形式 0.1(以 1.0 为界 - 自动区分,故阈值本身小于 1% 时请用小数形式) - - Returns: - np.ndarray: 与 S 等长的 ZIG 之字转向插值序列 - """ - S = np.asarray(S, dtype=float) - n = len(S) - if n == 0: - return np.array([], dtype=float) - if n == 1: - return S.copy() - - x = float(X) / 100.0 if float(X) > 1.0 else float(X) - if x <= 0: - return S.copy() - - ZIG_STATE_START = 0 - ZIG_STATE_RISE = 1 - ZIG_STATE_FALL = 2 - - peer_i = 0 - candidate_i = None - peers = [0] - state = ZIG_STATE_START - - for scan_i in range(1, n): - if scan_i == n - 1: - # 扫描到序列尾部:未确立的候选极值按当前方向收尾 - if candidate_i is None: - peers.append(scan_i) - else: - if state == ZIG_STATE_RISE: - if S[scan_i] >= S[candidate_i]: - peers.append(scan_i) - else: - peers.append(candidate_i) - if candidate_i != scan_i: - peers.append(scan_i) - elif state == ZIG_STATE_FALL: - if S[scan_i] <= S[candidate_i]: - peers.append(scan_i) - else: - peers.append(candidate_i) - if candidate_i != scan_i: - peers.append(scan_i) - else: - peers.append(scan_i) - break - - if state == ZIG_STATE_START: - if S[peer_i] != 0: - if S[scan_i] >= S[peer_i] * (1.0 + x): - candidate_i = scan_i - state = ZIG_STATE_RISE - elif S[scan_i] <= S[peer_i] * (1.0 - x): - candidate_i = scan_i - state = ZIG_STATE_FALL - elif state == ZIG_STATE_RISE: - if S[scan_i] >= S[candidate_i]: - candidate_i = scan_i - elif S[candidate_i] != 0 and S[scan_i] <= S[candidate_i] * (1.0 - x): - peer_i = candidate_i - peers.append(peer_i) - state = ZIG_STATE_FALL - candidate_i = scan_i - elif state == ZIG_STATE_FALL: - if S[scan_i] <= S[candidate_i]: - candidate_i = scan_i - elif S[candidate_i] != 0 and S[scan_i] >= S[candidate_i] * (1.0 + x): - peer_i = candidate_i - peers.append(peer_i) - state = ZIG_STATE_RISE - candidate_i = scan_i - - # 去除重复拐点并确保末端对齐 - clean_peers = [] - for p in peers: - if not clean_peers or p != clean_peers[-1]: - clean_peers.append(p) - if clean_peers[-1] != n - 1: - clean_peers.append(n - 1) - - # 拐点间线性插值 - z = np.zeros(n, dtype=float) - for i in range(len(clean_peers) - 1): - p_start = clean_peers[i] - p_end = clean_peers[i + 1] - v_start = S[p_start] - v_end = S[p_end] - if p_end == p_start: - z[p_start] = v_start - else: - slope = (v_end - v_start) / (p_end - p_start) - for j in range(p_end - p_start + 1): - z[p_start + j] = v_start + slope * j - - return RD(z) - - # 望大家能提交更多指标和函数 https://github.com/mpquant/MyTT diff --git a/src/easy_tdx/backtest/strategies/builtin.py b/src/easy_tdx/backtest/strategies/builtin.py index 989fada..3a7f750 100644 --- a/src/easy_tdx/backtest/strategies/builtin.py +++ b/src/easy_tdx/backtest/strategies/builtin.py @@ -30,7 +30,6 @@ from easy_tdx.MyTT import ( EMA, EMV, FSL, - HHV, KDJ, KTN, MA, @@ -39,7 +38,6 @@ from easy_tdx.MyTT import ( TAQ, TRIX, WR, - ZIG, ) __all__: list[str] = [] # 注册副作用即可,无需导出符号 @@ -693,113 +691,3 @@ class FslStrategy(ParametrizedStrategy): def entry_exit_masks(self) -> tuple[Any, Any]: """与 next() 同源(gold/dead 即 next() 判定用的同一组掩码数组)。""" return self.gold, self.dead - - -# ── ZIG 右侧突破回补 ───────────────────────────────────────────────────────── - - -@register_strategy( - name="zig_breakout", - label="ZIG 右侧突破回补", - description=( - "ZIG 向上启动(波谷确认)全仓买入;ZIG 见顶回落清仓并记录 N 日最高点," - "其后收盘突破前高×(1+确认比例) 时右侧回补。两路径买入均带硬止损," - "对冲 ZIG 波谷确认的前视偏差(未来函数,实盘信号会滞后)。" - ), -) -class ZigBreakoutStrategy(ParametrizedStrategy): - """ZIG 右侧突破回补(Re-entry on Breakout + 硬止损保护)。 - - ZIG 是未来函数:波峰/波谷只有在其后走势确认转向才回溯标出,回测里 - "波谷启动"信号天然偷看未来。本策略用两层保护缓解而非消除该偏差: - - 1. 买入即挂 ``stop_loss_pct`` 硬止损(引擎逐 bar 监控,跌破自动平仓), - 假波谷不至于深套; - 2. 卖出后不追 ZIG 新波谷,而是等价格**右侧突破**前高确认_pct 再回补, - 把"猜底"换成"确认后进场"。 - - 交易逻辑:: - - 空仓 + ZIG 上行 → 全仓买入(带止损) - 持仓 + ZIG 下行(见顶) → 全仓卖出,记录 HHV(high, N) 为前高 - 空仓 + 收盘 ≥ 前高×(1+确认) → 右侧回补(带止损) - - 注意:``_breakout_level`` 随持仓路径变化,信号不可向量化,故不实现 - ``entry_exit_masks``——引擎自动走逐 bar 回放路径(与 next() 完全一致)。 - """ - - params = [ - Param( - "zig_delta", - float, - default=10.0, - min_value=0.5, - max_value=50.0, - label="ZIG转向阈值%", - ), - Param( - "confirm_pct", - float, - default=2.0, - min_value=0.1, - max_value=20.0, - label="突破确认比例%", - ), - Param("hhv_period", int, default=20, min_value=5, max_value=120, label="前高周期"), - Param( - "stop_loss_pct", - float, - default=3.0, - min_value=0.0, - max_value=30.0, - label="硬止损%", - ), - ] - - def init(self) -> None: - self.zig = self.I(ZIG, self.data.close, self.p["zig_delta"]) - self.hhv = self.I(HHV, self.data.high, self.p["hhv_period"]) - # 见顶清仓时记录的前高(0 = 未记录,等待首次建仓-见顶周期) - self._breakout_level: float = 0.0 - - def next(self) -> None: - i = self._bar_index - if i == 0: - return - - cur_close = float(self.data.close[0]) - cur_zig = float(self.zig[i]) - prev_zig = float(self.zig[i - 1]) - cur_pos = self.position["size"] - - # 持仓:ZIG 见顶回落 → 清仓,并记录突破位(HHV 含未来 bar 已确认的高点) - if cur_pos > 0 and cur_zig < prev_zig: - self._breakout_level = float(self.hhv[i]) - self.sell(size=0) - return - - if cur_pos == 0: - # 路径 1:ZIG 向上启动(波谷确认)→ 初始建仓 - if cur_zig > prev_zig: - self._breakout_level = 0.0 - self._buy_with_stop() - return - - # 路径 2:右侧突破前高 → 回补(洗盘结束、主升确立) - if self._breakout_level > 0: - threshold = self._breakout_level * (1.0 + self.p["confirm_pct"] / 100.0) - if cur_close >= threshold: - self._breakout_level = 0.0 - self._buy_with_stop() - - def _buy_with_stop(self) -> None: - """市价全仓买入并按 ``stop_loss_pct`` 挂硬止损(0 = 不挂)。 - - 市价单(price=None)由引擎在下一根开盘成交,与本地其他内置策略 - 口径一致,避免信号 bar 收盘价成交的前视味道。 - """ - pct = self.p["stop_loss_pct"] / 100.0 - if pct > 0: - self.buy(size=0, stop_loss_pct=pct) - else: - self.buy(size=0) diff --git a/src/easy_tdx/backtest/strategies/presets.py b/src/easy_tdx/backtest/strategies/presets.py index bb7706c..2f360f5 100644 --- a/src/easy_tdx/backtest/strategies/presets.py +++ b/src/easy_tdx/backtest/strategies/presets.py @@ -98,12 +98,6 @@ STRATEGY_PRESETS: dict[str, dict[str, list[Any]]] = { # capital 仅作粗档扫描(1千万/1亿/10亿股),覆盖小盘→大盘 "capital": [1e7, 1e8, 1e9, 1e10], }, # 4 - # ── 之字转向类 ─────────────────────────────────────────────────────────── - "zig_breakout": { - # 转向阈值×确认比例 = 4×3;hhv/止损用默认(HHV20 / 3%) - "zig_delta": [5.0, 8.0, 10.0, 15.0], - "confirm_pct": [1.0, 2.0, 3.0], - }, # 12 } diff --git a/strategies/README.md b/strategies/README.md index 11c1383..383914f 100644 --- a/strategies/README.md +++ b/strategies/README.md @@ -24,7 +24,6 @@ easy-tdx backtest SH 600519 --strategy-file strategies/macd_cross.py --cash 5000 | `bias_reversal.py` | 乖离率反转 | 反转 | 震荡回归 | | `volume_price.py` | 量价配合 | 综合判断 | 放量突破 | | `obv_trend.py` | OBV 能量潮趋势 | 量价趋势 | 资金持续流入的上升趋势 | -| `zig_breakout.py` | ZIG 右侧突破回补 | 右侧突破/波段 | 波谷启动建仓,见顶卖出后右侧突破前高回补(带硬止损;ZIG 为未来函数,回测信号有前视性) | ## 编写自定义策略 diff --git a/strategies/zig_breakout.py b/strategies/zig_breakout.py deleted file mode 100644 index 29dac7f..0000000 --- a/strategies/zig_breakout.py +++ /dev/null @@ -1,83 +0,0 @@ -"""ZIG 右侧突破回补策略(Re-entry on Breakout + 硬止损保护)。 - -交易逻辑 --------- -1. **空仓**:ZIG 向上启动(底部波谷确认)→ 全仓买入建仓,挂硬止损(默认 3%)。 -2. **持仓**:ZIG 见顶回落 → 全仓卖出,并记录 N 日最高价为 breakout_level。 -3. **空仓等待回补**:收盘价突破 breakout_level × (1 + confirm_pct/100) - → 右侧突破确认,洗盘结束主升确立,全仓买入回补(同样带硬止损)。 -4. **风控保护**:买入后未见顶但跌破止损线,由引擎自动触发止损平仓, - 对冲 ZIG 波谷确认的前视偏差(ZIG 是未来函数,拐点回溯标出)。 - -注意:与内置注册表中的 ``zig_breakout``(``easy_tdx.backtest.strategies``) -同一套逻辑;本文件供 ``--strategy-file`` 离线扫描(``easy-tdx screen scan``) -使用,参数硬编码为默认档。 - -用法:: - - easy-tdx backtest SZ 300223 --strategy-file strategies/zig_breakout.py --table - easy-tdx screen scan --strategy strategies/zig_breakout.py --universe core -""" - -from easy_tdx.backtest import Strategy -from easy_tdx.MyTT import HHV, ZIG - - -class ZigBreakoutStrategy(Strategy): - """ZIG 右侧突破回补策略(含硬止损保护)。""" - - def __init__( - self, - zig_delta: float = 10.0, - confirm_pct: float = 2.0, - hhv_period: int = 20, - stop_loss_pct: float = 3.0, - ) -> None: - super().__init__() - self.zig_delta = zig_delta - self.confirm_pct = confirm_pct - self.hhv_period = hhv_period - self.stop_loss_pct = stop_loss_pct - - def init(self) -> None: - self.zig = self.I(ZIG, self.data.close, self.zig_delta) - self.hhv = self.I(HHV, self.data.high, self.hhv_period) - self._breakout_level: float = 0.0 - - def next(self) -> None: - i = self._bar_index - if i == 0: - return - - cur_close = float(self.data.close[0]) - cur_zig = float(self.zig[i]) - prev_zig = float(self.zig[i - 1]) - cur_pos = self.position["size"] - - # 持仓:ZIG 见顶 → 全仓卖出,记录突破位 - if cur_pos > 0 and cur_zig < prev_zig: - self._breakout_level = float(self.hhv[i]) - self.sell(size=0) - return - - # 空仓:两种买入路径(均带硬止损) - if cur_pos == 0: - # 路径 1:ZIG 向上启动(底部波谷确认)→ 初始建仓 - if cur_zig > prev_zig: - self._breakout_level = 0.0 - self._buy_with_stop() - return - - # 路径 2:右侧突破前高 → 回补建仓(洗盘结束、主升确立) - if self._breakout_level > 0: - threshold = self._breakout_level * (1.0 + self.confirm_pct / 100.0) - if cur_close >= threshold: - self._breakout_level = 0.0 - self._buy_with_stop() - - def _buy_with_stop(self) -> None: - pct = self.stop_loss_pct / 100.0 - if pct > 0: - self.buy(size=0, stop_loss_pct=pct) - else: - self.buy(size=0) diff --git a/tests/golden/backtest_metrics.json b/tests/golden/backtest_metrics.json index b1b43bc..088cd88 100644 --- a/tests/golden/backtest_metrics.json +++ b/tests/golden/backtest_metrics.json @@ -336,19 +336,6 @@ "ulcer_index": 0.0, "var_95": -0.0, "win_rate": 0.0 - }, - "zig_breakout": { - "cvar_95": 0.028842972139600086, - "max_consecutive_losses": 0, - "max_consecutive_wins": 5, - "max_drawdown": 0.08762079023098233, - "sharpe": 1.726724943367625, - "sqn": 3.2661255981598982, - "total_return": 0.881929152129431, - "total_trades": 5, - "ulcer_index": 0.032735452584468416, - "var_95": 0.021750558683001117, - "win_rate": 1.0 } } } diff --git a/tests/unit/test_backtest_engine_vector.py b/tests/unit/test_backtest_engine_vector.py index fee664a..524adfb 100644 --- a/tests/unit/test_backtest_engine_vector.py +++ b/tests/unit/test_backtest_engine_vector.py @@ -302,18 +302,12 @@ def test_auto_falls_back_on_mask_shape_mismatch() -> None: def test_vector_path_actually_used_for_builtins() -> None: """默认 signal_path='auto' 下内置策略确实走了向量化(防止回退被掩盖)。 - 例外白名单:信号依赖路径状态(无法用静态掩码等价表达)的策略, - 引擎对它们走逐 bar 回放(与 next() 完全一致),属设计而非回退。 + 若某策略信号依赖路径状态(无法用静态掩码等价表达),应在此说明并 + 考虑引擎走逐 bar 回放的白名单机制(当前无此类策略)。 """ from easy_tdx.backtest.strategy import Strategy as Base - # zig_breakout 的 _breakout_level(见顶清仓后记录的前高)随持仓路径 - # 变化,掩码不可表达;见 builtin.py 该策略的注释 - path_dependent = {"zig_breakout"} - for name in get_registry().names(): - if name in path_dependent: - continue strat_cls = get_registry().get(name).strategy_cls assert strat_cls.entry_exit_masks is not Base.entry_exit_masks, ( f"{name} 未实现 entry_exit_masks,auto 将永远走逐 bar" diff --git a/tests/unit/test_llm_history_store.py b/tests/unit/test_llm_history_store.py index 707f6fb..f8b82d3 100644 --- a/tests/unit/test_llm_history_store.py +++ b/tests/unit/test_llm_history_store.py @@ -31,18 +31,18 @@ class TestLlmHistoryStore: def test_context_roundtrip(self, store): store.add( _rec( - strategy="zig_breakout", - strategy_label="ZIG 右侧突破回补", + strategy="macd", + strategy_label="MACD 金叉", symbol="600519", category="DAY", - params={"zig_delta": 5.0, "confirm_pct": 2.0}, + params={"short": 12, "long": 26}, start_date="2024-01-01", end_date="2025-01-01", ) ) it = store.list_all()[0] - assert it.strategy == "zig_breakout" and it.symbol == "600519" - assert it.params == {"zig_delta": 5.0, "confirm_pct": 2.0} # JSON 往返保真 + assert it.strategy == "macd" and it.symbol == "600519" + assert it.params == {"short": 12, "long": 26} # JSON 往返保真 assert it.start_date == "2024-01-01" def test_corrupt_params_json_tolerated(self, store, tmp_path): diff --git a/tests/unit/test_mytt_zig.py b/tests/unit/test_mytt_zig.py deleted file mode 100644 index 64b3fda..0000000 --- a/tests/unit/test_mytt_zig.py +++ /dev/null @@ -1,66 +0,0 @@ -"""MyTT.ZIG 之字转向指标单元测试(借鉴 Fork 移植,v1.29)。 - -覆盖:边界输入(空/单根/零阈值)、单调序列恒等、V 型反转拐点标定、 -阈值两种写法(5 与 0.05)等价、输出形状与有限性。 -""" - -from __future__ import annotations - -import numpy as np - -from easy_tdx.MyTT import ZIG - - -def test_zig_empty_and_single(): - assert ZIG(np.array([]), 10).size == 0 - single = ZIG(np.array([42.0]), 10) - assert single.shape == (1,) and single[0] == 42.0 - - -def test_zig_zero_threshold_returns_self(): - s = np.array([1.0, 5.0, 2.0, 8.0]) - assert np.array_equal(ZIG(s, 0), s) - - -def test_zig_monotonic_series_identity(): - """单调序列无拐点,ZIG 退化为自身(RD 保留 3 位小数)。""" - line = np.linspace(1.0, 2.0, 50) - assert np.allclose(ZIG(line, 10), line, atol=1e-3) - - -def test_zig_v_shape_trough(): - """V 型反转:谷底被标为拐点,前后两段各自线性插值。""" - v = np.concatenate([np.linspace(100.0, 80.0, 30), np.linspace(80.0, 120.0, 40)]) - z = ZIG(v, 5) - assert z.shape == v.shape - assert np.isfinite(z).all() - assert abs(z[0] - 100) < 0.01 - assert abs(z[-1] - 120) < 0.01 - # 谷底(两个 80 中的后者,上升段起点)被精确对齐 - assert abs(z.min() - 80) < 0.01 - assert abs(z[30] - 80) < 0.01 - # 拐点间线性:下降段任意点是两端点的线性插值 - assert abs(z[15] - (100 + 80) / 2) < 0.01 - - -def test_zig_threshold_forms_equivalent(): - s = 100 + 10 * np.sin(np.arange(80) / 6.0) - assert np.allclose(ZIG(s, 5), ZIG(s, 0.05), atol=1e-9) - - -def test_zig_zigzag_alternating_peaks(): - """标准锯齿:每个预设峰谷都应成为拐点(ZIG 值在拐点处触及其价格)。""" - seg = [10.0, 13.0, 10.0, 13.0, 10.0, 13.0] # ±30% 摆动,阈值 10% 必转向 - s = np.array(seg) - z = ZIG(s, 10) - for i, price in enumerate(seg): - assert abs(z[i] - price) < 0.01, f"锯齿序列每根都是拐点: idx={i}" - - -def test_zig_noisy_series_shape(): - rng = np.random.default_rng(7) - s = 100 + np.cumsum(rng.normal(0, 1.5, 200)) - z = ZIG(s, 12) - assert z.shape == s.shape - assert np.isfinite(z).all() - assert z.min() >= s.min() - 1e-3 and z.max() <= s.max() + 1e-3 diff --git a/tests/unit/test_zig_strategy.py b/tests/unit/test_zig_strategy.py deleted file mode 100644 index fa661bf..0000000 --- a/tests/unit/test_zig_strategy.py +++ /dev/null @@ -1,97 +0,0 @@ -"""zig_breakout 内置策略单元测试(借鉴 Fork 移植,v1.29)。 - -覆盖:注册表登记与参数 schema、合成锯齿行情能产生交易、 -止损单挂在买入信号上(OCO bracket)、寻优预设网格登记。 -""" - -from __future__ import annotations - -import numpy as np -import pandas as pd -import pytest - -from easy_tdx.backtest.engine import BacktestEngine -from easy_tdx.backtest.strategies import get_registry -from easy_tdx.backtest.strategies.presets import STRATEGY_PRESETS - - -def _zigzag_df(n: int = 300, seed: int = 42) -> pd.DataFrame: - """先跌后大涨再回调的合成行情(触发 ZIG 波谷启动与见顶清仓)。""" - rng = np.random.default_rng(seed) - trend = np.concatenate( - [ - np.linspace(100, 80, n // 3), - np.linspace(80, 130, n * 2 // 5), - np.linspace(130, 110, n - n // 3 - n * 2 // 5), - ] - ) - close = trend + rng.normal(0, 0.8, len(trend)) - high = close + rng.uniform(0, 1.5, len(trend)) - low = close - rng.uniform(0, 1.5, len(trend)) - return pd.DataFrame( - { - "datetime": pd.date_range("2024-01-01", periods=len(trend), freq="B"), - "open": close + rng.normal(0, 0.3, len(trend)), - "high": high, - "low": low, - "close": close, - "vol": rng.integers(1e6, 5e6, len(trend)).astype(float), - "amount": close * 1e6, - } - ) - - -def test_registry_entry_and_params(): - entry = get_registry().get("zig_breakout") - assert entry.label == "ZIG 右侧突破回补" - names = [p.name for p in entry.params] - assert names == ["zig_delta", "confirm_pct", "hhv_period", "stop_loss_pct"] - defaults = {p.name: p.default for p in entry.params} - assert defaults == { - "zig_delta": 10.0, - "confirm_pct": 2.0, - "hhv_period": 20, - "stop_loss_pct": 3.0, - } - - -def test_build_validates_params(): - entry = get_registry().get("zig_breakout") - inst = entry.build({"zig_delta": 5}) - assert inst.p["zig_delta"] == 5.0 and inst.p["hhv_period"] == 20 - with pytest.raises(ValueError): - entry.build({"zig_delta": -1}) # 低于 min_value - - -def test_strategy_trades_and_bracket_stop(): - entry = get_registry().get("zig_breakout") - result = BacktestEngine(entry.build(), cash=1_000_000).run(_zigzag_df()) - assert len(result.trades) > 0 - # 锯齿行情应至少出现一次 BUY(trades 为 DataFrame) - assert (result.trades["direction"] == "BUY").any() - assert (result.trades["direction"] == "SELL").any() - - -def test_strategy_file_variant_loadable(): - """strategies/zig_breakout.py 独立文件可供 --strategy-file 加载。""" - import importlib.util - from pathlib import Path - - path = Path(__file__).resolve().parents[2] / "strategies" / "zig_breakout.py" - spec = importlib.util.spec_from_file_location("zig_file_test", path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - result = BacktestEngine(mod.ZigBreakoutStrategy(), cash=1_000_000).run(_zigzag_df()) - assert len(result.trades) > 0 - assert (result.trades["direction"] == "BUY").any() - - -def test_preset_grid_registered(): - assert "zig_breakout" in STRATEGY_PRESETS - grid = STRATEGY_PRESETS["zig_breakout"] - assert "zig_delta" in grid and "confirm_pct" in grid - # 笛卡尔积不超过寻优器上限 - n = 1 - for vals in grid.values(): - n *= len(vals) - assert n <= 200