refactor(BT-001C): share historical fixture provider

This commit is contained in:
Bu5hm4nn
2026-03-27 21:41:50 +01:00
parent 477514f838
commit 554a41a060
8 changed files with 236 additions and 110 deletions

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from enum import StrEnum
from typing import Any
from app.services.backtesting.historical_provider import DailyClosePoint, SyntheticHistoricalProvider
SEEDED_GLD_2024_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 WindowPolicy(StrEnum):
EXACT = "exact"
BOUNDED = "bounded"
@dataclass(frozen=True)
class SharedHistoricalFixtureSource:
feature_label: str
supported_symbol: str
history: tuple[DailyClosePoint, ...]
window_policy: WindowPolicy
@property
def start_date(self) -> date:
return self.history[0].date
@property
def end_date(self) -> date:
return self.history[-1].date
def load_daily_closes(self, symbol: str, start_date: date, end_date: date) -> list[DailyClosePoint]:
if start_date > end_date:
raise ValueError("start_date must be on or before end_date")
normalized_symbol = symbol.strip().upper()
if normalized_symbol != self.supported_symbol.strip().upper():
raise ValueError(
f"{self.feature_label} deterministic fixture data only supports {self.supported_symbol} on this page"
)
if self.window_policy is WindowPolicy.EXACT:
if start_date != self.start_date or end_date != self.end_date:
raise ValueError(
f"{self.feature_label} deterministic fixture data only supports {self.supported_symbol} "
f"on the seeded {self.start_date.isoformat()} through {self.end_date.isoformat()} window"
)
else:
if start_date < self.start_date or end_date > self.end_date:
raise ValueError(
f"{self.feature_label} deterministic fixture data only supports the seeded "
f"{self.start_date.isoformat()} through {self.end_date.isoformat()} window"
)
return [point for point in self.history if start_date <= point.date <= end_date]
class FixtureBoundSyntheticHistoricalProvider:
def __init__(self, base_provider: SyntheticHistoricalProvider, source: SharedHistoricalFixtureSource) -> None:
self.base_provider = base_provider
self.source = source
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)
return sorted(rows, key=lambda row: row.date)
def __getattr__(self, name: str) -> Any:
return getattr(self.base_provider, name)
def build_backtest_ui_fixture_source() -> SharedHistoricalFixtureSource:
return SharedHistoricalFixtureSource(
feature_label="BT-001A",
supported_symbol="GLD",
history=SEEDED_GLD_2024_FIXTURE_HISTORY,
window_policy=WindowPolicy.EXACT,
)
def build_event_comparison_fixture_source() -> SharedHistoricalFixtureSource:
return SharedHistoricalFixtureSource(
feature_label="BT-003A",
supported_symbol="GLD",
history=SEEDED_GLD_2024_FIXTURE_HISTORY,
window_policy=WindowPolicy.BOUNDED,
)
def bind_fixture_source(
base_provider: SyntheticHistoricalProvider,
source: SharedHistoricalFixtureSource,
) -> FixtureBoundSyntheticHistoricalProvider:
return FixtureBoundSyntheticHistoricalProvider(base_provider=base_provider, source=source)

View File

@@ -13,13 +13,7 @@ from app.models.backtest import (
ProviderRef,
TemplateRef,
)
from app.services.backtesting.historical_provider import (
DailyClosePoint,
HistoricalOptionMark,
HistoricalOptionPosition,
SyntheticHistoricalProvider,
SyntheticOptionQuote,
)
from app.services.backtesting.fixture_source import bind_fixture_source, build_backtest_ui_fixture_source
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
@@ -37,29 +31,6 @@ def _validate_initial_collateral(underlying_units: float, entry_spot: float, loa
)
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
@@ -67,40 +38,6 @@ class BacktestPageRunResult:
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,
@@ -112,7 +49,7 @@ class BacktestPageService:
provider=None,
)
self.template_service = template_service or base_service.template_service
fixture_provider = FixtureBoundHistoricalProvider(base_service.provider)
fixture_provider = bind_fixture_source(base_service.provider, build_backtest_ui_fixture_source())
self.backtest_service = copy(base_service)
self.backtest_service.provider = fixture_provider
self.backtest_service.template_service = self.template_service