feat(api): 回测参数校验—5提交入口统一拦截(2029结束日期可提交根治)——用户实况portfolio_ef8b655a零校验跑垃圾结果;sanguo_api/validation.py:L1静态(格式/先后/未来/区间≥30天/资金/费率0~1%)+L2数据最新日(600000日线锚定MAX(datetime),24h模块缓存,查库失败退化仅L1);接线CTA/组合/优化/因子/模拟盘回放5入口,400中文业务提示;前端5页日期控件禁选未来日期(disableFutureDate共享);+10测试含2029回归,API 115绿,前端build绿 [vps]
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""回测参数校验(L1 静态规则 + L2 数据最新日)单测 + 2029 回归。
|
||||
|
||||
背景:2026-08-15 用户实况——组合回测结束时间选 2029 也能提交
|
||||
(portfolio_ef8b655a),零校验跑出垃圾结果占任务位。
|
||||
"""
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from sanguo_api import validation as V
|
||||
|
||||
|
||||
def _ok_range():
|
||||
return "2024-01-01", "2024-06-30"
|
||||
|
||||
|
||||
# ===== L1 静态规则 =====
|
||||
|
||||
def test_future_end_rejected():
|
||||
"""用户报的 2029 场景:结束日期在未来必须 400。"""
|
||||
with pytest.raises(HTTPException) as e:
|
||||
V.validate_backtest_range("2024-01-01", "2029-12-31")
|
||||
assert e.value.status_code == 400
|
||||
assert "未来" in e.value.detail
|
||||
|
||||
|
||||
def test_bad_format_rejected():
|
||||
with pytest.raises(HTTPException) as e:
|
||||
V.validate_backtest_range("2024/01/01", "2024-06-30")
|
||||
assert e.value.status_code == 400
|
||||
assert "格式" in e.value.detail
|
||||
|
||||
|
||||
def test_start_after_end_rejected():
|
||||
with pytest.raises(HTTPException) as e:
|
||||
V.validate_backtest_range("2024-06-30", "2024-01-01")
|
||||
assert "早于" in e.value.detail
|
||||
|
||||
|
||||
def test_span_too_short_rejected():
|
||||
with pytest.raises(HTTPException) as e:
|
||||
V.validate_backtest_range("2024-01-01", "2024-01-15")
|
||||
assert "过短" in e.value.detail
|
||||
|
||||
|
||||
def test_valid_range_passes():
|
||||
s, t = _ok_range()
|
||||
assert V.validate_backtest_range(s, t) is None
|
||||
|
||||
|
||||
def test_capital_and_rate_checks():
|
||||
with pytest.raises(HTTPException):
|
||||
V.validate_capital("初始资金", 0)
|
||||
with pytest.raises(HTTPException):
|
||||
V.validate_capital("初始资金", -100)
|
||||
assert V.validate_capital("初始资金", 1_000_000) is None
|
||||
# 费率应填小数(万3=0.0003);填成 3 明显是百分数/万分位口径错
|
||||
with pytest.raises(HTTPException):
|
||||
V.validate_rate("佣金率", 3.0)
|
||||
assert V.validate_rate("佣金率", 0.0003) is None
|
||||
|
||||
|
||||
# ===== L2 数据最新日 =====
|
||||
|
||||
def test_end_beyond_latest_data_rejected(monkeypatch):
|
||||
# end 是过去日期(过 L1)但超过 mock 的数据最新日 → L2 拦
|
||||
monkeypatch.setattr(V, "get_latest_daily_date", lambda: "2026-07-31")
|
||||
with pytest.raises(HTTPException) as e:
|
||||
V.validate_backtest_range("2026-01-01", "2026-08-05")
|
||||
assert "数据最新到" in e.value.detail
|
||||
|
||||
|
||||
def test_latest_query_failure_degrades_to_l1(monkeypatch):
|
||||
"""查库失败返回 None → 只做 L1,数据层抖动不挡提交。"""
|
||||
monkeypatch.setattr(V, "get_latest_daily_date", lambda: None)
|
||||
assert V.validate_backtest_range("2026-01-01", "2026-08-05") is None
|
||||
|
||||
|
||||
def test_latest_date_cached(monkeypatch):
|
||||
"""24h 缓存:第二次调用不重复查库。"""
|
||||
calls = []
|
||||
monkeypatch.setattr(V, "_query_latest_daily_date", lambda: calls.append(1) or "2026-08-14")
|
||||
V._latest_cache = None # 清模块缓存
|
||||
assert V.get_latest_daily_date() == "2026-08-14"
|
||||
assert V.get_latest_daily_date() == "2026-08-14"
|
||||
assert len(calls) == 1
|
||||
V._latest_cache = None # 还原,避免污染其他测试
|
||||
|
||||
|
||||
# ===== 入口集成:组合回测 2029 必须进不了队列 =====
|
||||
|
||||
def test_portfolio_submit_2029_rejected(monkeypatch):
|
||||
"""回归:POST /portfolio/backtest end=2029 → 400,不产生任务。"""
|
||||
monkeypatch.setattr(V, "get_latest_daily_date", lambda: "2026-08-14")
|
||||
from sanguo_api.routes_portfolio import PortfolioBacktestRequest
|
||||
from sanguo_api.validation import validate_portfolio_request
|
||||
|
||||
req = PortfolioBacktestRequest(start_date="2024-01-01", end_date="2029-12-31")
|
||||
with pytest.raises(HTTPException) as e:
|
||||
validate_portfolio_request(req)
|
||||
assert e.value.status_code == 400
|
||||
Reference in New Issue
Block a user