"""Point-in-time 13D/13G ownership filing helpers for residual idle-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 OwnershipFilingEntry: symbol: str filing_date: dt.date as_of_date: dt.date form_type: str owner_name: str | None owner_key: str | None percent_owned: float | None aggregate_shares: float | None purpose_text: str | None purpose_housekeeping_flag: bool activist_flag: bool is_amendment: bool prior_percent_owned: float | None percent_delta_points: float | None prior_form_group: str | None is_initial_for_owner: bool is_13g_to_13d_transition: bool ownership_strength_score: int | None 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_bool(value: Any) -> bool: if isinstance(value, bool): return value if value is None: return False text = str(value).strip().lower() return text in {"1", "true", "t", "yes", "y"} class PointInTimeOwnershipCalendar: """Latest-known 13D/13G ownership events keyed by filing date.""" def __init__(self, entries: Iterable[OwnershipFilingEntry]) -> None: grouped: dict[dt.date, list[OwnershipFilingEntry]] = {} 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.percent_delta_points or 0.0), -(item.percent_owned or 0.0), not item.activist_flag, ), ) ) for filing_date, filing_entries in grouped.items() } @classmethod def from_parquet(cls, path: Path) -> PointInTimeOwnershipCalendar: table = pq.read_table(str(path)) rows = table.to_pylist() entries: list[OwnershipFilingEntry] = [] 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")) form_type = str(row.get("form_type") or "").strip().upper() if not symbol or filing_date is None or as_of_date is None or not form_type: continue entries.append( OwnershipFilingEntry( symbol=symbol, filing_date=filing_date, as_of_date=as_of_date, form_type=form_type, owner_name=str(row.get("owner_name") or "").strip() or None, owner_key=str(row.get("owner_key") or "").strip().lower() or None, percent_owned=_coerce_float(row.get("percent_owned")), aggregate_shares=_coerce_float(row.get("aggregate_shares")), purpose_text=str(row.get("purpose_text") or "").strip() or None, purpose_housekeeping_flag=_coerce_bool(row.get("purpose_housekeeping_flag")), activist_flag=_coerce_bool(row.get("activist_flag")), is_amendment=_coerce_bool(row.get("is_amendment")), prior_percent_owned=_coerce_float(row.get("prior_percent_owned")), percent_delta_points=_coerce_float(row.get("percent_delta_points")), prior_form_group=str(row.get("prior_form_group") or "").strip().upper() or None, is_initial_for_owner=_coerce_bool(row.get("is_initial_for_owner")), is_13g_to_13d_transition=_coerce_bool(row.get("is_13g_to_13d_transition")), ownership_strength_score=( int(row.get("ownership_strength_score")) if row.get("ownership_strength_score") is not None else None ), ) ) logger.info( "pit_ownership_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[OwnershipFilingEntry]: symbol_filter = { str(symbol).strip().upper() for symbol in (symbols or []) if str(symbol).strip() } rows: list[OwnershipFilingEntry] = [] 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_ownership_calendar(path_str: str) -> PointInTimeOwnershipCalendar | None: path = Path(path_str) if not path.exists(): logger.info("pit_ownership_calendar_missing", path=str(path)) return None return PointInTimeOwnershipCalendar.from_parquet(path) __all__ = [ "OwnershipFilingEntry", "PointInTimeOwnershipCalendar", "load_pit_ownership_calendar", ]