from __future__ import annotations from datetime import timedelta 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 SyntheticHistoricalProvider 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, ) 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, 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 materialize_scenario( self, preset: EventPreset, *, initial_portfolio: BacktestPortfolioState, template_slugs: tuple[str, ...] | None = None, provider_ref: ProviderRef | None = None, ) -> BacktestScenario: selected_template_slugs = tuple(template_slugs or preset.scenario_overrides.default_template_slugs) if not selected_template_slugs: raise ValueError("Event comparison requires at least one template slug") 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}") scenario_portfolio = BacktestPortfolioState( currency=initial_portfolio.currency, underlying_units=initial_portfolio.underlying_units, entry_spot=history[0].close, loan_amount=initial_portfolio.loan_amount, margin_call_ltv=initial_portfolio.margin_call_ltv, 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=history[0].date, end_date=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, ), )