feat(replay): M9-6 Bar Replay 线性重放(as_of 逐日仅用当时数据)

- domain/entities/replay.py:ReplayDay{top/events/counts}/ReplayResult 时间线
- ReplayService:universe.symbols 白名单必填(≤40)且重放交易日 ≤90(防全市场长任务);
  每个交易日以 <=as_of 数据经同一 signal/score 引擎生成帧
- POST /api/replays(边界校验)→ 时间线;供前端 Bar Replay 控件(v3 §20.6 阶段二)
- tests/test_replays.py:重放帧 == 回测 selection_history 逐调仓日一致;范围约束;
  后段暴涨股不泄漏进早段帧(未来函数);API 400/200;全量 pytest 通过
This commit is contained in:
Simon
2026-09-09 07:18:36 +08:00
parent 5bde8f9f5f
commit 37510c1b89
6 changed files with 327 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Bar Replay API(M9-6):POST /api/replays —— 线性重放选股/信号时间线。"""
from __future__ import annotations
from datetime import date
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from app.api.deps import ReplayServiceDep
from app.domain.entities.replay import ReplayResult
from app.domain.entities.selection import SelectionQuery
from app.domain.entities.signal import SignalRules
router = APIRouter(prefix="/replays", tags=["replays"])
class ReplayRequest(BaseModel):
query: SelectionQuery
rules: SignalRules = SignalRules()
start: date
end: date
top_n: int = Field(default=5, ge=1, le=20)
@router.post("", response_model=ReplayResult, summary="线性重放(as_of 逐日,仅用当时数据)")
def run_replay(req: ReplayRequest, service: ReplayServiceDep) -> ReplayResult:
if req.start >= req.end:
raise HTTPException(status_code=400, detail="start 必须早于 end")
try:
return service.replay(req.query, req.rules, req.start, req.end, req.top_n)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc