feat(api): 回测结果接口(strategy list/params + equity-curve/daily-pnl/trades + kline)
- strategy_registry 枚举 vnpy_ctastrategy 策略(兜底 STRATEGY_NAMES)
- /strategy/list、/strategy/{name}/params
- /task/{id}/equity-curve、/daily-pnl、/trades(BacktestResult JSON 化)
- /kline(read_db_daily 历史 K 线)
- 9 tests passed(4 strategy_registry + 5 routes)
This commit is contained in:
+70
-1
@@ -6,6 +6,8 @@ from pydantic import BaseModel
|
||||
from .schemas import CtaBacktestRequest, OptimizeRequest, FactorAnalysisRequest
|
||||
from .auth import verify_token as verify_token_impl, verify_password, create_token
|
||||
from .ws import manager
|
||||
from .strategy_registry import list_strategies, strategy_params
|
||||
from .kline import load_kline
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
@@ -138,4 +140,71 @@ async def task_ws(websocket: WebSocket, task_id: str, token: str = Query(...)):
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
manager.disconnect(task_id, websocket)
|
||||
manager.disconnect(task_id, websocket)
|
||||
|
||||
|
||||
# ===== Backtest UI support endpoints (S1.4 / S1.5) =====
|
||||
|
||||
def _df_to_records(df) -> list[dict]:
|
||||
"""DataFrame → list[dict] (empty-safe)."""
|
||||
if df is None:
|
||||
return []
|
||||
if hasattr(df, "empty") and df.empty:
|
||||
return []
|
||||
if hasattr(df, "to_dict"):
|
||||
return df.to_dict(orient="records")
|
||||
return list(df)
|
||||
|
||||
|
||||
@router.get("/strategy/list", dependencies=[Depends(verify_token)])
|
||||
def strategy_list():
|
||||
"""List available CTA strategies for the UI dropdown."""
|
||||
return {"strategies": list_strategies()}
|
||||
|
||||
|
||||
@router.get("/strategy/{name}/params", dependencies=[Depends(verify_token)])
|
||||
def strategy_params_route(name: str):
|
||||
"""Strategy parameters + defaults for the dynamic form."""
|
||||
return strategy_params(name)
|
||||
|
||||
|
||||
@router.get("/task/{task_id}/equity-curve", dependencies=[Depends(verify_token)])
|
||||
def equity_curve(task_id: str):
|
||||
r = get_orchestrator().get_result(task_id)
|
||||
if r is None:
|
||||
raise HTTPException(status_code=404, detail="result not ready")
|
||||
return {"task_id": task_id, "equity_curve": _df_to_records(r.equity_curve)}
|
||||
|
||||
|
||||
@router.get("/task/{task_id}/daily-pnl", dependencies=[Depends(verify_token)])
|
||||
def daily_pnl(task_id: str):
|
||||
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) or "balance" not in ec.columns:
|
||||
return {"task_id": task_id, "daily_pnl": []}
|
||||
import pandas as pd
|
||||
bal = pd.to_numeric(ec["balance"], errors="coerce")
|
||||
pnl = bal.diff().fillna(0.0)
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"daily_pnl": [{"date": str(d), "pnl": float(p)} for d, p in zip(ec["date"], pnl)],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/task/{task_id}/trades", dependencies=[Depends(verify_token)])
|
||||
def trades_route(task_id: str):
|
||||
r = get_orchestrator().get_result(task_id)
|
||||
if r is None:
|
||||
raise HTTPException(status_code=404, detail="result not ready")
|
||||
return {"task_id": task_id, "trades": _df_to_records(r.trades)}
|
||||
|
||||
|
||||
@router.get("/kline", dependencies=[Depends(verify_token)])
|
||||
def kline(symbol: str, start: str, end: str):
|
||||
"""Historical daily K-line for the backtest chart."""
|
||||
try:
|
||||
return {"symbol": symbol, "kline": load_kline(symbol, start, end)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"kline load failed: {type(e).__name__}: {e}")
|
||||
Reference in New Issue
Block a user