diff --git a/app/services/backtesting/comparison.py b/app/services/backtesting/comparison.py index 49b44f4..f58a975 100644 --- a/app/services/backtesting/comparison.py +++ b/app/services/backtesting/comparison.py @@ -121,7 +121,9 @@ class EventComparisonService: provider_ref: ProviderRef | None = None, history: list[DailyClosePoint] | None = None, ) -> BacktestScenario: - selected_template_slugs = tuple(template_slugs or preset.scenario_overrides.default_template_slugs) + 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") diff --git a/app/services/backtesting/ui_service.py b/app/services/backtesting/ui_service.py index 6f007bc..6703788 100644 --- a/app/services/backtesting/ui_service.py +++ b/app/services/backtesting/ui_service.py @@ -10,11 +10,7 @@ from app.models.backtest import ( ProviderRef, TemplateRef, ) -from app.services.backtesting.historical_provider import ( - DailyClosePoint, - HistoricalPriceSource, - YFinanceHistoricalPriceSource, -) +from app.services.backtesting.historical_provider import DailyClosePoint from app.services.backtesting.service import BacktestService from app.services.strategy_templates import StrategyTemplateService @@ -40,19 +36,18 @@ DETERMINISTIC_UI_FIXTURE_HISTORY: tuple[DailyClosePoint, ...] = ( ) -class FixtureFallbackHistoricalPriceSource: - def __init__(self, fallback: HistoricalPriceSource | None = None) -> None: - self.fallback = fallback or YFinanceHistoricalPriceSource() - +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 - and start_date == date(2024, 1, 2) - and end_date == date(2024, 1, 8) + normalized_symbol != SUPPORTED_BACKTEST_PAGE_SYMBOL + or start_date != date(2024, 1, 2) + or end_date != date(2024, 1, 8) ): - return list(DETERMINISTIC_UI_FIXTURE_HISTORY) - return self.fallback.load_daily_closes(normalized_symbol, start_date, end_date) + 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) @@ -75,7 +70,7 @@ class BacktestPageService: ) if backtest_service is None: provider = self.backtest_service.provider - provider.source = FixtureFallbackHistoricalPriceSource(provider.source) + provider.source = DeterministicBacktestFixtureSource() def template_options(self, symbol: str = "GLD") -> list[dict[str, str | int]]: return [ diff --git a/docs/roadmap/ROADMAP.yaml b/docs/roadmap/ROADMAP.yaml index bc685cb..95e00e0 100644 --- a/docs/roadmap/ROADMAP.yaml +++ b/docs/roadmap/ROADMAP.yaml @@ -1,5 +1,5 @@ version: 1 -updated_at: 2026-03-25 +updated_at: 2026-03-26 structure: backlog_dir: docs/roadmap/backlog in_progress_dir: docs/roadmap/in-progress diff --git a/tests/test_backtest_ui.py b/tests/test_backtest_ui.py index 32037f3..ab922ba 100644 --- a/tests/test_backtest_ui.py +++ b/tests/test_backtest_ui.py @@ -150,3 +150,38 @@ def test_backtest_page_service_validation_errors_are_user_facing(kwargs: dict[st with pytest.raises(ValueError, match=message): service.run_read_only_scenario(**kwargs) + + +def test_backtest_page_service_fails_closed_outside_seeded_fixture_window() -> None: + service = BacktestPageService() + + with pytest.raises(ValueError, match="deterministic fixture data only supports GLD"): + service.derive_entry_spot("GLD", date(2024, 1, 3), date(2024, 1, 8)) + + with pytest.raises(ValueError, match="deterministic fixture data only supports GLD"): + service.run_read_only_scenario( + symbol="GLD", + start_date=date(2024, 1, 3), + end_date=date(2024, 1, 8), + template_slug="protective-put-atm-12m", + underlying_units=1000.0, + loan_amount=68000.0, + margin_call_ltv=0.75, + ) + + +def test_backtest_preview_validation_reuses_supplied_entry_spot() -> None: + service = BacktestPageService() + + entry_spot = service.validate_preview_inputs( + symbol="GLD", + start_date=date(2024, 1, 2), + end_date=date(2024, 1, 8), + template_slug="protective-put-atm-12m", + underlying_units=1000.0, + loan_amount=68000.0, + margin_call_ltv=0.75, + entry_spot=100.0, + ) + + assert entry_spot == 100.0 diff --git a/tests/test_event_comparison.py b/tests/test_event_comparison.py index 2fc3686..2802ec0 100644 --- a/tests/test_event_comparison.py +++ b/tests/test_event_comparison.py @@ -281,4 +281,37 @@ def test_event_comparison_requires_template_selection() -> None: loan_amount=68_000.0, margin_call_ltv=0.75, ), + template_slugs=(), ) + + +def test_event_comparison_uses_preset_defaults_only_when_template_slugs_are_omitted() -> None: + provider = SyntheticHistoricalProvider( + source=FakeHistorySource(FIXTURE_HISTORY), + implied_volatility=0.35, + risk_free_rate=0.0, + ) + service = EventComparisonService( + provider=provider, + template_service=StrategyTemplateService(), + event_preset_service=EventPresetService(repository=FileEventPresetRepository()), + ) + + scenario = service.materialize_scenario( + service.event_preset_service.get_preset("gld-jan-2024-selloff"), + initial_portfolio=BacktestPortfolioState( + currency="USD", + underlying_units=1000.0, + entry_spot=100.0, + loan_amount=68_000.0, + margin_call_ltv=0.75, + ), + template_slugs=None, + ) + + assert [ref.slug for ref in scenario.template_refs] == [ + "protective-put-atm-12m", + "protective-put-95pct-12m", + "protective-put-90pct-12m", + "ladder-50-50-atm-95pct-12m", + ]