mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 13:24:18 +08:00
release: v1.30.1 — 移除 ZIG 未来函数指标与 zig_breakout 策略:回测数字可信优先
This commit is contained in:
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
本文件记录 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
|
## [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 碎片路径)。
|
**可编辑安装失效时给出友好报错**([#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 碎片路径)。
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ easy-tdx 要做的事很简单:**把机构的数据锁砸开,扔到每个普
|
|||||||
|
|
||||||
**这一步的意义**:取数自由(v1.0)→ 回测自由(v1.17)→ 防过拟合(v1.25)→ **认知自由(v1.29)**。前几步解决「数据与工具的不平等」,这一步解决「解读能力的不平等」——机构有研究员写晨报,你现在有自己的 AI 分析师。仍是 MIT 开源、零注册、零平台费,唯一成本是你自己模型的调用费(用 Ollama 本地模型连这个都是零)。
|
**这一步的意义**:取数自由(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/`),历史二次查询零网络。页面附三段新手科普:「(代客)」是什么意思、多单/空单/加减仓怎么看、以及最重要的一条——**排名表看不出是套保还是投机,空单多 ≠ 看空市场**(股指期货空单大头常是机构套保盘)。
|
**期货持仓雷达:中金所成交持仓排名(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/`),历史二次查询零网络。页面附三段新手科普:「(代客)」是什么意思、多单/空单/加减仓怎么看、以及最重要的一条——**排名表看不出是套保还是投机,空单多 ≠ 看空市场**(股指期货空单大头常是机构套保盘)。
|
||||||
|
|
||||||
|
|||||||
@@ -194,8 +194,8 @@ const LAYERS = [
|
|||||||
d:['DeepSeek · Qwen · 智谱 · Kimi · MiniMax…','OpenAI / Anthropic 兼容 + Ollama 本地'],
|
d:['DeepSeek · Qwen · 智谱 · Kimi · MiniMax…','OpenAI / Anthropic 兼容 + Ollama 本地'],
|
||||||
tip:'LlmClient 用标准库 urllib 实现(零三方依赖):Provider 预设 + ~/.easy_tdx 配置文件与 WebUI 双向同步,为回测报告生成自然语言解读,对话历史落 SQLite。' },
|
tip:'LlmClient 用标准库 urllib 实现(零三方依赖):Provider 预设 + ~/.easy_tdx 配置文件与 WebUI 双向同步,为回测报告生成自然语言解读,对话历史落 SQLite。' },
|
||||||
{ t:'示例策略库', s:'strategies/(仓库根目录)',
|
{ t:'示例策略库', s:'strategies/(仓库根目录)',
|
||||||
d:['~19 个 Strategy 子类','MA / MACD / 海龟 / ZIG / 捉妖 / 乖离 …','运行时动态加载进回测与扫描'],
|
d:['~16 个 Strategy 子类','MA / MACD / 海龟 / 捉妖 / 乖离 …','运行时动态加载进回测与扫描'],
|
||||||
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/ 注册表相互独立。' }
|
tip:'仓库根 strategies/ 目录:ma_cross、macd_cross、turtle_breakout、zhuoyao_momentum、bias_reversal 等 16 个示例策略文件,由 CLI(backtest --strategy-file、run-all --strategies-dir)与选股扫描在运行时动态 import;与包内 backtest/strategies/ 注册表相互独立。' }
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "easy-tdx"
|
name = "easy-tdx"
|
||||||
version = "1.29.2"
|
version = "1.30.1"
|
||||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -571,117 +571,4 @@ def FSL(CLOSE, VOL, CAPITAL): # 分水岭指标:多空趋势强弱分界(SW
|
|||||||
return RD(SWL), RD(SWS)
|
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
|
# 望大家能提交更多指标和函数 https://github.com/mpquant/MyTT
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from easy_tdx.MyTT import (
|
|||||||
EMA,
|
EMA,
|
||||||
EMV,
|
EMV,
|
||||||
FSL,
|
FSL,
|
||||||
HHV,
|
|
||||||
KDJ,
|
KDJ,
|
||||||
KTN,
|
KTN,
|
||||||
MA,
|
MA,
|
||||||
@@ -39,7 +38,6 @@ from easy_tdx.MyTT import (
|
|||||||
TAQ,
|
TAQ,
|
||||||
TRIX,
|
TRIX,
|
||||||
WR,
|
WR,
|
||||||
ZIG,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__: list[str] = [] # 注册副作用即可,无需导出符号
|
__all__: list[str] = [] # 注册副作用即可,无需导出符号
|
||||||
@@ -693,113 +691,3 @@ class FslStrategy(ParametrizedStrategy):
|
|||||||
def entry_exit_masks(self) -> tuple[Any, Any]:
|
def entry_exit_masks(self) -> tuple[Any, Any]:
|
||||||
"""与 next() 同源(gold/dead 即 next() 判定用的同一组掩码数组)。"""
|
"""与 next() 同源(gold/dead 即 next() 判定用的同一组掩码数组)。"""
|
||||||
return self.gold, self.dead
|
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)
|
|
||||||
|
|||||||
@@ -98,12 +98,6 @@ STRATEGY_PRESETS: dict[str, dict[str, list[Any]]] = {
|
|||||||
# capital 仅作粗档扫描(1千万/1亿/10亿股),覆盖小盘→大盘
|
# capital 仅作粗档扫描(1千万/1亿/10亿股),覆盖小盘→大盘
|
||||||
"capital": [1e7, 1e8, 1e9, 1e10],
|
"capital": [1e7, 1e8, 1e9, 1e10],
|
||||||
}, # 4
|
}, # 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ easy-tdx backtest SH 600519 --strategy-file strategies/macd_cross.py --cash 5000
|
|||||||
| `bias_reversal.py` | 乖离率反转 | 反转 | 震荡回归 |
|
| `bias_reversal.py` | 乖离率反转 | 反转 | 震荡回归 |
|
||||||
| `volume_price.py` | 量价配合 | 综合判断 | 放量突破 |
|
| `volume_price.py` | 量价配合 | 综合判断 | 放量突破 |
|
||||||
| `obv_trend.py` | OBV 能量潮趋势 | 量价趋势 | 资金持续流入的上升趋势 |
|
| `obv_trend.py` | OBV 能量潮趋势 | 量价趋势 | 资金持续流入的上升趋势 |
|
||||||
| `zig_breakout.py` | ZIG 右侧突破回补 | 右侧突破/波段 | 波谷启动建仓,见顶卖出后右侧突破前高回补(带硬止损;ZIG 为未来函数,回测信号有前视性) |
|
|
||||||
|
|
||||||
## 编写自定义策略
|
## 编写自定义策略
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -336,19 +336,6 @@
|
|||||||
"ulcer_index": 0.0,
|
"ulcer_index": 0.0,
|
||||||
"var_95": -0.0,
|
"var_95": -0.0,
|
||||||
"win_rate": 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,18 +302,12 @@ def test_auto_falls_back_on_mask_shape_mismatch() -> None:
|
|||||||
def test_vector_path_actually_used_for_builtins() -> None:
|
def test_vector_path_actually_used_for_builtins() -> None:
|
||||||
"""默认 signal_path='auto' 下内置策略确实走了向量化(防止回退被掩盖)。
|
"""默认 signal_path='auto' 下内置策略确实走了向量化(防止回退被掩盖)。
|
||||||
|
|
||||||
例外白名单:信号依赖路径状态(无法用静态掩码等价表达)的策略,
|
若某策略信号依赖路径状态(无法用静态掩码等价表达),应在此说明并
|
||||||
引擎对它们走逐 bar 回放(与 next() 完全一致),属设计而非回退。
|
考虑引擎走逐 bar 回放的白名单机制(当前无此类策略)。
|
||||||
"""
|
"""
|
||||||
from easy_tdx.backtest.strategy import Strategy as Base
|
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():
|
for name in get_registry().names():
|
||||||
if name in path_dependent:
|
|
||||||
continue
|
|
||||||
strat_cls = get_registry().get(name).strategy_cls
|
strat_cls = get_registry().get(name).strategy_cls
|
||||||
assert strat_cls.entry_exit_masks is not Base.entry_exit_masks, (
|
assert strat_cls.entry_exit_masks is not Base.entry_exit_masks, (
|
||||||
f"{name} 未实现 entry_exit_masks,auto 将永远走逐 bar"
|
f"{name} 未实现 entry_exit_masks,auto 将永远走逐 bar"
|
||||||
|
|||||||
@@ -31,18 +31,18 @@ class TestLlmHistoryStore:
|
|||||||
def test_context_roundtrip(self, store):
|
def test_context_roundtrip(self, store):
|
||||||
store.add(
|
store.add(
|
||||||
_rec(
|
_rec(
|
||||||
strategy="zig_breakout",
|
strategy="macd",
|
||||||
strategy_label="ZIG 右侧突破回补",
|
strategy_label="MACD 金叉",
|
||||||
symbol="600519",
|
symbol="600519",
|
||||||
category="DAY",
|
category="DAY",
|
||||||
params={"zig_delta": 5.0, "confirm_pct": 2.0},
|
params={"short": 12, "long": 26},
|
||||||
start_date="2024-01-01",
|
start_date="2024-01-01",
|
||||||
end_date="2025-01-01",
|
end_date="2025-01-01",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
it = store.list_all()[0]
|
it = store.list_all()[0]
|
||||||
assert it.strategy == "zig_breakout" and it.symbol == "600519"
|
assert it.strategy == "macd" and it.symbol == "600519"
|
||||||
assert it.params == {"zig_delta": 5.0, "confirm_pct": 2.0} # JSON 往返保真
|
assert it.params == {"short": 12, "long": 26} # JSON 往返保真
|
||||||
assert it.start_date == "2024-01-01"
|
assert it.start_date == "2024-01-01"
|
||||||
|
|
||||||
def test_corrupt_params_json_tolerated(self, store, tmp_path):
|
def test_corrupt_params_json_tolerated(self, store, tmp_path):
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user