feat(BT-003B): add event comparison drilldown
This commit is contained in:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user