feat(api): 回测结果API加relative_metrics+基准曲线/风险序列/持仓/日志4端点

This commit is contained in:
2026-07-11 13:48:23 +08:00
parent b9197a8889
commit 304844903c
3 changed files with 493 additions and 1 deletions
+116 -1
View File
@@ -62,6 +62,10 @@ def login(req: LoginRequest):
@router.post("/backtest/cta", dependencies=[Depends(verify_token)])
async def submit_cta(req: CtaBacktestRequest):
"""Submit CTA backtest task"""
# Validate benchmark parameter
if req.benchmark not in ("hs300", "zz500"):
raise HTTPException(status_code=422, detail=f"Invalid benchmark: {req.benchmark}. Must be 'hs300' or 'zz500'")
cls = get_strategy_class(req.strategy)
if cls is None:
raise HTTPException(status_code=400, detail=f"未知策略: {req.strategy}")
@@ -130,9 +134,21 @@ def get_result(task_id: str):
r = get_orchestrator().get_result(task_id)
if r is None:
raise HTTPException(status_code=404, detail="result not ready")
# Extract relative metrics from statistics
relative_metrics = {}
relative_fields = [
"alpha", "beta", "sortino_ratio", "information_ratio",
"annual_volatility", "benchmark_return", "benchmark_volatility"
]
for field in relative_fields:
if field in r.statistics:
relative_metrics[field] = r.statistics[field]
return {
"task_id": task_id,
"statistics": r.statistics,
"relative_metrics": relative_metrics,
"symbol": r.symbol,
"start": r.start,
"end": r.end,
@@ -306,4 +322,103 @@ def optimization_results(task_id: str):
"params": getattr(r, "params", {}),
"statistics": getattr(r, "statistics", {}),
})
return {"task_id": task_id, "results": rows}
return {"task_id": task_id, "results": rows}
# ===== Task 4: Backtest result API extensions =====
def _get_metrics_file_path(task_id: str) -> str | None:
"""Get the metrics file path for a task_id from orchestrator config."""
orch = get_orchestrator()
if hasattr(orch, 'db_path') and orch.db_path:
file_dir = os.path.dirname(os.path.abspath(orch.db_path))
metrics_file = os.path.join(file_dir, f"{task_id}_metrics.json")
if os.path.exists(metrics_file):
return metrics_file
return None
@router.get("/task/{task_id}/benchmark-curve", dependencies=[Depends(verify_token)])
def benchmark_curve(task_id: str):
"""Get benchmark curve data (strategy vs benchmark)."""
metrics_file = _get_metrics_file_path(task_id)
if not metrics_file:
raise HTTPException(status_code=404, detail="metrics file not found")
import json
with open(metrics_file, 'r') as f:
data = json.load(f)
series = data.get("series", {})
equity_curve = series.get("equity_curve", {})
benchmark_curve = series.get("benchmark_curve", {})
return {
"dates": equity_curve.get("dates", []),
"strategy": equity_curve.get("values", []),
"benchmark": benchmark_curve.get("values", [])
}
@router.get("/task/{task_id}/risk-series", dependencies=[Depends(verify_token)])
def risk_series(task_id: str):
"""Get risk series data (alpha, beta, drawdown)."""
metrics_file = _get_metrics_file_path(task_id)
if not metrics_file:
raise HTTPException(status_code=404, detail="metrics file not found")
import json
with open(metrics_file, 'r') as f:
data = json.load(f)
series = data.get("series", {})
alpha = series.get("alpha", {})
beta = series.get("beta", {})
drawdown = series.get("drawdown", {})
return {
"dates": alpha.get("dates", []),
"alpha": alpha.get("values", []),
"beta": beta.get("values", []),
"drawdown": drawdown.get("values", [])
}
@router.get("/task/{task_id}/daily-holdings", dependencies=[Depends(verify_token)])
def daily_holdings(task_id: str):
"""Get daily holdings data from equity curve."""
r = get_orchestrator().get_result(task_id)
if r is None:
raise HTTPException(status_code=404, detail="result not ready")
ec = r.equity_curve
if ec is None or (hasattr(ec, "empty") and ec.empty):
return {"task_id": task_id, "daily_holdings": []}
# Return balance and return data as holdings
holdings = []
for _, row in ec.iterrows():
holding = {"date": str(row.get("date", ""))}
if "balance" in row:
holding["balance"] = float(row["balance"])
if "return" in row:
holding["return"] = float(row["return"])
holdings.append(holding)
return {"task_id": task_id, "daily_holdings": holdings}
@router.get("/task/{task_id}/log", dependencies=[Depends(verify_token)])
def log_endpoint(task_id: str):
"""Get backtest log text."""
orch = get_orchestrator()
if hasattr(orch, 'db_path') and orch.db_path:
file_dir = os.path.dirname(os.path.abspath(orch.db_path))
log_file = os.path.join(file_dir, f"{task_id}.log")
if os.path.exists(log_file):
with open(log_file, 'r') as f:
log_content = f.read()
return {"task_id": task_id, "log": log_content}
# Return empty log if file doesn't exist
return {"task_id": task_id, "log": ""}