from __future__ import annotations from copy import copy from dataclasses import dataclass from datetime import date from math import isclose from app.backtesting.engine import SyntheticBacktestEngine from app.domain.backtesting_math import materialize_backtest_portfolio_state from app.models.backtest import ( BacktestRunResult, BacktestScenario, ProviderRef, TemplateRef, ) from app.services.backtesting.historical_provider import ( DailyClosePoint, HistoricalOptionMark, HistoricalOptionPosition, SyntheticHistoricalProvider, SyntheticOptionQuote, ) from app.services.backtesting.input_normalization import normalize_historical_scenario_inputs from app.services.backtesting.service import BacktestService from app.services.strategy_templates import StrategyTemplateService SUPPORTED_BACKTEST_PAGE_SYMBOL = "GLD" def _validate_initial_collateral(underlying_units: float, entry_spot: float, loan_amount: float) -> None: initial_collateral_value = underlying_units * entry_spot if loan_amount >= initial_collateral_value: raise ValueError( "Historical scenario starts undercollateralized: " f"loan ${loan_amount:,.0f} exceeds initial collateral ${initial_collateral_value:,.0f} " f"at entry spot ${entry_spot:,.2f}. Reduce loan amount or increase underlying units." ) DETERMINISTIC_UI_FIXTURE_HISTORY: tuple[DailyClosePoint, ...] = ( DailyClosePoint(date=date(2024, 1, 2), close=100.0), DailyClosePoint(date=date(2024, 1, 3), close=96.0), DailyClosePoint(date=date(2024, 1, 4), close=92.0), DailyClosePoint(date=date(2024, 1, 5), close=88.0), DailyClosePoint(date=date(2024, 1, 8), close=85.0), ) class DeterministicBacktestFixtureSource: def load_daily_closes(self, symbol: str, start_date: date, end_date: date) -> list[DailyClosePoint]: normalized_symbol = symbol.strip().upper() if ( normalized_symbol != SUPPORTED_BACKTEST_PAGE_SYMBOL or start_date != date(2024, 1, 2) or end_date != date(2024, 1, 8) ): raise ValueError( "BT-001A deterministic fixture data only supports GLD on the seeded 2024-01-02 through 2024-01-08 window" ) return list(DETERMINISTIC_UI_FIXTURE_HISTORY) @dataclass(frozen=True) class BacktestPageRunResult: scenario: BacktestScenario run_result: BacktestRunResult entry_spot: float class FixtureBoundHistoricalProvider: def __init__(self, base_provider: SyntheticHistoricalProvider) -> None: self.base_provider = base_provider self.source = DeterministicBacktestFixtureSource() self.provider_id = base_provider.provider_id self.pricing_mode = base_provider.pricing_mode self.implied_volatility = base_provider.implied_volatility self.risk_free_rate = base_provider.risk_free_rate def load_history(self, symbol: str, start_date: date, end_date: date) -> list[DailyClosePoint]: rows = self.source.load_daily_closes(symbol, start_date, end_date) filtered = [row for row in rows if start_date <= row.date <= end_date] return sorted(filtered, key=lambda row: row.date) def validate_provider_ref(self, provider_ref: ProviderRef) -> None: self.base_provider.validate_provider_ref(provider_ref) def resolve_expiry(self, trading_days: list[DailyClosePoint], as_of_date: date, target_expiry_days: int) -> date: return self.base_provider.resolve_expiry(trading_days, as_of_date, target_expiry_days) def price_option(self, **kwargs: object) -> SyntheticOptionQuote: return self.base_provider.price_option(**kwargs) def open_position(self, **kwargs: object) -> HistoricalOptionPosition: return self.base_provider.open_position(**kwargs) def mark_position(self, position: HistoricalOptionPosition, **kwargs: object) -> HistoricalOptionMark: return self.base_provider.mark_position(position, **kwargs) @staticmethod def intrinsic_value(*, option_type: str, spot: float, strike: float) -> float: return SyntheticHistoricalProvider.intrinsic_value(option_type=option_type, spot=spot, strike=strike) class BacktestPageService: def __init__( self, backtest_service: BacktestService | None = None, template_service: StrategyTemplateService | None = None, ) -> None: base_service = backtest_service or BacktestService( template_service=template_service, provider=None, ) self.template_service = template_service or base_service.template_service fixture_provider = FixtureBoundHistoricalProvider(base_service.provider) self.backtest_service = copy(base_service) self.backtest_service.provider = fixture_provider self.backtest_service.template_service = self.template_service self.backtest_service.engine = SyntheticBacktestEngine(fixture_provider) def template_options(self, symbol: str = "GLD") -> list[dict[str, str | int]]: return [ { "label": template.display_name, "slug": template.slug, "version": template.version, "description": template.description, } for template in self.template_service.list_active_templates(symbol) ] def derive_entry_spot(self, symbol: str, start_date: date, end_date: date) -> float: history = self.backtest_service.provider.load_history(symbol.strip().upper(), start_date, end_date) if not history: raise ValueError("No historical prices found for scenario window") if history[0].date != start_date: raise ValueError( "Scenario start date must match the first available historical close for entry-at-start backtests" ) return history[0].close def validate_preview_inputs( self, *, symbol: str, start_date: date, end_date: date, template_slug: str, underlying_units: float, loan_amount: float, margin_call_ltv: float, entry_spot: float | None = None, ) -> float: normalized_symbol = symbol.strip().upper() if not normalized_symbol: raise ValueError("Symbol is required") if normalized_symbol != SUPPORTED_BACKTEST_PAGE_SYMBOL: raise ValueError("BT-001A backtests are currently limited to GLD on this page") if start_date > end_date: raise ValueError("Start date must be on or before end date") normalized_inputs = normalize_historical_scenario_inputs( underlying_units=underlying_units, loan_amount=loan_amount, margin_call_ltv=margin_call_ltv, ) if not template_slug: raise ValueError("Template selection is required") self.template_service.get_template(template_slug) derived_entry_spot = self.derive_entry_spot(normalized_symbol, start_date, end_date) if entry_spot is not None and not isclose( entry_spot, derived_entry_spot, rel_tol=BacktestService.ENTRY_SPOT_REL_TOLERANCE, abs_tol=BacktestService.ENTRY_SPOT_ABS_TOLERANCE, ): raise ValueError( f"Supplied entry spot ${entry_spot:,.2f} does not match derived historical entry spot ${derived_entry_spot:,.2f}" ) _validate_initial_collateral( normalized_inputs.underlying_units, derived_entry_spot, normalized_inputs.loan_amount, ) return derived_entry_spot def run_read_only_scenario( self, *, symbol: str, start_date: date, end_date: date, template_slug: str, underlying_units: float, loan_amount: float, margin_call_ltv: float, ) -> BacktestPageRunResult: normalized_symbol = symbol.strip().upper() entry_spot = self.validate_preview_inputs( symbol=normalized_symbol, start_date=start_date, end_date=end_date, template_slug=template_slug, underlying_units=underlying_units, loan_amount=loan_amount, margin_call_ltv=margin_call_ltv, ) normalized_inputs = normalize_historical_scenario_inputs( underlying_units=underlying_units, loan_amount=loan_amount, margin_call_ltv=margin_call_ltv, ) template = self.template_service.get_template(template_slug) initial_portfolio = materialize_backtest_portfolio_state( symbol=normalized_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, ) scenario = BacktestScenario( scenario_id=( f"{normalized_symbol.lower()}-{start_date.isoformat()}-{end_date.isoformat()}-{template.slug}" ), display_name=f"{normalized_symbol} backtest {start_date.isoformat()} → {end_date.isoformat()}", symbol=normalized_symbol, start_date=start_date, end_date=end_date, initial_portfolio=initial_portfolio, template_refs=(TemplateRef(slug=template.slug, version=template.version),), provider_ref=ProviderRef( provider_id=self.backtest_service.provider.provider_id, pricing_mode=self.backtest_service.provider.pricing_mode, ), ) return BacktestPageRunResult( scenario=scenario, run_result=self.backtest_service.run_scenario(scenario), entry_spot=entry_spot, )