from __future__ import annotations import json from datetime import date from pathlib import Path from app.models.event_preset import EventPreset, EventScenarioOverrides DEFAULT_EVENT_PRESET_FILE = Path(__file__).resolve().parents[2] / "config" / "event_presets.json" def default_event_presets() -> list[EventPreset]: return [ EventPreset( event_preset_id="gld-jan-2024-selloff-v1", slug="gld-jan-2024-selloff", display_name="GLD January 2024 Selloff", symbol="GLD", window_start=date(2024, 1, 2), window_end=date(2024, 1, 8), anchor_date=date(2024, 1, 4), event_type="selloff", tags=("system", "selloff", "macro"), description="Short January 2024 selloff window for deterministic synthetic event comparisons.", scenario_overrides=EventScenarioOverrides( default_template_slugs=( "protective-put-atm-12m", "protective-put-95pct-12m", "protective-put-90pct-12m", "ladder-50-50-atm-95pct-12m", ) ), ), EventPreset( event_preset_id="gld-jan-2024-drawdown-v1", slug="gld-jan-2024-drawdown", display_name="GLD January 2024 Drawdown", symbol="GLD", window_start=date(2024, 1, 2), window_end=date(2024, 1, 8), anchor_date=date(2024, 1, 5), event_type="selloff", tags=("system", "drawdown"), description="January 2024 drawdown preset for deterministic synthetic event comparison runs.", scenario_overrides=EventScenarioOverrides( lookback_days=0, recovery_days=0, default_template_slugs=( "protective-put-atm-12m", "ladder-50-50-atm-95pct-12m", "ladder-33-33-33-atm-95pct-90pct-12m", ), ), ), EventPreset( event_preset_id="gld-jan-2024-stress-window-v1", slug="gld-jan-2024-stress-window", display_name="GLD January 2024 Stress Window", symbol="GLD", window_start=date(2024, 1, 2), window_end=date(2024, 1, 8), anchor_date=None, event_type="stress_test", tags=("system", "stress_test"), description="Stress-window preset with a modest warmup and recovery tail for report scaffolding.", scenario_overrides=EventScenarioOverrides( lookback_days=0, recovery_days=0, default_template_slugs=( "protective-put-atm-12m", "protective-put-95pct-12m", ), ), ), ] class FileEventPresetRepository: def __init__(self, path: str | Path = DEFAULT_EVENT_PRESET_FILE) -> None: self.path = Path(path) def list_presets(self) -> list[EventPreset]: self._ensure_seeded() payload = json.loads(self.path.read_text()) return [EventPreset.from_dict(item) for item in payload.get("presets", [])] def get_by_slug(self, slug: str) -> EventPreset | None: return next((preset for preset in self.list_presets() if preset.slug == slug), None) def save_all(self, presets: list[EventPreset]) -> None: self.path.parent.mkdir(parents=True, exist_ok=True) payload = {"presets": [preset.to_dict() for preset in presets]} self.path.write_text(json.dumps(payload, indent=2) + "\n") def _ensure_seeded(self) -> None: if self.path.exists(): return self.save_all(default_event_presets()) class EventPresetService: def __init__(self, repository: FileEventPresetRepository | None = None) -> None: self.repository = repository or FileEventPresetRepository() def list_presets(self, symbol: str | None = None) -> list[EventPreset]: presets = self.repository.list_presets() if symbol is None: return presets normalized_symbol = symbol.strip().upper() return [preset for preset in presets if preset.symbol.strip().upper() == normalized_symbol] def get_preset(self, slug: str) -> EventPreset: preset = self.repository.get_by_slug(slug) if preset is None: raise KeyError(f"Unknown event preset: {slug}") return preset