feat(BT-003B): add event comparison drilldown

This commit is contained in:
Bu5hm4nn
2026-03-26 22:05:31 +01:00
parent bdf56ecebe
commit 3c9ff201e1
8 changed files with 329 additions and 21 deletions

View File

@@ -309,6 +309,8 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
)
render_selected_summary(entry_spot=float(scenario.initial_portfolio.entry_spot))
chart_model = service.chart_model(report)
drilldown_options = service.drilldown_options(report)
initial_drilldown_slug = next(iter(drilldown_options), None)
with result_panel:
with ui.card().classes(
@@ -388,6 +390,132 @@ def _render_event_comparison_page(workspace_id: str | None = None) -> None:
row_key="rank",
).classes("w-full")
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("Strategy Drilldown").classes("text-lg font-semibold text-slate-900 dark:text-slate-100")
ui.label(
"Select a ranked strategy to inspect margin-call pressure, payoff realization, and the full seeded daily path."
).classes("text-sm text-slate-500 dark:text-slate-400")
drilldown_select = ui.select(
drilldown_options,
value=initial_drilldown_slug,
label="Strategy drilldown",
).classes("w-full")
drilldown_container = ui.column().classes("w-full gap-4")
def render_drilldown() -> None:
drilldown_container.clear()
if drilldown_select.value is None:
return
drilldown = service.drilldown_model(report, template_slug=str(drilldown_select.value))
breach_dates = ", ".join(drilldown.breach_dates) if drilldown.breach_dates else "None"
worst_ltv_point = (
f"{drilldown.worst_ltv_date} · {drilldown.worst_ltv_hedged:.1%}"
if drilldown.worst_ltv_date is not None
else "Unavailable"
)
with drilldown_container:
ui.label(f"Selected strategy: {drilldown.template_name}").classes(
"text-lg font-semibold text-slate-900 dark:text-slate-100"
)
ui.label(
f"Rank #{drilldown.rank} · {'Survived margin call' if drilldown.survived_margin_call else 'Breached margin threshold'}"
).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 = [
("Margin-call days", str(drilldown.margin_call_days_hedged)),
("Payoff realized", f"${drilldown.total_option_payoff_realized:,.0f}"),
("Hedge cost", f"${drilldown.hedge_cost:,.0f}"),
("Final equity", f"${drilldown.final_equity:,.0f}"),
]
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")
with ui.grid(columns=2).classes("w-full gap-4 max-md:grid-cols-1"):
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("Worst LTV point").classes("text-sm text-slate-500 dark:text-slate-400")
ui.label(worst_ltv_point).classes(
"text-xl font-bold text-slate-900 dark:text-slate-100"
)
with ui.card().classes(
"rounded-xl border border-amber-200 bg-amber-50 p-4 shadow-none dark:border-amber-900/60 dark:bg-amber-950/30"
):
ui.label("Margin threshold breach dates").classes(
"text-sm text-amber-700 dark:text-amber-300"
)
ui.label(breach_dates).classes(
"text-base font-semibold text-amber-800 dark:text-amber-200"
)
with ui.card().classes(
"w-full rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-none dark:border-slate-800 dark:bg-slate-950"
):
ui.label("Daily path details").classes(
"text-base 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": "option_market_value",
"label": "Option value",
"field": "option_market_value",
"align": "right",
},
{
"name": "realized_option_cashflow",
"label": "Payoff realized",
"field": "realized_option_cashflow",
"align": "right",
},
{
"name": "ltv_hedged",
"label": "Hedged LTV",
"field": "ltv_hedged",
"align": "right",
},
{
"name": "margin_call_hedged",
"label": "Breach",
"field": "margin_call_hedged",
"align": "center",
},
],
rows=[
{
"date": row.date,
"spot_close": f"${row.spot_close:,.2f}",
"net_portfolio_value": f"${row.net_portfolio_value:,.0f}",
"option_market_value": f"${row.option_market_value:,.0f}",
"realized_option_cashflow": f"${row.realized_option_cashflow:,.0f}",
"ltv_hedged": f"{row.ltv_hedged:.1%}",
"margin_call_hedged": "Yes" if row.margin_call_hedged else "No",
}
for row in drilldown.rows
],
row_key="date",
).classes("w-full")
drilldown_select.on_value_change(lambda _: render_drilldown())
render_drilldown()
with ui.card().classes(
"w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
):

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from app.models.backtest import BacktestScenario, EventComparisonReport
from app.models.backtest import BacktestScenario, EventComparisonRanking, EventComparisonReport
from app.services.backtesting.comparison import EventComparisonService
from app.services.backtesting.historical_provider import DailyClosePoint, SyntheticHistoricalProvider
from app.services.backtesting.input_normalization import normalize_historical_scenario_inputs
@@ -63,6 +63,35 @@ class EventComparisonChartModel:
series: tuple[EventComparisonChartSeries, ...]
@dataclass(frozen=True)
class EventComparisonDrilldownRow:
date: str
spot_close: float
net_portfolio_value: float
option_market_value: float
realized_option_cashflow: float
ltv_unhedged: float
ltv_hedged: float
margin_call_hedged: bool
active_position_ids: tuple[str, ...]
@dataclass(frozen=True)
class EventComparisonDrilldownModel:
rank: int
template_slug: str
template_name: str
survived_margin_call: bool
margin_call_days_hedged: int
total_option_payoff_realized: float
hedge_cost: float
final_equity: float
worst_ltv_hedged: float
worst_ltv_date: str | None
breach_dates: tuple[str, ...]
rows: tuple[EventComparisonDrilldownRow, ...]
class EventComparisonPageService:
def __init__(
self,
@@ -181,9 +210,9 @@ class EventComparisonPageService:
preview = self.comparison_service.preview_scenario_from_inputs(
preset_slug=preset.slug,
template_slugs=template_slugs,
underlying_units=underlying_units,
loan_amount=loan_amount,
margin_call_ltv=margin_call_ltv,
underlying_units=normalized_inputs.underlying_units,
loan_amount=normalized_inputs.loan_amount,
margin_call_ltv=normalized_inputs.margin_call_ltv,
)
except ValueError as exc:
if str(exc) == "loan_amount must be less than initial collateral value":
@@ -232,3 +261,60 @@ class EventComparisonPageService:
)
)
return EventComparisonChartModel(dates=dates, series=tuple(series))
@staticmethod
def drilldown_model(
report: EventComparisonReport,
*,
template_slug: str | None = None,
) -> EventComparisonDrilldownModel:
ranking = EventComparisonPageService._select_ranking(report, template_slug=template_slug)
daily_path = ranking.result.daily_path
worst_ltv_point = max(daily_path, key=lambda point: point.ltv_hedged, default=None)
breach_dates = tuple(point.date.isoformat() for point in daily_path if point.margin_call_hedged)
return EventComparisonDrilldownModel(
rank=ranking.rank,
template_slug=ranking.template_slug,
template_name=ranking.template_name,
survived_margin_call=ranking.survived_margin_call,
margin_call_days_hedged=ranking.margin_call_days_hedged,
total_option_payoff_realized=ranking.result.summary_metrics.total_option_payoff_realized,
hedge_cost=ranking.hedge_cost,
final_equity=ranking.final_equity,
worst_ltv_hedged=ranking.max_ltv_hedged,
worst_ltv_date=worst_ltv_point.date.isoformat() if worst_ltv_point is not None else None,
breach_dates=breach_dates,
rows=tuple(
EventComparisonDrilldownRow(
date=point.date.isoformat(),
spot_close=point.spot_close,
net_portfolio_value=point.net_portfolio_value,
option_market_value=point.option_market_value,
realized_option_cashflow=point.realized_option_cashflow,
ltv_unhedged=point.ltv_unhedged,
ltv_hedged=point.ltv_hedged,
margin_call_hedged=point.margin_call_hedged,
active_position_ids=point.active_position_ids,
)
for point in daily_path
),
)
@staticmethod
def drilldown_options(report: EventComparisonReport) -> dict[str, str]:
return {ranking.template_slug: f"#{ranking.rank}{ranking.template_name}" for ranking in report.rankings}
@staticmethod
def _select_ranking(
report: EventComparisonReport,
*,
template_slug: str | None = None,
) -> EventComparisonRanking:
if not report.rankings:
raise ValueError("Event comparison report has no ranked results")
if template_slug is None:
return report.rankings[0]
for ranking in report.rankings:
if ranking.template_slug == template_slug:
return ranking
raise ValueError(f"Unknown ranked template: {template_slug}")