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,109 @@
|
||||
"""回测参数校验(2026-08-15:此前结束时间选 2029 也能提交,零校验跑垃圾结果)。
|
||||
|
||||
L1 静态规则零 IO 秒判(格式/先后/未来/区间长度/资金/费率);
|
||||
L2 数据可用性查 dbbardata 锚定标的(600000 日线)最新日期,
|
||||
模块级缓存 24h——只在提交时服务端调用一次,页面加载零开销;
|
||||
查库失败返回 None 自动退化为仅 L1,数据层抖动不挡正常提交。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import time
|
||||
from datetime import date, datetime
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
# 锚定标的:600000 浦发银行,上市以来从未长期停牌,是「数据灌到哪天」的可靠探针
|
||||
_ANCHOR_SYMBOL = "600000"
|
||||
_ANCHOR_EXCHANGE = "SSE"
|
||||
_CACHE_TTL_SEC = 24 * 3600
|
||||
_MIN_SPAN_DAYS = 30
|
||||
_RATE_MAX = 0.01 # 费率上限 1%:填 3 这种是百分数/万分位口径错
|
||||
|
||||
_latest_cache: tuple[float, str] | None = None # (查询时刻, 最新日期)
|
||||
|
||||
|
||||
def _parse(name: str, value: str) -> date:
|
||||
try:
|
||||
return datetime.strptime(value, "%Y-%m-%d").date()
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(400, f"{name}格式应为 YYYY-MM-DD: {value!r}")
|
||||
|
||||
|
||||
def validate_backtest_range(start: str, end: str, min_days: int = _MIN_SPAN_DAYS) -> None:
|
||||
"""L1+L2 区间校验,非法即抛 400(中文业务提示)。合法返回 None。"""
|
||||
s = _parse("开始日期", start)
|
||||
e = _parse("结束日期", end)
|
||||
if s >= e:
|
||||
raise HTTPException(400, f"开始日期({start})必须早于结束日期({end})")
|
||||
if (e - s).days < min_days:
|
||||
raise HTTPException(400, f"区间过短(不足 {min_days} 天),统计无参考意义,请至少选 {min_days} 天")
|
||||
today = date.today().isoformat()
|
||||
if end > today:
|
||||
raise HTTPException(400, f"结束日期 {end} 在未来,请选择历史日期(今天: {today})")
|
||||
latest = get_latest_daily_date()
|
||||
if latest and end > latest:
|
||||
raise HTTPException(400, f"结束日期 {end} 超出数据范围:日线数据最新到 {latest}")
|
||||
|
||||
|
||||
def validate_capital(name: str, value: float) -> None:
|
||||
if value <= 0:
|
||||
raise HTTPException(400, f"{name}需大于 0,当前: {value}")
|
||||
|
||||
|
||||
def validate_rate(name: str, value: float) -> None:
|
||||
if not (0 <= value <= _RATE_MAX):
|
||||
raise HTTPException(400, f"{name}应在 0~{_RATE_MAX:g} 之间(小数,万3=0.0003),当前: {value}")
|
||||
|
||||
|
||||
def validate_cta_request(req) -> None:
|
||||
"""CTA 回测(个股/优化共用):区间 + 资金 + 费率。"""
|
||||
validate_backtest_range(req.start, req.end)
|
||||
validate_capital("初始资金", req.capital)
|
||||
validate_rate("佣金率", req.commission_rate)
|
||||
validate_rate("印花税率", req.stamp_duty_rate)
|
||||
validate_rate("过户费率", req.transfer_fee_rate)
|
||||
validate_rate("滑点", req.slippage)
|
||||
|
||||
|
||||
def validate_portfolio_request(req) -> None:
|
||||
"""组合回测:区间 + 资金 + 费率(字段名与 CTA 不同:initial_cash/start_date)。"""
|
||||
validate_backtest_range(req.start_date, req.end_date)
|
||||
validate_capital("初始资金", req.initial_cash)
|
||||
validate_rate("佣金率", req.commission_rate)
|
||||
validate_rate("印花税率", req.stamp_duty_rate)
|
||||
validate_rate("滑点", req.slippage)
|
||||
|
||||
|
||||
def _query_latest_daily_date() -> str | None:
|
||||
"""直查 dbbardata 锚定标的日线最大日期;任何异常返回 None(退化为仅 L1)。"""
|
||||
try:
|
||||
from sanguo_data.config import load_config, find_config_path
|
||||
|
||||
db_path = load_config(find_config_path()).data_paths.get("vnpy_db")
|
||||
if not db_path:
|
||||
return None
|
||||
conn = sqlite3.connect(db_path, timeout=5)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT MAX(substr(datetime,1,10)) FROM dbbardata "
|
||||
"WHERE symbol=? AND exchange=? AND interval='d'",
|
||||
(_ANCHOR_SYMBOL, _ANCHOR_EXCHANGE),
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
return row[0] if row and row[0] else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_latest_daily_date() -> str | None:
|
||||
"""数据最新交易日(带 24h 缓存)。日线 18 点后更新,无需每次提交都查。"""
|
||||
global _latest_cache
|
||||
now = time.time()
|
||||
if _latest_cache and now - _latest_cache[0] < _CACHE_TTL_SEC:
|
||||
return _latest_cache[1]
|
||||
latest = _query_latest_daily_date()
|
||||
if latest:
|
||||
_latest_cache = (now, latest)
|
||||
return latest
|
||||
Reference in New Issue
Block a user