"""Point-in-time Form 4 cluster helpers for leakage-safe residual cash sleeves.""" from __future__ import annotations import datetime as dt from dataclasses import dataclass from functools import lru_cache from pathlib import Path from typing import Any, Iterable import pyarrow.parquet as pq from libs.common.logging import get_logger logger = get_logger(__name__) @dataclass(frozen=True) class Form4ClusterEntry: symbol: str filing_date: dt.date as_of_date: dt.date total_value: float owner_count: int transaction_count: int event_day_count: int max_purchase_pct: float median_purchase_pct: float weighted_purchase_pct: float max_lag_days: int | None = None min_lag_days: int | None = None transaction_span_days: int = 0 officer_count: int = 0 director_count: int = 0 ten_percent_owner_count: int = 0 other_role_count: int = 0 ceo_count: int = 0 cfo_count: int = 0 c_suite_count: int = 0 role_weight_score: float = 0.0 has_officer_or_director: bool = False def _coerce_date(value: Any) -> dt.date | None: if value is None: return None if isinstance(value, dt.datetime): return value.date() if isinstance(value, dt.date): return value text = str(value).strip() if not text: return None try: return dt.date.fromisoformat(text[:10]) except ValueError: return None def _coerce_float(value: Any) -> float | None: if value is None: return None try: return float(value) except (TypeError, ValueError): return None def _coerce_int(value: Any) -> int | None: if value is None: return None try: return int(value) except (TypeError, ValueError): return None class PointInTimeForm4Calendar: """Latest-known same-day Form 4 cluster events by filing date.""" def __init__(self, entries: Iterable[Form4ClusterEntry]) -> None: grouped: dict[dt.date, list[Form4ClusterEntry]] = {} for entry in entries: grouped.setdefault(entry.filing_date, []).append(entry) self._entries_by_filing_date = { filing_date: tuple(sorted( filing_entries, key=lambda item: ( item.symbol, -item.owner_count, -item.total_value, -item.weighted_purchase_pct, ), )) for filing_date, filing_entries in grouped.items() } @classmethod def from_parquet(cls, path: Path) -> PointInTimeForm4Calendar: table = pq.read_table(str(path)) rows = table.to_pylist() entries: list[Form4ClusterEntry] = [] for row in rows: symbol = str(row.get("symbol") or "").strip().upper() filing_date = _coerce_date(row.get("filing_date")) as_of_date = _coerce_date(row.get("as_of_date") or row.get("filing_date")) total_value = _coerce_float(row.get("total_value")) owner_count = _coerce_int(row.get("owner_count")) transaction_count = _coerce_int(row.get("transaction_count")) event_day_count = _coerce_int(row.get("event_day_count")) max_purchase_pct = _coerce_float(row.get("max_purchase_pct")) median_purchase_pct = _coerce_float(row.get("median_purchase_pct")) weighted_purchase_pct = _coerce_float(row.get("weighted_purchase_pct")) if ( not symbol or filing_date is None or as_of_date is None or total_value is None or owner_count is None or transaction_count is None or event_day_count is None or max_purchase_pct is None or median_purchase_pct is None or weighted_purchase_pct is None ): continue entries.append( Form4ClusterEntry( symbol=symbol, filing_date=filing_date, as_of_date=as_of_date, total_value=total_value, owner_count=owner_count, transaction_count=transaction_count, event_day_count=event_day_count, max_purchase_pct=max_purchase_pct, median_purchase_pct=median_purchase_pct, weighted_purchase_pct=weighted_purchase_pct, max_lag_days=_coerce_int(row.get("max_lag_days")), min_lag_days=_coerce_int(row.get("min_lag_days")), transaction_span_days=_coerce_int(row.get("transaction_span_days")) or 0, officer_count=_coerce_int(row.get("officer_count")) or 0, director_count=_coerce_int(row.get("director_count")) or 0, ten_percent_owner_count=_coerce_int(row.get("ten_percent_owner_count")) or 0, other_role_count=_coerce_int(row.get("other_role_count")) or 0, ceo_count=_coerce_int(row.get("ceo_count")) or 0, cfo_count=_coerce_int(row.get("cfo_count")) or 0, c_suite_count=_coerce_int(row.get("c_suite_count")) or 0, role_weight_score=_coerce_float(row.get("role_weight_score")) or 0.0, has_officer_or_director=bool(row.get("has_officer_or_director") or False), ) ) logger.info( "pit_form4_calendar_loaded", path=str(path), rows=len(entries), filing_dates=len({entry.filing_date for entry in entries}), symbols=len({entry.symbol for entry in entries}), ) return cls(entries) def get_events_between( self, *, start_filing_date: dt.date, end_filing_date: dt.date, symbols: Iterable[str] | None = None, ) -> list[Form4ClusterEntry]: symbol_filter = { str(symbol).strip().upper() for symbol in (symbols or []) if str(symbol).strip() } rows: list[Form4ClusterEntry] = [] current = start_filing_date while current <= end_filing_date: for entry in self._entries_by_filing_date.get(current, ()): if symbol_filter and entry.symbol not in symbol_filter: continue rows.append(entry) current += dt.timedelta(days=1) return rows @lru_cache(maxsize=8) def load_pit_form4_calendar(path_str: str) -> PointInTimeForm4Calendar | None: path = Path(path_str) if not path.exists(): logger.info("pit_form4_calendar_missing", path=str(path)) return None return PointInTimeForm4Calendar.from_parquet(path) __all__ = [ "Form4ClusterEntry", "PointInTimeForm4Calendar", "load_pit_form4_calendar", ]