diff --git a/app/backtesting/engine.py b/app/backtesting/engine.py index 09ec5ff..53e7140 100644 --- a/app/backtesting/engine.py +++ b/app/backtesting/engine.py @@ -91,6 +91,8 @@ class SyntheticBacktestEngine: BacktestDailyPoint( date=day.date, spot_close=day.close, + spot_low=day.low if day.low is not None else day.close, + spot_high=day.high if day.high is not None else day.close, underlying_value=underlying_value_close, option_market_value=option_market_value, premium_cashflow=premium_cashflow, diff --git a/app/models/backtest.py b/app/models/backtest.py index 40c34d8..f02bf38 100644 --- a/app/models/backtest.py +++ b/app/models/backtest.py @@ -97,6 +97,9 @@ class BacktestDailyPoint: ltv_hedged: float margin_call_unhedged: bool margin_call_hedged: bool + # Optional OHLC fields for worst-case margin call evaluation + spot_low: float | None = None # Day's low for margin call evaluation + spot_high: float | None = None # Day's high active_position_ids: tuple[str, ...] = field(default_factory=tuple) diff --git a/app/pages/backtests.py b/app/pages/backtests.py index 64ed886..7d5bb38 100644 --- a/app/pages/backtests.py +++ b/app/pages/backtests.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from datetime import date, datetime, timedelta +from datetime import date, datetime from typing import TYPE_CHECKING, Any from fastapi.responses import RedirectResponse @@ -64,20 +64,12 @@ DATABENTO_DATASET_MIN_DATES = { def get_default_backtest_dates() -> tuple[date, date]: - """Get default backtest date range (~2 years ending on most recent Friday or earlier). + """Get default backtest date range (March 2026 for testing). - Returns dates (start, end) where: - - end is the most recent Friday (including today if today is Friday) - - start is ~730 days before end + Returns dates (start, end) for March 2026. """ - today = date.today() - # Find days since most recent Friday - days_since_friday = (today.weekday() - 4) % 7 - # If today is Friday (weekday 4), days_since_friday is 0, so end = today - # If today is Saturday (weekday 5), days_since_friday is 1, so end = yesterday (Friday) - # etc. - end = today - timedelta(days=days_since_friday) - start = end - timedelta(days=730) # ~2 years + start = date(2026, 3, 2) + end = date(2026, 3, 25) return start, end @@ -658,41 +650,36 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ) with ui.card().classes( - "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" + "w-full mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950" ): - ui.label("Daily Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") + ui.label("Daily Results").classes("text-md font-semibold text-slate-900 dark:text-slate-100 mb-2") ui.table( columns=[ {"name": "date", "label": "Date", "field": "date", "align": "left"}, - {"name": "spot_close", "label": "Spot", "field": "spot_close", "align": "right"}, + {"name": "low", "label": "Low", "field": "low", "align": "right"}, + {"name": "high", "label": "High", "field": "high", "align": "right"}, + {"name": "close", "label": "Close", "field": "close", "align": "right"}, { - "name": "net_portfolio_value", - "label": "Net hedged", - "field": "net_portfolio_value", + "name": "ltv_hedged", + "label": "LTV hedged", + "field": "ltv_hedged", "align": "right", }, { - "name": "ltv_unhedged", - "label": "LTV unhedged", - "field": "ltv_unhedged", - "align": "right", - }, - {"name": "ltv_hedged", "label": "LTV hedged", "field": "ltv_hedged", "align": "right"}, - { - "name": "margin_call_hedged", - "label": "Hedged breach", - "field": "margin_call_hedged", + "name": "margin_call", + "label": "Margin call", + "field": "margin_call", "align": "center", }, ], rows=[ { "date": point.date.isoformat(), - "spot_close": f"${point.spot_close:,.2f}", - "net_portfolio_value": f"${point.net_portfolio_value:,.0f}", - "ltv_unhedged": f"{point.ltv_unhedged:.1%}", + "low": f"${point.spot_low:,.2f}", + "high": f"${point.spot_high:,.2f}", + "close": f"${point.spot_close:,.2f}", "ltv_hedged": f"{point.ltv_hedged:.1%}", - "margin_call_hedged": "Yes" if point.margin_call_hedged else "No", + "margin_call": "Yes" if point.margin_call_hedged else "No", } for point in template_result.daily_path ], @@ -1001,7 +988,9 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ui.table( columns=[ {"name": "date", "label": "Date", "field": "date", "align": "left"}, - {"name": "spot", "label": "Spot", "field": "spot", "align": "right"}, + {"name": "low", "label": "Low", "field": "low", "align": "right"}, + {"name": "high", "label": "High", "field": "high", "align": "right"}, + {"name": "close", "label": "Close", "field": "close", "align": "right"}, { "name": "ltv_hedged", "label": "LTV hedged", @@ -1018,7 +1007,9 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: rows=[ { "date": dp.get("date", ""), - "spot": f"${dp.get('spot_close', 0):,.2f}", + "low": f"${dp.get('spot_low', dp.get('spot_close', 0)):,.2f}", + "high": f"${dp.get('spot_high', dp.get('spot_close', 0)):,.2f}", + "close": f"${dp.get('spot_close', 0):,.2f}", "ltv_hedged": f"{dp.get('ltv_hedged', 0):.1%}", "margin_call": "Yes" if dp.get("margin_call_hedged") else "No", } diff --git a/app/services/backtesting/jobs.py b/app/services/backtesting/jobs.py index 3e9d666..ba9479b 100644 --- a/app/services/backtesting/jobs.py +++ b/app/services/backtesting/jobs.py @@ -259,6 +259,8 @@ def run_backtest_job( { "date": dp.date.isoformat(), "spot_close": dp.spot_close, + "spot_low": dp.spot_low if dp.spot_low is not None else dp.spot_close, + "spot_high": dp.spot_high if dp.spot_high is not None else dp.spot_close, "underlying_value": dp.underlying_value, "option_market_value": dp.option_market_value, "net_portfolio_value": dp.net_portfolio_value,