mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 23:54:21 +08:00
feat(backtest): 成交价精简 + 一键寻优全策略 + 预设网格
- 成交价精简为 next_open/next_close,删除 this_close/worst/best 三种 非真实模式(this_close 有未来函数偏差,worst/best 为压力测试边界) - 初始资金默认统一为 1,000,000(原 10万/20万) - 新增 presets.py:18 策略各配 1-2 参数的预设寻优网格(笛卡尔积≤200) - registry.to_schema() 返回 preset_grid 字段供前端自动填充 - 新增 POST /backtest/optimize-all/run/async:逐策略预设网格寻优 + 全局排名(OptimizeAllResult:ranking/best/per_strategy/total_grid_points) - 新增 optimize-all 端到端单测 2 例(838 单测全绿)
This commit is contained in:
@@ -185,17 +185,6 @@ def test_chanlun_injection():
|
||||
assert len(result.trades) >= 1
|
||||
|
||||
|
||||
def test_this_close_warning_in_config():
|
||||
"""Test future_leak_warning in config when using this_close."""
|
||||
df = _make_df(n=50)
|
||||
engine = BacktestEngine(MACrossStrategy, execution="this_close")
|
||||
result = engine.run(df)
|
||||
|
||||
# Config should have future_leak_warning
|
||||
# Note: MACrossStrategy may not generate signals, so warning might be False
|
||||
assert "future_leak_warning" in result.config
|
||||
|
||||
|
||||
def test_config_snapshot():
|
||||
"""Test config contains correct cash and commission."""
|
||||
df = _make_df(n=50)
|
||||
|
||||
@@ -77,74 +77,6 @@ class TestExecutionModes:
|
||||
assert len(trades) == 1
|
||||
assert trades[0].price == 102.0 # df["close"].iloc[1]
|
||||
|
||||
def test_this_close(self) -> None:
|
||||
"""this_close: 当前K线的收盘价。"""
|
||||
df = _make_df(10)
|
||||
sim = OrderSimulator(df, execution="this_close")
|
||||
|
||||
signals = [_buy_signal(0, size=100)]
|
||||
trades = sim.simulate(signals, cash=20000, position=0)
|
||||
|
||||
assert len(trades) == 1
|
||||
assert trades[0].price == 101.0 # df["close"].iloc[0]
|
||||
|
||||
def test_this_close_future_leak_warning(self) -> None:
|
||||
"""this_close 模式应设置 future_leak_warning 标志。"""
|
||||
df = _make_df(10)
|
||||
sim = OrderSimulator(df, execution="this_close")
|
||||
|
||||
assert sim.future_leak_warning is False
|
||||
|
||||
# 执行模拟后应设置标志
|
||||
signals = [_buy_signal(0, size=100)]
|
||||
sim.simulate(signals, cash=20000, position=0)
|
||||
|
||||
assert sim.future_leak_warning is True
|
||||
|
||||
def test_worst_price_buy(self) -> None:
|
||||
"""worst: 买入取最高价。"""
|
||||
df = _make_df(10)
|
||||
sim = OrderSimulator(df, execution="worst")
|
||||
|
||||
signals = [_buy_signal(0, size=100)]
|
||||
trades = sim.simulate(signals, cash=20000, position=0)
|
||||
|
||||
assert len(trades) == 1
|
||||
assert trades[0].price == 103.0 # df["high"].iloc[1]
|
||||
|
||||
def test_worst_price_sell(self) -> None:
|
||||
"""worst: 卖出取最低价。"""
|
||||
df = _make_df(10)
|
||||
sim = OrderSimulator(df, execution="worst")
|
||||
|
||||
signals = [_sell_signal(0, size=100)]
|
||||
trades = sim.simulate(signals, cash=0, position=200)
|
||||
|
||||
assert len(trades) == 1
|
||||
assert trades[0].price == 100.0 # df["low"].iloc[1]
|
||||
|
||||
def test_best_price_buy(self) -> None:
|
||||
"""best: 买入取最低价。"""
|
||||
df = _make_df(10)
|
||||
sim = OrderSimulator(df, execution="best")
|
||||
|
||||
signals = [_buy_signal(0, size=100)]
|
||||
trades = sim.simulate(signals, cash=20000, position=0)
|
||||
|
||||
assert len(trades) == 1
|
||||
assert trades[0].price == 100.0 # df["low"].iloc[1]
|
||||
|
||||
def test_best_price_sell(self) -> None:
|
||||
"""best: 卖出取最高价。"""
|
||||
df = _make_df(10)
|
||||
sim = OrderSimulator(df, execution="best")
|
||||
|
||||
signals = [_sell_signal(0, size=100)]
|
||||
trades = sim.simulate(signals, cash=0, position=200)
|
||||
|
||||
assert len(trades) == 1
|
||||
assert trades[0].price == 103.0 # df["high"].iloc[1]
|
||||
|
||||
|
||||
# ── Test Position Modes ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -489,16 +421,3 @@ class TestNonContinuousIndex:
|
||||
# position 1 的 open = 101.0;旧代码会用 label 10 当位置 → iloc[10] 越界
|
||||
assert trades[0].price == 101.0
|
||||
assert trades[0].rejected is False
|
||||
|
||||
def test_this_close_with_non_continuous_index(self) -> None:
|
||||
"""this_close 模式下信号在 bar 2(label=30),应在同根 close 成交。"""
|
||||
df = _make_df(10)
|
||||
df.index = [10 * (i + 1) for i in range(len(df))]
|
||||
sim = OrderSimulator(df, execution="this_close")
|
||||
|
||||
signals = [_buy_signal(2, size=100)]
|
||||
trades = sim.simulate(signals, cash=20000, position=0)
|
||||
|
||||
assert len(trades) == 1
|
||||
# position 2 的 close = 103.0
|
||||
assert trades[0].price == 103.0
|
||||
|
||||
@@ -191,7 +191,7 @@ def test_backtest_request_defaults():
|
||||
from easy_tdx.web.backtest_schemas import BacktestRequest
|
||||
|
||||
req = BacktestRequest(strategy="ma_cross", symbol="SZ:000001")
|
||||
assert req.cash == 100000.0
|
||||
assert req.cash == 1_000_000.0
|
||||
assert req.commission == 0.0003
|
||||
assert req.execution == "next_open"
|
||||
assert req.category == "DAY"
|
||||
@@ -704,7 +704,7 @@ def test_portfolio_request_defaults():
|
||||
from easy_tdx.web.backtest_schemas import PortfolioBacktestRequest
|
||||
|
||||
req = PortfolioBacktestRequest(strategy="ma_cross", stocks=["SZ:000001"])
|
||||
assert req.cash == 200000.0
|
||||
assert req.cash == 1_000_000.0
|
||||
assert req.category == "DAY"
|
||||
|
||||
|
||||
@@ -899,6 +899,55 @@ def test_optimize_single_param_no_heatmap(client, sample_ohlcv):
|
||||
assert final["result"]["heatmap"] is None
|
||||
|
||||
|
||||
def test_optimize_all_endpoint(client, sample_ohlcv):
|
||||
"""POST /backtest/optimize-all/run/async 端到端:逐策略预设网格寻优 + 全局排名。"""
|
||||
resp = client.post(
|
||||
"/api/v1/backtest/optimize-all/run/async",
|
||||
json={
|
||||
"cash": 1_000_000,
|
||||
"ohlcv": sample_ohlcv,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 202, resp.text
|
||||
task_id = resp.json()["task_id"]
|
||||
|
||||
final = None
|
||||
for _ in range(400):
|
||||
poll = client.get(f"/api/v1/backtest/tasks/{task_id}")
|
||||
final = poll.json()
|
||||
if final["status"] in ("done", "failed"):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
assert final["status"] == "done", final
|
||||
result = final["result"]
|
||||
# 排名按 total_return 降序、best 指向第一名、各策略最优点齐全
|
||||
assert "ranking" in result and len(result["ranking"]) > 0
|
||||
assert "best" in result and result["best"] is not None
|
||||
assert "per_strategy" in result and len(result["per_strategy"]) == len(result["ranking"])
|
||||
assert "total_grid_points" in result and result["total_grid_points"] > 0
|
||||
# ranking 降序校验
|
||||
returns = [r["total_return"] for r in result["ranking"]]
|
||||
assert returns == sorted(returns, reverse=True)
|
||||
# best == ranking[0]
|
||||
assert result["best"]["strategy"] == result["ranking"][0]["strategy"]
|
||||
# 合计网格点 == 各策略 grid_points 之和
|
||||
assert result["total_grid_points"] == sum(r["grid_points"] for r in result["ranking"])
|
||||
|
||||
|
||||
def test_optimize_all_request_validation():
|
||||
"""optimize-all 请求必须提供数据源。"""
|
||||
from easy_tdx.web.backtest_schemas import OptimizeAllBacktestRequest
|
||||
|
||||
# 缺数据源
|
||||
with pytest.raises(ValueError):
|
||||
OptimizeAllBacktestRequest()
|
||||
# 合法
|
||||
req = OptimizeAllBacktestRequest(symbol="SZ:000001")
|
||||
assert req.cash == 1_000_000.0
|
||||
assert req.execution == "next_open"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5: 任务列表端点(对比页用)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user