fix: correct backtest job result serialization and add Playwright test fixtures

- Fix BacktestPageRunResult serialization in jobs.py to correctly access
  nested fields from scenario and run_result objects
- Add test_backtest_job.py with comprehensive tests for job execution
- Add conftest_playwright.py with ServerManager that starts FastAPI server
  for Playwright tests using uvicorn
- Add test_playwright_server.py with E2E tests using the server fixture

The job serialization bug was causing backtest results to fail silently
because it was trying to access non-existent fields on BacktestPageRunResult.
This commit is contained in:
Bu5hm4nn
2026-04-04 18:27:34 +02:00
parent 6c35efde0f
commit d835544e58
4 changed files with 501 additions and 19 deletions

View File

@@ -214,30 +214,64 @@ def run_backtest_job(
)
# Convert result to dict for serialization
# BacktestPageRunResult has: scenario, run_result, entry_spot, data_source, data_cost_usd
template_results = result.run_result.template_results
first_template = template_results[0] if template_results else None
summary = first_template.summary_metrics if first_template else None
result_dict = {
"scenario_name": result.scenario_name,
"entry_date": result.entry_date.isoformat() if result.entry_date else None,
"scenario_id": result.scenario.scenario_id,
"scenario_name": result.scenario.display_name,
"symbol": result.scenario.symbol,
"start_date": result.scenario.start_date.isoformat(),
"end_date": result.scenario.end_date.isoformat(),
"entry_spot": result.entry_spot,
"underlying_units": result.underlying_units,
"loan_amount": result.loan_amount,
"margin_call_ltv": result.margin_call_ltv,
"total_pnl": result.total_pnl,
"total_pnl_pct": result.total_pnl_pct,
"hedging_cost": result.hedging_cost,
"hedging_cost_pct": result.hedging_cost_pct,
"unhedged_pnl": result.unhedged_pnl,
"unhedged_pnl_pct": result.unhedged_pnl_pct,
"margin_calls": result.margin_calls,
"margin_call_events": [
"underlying_units": result.scenario.initial_portfolio.underlying_units,
"loan_amount": result.scenario.initial_portfolio.loan_amount,
"margin_call_ltv": result.scenario.initial_portfolio.margin_call_ltv,
"data_source": result.data_source,
"data_cost_usd": result.data_cost_usd,
# Summary metrics from first template result
"start_value": summary.start_value if summary else 0.0,
"end_value_hedged_net": summary.end_value_hedged_net if summary else 0.0,
"total_hedge_cost": summary.total_hedge_cost if summary else 0.0,
"max_ltv_hedged": summary.max_ltv_hedged if summary else 0.0,
"max_ltv_unhedged": summary.max_ltv_unhedged if summary else 0.0,
"margin_call_days_hedged": summary.margin_call_days_hedged if summary else 0,
"margin_call_days_unhedged": summary.margin_call_days_unhedged if summary else 0,
"margin_threshold_breached_hedged": summary.margin_threshold_breached_hedged if summary else False,
"margin_threshold_breached_unhedged": summary.margin_threshold_breached_unhedged if summary else False,
# Template results with full daily path
"template_results": [
{
"date": event.date.isoformat(),
"price": event.price,
"ltv": event.ltv,
"action": event.action,
"template_slug": tr.template_slug,
"template_name": tr.template_name,
"summary_metrics": {
"start_value": tr.summary_metrics.start_value,
"end_value_hedged_net": tr.summary_metrics.end_value_hedged_net,
"total_hedge_cost": tr.summary_metrics.total_hedge_cost,
"max_ltv_hedged": tr.summary_metrics.max_ltv_hedged,
"max_ltv_unhedged": tr.summary_metrics.max_ltv_unhedged,
"margin_call_days_hedged": tr.summary_metrics.margin_call_days_hedged,
"margin_call_days_unhedged": tr.summary_metrics.margin_call_days_unhedged,
},
"daily_path": [
{
"date": dp.date.isoformat(),
"spot_close": dp.spot_close,
"underlying_value": dp.underlying_value,
"option_market_value": dp.option_market_value,
"net_portfolio_value": dp.net_portfolio_value,
"ltv_hedged": dp.ltv_hedged,
"ltv_unhedged": dp.ltv_unhedged,
"margin_call_hedged": dp.margin_call_hedged,
"margin_call_unhedged": dp.margin_call_unhedged,
}
for dp in tr.daily_path
],
}
for event in (result.margin_call_events or [])
for tr in template_results
],
"prices": [{"date": p.date.isoformat(), "close": p.close} for p in (result.prices or [])],
}
# Stage 4: Complete