chore: pre-existing in-tree work (event-study/research/vintages/prices + migration script + integrity docs)
Committing the prior uncommitted working-tree state that predates this session's data-source work (was already modified/untracked at session start) so the tree is clean before push. Includes: event-study + research report integrity/forward observation work, prices tests, research hash migration script, and the 2026-08-23/24 engineering-log + test-evidence notes. Verified green as part of the full 362-test suite.
This commit is contained in:
@@ -6,6 +6,7 @@ import math
|
||||
from datetime import date, datetime, timezone
|
||||
from statistics import fmean
|
||||
from typing import Any, Mapping, Sequence
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
class EventStudyError(ValueError):
|
||||
@@ -34,6 +35,18 @@ def _canonical_timestamp(value: str) -> str:
|
||||
return parsed.astimezone(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _parse_known_at(value: Any, symbol: str) -> datetime:
|
||||
if not isinstance(value, str):
|
||||
raise EventStudyError(f"known_at is required for {symbol}")
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise EventStudyError(f"invalid known_at for {symbol}") from exc
|
||||
if parsed.tzinfo is None:
|
||||
raise EventStudyError(f"known_at must include a timezone for {symbol}")
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def assess_backtest_readiness(vintages: Sequence[Mapping[str, Any]], min_events: int = 12) -> dict[str, Any]:
|
||||
if isinstance(min_events, bool) or min_events < 1:
|
||||
raise EventStudyError("min_events must be positive")
|
||||
@@ -63,13 +76,16 @@ def assess_backtest_readiness(vintages: Sequence[Mapping[str, Any]], min_events:
|
||||
}
|
||||
|
||||
|
||||
def _price_map(symbol: str, rows: Sequence[Mapping[str, Any]]) -> dict[date, float]:
|
||||
def _price_map(symbol: str, rows: Sequence[Mapping[str, Any]], *, require_known_at: bool = False) -> dict[date, tuple[float, datetime | None]]:
|
||||
if not rows:
|
||||
raise EventStudyError(f"missing prices for {symbol}")
|
||||
values: dict[date, float] = {}
|
||||
values: dict[date, tuple[float, datetime | None]] = {}
|
||||
for row in rows:
|
||||
try:
|
||||
trading_day = _parse_date(str(row["date"]))
|
||||
session_date_value = row.get("session_date")
|
||||
if session_date_value is None:
|
||||
session_date_value = row["date"]
|
||||
trading_day = _parse_date(str(session_date_value))
|
||||
close = float(row["close"])
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise EventStudyError(f"invalid price row for {symbol}") from exc
|
||||
@@ -77,11 +93,22 @@ def _price_map(symbol: str, rows: Sequence[Mapping[str, Any]]) -> dict[date, flo
|
||||
raise EventStudyError(f"invalid close for {symbol}")
|
||||
if trading_day in values:
|
||||
raise EventStudyError(f"duplicate price date for {symbol}")
|
||||
values[trading_day] = close
|
||||
known_at = _parse_known_at(row.get("known_at"), symbol) if require_known_at else None
|
||||
if known_at is not None:
|
||||
timezone_name = row.get("market_timezone")
|
||||
if not isinstance(timezone_name, str) or not timezone_name.strip():
|
||||
raise EventStudyError(f"market_timezone is required for {symbol}")
|
||||
try:
|
||||
market_date = known_at.astimezone(ZoneInfo(timezone_name)).date()
|
||||
except Exception as exc:
|
||||
raise EventStudyError(f"invalid market_timezone for {symbol}") from exc
|
||||
if market_date > trading_day:
|
||||
raise EventStudyError(f"known_at is after session date for {symbol}")
|
||||
values[trading_day] = (close, known_at)
|
||||
return dict(sorted(values.items()))
|
||||
|
||||
|
||||
def _window_return(series: dict[date, float], event_date: date, window: int, symbol: str, execution_lag_sessions: int) -> float:
|
||||
def _window_return(series: dict[date, tuple[float, datetime | None]], event_date: date, window: int, symbol: str, execution_lag_sessions: int) -> float:
|
||||
dates = list(series)
|
||||
anchor_candidates = [index for index, trading_day in enumerate(dates) if trading_day >= event_date]
|
||||
if not anchor_candidates:
|
||||
@@ -96,7 +123,7 @@ def _window_return(series: dict[date, float], event_date: date, window: int, sym
|
||||
end = anchor + window
|
||||
if end >= len(dates):
|
||||
raise EventStudyError(f"insufficient price history for {symbol} window {window}")
|
||||
return series[dates[end]] / series[dates[anchor]] - 1.0
|
||||
return series[dates[end]][0] / series[dates[anchor]][0] - 1.0
|
||||
|
||||
|
||||
def run_event_study(
|
||||
@@ -108,6 +135,7 @@ def run_event_study(
|
||||
cost_bps: float = 0.0,
|
||||
min_events: int = 12,
|
||||
execution_lag_sessions: int = 1,
|
||||
require_price_known_at: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Calculate weighted post-publication returns from point-in-time events."""
|
||||
|
||||
@@ -128,8 +156,13 @@ def run_event_study(
|
||||
if not math.isfinite(cost_bps) or cost_bps < 0:
|
||||
raise EventStudyError("cost_bps must be finite and non-negative")
|
||||
|
||||
normalized_prices = {str(symbol).upper(): _price_map(str(symbol).upper(), rows) for symbol, rows in prices.items()}
|
||||
normalized_benchmark = _price_map("benchmark", benchmark_prices) if benchmark_prices is not None else None
|
||||
if not isinstance(require_price_known_at, bool):
|
||||
raise EventStudyError("require_price_known_at must be boolean")
|
||||
normalized_prices = {
|
||||
str(symbol).upper(): _price_map(str(symbol).upper(), rows, require_known_at=require_price_known_at)
|
||||
for symbol, rows in prices.items()
|
||||
}
|
||||
normalized_benchmark = _price_map("benchmark", benchmark_prices, require_known_at=require_price_known_at) if benchmark_prices is not None else None
|
||||
seen_event_ids: set[str] = set()
|
||||
event_rows: list[dict[str, Any]] = []
|
||||
for event in events:
|
||||
@@ -189,4 +222,5 @@ def run_event_study(
|
||||
"event_count": len(event_rows),
|
||||
"windows": window_results,
|
||||
"min_events": min_events,
|
||||
"price_known_at_required": require_price_known_at,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user