mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
release: v1.30.1 — 移除 ZIG 未来函数指标与 zig_breakout 策略:回测数字可信优先
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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