feat(BT-003A): add event comparison page
This commit is contained in:
@@ -11,7 +11,7 @@ from app.models.backtest import (
|
||||
TemplateRef,
|
||||
)
|
||||
from app.models.event_preset import EventPreset
|
||||
from app.services.backtesting.historical_provider import SyntheticHistoricalProvider
|
||||
from app.services.backtesting.historical_provider import DailyClosePoint, SyntheticHistoricalProvider
|
||||
from app.services.backtesting.service import BacktestService
|
||||
from app.services.event_presets import EventPresetService
|
||||
from app.services.strategy_templates import StrategyTemplateService
|
||||
@@ -48,6 +48,111 @@ class EventComparisonService:
|
||||
template_slugs=template_slugs,
|
||||
provider_ref=provider_ref,
|
||||
)
|
||||
return self._compare_materialized_event(preset=preset, scenario=scenario)
|
||||
|
||||
def compare_event_from_inputs(
|
||||
self,
|
||||
*,
|
||||
preset_slug: str,
|
||||
underlying_units: float,
|
||||
loan_amount: float,
|
||||
margin_call_ltv: float,
|
||||
template_slugs: tuple[str, ...] | None = None,
|
||||
currency: str = "USD",
|
||||
cash_balance: float = 0.0,
|
||||
financing_rate: float = 0.0,
|
||||
provider_ref: ProviderRef | None = None,
|
||||
) -> EventComparisonReport:
|
||||
preset = self.event_preset_service.get_preset(preset_slug)
|
||||
scenario = self.preview_scenario_from_inputs(
|
||||
preset_slug=preset_slug,
|
||||
underlying_units=underlying_units,
|
||||
loan_amount=loan_amount,
|
||||
margin_call_ltv=margin_call_ltv,
|
||||
template_slugs=template_slugs,
|
||||
currency=currency,
|
||||
cash_balance=cash_balance,
|
||||
financing_rate=financing_rate,
|
||||
provider_ref=provider_ref,
|
||||
)
|
||||
return self._compare_materialized_event(preset=preset, scenario=scenario)
|
||||
|
||||
def preview_scenario_from_inputs(
|
||||
self,
|
||||
*,
|
||||
preset_slug: str,
|
||||
underlying_units: float,
|
||||
loan_amount: float,
|
||||
margin_call_ltv: float,
|
||||
template_slugs: tuple[str, ...] | None = None,
|
||||
currency: str = "USD",
|
||||
cash_balance: float = 0.0,
|
||||
financing_rate: float = 0.0,
|
||||
provider_ref: ProviderRef | None = None,
|
||||
) -> BacktestScenario:
|
||||
preset = self.event_preset_service.get_preset(preset_slug)
|
||||
history = self._load_preset_history(preset)
|
||||
entry_spot = history[0].close
|
||||
initial_portfolio = BacktestPortfolioState(
|
||||
currency=currency,
|
||||
underlying_units=underlying_units,
|
||||
entry_spot=entry_spot,
|
||||
loan_amount=loan_amount,
|
||||
margin_call_ltv=margin_call_ltv,
|
||||
cash_balance=cash_balance,
|
||||
financing_rate=financing_rate,
|
||||
)
|
||||
return self.materialize_scenario(
|
||||
preset,
|
||||
initial_portfolio=initial_portfolio,
|
||||
template_slugs=template_slugs,
|
||||
provider_ref=provider_ref,
|
||||
history=history,
|
||||
)
|
||||
|
||||
def materialize_scenario(
|
||||
self,
|
||||
preset: EventPreset,
|
||||
*,
|
||||
initial_portfolio: BacktestPortfolioState,
|
||||
template_slugs: tuple[str, ...] | None = None,
|
||||
provider_ref: ProviderRef | None = None,
|
||||
history: list[DailyClosePoint] | None = None,
|
||||
) -> BacktestScenario:
|
||||
selected_template_slugs = tuple(template_slugs or preset.scenario_overrides.default_template_slugs)
|
||||
if not selected_template_slugs:
|
||||
raise ValueError("Event comparison requires at least one template slug")
|
||||
|
||||
resolved_history = history or self._load_preset_history(preset)
|
||||
scenario_portfolio = BacktestPortfolioState(
|
||||
currency=initial_portfolio.currency,
|
||||
underlying_units=initial_portfolio.underlying_units,
|
||||
entry_spot=resolved_history[0].close,
|
||||
loan_amount=initial_portfolio.loan_amount,
|
||||
margin_call_ltv=initial_portfolio.margin_call_ltv,
|
||||
cash_balance=initial_portfolio.cash_balance,
|
||||
financing_rate=initial_portfolio.financing_rate,
|
||||
)
|
||||
template_refs = tuple(
|
||||
TemplateRef(slug=slug, version=self.template_service.get_template(slug).version)
|
||||
for slug in selected_template_slugs
|
||||
)
|
||||
return BacktestScenario(
|
||||
scenario_id=f"event-{preset.slug}",
|
||||
display_name=preset.display_name,
|
||||
symbol=preset.symbol,
|
||||
start_date=resolved_history[0].date,
|
||||
end_date=resolved_history[-1].date,
|
||||
initial_portfolio=scenario_portfolio,
|
||||
template_refs=template_refs,
|
||||
provider_ref=provider_ref
|
||||
or ProviderRef(
|
||||
provider_id=self.provider.provider_id,
|
||||
pricing_mode=self.provider.pricing_mode,
|
||||
),
|
||||
)
|
||||
|
||||
def _compare_materialized_event(self, *, preset: EventPreset, scenario: BacktestScenario) -> EventComparisonReport:
|
||||
run_result = self.backtest_service.run_scenario(scenario)
|
||||
ranked_results = sorted(
|
||||
run_result.template_results,
|
||||
@@ -65,6 +170,7 @@ class EventComparisonService:
|
||||
template_slug=result.template_slug,
|
||||
template_name=result.template_name,
|
||||
survived_margin_call=not result.summary_metrics.margin_threshold_breached_hedged,
|
||||
margin_call_days_hedged=result.summary_metrics.margin_call_days_hedged,
|
||||
max_ltv_hedged=result.summary_metrics.max_ltv_hedged,
|
||||
hedge_cost=result.summary_metrics.total_hedge_cost,
|
||||
final_equity=result.summary_metrics.end_value_hedged_net,
|
||||
@@ -79,48 +185,10 @@ class EventComparisonService:
|
||||
run_result=run_result,
|
||||
)
|
||||
|
||||
def materialize_scenario(
|
||||
self,
|
||||
preset: EventPreset,
|
||||
*,
|
||||
initial_portfolio: BacktestPortfolioState,
|
||||
template_slugs: tuple[str, ...] | None = None,
|
||||
provider_ref: ProviderRef | None = None,
|
||||
) -> BacktestScenario:
|
||||
selected_template_slugs = tuple(template_slugs or preset.scenario_overrides.default_template_slugs)
|
||||
if not selected_template_slugs:
|
||||
raise ValueError("Event comparison requires at least one template slug")
|
||||
|
||||
def _load_preset_history(self, preset: EventPreset) -> list[DailyClosePoint]:
|
||||
requested_start = preset.window_start - timedelta(days=preset.scenario_overrides.lookback_days or 0)
|
||||
requested_end = preset.window_end + timedelta(days=preset.scenario_overrides.recovery_days or 0)
|
||||
history = self.provider.load_history(preset.symbol, requested_start, requested_end)
|
||||
if not history:
|
||||
raise ValueError(f"No historical prices found for event preset: {preset.slug}")
|
||||
|
||||
scenario_portfolio = BacktestPortfolioState(
|
||||
currency=initial_portfolio.currency,
|
||||
underlying_units=initial_portfolio.underlying_units,
|
||||
entry_spot=history[0].close,
|
||||
loan_amount=initial_portfolio.loan_amount,
|
||||
margin_call_ltv=initial_portfolio.margin_call_ltv,
|
||||
cash_balance=initial_portfolio.cash_balance,
|
||||
financing_rate=initial_portfolio.financing_rate,
|
||||
)
|
||||
template_refs = tuple(
|
||||
TemplateRef(slug=slug, version=self.template_service.get_template(slug).version)
|
||||
for slug in selected_template_slugs
|
||||
)
|
||||
return BacktestScenario(
|
||||
scenario_id=f"event-{preset.slug}",
|
||||
display_name=preset.display_name,
|
||||
symbol=preset.symbol,
|
||||
start_date=history[0].date,
|
||||
end_date=history[-1].date,
|
||||
initial_portfolio=scenario_portfolio,
|
||||
template_refs=template_refs,
|
||||
provider_ref=provider_ref
|
||||
or ProviderRef(
|
||||
provider_id=self.provider.provider_id,
|
||||
pricing_mode=self.provider.pricing_mode,
|
||||
),
|
||||
)
|
||||
return history
|
||||
|
||||
Reference in New Issue
Block a user