from __future__ import annotations from datetime import timedelta from app.domain.backtesting_math import materialize_backtest_portfolio_state from app.models.backtest import ( BacktestPortfolioState, BacktestScenario, EventComparisonRanking, EventComparisonReport, ProviderRef, TemplateRef, ) from app.models.event_preset import EventPreset from app.services.backtesting.historical_provider import DailyClosePoint, SyntheticHistoricalProvider from app.services.backtesting.input_normalization import normalize_historical_scenario_inputs from app.services.backtesting.service import BacktestService from app.services.event_presets import EventPresetService from app.services.strategy_templates import StrategyTemplateService class EventComparisonService: def __init__( self, provider: SyntheticHistoricalProvider | None = None, template_service: StrategyTemplateService | None = None, event_preset_service: EventPresetService | None = None, backtest_service: BacktestService | None = None, ) -> None: self.provider = provider or SyntheticHistoricalProvider() self.template_service = template_service or StrategyTemplateService() self.event_preset_service = event_preset_service or EventPresetService() self.backtest_service = backtest_service or BacktestService( provider=self.provider, template_service=self.template_service, ) def compare_event( self, *, preset_slug: str, initial_portfolio: BacktestPortfolioState, template_slugs: tuple[str, ...] | None = None, provider_ref: ProviderRef | None = None, ) -> EventComparisonReport: preset = self.event_preset_service.get_preset(preset_slug) scenario = self.materialize_scenario( preset, initial_portfolio=initial_portfolio, template_slugs=template_slugs, provider_ref=provider_ref, ) return self._compare_materialized_event(preset=preset, scenario=scenario) def compare_event_from_inputs( self, *, preset_slug: str, underlying_units: float, loan_amount: float, margin_call_ltv: float, template_slugs: tuple[str, ...] | None = None, currency: str = "USD", cash_balance: float = 0.0, financing_rate: float = 0.0, provider_ref: ProviderRef | None = None, ) -> EventComparisonReport: normalized_inputs = normalize_historical_scenario_inputs( underlying_units=underlying_units, loan_amount=loan_amount, margin_call_ltv=margin_call_ltv, currency=currency, cash_balance=cash_balance, financing_rate=financing_rate, ) preset = self.event_preset_service.get_preset(preset_slug) scenario = self.preview_scenario_from_inputs( preset_slug=preset_slug, underlying_units=normalized_inputs.underlying_units, loan_amount=normalized_inputs.loan_amount, margin_call_ltv=normalized_inputs.margin_call_ltv, template_slugs=template_slugs, currency=normalized_inputs.currency, cash_balance=normalized_inputs.cash_balance, financing_rate=normalized_inputs.financing_rate, provider_ref=provider_ref, ) return self._compare_materialized_event(preset=preset, scenario=scenario) def preview_scenario_from_inputs( self, *, preset_slug: str, underlying_units: float, loan_amount: float, margin_call_ltv: float, template_slugs: tuple[str, ...] | None = None, currency: str = "USD", cash_balance: float = 0.0, financing_rate: float = 0.0, provider_ref: ProviderRef | None = None, ) -> BacktestScenario: normalized_inputs = normalize_historical_scenario_inputs( underlying_units=underlying_units, loan_amount=loan_amount, margin_call_ltv=margin_call_ltv, currency=currency, cash_balance=cash_balance, financing_rate=financing_rate, ) preset = self.event_preset_service.get_preset(preset_slug) history = self._load_preset_history(preset) entry_spot = history[0].close initial_portfolio = materialize_backtest_portfolio_state( symbol=preset.symbol, underlying_units=normalized_inputs.underlying_units, entry_spot=entry_spot, loan_amount=normalized_inputs.loan_amount, margin_call_ltv=normalized_inputs.margin_call_ltv, currency=normalized_inputs.currency, cash_balance=normalized_inputs.cash_balance, financing_rate=normalized_inputs.financing_rate, ) return self.materialize_scenario( preset, initial_portfolio=initial_portfolio, template_slugs=template_slugs, provider_ref=provider_ref, history=history, ) def materialize_scenario( self, preset: EventPreset, *, initial_portfolio: BacktestPortfolioState, template_slugs: tuple[str, ...] | None = None, provider_ref: ProviderRef | None = None, history: list[DailyClosePoint] | None = None, ) -> BacktestScenario: selected_template_slugs = ( tuple(preset.scenario_overrides.default_template_slugs) if template_slugs is None else tuple(template_slugs) ) if not selected_template_slugs: raise ValueError("Event comparison requires at least one template slug") resolved_history = self._load_preset_history(preset) if history is None else history if not resolved_history: raise ValueError("Event comparison history must not be empty") scenario_portfolio = materialize_backtest_portfolio_state( symbol=preset.symbol, underlying_units=initial_portfolio.underlying_units, entry_spot=resolved_history[0].close, loan_amount=initial_portfolio.loan_amount, margin_call_ltv=initial_portfolio.margin_call_ltv, currency=initial_portfolio.currency, cash_balance=initial_portfolio.cash_balance, financing_rate=initial_portfolio.financing_rate, ) template_refs = tuple( TemplateRef(slug=slug, version=self.template_service.get_template(slug).version) for slug in selected_template_slugs ) return BacktestScenario( scenario_id=f"event-{preset.slug}", display_name=preset.display_name, symbol=preset.symbol, start_date=resolved_history[0].date, end_date=resolved_history[-1].date, initial_portfolio=scenario_portfolio, template_refs=template_refs, provider_ref=provider_ref or ProviderRef( provider_id=self.provider.provider_id, pricing_mode=self.provider.pricing_mode, ), ) def _compare_materialized_event(self, *, preset: EventPreset, scenario: BacktestScenario) -> EventComparisonReport: run_result = self.backtest_service.run_scenario(scenario) ranked_results = sorted( run_result.template_results, key=lambda result: ( result.summary_metrics.margin_call_days_hedged, result.summary_metrics.max_ltv_hedged, result.summary_metrics.total_hedge_cost, -result.summary_metrics.end_value_hedged_net, result.template_slug, ), ) rankings = tuple( EventComparisonRanking( rank=index, template_slug=result.template_slug, template_name=result.template_name, survived_margin_call=not result.summary_metrics.margin_threshold_breached_hedged, margin_call_days_hedged=result.summary_metrics.margin_call_days_hedged, max_ltv_hedged=result.summary_metrics.max_ltv_hedged, hedge_cost=result.summary_metrics.total_hedge_cost, final_equity=result.summary_metrics.end_value_hedged_net, result=result, ) for index, result in enumerate(ranked_results, start=1) ) return EventComparisonReport( event_preset=preset, scenario=scenario, rankings=rankings, run_result=run_result, ) def _load_preset_history(self, preset: EventPreset) -> list[DailyClosePoint]: requested_start = preset.window_start - timedelta(days=preset.scenario_overrides.lookback_days or 0) requested_end = preset.window_end + timedelta(days=preset.scenario_overrides.recovery_days or 0) history = self.provider.load_history(preset.symbol, requested_start, requested_end) if not history: raise ValueError(f"No historical prices found for event preset: {preset.slug}") return history