"""研究执行 API:/api/factor-tests 与 /api/backtests。 Phase 3 为同步执行(样本有限);Phase 4 将改为 Job + SSE 异步(接口契约不变)。 最近一次结果在内存中可读,便于前端展示;持久化实验归档在 Phase 4。 """ from __future__ import annotations from fastapi import APIRouter, HTTPException from app.api.deps import ResearchServiceDep from app.domain.entities.research import ( BacktestResult, FactorTestReport, ResearchSpec, ) from app.quant.factors import FactorError router = APIRouter(tags=["research"]) # 内存中的最近结果(Phase 4 迁移到 Experiment 表) _LAST_BACKTEST: dict[str, BacktestResult] = {} _LAST_FACTOR_TEST: dict[str, FactorTestReport] = {} @router.post("/factor-tests", response_model=FactorTestReport, summary="运行单因子测试(同步)") def run_factor_test( spec: ResearchSpec, service: ResearchServiceDep, ) -> FactorTestReport: try: report = service.run_factor_test(spec) except (ValueError, FactorError) as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc _LAST_FACTOR_TEST["default"] = report return report @router.post("/backtests", response_model=BacktestResult, summary="运行回测(同步)") def run_backtest( spec: ResearchSpec, service: ResearchServiceDep, ) -> BacktestResult: try: result = service.run_backtest(spec) except (ValueError, FactorError) as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc _LAST_BACKTEST["default"] = result return result @router.get("/backtests/last", response_model=BacktestResult, summary="最近一次回测结果") def last_backtest() -> BacktestResult: if "default" not in _LAST_BACKTEST: raise HTTPException(status_code=404, detail="尚无回测结果,请先 POST /api/backtests") return _LAST_BACKTEST["default"]