feat: /form4 auto-fetch fallback + targeted backfill 지원
- /form4/{ticker}, /form4/aggregate/{ticker}: DB에 ticker 데이터 없으면 SEC에서 800일치 자동 fetch (index_form4s)
- force_refresh 파라미터 추가로 on-demand 재인덱싱 지원
- Form4Response, Form4AggregateResponse에 metadata 필드 추가 (auto_fetched, fetched_count)
- bootstrap_form4_by_ticker.py: --tickers 옵션 추가로 특정 ticker만 targeted backfill 가능
- audit_form4_coverage.py: universe 커버리지 검증 스크립트 신규
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
parent
93a81fd35a
commit
f19d31f125
@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Audit Form 4 coverage for all universe_ticker_registry tickers.
|
||||||
|
|
||||||
|
Prints per-ticker earliest filing_date and P-code buy count.
|
||||||
|
Flags tickers with no data or earliest date after the baseline.
|
||||||
|
|
||||||
|
Usage (inside container):
|
||||||
|
python scripts/audit_form4_coverage.py [--baseline YYYY-MM-DD]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
|
||||||
|
async def audit(baseline: date) -> None:
|
||||||
|
from app.core.database import AsyncSessionLocal
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
rows = await db.execute(text("""
|
||||||
|
WITH u AS (SELECT ticker FROM universe_ticker_registry ORDER BY ticker),
|
||||||
|
e AS (
|
||||||
|
SELECT ticker,
|
||||||
|
MIN(filing_date)::date AS earliest,
|
||||||
|
COUNT(*) AS total_rows,
|
||||||
|
SUM(CASE WHEN transaction_code = 'P' AND shares > 0 THEN 1 ELSE 0 END) AS p_buys
|
||||||
|
FROM insider_transactions
|
||||||
|
GROUP BY ticker
|
||||||
|
)
|
||||||
|
SELECT u.ticker, e.earliest, e.total_rows, e.p_buys
|
||||||
|
FROM u LEFT JOIN e USING (ticker)
|
||||||
|
ORDER BY e.earliest NULLS FIRST, u.ticker
|
||||||
|
"""))
|
||||||
|
results = rows.fetchall()
|
||||||
|
|
||||||
|
no_data, late, ok = [], [], []
|
||||||
|
for ticker, earliest, total_rows, p_buys in results:
|
||||||
|
if earliest is None:
|
||||||
|
no_data.append(ticker)
|
||||||
|
elif earliest > baseline:
|
||||||
|
late.append((ticker, earliest, total_rows, p_buys))
|
||||||
|
else:
|
||||||
|
ok.append((ticker, earliest, total_rows, p_buys))
|
||||||
|
|
||||||
|
print(f"\n=== Form 4 Coverage Audit (baseline: {baseline}) ===")
|
||||||
|
print(f"Universe: {len(results)} tickers | OK: {len(ok)} | Late: {len(late)} | No data: {len(no_data)}\n")
|
||||||
|
|
||||||
|
if no_data:
|
||||||
|
print(f"NO DATA ({len(no_data)}): {', '.join(no_data)}\n")
|
||||||
|
|
||||||
|
if late:
|
||||||
|
print(f"LATE (earliest > {baseline}):")
|
||||||
|
for ticker, earliest, total_rows, p_buys in late:
|
||||||
|
print(f" {ticker:<8} earliest={earliest} rows={total_rows or 0} P-buys={p_buys or 0}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"OK: {len(ok)} tickers meet {baseline} baseline")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--baseline", default="2024-04-01")
|
||||||
|
args = parser.parse_args()
|
||||||
|
baseline = date.fromisoformat(args.baseline)
|
||||||
|
asyncio.run(audit(baseline))
|
||||||
Loading…
Reference in New Issue