release: v1.30.1 — 移除 ZIG 未来函数指标与 zig_breakout 策略:回测数字可信优先

This commit is contained in:
Justin Gu
2026-09-03 02:02:19 +08:00
parent 1e95bab476
commit 1433f4eed0
14 changed files with 21 additions and 508 deletions
+2 -8
View File
@@ -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_masksauto 将永远走逐 bar"
+5 -5
View File
@@ -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):
-66
View File
@@ -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
-97
View File
@@ -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
# 锯齿行情应至少出现一次 BUYtrades 为 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