You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
130 lines
4.8 KiB
Python
130 lines
4.8 KiB
Python
"""Build a point-in-time 13D/13G ownership event cache from SEC filings."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pyarrow as pa
|
|
import pyarrow.parquet as pq
|
|
import requests
|
|
|
|
from apps.backtester.run import _build_merged_snapshot_store, load_manifest, resolve_config
|
|
from apps.tools.ownership_13d13g_probe import (
|
|
USER_AGENT,
|
|
_build_events,
|
|
_load_matched_filings,
|
|
)
|
|
from libs.common.logging import configure_logging
|
|
|
|
|
|
def _symbol_universe(config_path: str) -> set[str]:
|
|
manifest = load_manifest(config_path)
|
|
config = resolve_config(manifest)
|
|
store = _build_merged_snapshot_store(
|
|
manifest,
|
|
config,
|
|
snapshot_dir_override=None,
|
|
)
|
|
return {str(symbol).upper() for symbol in store._bars.keys()}
|
|
|
|
|
|
def _write_rows(path: Path, rows: list[dict[str, Any]]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
table = pa.Table.from_pylist(rows)
|
|
pq.write_table(table, str(path))
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Build PIT 13D/13G ownership cache from SEC filings")
|
|
parser.add_argument("--config", action="append", required=True)
|
|
parser.add_argument("--start", required=True, help="Start filing date YYYY-MM-DD")
|
|
parser.add_argument("--end", required=True, help="End filing date YYYY-MM-DD")
|
|
parser.add_argument("--output", default="data/reference/ownership_13d13g_events_pit.parquet")
|
|
parser.add_argument("--cache-dir", default="data/cache/sec_13d13g")
|
|
parser.add_argument("--include-13g-amendments", action="store_true")
|
|
parser.add_argument("--user-agent", default=USER_AGENT)
|
|
args = parser.parse_args()
|
|
|
|
configure_logging("INFO")
|
|
start_date = __import__("datetime").date.fromisoformat(args.start)
|
|
end_date = __import__("datetime").date.fromisoformat(args.end)
|
|
|
|
allowed_symbols: set[str] = set()
|
|
representative_store = None
|
|
for config_path in args.config:
|
|
manifest = load_manifest(config_path)
|
|
config = resolve_config(manifest)
|
|
store = _build_merged_snapshot_store(
|
|
manifest,
|
|
config,
|
|
snapshot_dir_override=None,
|
|
).slice_by_date_range(start_date, end_date)
|
|
allowed_symbols.update(str(symbol).upper() for symbol in store._bars.keys())
|
|
if representative_store is None:
|
|
representative_store = store
|
|
|
|
if representative_store is None:
|
|
raise SystemExit("No config store resolved.")
|
|
|
|
session = requests.Session()
|
|
session.headers.update({"User-Agent": args.user_agent})
|
|
|
|
# The probe helper matches only symbols available in the snapshot store.
|
|
representative_store._bars = {
|
|
symbol: representative_store._bars[symbol]
|
|
for symbol in list(representative_store._bars.keys())
|
|
if str(symbol).upper() in allowed_symbols
|
|
}
|
|
matched_filings, form_counts = _load_matched_filings(
|
|
store=representative_store,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
cache_dir=Path(args.cache_dir),
|
|
include_13g_amendments=bool(args.include_13g_amendments),
|
|
session=session,
|
|
)
|
|
events, parse_stats = _build_events(
|
|
records=matched_filings,
|
|
cache_dir=Path(args.cache_dir),
|
|
session=session,
|
|
)
|
|
|
|
rows: list[dict[str, Any]] = []
|
|
for event in events:
|
|
rows.append(
|
|
{
|
|
"symbol": event.symbol,
|
|
"filing_date": event.filing_date.isoformat(),
|
|
"as_of_date": event.filing_date.isoformat(),
|
|
"form_type": event.form_type,
|
|
"percent_owned": event.percent_owned,
|
|
"aggregate_shares": event.aggregate_shares,
|
|
"purpose_text": event.purpose_text,
|
|
"purpose_housekeeping_flag": event.purpose_housekeeping_flag,
|
|
"activist_flag": event.activist_flag,
|
|
"is_amendment": event.is_amendment,
|
|
"prior_percent_owned": event.prior_percent_owned,
|
|
"percent_delta_points": event.percent_delta_points,
|
|
"prior_form_group": event.prior_form_group,
|
|
"is_initial_for_owner": event.is_initial_for_owner,
|
|
"is_13g_to_13d_transition": event.is_13g_to_13d_transition,
|
|
"ownership_strength_score": event.ownership_strength_score,
|
|
"owner_name": event.owner_name,
|
|
"owner_key": event.owner_key,
|
|
"event_date": event.event_date.isoformat() if event.event_date else None,
|
|
"source": "sec_13d13g_filing",
|
|
}
|
|
)
|
|
|
|
_write_rows(Path(args.output), rows)
|
|
print(
|
|
f"wrote {len(rows)} ownership events to {args.output} | "
|
|
f"matched_filings={len(matched_filings)} | parsed={parse_stats.get('parsed', 0)} | "
|
|
f"forms={form_counts}"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|