fix: backtest submission shows no results — 4 bugs fixed
- ID mismatch: API generated a throwaway UUID while BacktestReplay generated its own internally. Frontend polled with wrong ID and never found the DB row. Now pre-generate ID in endpoint and pass it to BacktestReplay. - Field name: API returned 'backtest_id' but frontend read 'data.id'. Unified to 'id' everywhere. - No polling: useBacktestResult fired once and never refreshed. Added refetchInterval that polls every 2s while status is running. - Response shape: GET endpoint nested results under 'result' object but frontend expected flat fields. Flattened response to match BacktestResult type. - Added running/failed/completed status indicators in BacktestPanel.
This commit is contained in:
+36
-40
@@ -14,7 +14,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Any, Optional
|
||||
|
||||
import asyncpg
|
||||
@@ -490,6 +490,8 @@ async def launch_backtest(body: BacktestRequest) -> dict[str, str]:
|
||||
"""Launch a backtest run and return its ID.
|
||||
|
||||
Task 32.5: Uses BacktestReplay to run the backtest in a background task.
|
||||
The backtest_id is pre-generated and passed to BacktestReplay so the
|
||||
frontend can poll for results using the same ID.
|
||||
"""
|
||||
if engine is None:
|
||||
raise HTTPException(503, "Engine not initialised")
|
||||
@@ -499,6 +501,8 @@ async def launch_backtest(body: BacktestRequest) -> dict[str, str]:
|
||||
from services.trading.backtest_replay import BacktestReplay
|
||||
from services.trading.backtester import BacktestConfig
|
||||
|
||||
backtest_id = str(uuid.uuid4())
|
||||
|
||||
bt_config = BacktestConfig(
|
||||
start_date=date_type.fromisoformat(body.start_date),
|
||||
end_date=date_type.fromisoformat(body.end_date),
|
||||
@@ -512,15 +516,12 @@ async def launch_backtest(body: BacktestRequest) -> dict[str, str]:
|
||||
|
||||
async def _run_backtest():
|
||||
try:
|
||||
await replay.run(bt_config)
|
||||
await replay.run(bt_config, backtest_id=backtest_id)
|
||||
except Exception:
|
||||
logger.exception("Backtest failed")
|
||||
|
||||
asyncio.create_task(_run_backtest())
|
||||
# Generate a backtest_id — the replay generates its own, but we return
|
||||
# a placeholder immediately. The actual ID is in backtest_runs table.
|
||||
backtest_id = str(uuid.uuid4())
|
||||
return {"backtest_id": backtest_id, "status": "running"}
|
||||
return {"id": backtest_id, "status": "running"}
|
||||
|
||||
|
||||
@app.get("/api/trading/backtest/{backtest_id}")
|
||||
@@ -528,14 +529,12 @@ async def get_backtest(backtest_id: str) -> dict[str, Any]:
|
||||
"""Retrieve backtest results from PostgreSQL.
|
||||
|
||||
Task 32.5: Queries backtest_runs and backtest_trades tables.
|
||||
Returns a flat object matching the frontend BacktestResult type.
|
||||
"""
|
||||
if engine is None or engine.pool is None:
|
||||
# Fallback for when pool is not available
|
||||
return {
|
||||
"backtest_id": backtest_id,
|
||||
"id": backtest_id,
|
||||
"status": "pending",
|
||||
"config": None,
|
||||
"result": None,
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -545,25 +544,23 @@ async def get_backtest(backtest_id: str) -> dict[str, Any]:
|
||||
)
|
||||
if row is None:
|
||||
return {
|
||||
"backtest_id": backtest_id,
|
||||
"id": backtest_id,
|
||||
"status": "not_found",
|
||||
"config": None,
|
||||
"result": None,
|
||||
}
|
||||
|
||||
row_dict = dict(row)
|
||||
# Convert non-serializable types
|
||||
for key, val in row_dict.items():
|
||||
if isinstance(val, (datetime,)):
|
||||
row_dict[key] = val.isoformat()
|
||||
elif hasattr(val, "__str__") and not isinstance(val, (str, int, float, bool, type(None))):
|
||||
row_dict[key] = str(val)
|
||||
|
||||
# Parse equity_curve from JSONB
|
||||
equity_curve = row_dict.get("equity_curve", [])
|
||||
if isinstance(equity_curve, str):
|
||||
import json as _json
|
||||
equity_curve = _json.loads(equity_curve)
|
||||
|
||||
# Fetch trades
|
||||
trades = []
|
||||
try:
|
||||
trade_rows = await engine.pool.fetch(
|
||||
"SELECT * FROM backtest_trades WHERE backtest_id = $1",
|
||||
"SELECT * FROM backtest_trades WHERE backtest_id = $1 ORDER BY created_at",
|
||||
backtest_id,
|
||||
)
|
||||
for tr in trade_rows:
|
||||
@@ -571,6 +568,8 @@ async def get_backtest(backtest_id: str) -> dict[str, Any]:
|
||||
for key, val in trade_dict.items():
|
||||
if isinstance(val, (datetime,)):
|
||||
trade_dict[key] = val.isoformat()
|
||||
elif isinstance(val, date):
|
||||
trade_dict[key] = val.isoformat()
|
||||
elif hasattr(val, "__str__") and not isinstance(val, (str, int, float, bool, type(None))):
|
||||
trade_dict[key] = str(val)
|
||||
trades.append(trade_dict)
|
||||
@@ -578,32 +577,29 @@ async def get_backtest(backtest_id: str) -> dict[str, Any]:
|
||||
pass
|
||||
|
||||
return {
|
||||
"backtest_id": backtest_id,
|
||||
"id": str(row_dict.get("id", backtest_id)),
|
||||
"start_date": str(row_dict.get("start_date", "")),
|
||||
"end_date": str(row_dict.get("end_date", "")),
|
||||
"initial_capital": row_dict.get("initial_capital"),
|
||||
"risk_tier": row_dict.get("risk_tier"),
|
||||
"config": row_dict.get("config", {}),
|
||||
"total_return": row_dict.get("total_return"),
|
||||
"sharpe_ratio": row_dict.get("sharpe_ratio"),
|
||||
"max_drawdown": row_dict.get("max_drawdown"),
|
||||
"win_rate": row_dict.get("win_rate"),
|
||||
"profit_factor": row_dict.get("profit_factor"),
|
||||
"trade_count": row_dict.get("trade_count"),
|
||||
"equity_curve": equity_curve,
|
||||
"trades": trades,
|
||||
"status": row_dict.get("status", "unknown"),
|
||||
"config": {
|
||||
"start_date": str(row_dict.get("start_date", "")),
|
||||
"end_date": str(row_dict.get("end_date", "")),
|
||||
"initial_capital": row_dict.get("initial_capital"),
|
||||
"risk_tier": row_dict.get("risk_tier"),
|
||||
},
|
||||
"result": {
|
||||
"total_return": row_dict.get("total_return"),
|
||||
"sharpe_ratio": row_dict.get("sharpe_ratio"),
|
||||
"max_drawdown": row_dict.get("max_drawdown"),
|
||||
"win_rate": row_dict.get("win_rate"),
|
||||
"profit_factor": row_dict.get("profit_factor"),
|
||||
"trade_count": row_dict.get("trade_count"),
|
||||
"equity_curve": row_dict.get("equity_curve"),
|
||||
"trades": trades,
|
||||
},
|
||||
"completed_at": row_dict["completed_at"].isoformat() if row_dict.get("completed_at") else None,
|
||||
"created_at": row_dict["created_at"].isoformat() if row_dict.get("created_at") else None,
|
||||
}
|
||||
except Exception:
|
||||
logger.debug("Could not query backtest results — tables may not exist")
|
||||
return {
|
||||
"backtest_id": backtest_id,
|
||||
"id": backtest_id,
|
||||
"status": "pending",
|
||||
"config": None,
|
||||
"result": None,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,16 +39,18 @@ class BacktestReplay:
|
||||
self.pool = pool
|
||||
self._perf = PerformanceComputer()
|
||||
|
||||
async def run(self, config: BacktestConfig) -> BacktestResult:
|
||||
async def run(self, config: BacktestConfig, backtest_id: str | None = None) -> BacktestResult:
|
||||
"""Execute a full backtest replay.
|
||||
|
||||
Args:
|
||||
config: Backtest configuration (date range, capital, risk tier).
|
||||
backtest_id: Optional pre-generated ID. If not provided, one is generated.
|
||||
|
||||
Returns:
|
||||
BacktestResult with metrics, trade log, and equity curve.
|
||||
"""
|
||||
backtest_id = str(uuid.uuid4())
|
||||
if backtest_id is None:
|
||||
backtest_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
# Fetch historical recommendations
|
||||
|
||||
Reference in New Issue
Block a user