mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
feat(backtest): 参数网格寻优(optimizer + 前端寻优页)
对单个策略的 1-2 个参数做网格搜索,遍历用户指定的取值列表笛卡尔积, 每个组合跑一次回测,按 total_return 排序,返回排名表 + 热力图。 后端: - ParamGridOptimizer(backtest/optimizer.py):itertools.product 遍历网格, 每点 entry.build(params) + BacktestEngine.run(df),复用同一 DataFrame - 网格大小上限 200 防组合爆炸,单点失败容错(跳过不中断) - 2 参数时生成热力图矩阵(x/y 轴取值 + cell 收益率) - POST /backtest/optimize/run/async 端点(后台任务) - OptimizeBacktestRequest schema(param_grid 1-2 参数) 前端(/optimize 寻优页): - ParamGridPicker:勾选 1-2 个寻优参数,逗号分隔填取值列表 - OptimizeResultTable:网格点排名表(按收益降序,最优高亮) - OptimizeHeatmap:2 参数热力图(ECharts heatmap,绿→红映射收益) - 最优点「查看」按钮跳转单标的页用该参数回测 测试:821 passed(+10 寻优器单测 + 3 寻优路由测试)
This commit is contained in:
@@ -807,3 +807,93 @@ def test_portfolio_backtest_bad_strategy(client, monkeypatch):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert final["status"] == "failed"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 4: 参数网格寻优路由
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_optimize_request_validation():
|
||||
"""寻优请求校验:param_grid 非空、至少 1 个数据源。"""
|
||||
from easy_tdx.web.backtest_schemas import OptimizeBacktestRequest
|
||||
|
||||
one_bar = [
|
||||
{
|
||||
"datetime": "2024-01-01",
|
||||
"open": 1,
|
||||
"high": 1,
|
||||
"low": 1,
|
||||
"close": 1,
|
||||
"vol": 1,
|
||||
"amount": 1,
|
||||
}
|
||||
]
|
||||
req = OptimizeBacktestRequest(
|
||||
strategy="ma_cross",
|
||||
param_grid={"fast": [5, 10], "slow": [20, 30]},
|
||||
ohlcv=one_bar,
|
||||
)
|
||||
assert len(req.param_grid) == 2
|
||||
with pytest.raises(ValueError):
|
||||
OptimizeBacktestRequest(strategy="ma_cross", param_grid={"fast": [5]}) # 缺数据源
|
||||
with pytest.raises(ValueError):
|
||||
OptimizeBacktestRequest( # param_grid > 2 参数
|
||||
strategy="ma_cross",
|
||||
param_grid={"a": [1], "b": [2], "c": [3]},
|
||||
ohlcv=one_bar,
|
||||
)
|
||||
|
||||
|
||||
def test_optimize_endpoint(client, sample_ohlcv):
|
||||
"""POST /backtest/optimize/run/async 端到端(内联数据)。"""
|
||||
resp = client.post(
|
||||
"/api/v1/backtest/optimize/run/async",
|
||||
json={
|
||||
"strategy": "ma_cross",
|
||||
"param_grid": {"fast": [5, 10], "slow": [20, 30]},
|
||||
"cash": 100000,
|
||||
"ohlcv": sample_ohlcv,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 202, resp.text
|
||||
task_id = resp.json()["task_id"]
|
||||
|
||||
final = None
|
||||
for _ in range(200):
|
||||
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"]
|
||||
assert result["strategy"] == "ma_cross"
|
||||
assert result["param_names"] == ["fast", "slow"]
|
||||
assert len(result["results"]) == 4
|
||||
assert result["best"] is not None
|
||||
assert result["heatmap"] is not None
|
||||
assert len(result["heatmap"]["data"]) == 4
|
||||
|
||||
|
||||
def test_optimize_single_param_no_heatmap(client, sample_ohlcv):
|
||||
"""单参数寻优不应返回热力图。"""
|
||||
resp = client.post(
|
||||
"/api/v1/backtest/optimize/run/async",
|
||||
json={
|
||||
"strategy": "rsi_reversal",
|
||||
"param_grid": {"n": [7, 14, 21]},
|
||||
"ohlcv": sample_ohlcv,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 202
|
||||
task_id = resp.json()["task_id"]
|
||||
for _ in range(200):
|
||||
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"
|
||||
assert final["result"]["heatmap"] is None
|
||||
|
||||
Reference in New Issue
Block a user