117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
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
|