mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
参数排名从单窗样本内得分改为 K 段折外稳健分 R = 0.6×中位 + 0.4×最差 (设计: docs/robust-optimizer-plan.md), 四阶段全部落地: - guards 共享护栏: 主目标白名单 + 最小交易数门槛 (fail-closed, 缺交易数 按不达标处理), 两模式一处实现 - robust_optimizer: 等分切窗 (2-6 段, 每段≥14天) / 稳健分 / 冠军收敛诊断 (unanimous/majority/scattered) / Top-20 全窗 IS 细化与退化率 / 高原判据 (邻域得分衰减, 零额外回测); worker 接线 robust_optimize 任务 - 滚动样本外检验补强: 每折 OOS 最小交易数门槛 (无效折透明跳过), 前端折预览 / 参数漂移轨迹 / 采用最新折参数 - 前端: 冠军卡 (收敛/高原徽章/乐观偏差) + K 段排名表 + 验证窗预览 + 应用参数闭环 (POST /strategies/config, 可逆); SSE 重连与跨设备接回 - P4 摒弃旧路径: 删 /optimize/stream 与 /optimize/cancel 端点及 worker optimize 任务类型; 白名单全面生效 (optimizer.VALID_OBJECTIVES 收敛到 guards; walkforward 端点校验退化目标 400) - 附带修复: 叠加条件×矩阵路径 SignalMatrix 只读违规 (np.where 产物 可写), optimizer_trial 轻量统计恒保留 n_trades 实测: 126 组合全市场网格 1.71× 旧版耗时 (视图复用优化自 2.01×; 差额 为引擎层每 run 固定开销, 详见方案文档第九节); 全量回归 3131 通过。 api.ts 同时带入稳健优化折预览客户端与声效语音包 API 客户端 (语音包功能随后续提交落地界面)。 Signed-off-by: shy3130 <shy3130@qq.com>
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
"""回测 SSE 端点的 start/end 入参校验 — 非法日期返回 400 而不是 500。
|
|
|
|
signals.py 的 `/intraday/replay` 与 mining.py 的 `MiningRunRequest` 都把
|
|
`date.fromisoformat` 包在校验里, 非法日期给出 400/422; backtest 的三个 SSE
|
|
端点直接裸调 `date.fromisoformat`, 同样的入参会抛未捕获 ValueError。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.api.backtest import router
|
|
from app.config import settings
|
|
|
|
# (路径, 该端点的必填参数)
|
|
STREAMS = [
|
|
("/api/backtest/strategy/stream", {"strategy_id": "ma_cross"}),
|
|
# 旧 /optimize/stream 单窗样本内路径已于 P4 移除, 由稳健优化接替
|
|
("/api/backtest/optimize-robust/stream", {"strategy_id": "ma_cross", "param_grid": '{"n": [5]}'}),
|
|
("/api/backtest/walkforward/stream", {"strategy_id": "ma_cross", "param_grid": '{"n": [5]}'}),
|
|
]
|
|
STREAM_IDS = [path.rsplit("/", 2)[1] for path, _ in STREAMS]
|
|
|
|
BAD_DATES = ["not-a-date", "2026-13-01", "2026/09/04", ""]
|
|
|
|
|
|
@pytest.fixture()
|
|
def client() -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
# raise_server_exceptions=False: 未捕获异常表现为 500 响应, 与线上行为一致
|
|
return TestClient(app, raise_server_exceptions=False)
|
|
|
|
|
|
@pytest.mark.parametrize("path,extra", STREAMS, ids=STREAM_IDS)
|
|
@pytest.mark.parametrize("bad", BAD_DATES)
|
|
def test_malformed_end_returns_400(client, path, extra, bad):
|
|
if not bad:
|
|
pytest.skip("空串走默认值分支, 不属于非法日期")
|
|
resp = client.get(path, params={**extra, "end": bad})
|
|
assert resp.status_code == 400, resp.text
|
|
assert "日期" in resp.json()["detail"]
|
|
|
|
|
|
@pytest.mark.parametrize("path,extra", STREAMS, ids=STREAM_IDS)
|
|
@pytest.mark.parametrize("bad", BAD_DATES)
|
|
def test_malformed_start_returns_400(client, path, extra, bad):
|
|
if not bad:
|
|
pytest.skip("空串走默认值分支, 不属于非法日期")
|
|
resp = client.get(path, params={**extra, "start": bad, "end": "2026-09-04"})
|
|
assert resp.status_code == 400, resp.text
|
|
assert "日期" in resp.json()["detail"]
|
|
|
|
|
|
@pytest.mark.parametrize("path,extra", STREAMS, ids=STREAM_IDS)
|
|
def test_valid_dates_still_accepted(client, monkeypatch, path, extra):
|
|
"""合法日期必须照旧进入事件流, 证明这不是把入口收窄成"什么都不收"。
|
|
|
|
打开服务端范围保护并给一个超阈值的窗口, 事件流会立刻以 error 事件收尾,
|
|
既不触发真实回测, 又能证明日期解析已经放行。
|
|
"""
|
|
monkeypatch.setattr(settings, "backtest_range_guard", True)
|
|
params = {**extra, "start": "2020-01-01", "end": "2026-09-04"}
|
|
if "walkforward" in path:
|
|
# walkforward 的 guard 作用于单折窗口, 不是总区间
|
|
params.update({"train_days": 400, "test_days": 400})
|
|
resp = client.get(path, params=params)
|
|
assert resp.status_code == 200, resp.text
|
|
assert "event: error" in resp.text
|