Files
vault-dash/app/services/event_presets.py

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