- evaluation.factor_correlation_report:多因子共同日期 ∩ 后逐日横截面 Spearman 相关 取均值 → FactorCorrelationReport(冗余剔除前置,v3 §12 Correlation→Redundancy) - ResearchService.run_factor_correlation + POST /api/factor-correlations (universe/factors/period;与其它研究同装配口径) - tests/test_factor_correlation.py:矩阵对角=1/近线性±相关符号/对称/无共同日期补零、 API 冒烟;全量 pytest 通过
99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
"""研究执行 API:/api/factor-tests 与 /api/backtests。
|
||
|
||
Phase 3 为同步执行(样本有限);Phase 4 将改为 Job + SSE 异步(接口契约不变)。
|
||
最近一次结果在内存中可读,便于前端展示;持久化实验归档在 Phase 4。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import date
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel, Field
|
||
|
||
from app.api.deps import ResearchServiceDep
|
||
from app.domain.entities.research import (
|
||
BacktestResult,
|
||
FactorCorrelationReport,
|
||
FactorSpec,
|
||
FactorTestReport,
|
||
ResearchSpec,
|
||
SelectionSpec,
|
||
UniverseSpec,
|
||
)
|
||
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"]
|
||
|
||
|
||
class FactorCorrelationRequest(BaseModel):
|
||
"""因子相关性分析请求(v3 §12)。"""
|
||
|
||
universe: UniverseSpec = UniverseSpec()
|
||
factors: list[FactorSpec] = Field(min_length=2)
|
||
period: tuple[date, date]
|
||
price_adjustment: str = Field(default="none", pattern="^(none|qfq)$")
|
||
|
||
|
||
@router.post(
|
||
"/factor-correlations",
|
||
response_model=FactorCorrelationReport,
|
||
summary="多因子两两相关(横截面 Spearman)",
|
||
)
|
||
def run_factor_correlation(
|
||
req: FactorCorrelationRequest,
|
||
service: ResearchServiceDep,
|
||
) -> FactorCorrelationReport:
|
||
if req.period[0] >= req.period[1]:
|
||
raise HTTPException(status_code=400, detail="period 必须满足 start < end")
|
||
spec = ResearchSpec(
|
||
type="factor_test",
|
||
universe=req.universe,
|
||
price_adjustment=req.price_adjustment,
|
||
factors=req.factors,
|
||
selection=SelectionSpec(top_n=10),
|
||
rebalance="monthly",
|
||
period=req.period,
|
||
)
|
||
try:
|
||
return service.run_factor_correlation(spec)
|
||
except (ValueError, FactorError) as exc:
|
||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|