""" FINRA short-volume 2020 갭 메우기 (2020-04-01 ~ 2020-10-31). DB 조사 결과: 전 구간(2018-08 ~ 2026-05)에서 유일한 데이터 결손이 2020-04-01 ~ 2020-10-31 (약 138 평일). COVID 폭락장 + 회복장을 포함하는 이 구간이 없으면 멀티레짐 검정력이 크게 훼손된다. 특성: - FINRA CDN 공개 무료 (API 키 불필요) - idempotent: 이미 수집된 날짜는 자동 스킵 (_count_for_date 검사) - 주말/공휴일(파일 없는 날)은 404 수신 후 자동 스킵 - 1년치 약 20-40분 소요 → 이 스크립트는 7개월, 약 10-20분 예상 Usage (컨테이너 내부): python scripts/backfill_finra_2020_gap.py Usage (호스트): docker exec stock_oracle_api python scripts/backfill_finra_2020_gap.py """ import asyncio import fcntl import logging import os import sys from datetime import date sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("finra_gap_fill") _LOCK_FILE = "/tmp/backfill_finra_2020.lock" # 2020-10 is partially ingested (15 days) → start from 2020-04-01, end 2020-11-01 _GAP_START = date(2020, 4, 1) _GAP_END = date(2020, 11, 1) async def run_gap_fill(): from app.core.database import AsyncSessionLocal from app.services.finra_short_volume_service import FinraShortVolumeService logger.info(f"Starting FINRA gap fill: {_GAP_START} → {_GAP_END}") svc = FinraShortVolumeService() async with AsyncSessionLocal() as db: inserted_total = await svc.ingest_date_range( db=db, start_date=_GAP_START, end_date=_GAP_END, force_refresh=False, # skip already-ingested dates ) logger.info("=" * 60) logger.info("FINRA 2020 GAP FILL COMPLETE") logger.info(f" Period : {_GAP_START} → {_GAP_END}") logger.info(f" Inserted : {inserted_total:,} rows (0 = all dates already present or holiday)") logger.info("=" * 60) logger.info("Verify with:") logger.info(" SELECT to_char(date,'YYYY-MM'), COUNT(DISTINCT date::date)") logger.info(" FROM finra_short_volume") logger.info(" WHERE date >= '2020-03-01' AND date < '2021-02-01'") logger.info(" GROUP BY 1 ORDER BY 1;") if __name__ == "__main__": lock_fh = open(_LOCK_FILE, "w") try: fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: logger.error("Gap fill is already running (lock file held). Exiting.") sys.exit(1) try: asyncio.run(run_gap_fill()) finally: fcntl.flock(lock_fh, fcntl.LOCK_UN) lock_fh.close() try: os.unlink(_LOCK_FILE) except OSError: pass