diff --git a/app/pages/backtests.py b/app/pages/backtests.py index 9278e2d..a453e94 100644 --- a/app/pages/backtests.py +++ b/app/pages/backtests.py @@ -255,9 +255,9 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: default_data_source = "databento" default_dataset = "XNAS.BASIC" default_schema = "ohlcv-1d" - # Use a start date that's valid for the default dataset (XNAS.BASIC starts 2024-07-01) - default_start_date = date(2024, 7, 1).isoformat() - default_end_date = date(2024, 12, 31).isoformat() + # Default to March 2026 for testing + default_start_date = date(2026, 3, 2).isoformat() + default_end_date = date(2026, 3, 25).isoformat() default_symbol = "GLD" default_start_price = 0.0 @@ -621,12 +621,17 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ui.label(f"Template: {template_result.template_name}").classes( "text-sm text-slate-500 dark:text-slate-400" ) + # Get option contracts from first day (constant throughout backtest) + option_contracts = ( + template_result.daily_path[0].option_contracts if template_result.daily_path else 0.0 + ) with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"): cards = [ ("Start value", f"${summary.start_value:,.0f}"), ("End value hedged", f"${summary.end_value_hedged_net:,.0f}"), ("Max LTV hedged", f"{summary.max_ltv_hedged:.1%}"), ("Hedge cost", f"${summary.total_hedge_cost:,.0f}"), + ("Option contracts", f"{option_contracts:,.0f}"), ("Margin call days hedged", str(summary.margin_call_days_hedged)), ("Margin call days unhedged", str(summary.margin_call_days_unhedged)), ( @@ -671,12 +676,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: "field": "option_value", "align": "right", }, - { - "name": "contracts", - "label": "Contracts", - "field": "contracts", - "align": "right", - }, { "name": "ltv_hedged", "label": "LTV hedged", @@ -698,7 +697,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: "close": f"${point.spot_close:,.2f}", "portfolio_value": f"${point.net_portfolio_value:,.0f}", "option_value": f"${point.option_market_value:,.0f}", - "contracts": f"{point.option_contracts:,.0f}", "ltv_hedged": f"{point.ltv_hedged:.1%}", "margin_call": "Yes" if point.margin_call_hedged else "No", } @@ -982,10 +980,16 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400") ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100") + # Get option contracts from first day (constant throughout backtest) + daily_path = first_template.get("daily_path", []) + first_day = daily_path[0] if daily_path else {} + option_contracts = first_day.get("option_contracts", 0) + # Additional metrics row with ui.grid(columns=4).classes("w-full gap-4 max-lg:grid-cols-2 max-sm:grid-cols-1"): extra_data = [ ("Max LTV hedged", f"{max_ltv_hedged:.1%}"), + ("Option contracts", f"{option_contracts:,.0f}"), ("Margin call days hedged", str(margin_days_hedged)), ("Margin call days unhedged", str(margin_days_unhedged)), ("Breached threshold", "No" if margin_days_hedged == 0 else "Yes"), @@ -998,7 +1002,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: ui.label(value).classes("text-2xl font-bold text-slate-900 dark:text-slate-100") # Daily path table - daily_path = first_template.get("daily_path", []) if daily_path: with ui.card().classes( "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" @@ -1024,12 +1027,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: "field": "option_value", "align": "right", }, - { - "name": "contracts", - "label": "Contracts", - "field": "contracts", - "align": "right", - }, { "name": "ltv_hedged", "label": "LTV hedged", @@ -1051,7 +1048,6 @@ def _render_backtests_page(workspace_id: str | None = None) -> None: "close": f"${dp.get('spot_close', 0):,.2f}", "portfolio_value": f"${dp.get('net_portfolio_value', 0):,.0f}", "option_value": f"${dp.get('option_market_value', 0):,.0f}", - "contracts": f"{dp.get('option_contracts', 0):,.0f}", "ltv_hedged": f"{dp.get('ltv_hedged', 0):.1%}", "margin_call": "Yes" if dp.get("margin_call_hedged") else "No", } diff --git a/docs/roadmap/ROADMAP.yaml b/docs/roadmap/ROADMAP.yaml index f623672..492c213 100644 --- a/docs/roadmap/ROADMAP.yaml +++ b/docs/roadmap/ROADMAP.yaml @@ -13,6 +13,8 @@ notes: - Pre-alpha policy: we may cut or replace old features without backward compatibility until alpha is declared. - Alpha migration policy: once alpha is declared, compatibility only needs to move forward; backward migrations are not required. priority_queue: + - BT-005 + - BT-004 - EXEC-002 - DATA-DB-005 - DATA-002A @@ -48,6 +50,8 @@ recently_completed: - CORE-002B states: backlog: + - BT-005 + - BT-004 - DATA-DB-005 - DATA-DB-006 - EXEC-002 diff --git a/docs/roadmap/backlog/BT-004-backtest-visualization-chart.yaml b/docs/roadmap/backlog/BT-004-backtest-visualization-chart.yaml new file mode 100644 index 0000000..412ba51 --- /dev/null +++ b/docs/roadmap/backlog/BT-004-backtest-visualization-chart.yaml @@ -0,0 +1,19 @@ +id: BT-004 +title: Backtest Visualization Chart +status: backlog +priority: P1 +effort: M +depends_on: + - BT-001 +tags: [backtesting, visualization, charts] +summary: Add interactive candlestick chart showing price OHLC with portfolio value line overlay. +acceptance_criteria: + - Candlestick chart displays OHLC data (open, high, low, close) for each day. + - Portfolio value (underlying + option) shown as line on secondary Y-axis. + - Chart updates when backtest results change. + - Chart is responsive and readable on mobile/tablet viewports. + - Chart library matches existing app styling (dark mode support). +notes: | + Consider using a lightweight charting library that integrates well with NiceGUI. + Portfolio value line should be clearly distinguishable from candlesticks. + May want to add toggle for showing/hiding different series. \ No newline at end of file diff --git a/docs/roadmap/backlog/BT-005-defer-entry-spot-derivation.yaml b/docs/roadmap/backlog/BT-005-defer-entry-spot-derivation.yaml new file mode 100644 index 0000000..0cc86f6 --- /dev/null +++ b/docs/roadmap/backlog/BT-005-defer-entry-spot-derivation.yaml @@ -0,0 +1,22 @@ +id: BT-005 +title: Defer Entry Spot Derivation to Backtest Run +status: backlog +priority: P1 +effort: S +depends_on: [] +tags: [backtesting, ux, performance] +summary: Only derive entry spot from historical data when user clicks Run, not on every form change. +acceptance_criteria: + - Entry spot is NOT fetched when user changes start/end dates. + - Entry spot is NOT fetched when user changes other form fields. + - Entry spot IS derived when user clicks the Run button. + - Form remains responsive during date/field changes (no API calls blocking). + - Clear loading indicator shows when entry spot is being fetched during run. + - Previous entry spot value is retained until new one is derived. +notes: + Current behavior calls derive_entry_spot on every date change which causes + API errors if user is still configuring other fields. + + The refresh_workspace_seeded_units function should not be called on date changes. + Entry spot derivation should happen inside start_backtest or as a separate + explicit "fetch spot" button if user wants to preview. \ No newline at end of file