refactor(pre-alpha): fail closed on historical fixture bounds
This commit is contained in:
@@ -121,7 +121,9 @@ class EventComparisonService:
|
|||||||
provider_ref: ProviderRef | None = None,
|
provider_ref: ProviderRef | None = None,
|
||||||
history: list[DailyClosePoint] | None = None,
|
history: list[DailyClosePoint] | None = None,
|
||||||
) -> BacktestScenario:
|
) -> 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:
|
if not selected_template_slugs:
|
||||||
raise ValueError("Event comparison requires at least one template slug")
|
raise ValueError("Event comparison requires at least one template slug")
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ from app.models.backtest import (
|
|||||||
ProviderRef,
|
ProviderRef,
|
||||||
TemplateRef,
|
TemplateRef,
|
||||||
)
|
)
|
||||||
from app.services.backtesting.historical_provider import (
|
from app.services.backtesting.historical_provider import DailyClosePoint
|
||||||
DailyClosePoint,
|
|
||||||
HistoricalPriceSource,
|
|
||||||
YFinanceHistoricalPriceSource,
|
|
||||||
)
|
|
||||||
from app.services.backtesting.service import BacktestService
|
from app.services.backtesting.service import BacktestService
|
||||||
from app.services.strategy_templates import StrategyTemplateService
|
from app.services.strategy_templates import StrategyTemplateService
|
||||||
|
|
||||||
@@ -40,19 +36,18 @@ DETERMINISTIC_UI_FIXTURE_HISTORY: tuple[DailyClosePoint, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FixtureFallbackHistoricalPriceSource:
|
class DeterministicBacktestFixtureSource:
|
||||||
def __init__(self, fallback: HistoricalPriceSource | None = None) -> None:
|
|
||||||
self.fallback = fallback or YFinanceHistoricalPriceSource()
|
|
||||||
|
|
||||||
def load_daily_closes(self, symbol: str, start_date: date, end_date: date) -> list[DailyClosePoint]:
|
def load_daily_closes(self, symbol: str, start_date: date, end_date: date) -> list[DailyClosePoint]:
|
||||||
normalized_symbol = symbol.strip().upper()
|
normalized_symbol = symbol.strip().upper()
|
||||||
if (
|
if (
|
||||||
normalized_symbol == SUPPORTED_BACKTEST_PAGE_SYMBOL
|
normalized_symbol != SUPPORTED_BACKTEST_PAGE_SYMBOL
|
||||||
and start_date == date(2024, 1, 2)
|
or start_date != date(2024, 1, 2)
|
||||||
and end_date == date(2024, 1, 8)
|
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)
|
return list(DETERMINISTIC_UI_FIXTURE_HISTORY)
|
||||||
return self.fallback.load_daily_closes(normalized_symbol, start_date, end_date)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -75,7 +70,7 @@ class BacktestPageService:
|
|||||||
)
|
)
|
||||||
if backtest_service is None:
|
if backtest_service is None:
|
||||||
provider = self.backtest_service.provider
|
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]]:
|
def template_options(self, symbol: str = "GLD") -> list[dict[str, str | int]]:
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
version: 1
|
version: 1
|
||||||
updated_at: 2026-03-25
|
updated_at: 2026-03-26
|
||||||
structure:
|
structure:
|
||||||
backlog_dir: docs/roadmap/backlog
|
backlog_dir: docs/roadmap/backlog
|
||||||
in_progress_dir: docs/roadmap/in-progress
|
in_progress_dir: docs/roadmap/in-progress
|
||||||
|
|||||||
@@ -150,3 +150,38 @@ def test_backtest_page_service_validation_errors_are_user_facing(kwargs: dict[st
|
|||||||
|
|
||||||
with pytest.raises(ValueError, match=message):
|
with pytest.raises(ValueError, match=message):
|
||||||
service.run_read_only_scenario(**kwargs)
|
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
|
||||||
|
|||||||
@@ -281,4 +281,37 @@ def test_event_comparison_requires_template_selection() -> None:
|
|||||||
loan_amount=68_000.0,
|
loan_amount=68_000.0,
|
||||||
margin_call_ltv=0.75,
|
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",
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user