from __future__ import annotations from datetime import date, datetime from fastapi.responses import RedirectResponse from nicegui import ui from app.domain.backtesting_math import asset_quantity_from_workspace_config from app.models.workspace import get_workspace_repository from app.pages.common import dashboard_page, split_page_panes from app.services.backtesting.ui_service import BacktestPageRunResult, BacktestPageService def _chart_options(result: BacktestPageRunResult) -> dict: template_result = result.run_result.template_results[0] return { "tooltip": {"trigger": "axis"}, "legend": {"data": ["Spot", "LTV hedged", "LTV unhedged"]}, "xAxis": {"type": "category", "data": [point.date.isoformat() for point in template_result.daily_path]}, "yAxis": [ {"type": "value", "name": "Spot"}, {"type": "value", "name": "LTV", "min": 0, "max": 1, "axisLabel": {"formatter": "{value}"}}, ], "series": [ { "name": "Spot", "type": "line", "smooth": True, "data": [round(point.spot_close, 2) for point in template_result.daily_path], "lineStyle": {"color": "#0ea5e9"}, }, { "name": "LTV hedged", "type": "line", "yAxisIndex": 1, "smooth": True, "data": [round(point.ltv_hedged, 4) for point in template_result.daily_path], "lineStyle": {"color": "#22c55e"}, }, { "name": "LTV unhedged", "type": "line", "yAxisIndex": 1, "smooth": True, "data": [round(point.ltv_unhedged, 4) for point in template_result.daily_path], "lineStyle": {"color": "#ef4444"}, }, ], } @ui.page("/{workspace_id}/backtests") def workspace_backtests_page(workspace_id: str) -> None: repo = get_workspace_repository() if not repo.workspace_exists(workspace_id): return RedirectResponse(url="/", status_code=307) _render_backtests_page(workspace_id=workspace_id) def _render_backtests_page(workspace_id: str | None = None) -> None: service = BacktestPageService() template_options = service.template_options("GLD") select_options = {str(option["slug"]): str(option["label"]) for option in template_options} default_template_slug = str(template_options[0]["slug"]) if template_options else None repo = get_workspace_repository() config = repo.load_portfolio_config(workspace_id) if workspace_id else None default_entry_spot = service.derive_entry_spot("GLD", date(2024, 1, 2), date(2024, 1, 8)) default_units = ( asset_quantity_from_workspace_config(config, entry_spot=default_entry_spot, symbol="GLD") if config is not None and default_entry_spot > 0 else 1000.0 ) default_loan = float(config.loan_amount) if config else 68000.0 default_margin_call_ltv = float(config.margin_threshold) if config else 0.75 with dashboard_page( "Backtests", "Run a thin read-only historical scenario over the BT-001 synthetic engine and inspect the daily path.", "backtests", workspace_id=workspace_id, ): left_pane, right_pane = split_page_panes( left_testid="backtests-left-pane", right_testid="backtests-right-pane", ) with left_pane: with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Scenario Form").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ui.label( "Entry spot is auto-derived from the first historical close in the selected window so the scenario stays consistent with BT-001 entry timing." ).classes("text-sm text-slate-500 dark:text-slate-400") ui.label("BT-001A currently supports GLD only for this thin read-only page.").classes( "text-sm text-slate-500 dark:text-slate-400" ) if workspace_id: ui.label("Workspace defaults seed underlying units, loan amount, and margin threshold.").classes( "text-sm text-slate-500 dark:text-slate-400" ) symbol_input = ui.input("Symbol", value="GLD").props("readonly").classes("w-full") start_input = ui.input("Start date", value="2024-01-02").classes("w-full") end_input = ui.input("End date", value="2024-01-08").classes("w-full") template_select = ui.select(select_options, value=default_template_slug, label="Template").classes( "w-full" ) units_input = ui.number("Underlying units", value=default_units, min=0.0001, step=1).classes("w-full") loan_input = ui.number("Loan amount", value=default_loan, min=0, step=1000).classes("w-full") ltv_input = ui.number( "Margin call LTV", value=default_margin_call_ltv, min=0.01, max=0.99, step=0.01, ).classes("w-full") entry_spot_hint = ui.label("Entry spot will be auto-derived on run.").classes( "text-sm text-slate-500 dark:text-slate-400" ) validation_label = ui.label("").classes("text-sm text-rose-600 dark:text-rose-300") run_button = ui.button("Run backtest").props("color=primary") seeded_summary = ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ) with right_pane: result_panel = ui.column().classes("w-full gap-6") def parse_iso_date(raw: object, field_name: str) -> date: try: return datetime.strptime(str(raw), "%Y-%m-%d").date() except ValueError as exc: raise ValueError(f"{field_name} must be in YYYY-MM-DD format") from exc def render_seeded_summary(*, entry_spot: float | None = None) -> None: seeded_summary.clear() resolved_entry_spot = entry_spot if resolved_entry_spot is None: try: resolved_entry_spot = service.derive_entry_spot( str(symbol_input.value or "GLD"), parse_iso_date(start_input.value, "Start date"), parse_iso_date(end_input.value, "End date"), ) except (ValueError, KeyError): resolved_entry_spot = None with seeded_summary: ui.label("Scenario Summary").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") with ui.grid(columns=1).classes("w-full gap-4 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2"): cards = [ ("Template", select_options.get(str(template_select.value), str(template_select.value or "—"))), ("Underlying units", f"{float(units_input.value or 0.0):,.0f}"), ("Loan amount", f"${float(loan_input.value or 0.0):,.0f}"), ("Margin call LTV", f"{float(ltv_input.value or 0.0):.1%}"), ( "Date range", f"{str(start_input.value or '—')} → {str(end_input.value or '—')}", ), ( "Entry spot", f"${resolved_entry_spot:,.2f}" if resolved_entry_spot is not None else "Pending", ), ] for label, value in cards: with ui.card().classes( "rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950" ): ui.label(label).classes("text-sm text-slate-500 dark:text-slate-400") ui.label(value).classes("text-xl font-bold text-slate-900 dark:text-slate-100") def render_result_validation(message: str) -> None: result_panel.clear() with result_panel: with ui.card().classes( "w-full rounded-2xl border border-amber-200 bg-amber-50 shadow-sm dark:border-amber-900/60 dark:bg-amber-950/30" ): ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ui.label(message).classes("text-sm text-amber-800 dark:text-amber-200") def render_result(result: BacktestPageRunResult) -> None: result_panel.clear() template_result = result.run_result.template_results[0] summary = template_result.summary_metrics entry_spot_hint.set_text(f"Auto-derived entry spot: ${result.entry_spot:,.2f}") render_seeded_summary(entry_spot=result.entry_spot) with result_panel: with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Scenario Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ui.label(f"Template: {template_result.template_name}").classes( "text-sm text-slate-500 dark:text-slate-400" ) 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}"), ("Margin call days hedged", str(summary.margin_call_days_hedged)), ("Margin call days unhedged", str(summary.margin_call_days_unhedged)), ( "Hedged survived", "Yes" if not summary.margin_threshold_breached_hedged else "No", ), ( "Unhedged breached", "Yes" if summary.margin_threshold_breached_unhedged else "No", ), ] for label, value in cards: with ui.card().classes( "rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950" ): 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") ui.echart(_chart_options(result)).classes( "h-96 w-full rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900" ) with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Daily Results").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ui.table( columns=[ {"name": "date", "label": "Date", "field": "date", "align": "left"}, {"name": "spot_close", "label": "Spot", "field": "spot_close", "align": "right"}, { "name": "net_portfolio_value", "label": "Net hedged", "field": "net_portfolio_value", "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", "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%}", "ltv_hedged": f"{point.ltv_hedged:.1%}", "margin_call_hedged": "Yes" if point.margin_call_hedged else "No", } for point in template_result.daily_path ], row_key="date", ).classes("w-full") def refresh_workspace_seeded_units() -> None: if not workspace_id or config is None or config.gold_value is None: return try: entry_spot = service.derive_entry_spot( str(symbol_input.value or "GLD"), parse_iso_date(start_input.value, "Start date"), parse_iso_date(end_input.value, "End date"), ) except (ValueError, KeyError): render_seeded_summary(entry_spot=None) return units_input.value = asset_quantity_from_workspace_config(config, entry_spot=entry_spot, symbol="GLD") entry_spot_hint.set_text(f"Auto-derived entry spot: ${entry_spot:,.2f}") render_seeded_summary(entry_spot=entry_spot) def run_backtest() -> None: validation_label.set_text("") try: result = service.run_read_only_scenario( symbol=str(symbol_input.value or ""), start_date=parse_iso_date(start_input.value, "Start date"), end_date=parse_iso_date(end_input.value, "End date"), template_slug=str(template_select.value or ""), underlying_units=float(units_input.value or 0.0), loan_amount=float(loan_input.value or 0.0), margin_call_ltv=float(ltv_input.value or 0.0), ) except (ValueError, KeyError) as exc: render_seeded_summary(entry_spot=None) validation_label.set_text(str(exc)) render_result_validation(str(exc)) return except Exception: render_seeded_summary(entry_spot=None) message = "Backtest failed. Please verify the scenario inputs and try again." validation_label.set_text(message) render_result_validation(message) return render_result(result) if workspace_id: start_input.on_value_change(lambda _event: refresh_workspace_seeded_units()) end_input.on_value_change(lambda _event: refresh_workspace_seeded_units()) template_select.on_value_change(lambda _event: render_seeded_summary()) units_input.on_value_change(lambda _event: render_seeded_summary()) loan_input.on_value_change(lambda _event: render_seeded_summary()) ltv_input.on_value_change(lambda _event: render_seeded_summary()) run_button.on_click(lambda: run_backtest()) render_seeded_summary(entry_spot=default_entry_spot) run_backtest()