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:
Kunthawat Greethong
2026-08-29 09:19:24 +07:00
parent dbb787c50a
commit ead9aeb25c
22 changed files with 2326 additions and 66 deletions

View File

@@ -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,
}