Files
qlib/backend/app/api/research.py
T
Simon 0d05bfd187 feat(factor): C1 因子相关性分析(横截面 Spearman 矩阵 + API)
- 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 通过
2026-09-09 07:31:49 +08:00

99 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""研究执行 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