from __future__ import annotations from datetime import date import pytest from app.services.backtesting.historical_provider import SyntheticHistoricalProvider from app.services.backtesting.service import BacktestService from app.services.backtesting.ui_service import BacktestPageService from tests.helpers_backtest_sources import StaticBacktestSource def test_backtest_page_service_uses_fixture_window_for_deterministic_run() -> None: service = BacktestPageService() entry_spot = service.derive_entry_spot("GLD", date(2024, 1, 2), date(2024, 1, 8)) result = service.run_read_only_scenario( 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, ) assert entry_spot == 100.0 assert result.scenario.initial_portfolio.entry_spot == 100.0 assert result.run_result.template_results[0].summary_metrics.margin_threshold_breached_unhedged is True assert result.run_result.template_results[0].summary_metrics.margin_threshold_breached_hedged is False assert [point.date.isoformat() for point in result.run_result.template_results[0].daily_path] == [ "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05", "2024-01-08", ] def test_backtest_non_default_template_slug_runs_successfully() -> None: service = BacktestPageService() options = service.template_options("GLD") non_default_slug = str(options[1]["slug"]) result = service.run_read_only_scenario( symbol="GLD", start_date=date(2024, 1, 2), end_date=date(2024, 1, 8), template_slug=non_default_slug, underlying_units=1000.0, loan_amount=68000.0, margin_call_ltv=0.75, ) assert result.scenario.template_refs[0].slug == non_default_slug assert result.run_result.template_results[0].template_slug == non_default_slug def test_backtest_page_service_keeps_fixture_history_while_using_caller_portfolio_inputs() -> None: service = BacktestPageService() result = service.run_read_only_scenario( symbol="GLD", start_date=date(2024, 1, 2), end_date=date(2024, 1, 8), template_slug="protective-put-atm-12m", underlying_units=9680.0, loan_amount=222000.0, margin_call_ltv=0.80, ) assert result.entry_spot == 100.0 assert result.scenario.initial_portfolio.underlying_units == 9680.0 assert result.scenario.initial_portfolio.loan_amount == 222000.0 assert result.scenario.initial_portfolio.margin_call_ltv == 0.80 assert [point.date.isoformat() for point in result.run_result.template_results[0].daily_path] == [ "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05", "2024-01-08", ] @pytest.mark.parametrize( ("kwargs", "message"), [ ( { "symbol": "", "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, }, "Symbol is required", ), ( { "symbol": "GLD", "start_date": date(2024, 1, 8), "end_date": date(2024, 1, 2), "template_slug": "protective-put-atm-12m", "underlying_units": 1000.0, "loan_amount": 68000.0, "margin_call_ltv": 0.75, }, "Start date must be on or before end date", ), ( { "symbol": "GLD", "start_date": date(2024, 1, 2), "end_date": date(2024, 1, 8), "template_slug": "protective-put-atm-12m", "underlying_units": 0.0, "loan_amount": 68000.0, "margin_call_ltv": 0.75, }, "Underlying units must be positive", ), ( { "symbol": "TLT", "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, }, "BT-001A backtests are currently limited to GLD on this page", ), ( { "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": 145000.0, "margin_call_ltv": 0.75, }, "Historical scenario starts undercollateralized", ), ], ) def test_backtest_page_service_validation_errors_are_user_facing(kwargs: dict[str, object], message: str) -> None: service = BacktestPageService() 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_requires_supported_fixture_window_even_with_supplied_entry_spot() -> None: service = BacktestPageService() with pytest.raises(ValueError, match="deterministic fixture data only supports GLD"): service.validate_preview_inputs( 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, entry_spot=100.0, ) def test_backtest_preview_validation_accepts_close_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.005, ) assert entry_spot == 100.0 def test_backtest_preview_validation_rejects_mismatched_supplied_entry_spot() -> None: service = BacktestPageService() with pytest.raises(ValueError, match="does not match derived historical 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=99.0, ) def test_backtest_page_service_does_not_mutate_injected_backtest_service() -> None: provider = SyntheticHistoricalProvider( source=StaticBacktestSource(), implied_volatility=0.2, risk_free_rate=0.01, ) injected_service = BacktestService(provider=provider) page_service = BacktestPageService(backtest_service=injected_service) history = injected_service.provider.load_history("GLD", date(2024, 1, 3), date(2024, 1, 3)) assert history[0].close == 123.0 assert page_service.template_service is injected_service.template_service assert page_service.backtest_service is not injected_service assert page_service.backtest_service.provider.implied_volatility == 0.2 assert page_service.backtest_service.provider.risk_free_rate == 0.01 seeded_history = page_service.backtest_service.provider.load_history("GLD", date(2024, 1, 2), date(2024, 1, 8)) assert seeded_history[0].close == 100.0 def test_backtest_page_service_uses_injected_provider_identity_in_provider_ref() -> None: class CustomProvider(SyntheticHistoricalProvider): provider_id = "custom_v1" pricing_mode = "custom_mode" provider = CustomProvider(source=StaticBacktestSource(), implied_volatility=0.2, risk_free_rate=0.01) injected_service = BacktestService(provider=provider) page_service = BacktestPageService(backtest_service=injected_service) result = page_service.run_read_only_scenario( 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, ) assert result.scenario.provider_ref.provider_id == "custom_v1" assert result.scenario.provider_ref.pricing_mode == "custom_mode"