feat(BT-003): add event preset backtest comparison
This commit is contained in:
105
app/models/event_preset.py
Normal file
105
app/models/event_preset.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Literal
|
||||
|
||||
EventType = Literal["selloff", "recovery", "stress_test"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EventScenarioOverrides:
|
||||
lookback_days: int | None = None
|
||||
recovery_days: int | None = None
|
||||
default_template_slugs: tuple[str, ...] = field(default_factory=tuple)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.lookback_days is not None and self.lookback_days < 0:
|
||||
raise ValueError("lookback_days must be non-negative")
|
||||
if self.recovery_days is not None and self.recovery_days < 0:
|
||||
raise ValueError("recovery_days must be non-negative")
|
||||
if any(not slug for slug in self.default_template_slugs):
|
||||
raise ValueError("default_template_slugs must not contain empty values")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"lookback_days": self.lookback_days,
|
||||
"recovery_days": self.recovery_days,
|
||||
"default_template_slugs": list(self.default_template_slugs),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, object] | None) -> EventScenarioOverrides:
|
||||
if payload is None:
|
||||
return cls()
|
||||
return cls(
|
||||
lookback_days=payload.get("lookback_days"), # type: ignore[arg-type]
|
||||
recovery_days=payload.get("recovery_days"), # type: ignore[arg-type]
|
||||
default_template_slugs=tuple(payload.get("default_template_slugs", [])), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EventPreset:
|
||||
event_preset_id: str
|
||||
slug: str
|
||||
display_name: str
|
||||
symbol: str
|
||||
window_start: date
|
||||
window_end: date
|
||||
anchor_date: date | None
|
||||
event_type: EventType
|
||||
tags: tuple[str, ...] = field(default_factory=tuple)
|
||||
description: str = ""
|
||||
scenario_overrides: EventScenarioOverrides = field(default_factory=EventScenarioOverrides)
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.event_preset_id:
|
||||
raise ValueError("event_preset_id is required")
|
||||
if not self.slug:
|
||||
raise ValueError("slug is required")
|
||||
if not self.display_name:
|
||||
raise ValueError("display_name is required")
|
||||
if not self.symbol:
|
||||
raise ValueError("symbol is required")
|
||||
if self.window_start > self.window_end:
|
||||
raise ValueError("window_start must be on or before window_end")
|
||||
if self.anchor_date is not None and not (self.window_start <= self.anchor_date <= self.window_end):
|
||||
raise ValueError("anchor_date must fall inside the event window")
|
||||
if self.event_type not in {"selloff", "recovery", "stress_test"}:
|
||||
raise ValueError("unsupported event_type")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"event_preset_id": self.event_preset_id,
|
||||
"slug": self.slug,
|
||||
"display_name": self.display_name,
|
||||
"symbol": self.symbol,
|
||||
"window_start": self.window_start.isoformat(),
|
||||
"window_end": self.window_end.isoformat(),
|
||||
"anchor_date": self.anchor_date.isoformat() if self.anchor_date is not None else None,
|
||||
"event_type": self.event_type,
|
||||
"tags": list(self.tags),
|
||||
"description": self.description,
|
||||
"scenario_overrides": self.scenario_overrides.to_dict(),
|
||||
"created_at": self.created_at.isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, object]) -> EventPreset:
|
||||
anchor_date = payload.get("anchor_date")
|
||||
return cls(
|
||||
event_preset_id=str(payload["event_preset_id"]),
|
||||
slug=str(payload["slug"]),
|
||||
display_name=str(payload["display_name"]),
|
||||
symbol=str(payload["symbol"]),
|
||||
window_start=date.fromisoformat(str(payload["window_start"])),
|
||||
window_end=date.fromisoformat(str(payload["window_end"])),
|
||||
anchor_date=date.fromisoformat(str(anchor_date)) if anchor_date else None,
|
||||
event_type=payload["event_type"], # type: ignore[arg-type]
|
||||
tags=tuple(payload.get("tags", [])), # type: ignore[arg-type]
|
||||
description=str(payload.get("description", "")),
|
||||
scenario_overrides=EventScenarioOverrides.from_dict(payload.get("scenario_overrides")), # type: ignore[arg-type]
|
||||
created_at=datetime.fromisoformat(str(payload["created_at"])),
|
||||
)
|
||||
Reference in New Issue
Block a user