#!/usr/bin/env python3 """절전·재부팅 후 캔들 공백 자동 백필 (cron 수집 전 단계). 증분 수집(최신 200봉)으로 못 채우는 공백이 있는 (심볼, 인터벌)만 골라 00_download_candles.py --full --days N 으로 채운다. 공백이 없으면 즉시 종료(약 0.1초). python scripts/00_backfill_gaps.py # 점검 후 필요 시 백필 python scripts/00_backfill_gaps.py --dry-run # 계획만 출력 python scripts/00_backfill_gaps.py --dry-run --assume-now "2026-09-12 09:00:00" """ from __future__ import annotations import argparse import logging import subprocess import sys from datetime import datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src" if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) from bithumb.config import load_settings # noqa: E402 from bithumb.data.candle_store import CandleStore # noqa: E402 from bithumb.data.gap_backfill import plan_backfill # noqa: E402 logger = logging.getLogger("backfill") def main() -> int: parser = argparse.ArgumentParser(description="캔들 공백 자동 백필") parser.add_argument("--dry-run", action="store_true", help="계획만 출력") parser.add_argument("--assume-now", default=None, help="테스트용 기준 시각 'YYYY-MM-DD HH:MM:SS'") parser.add_argument("--max-days", type=int, default=30, help="백필 상한 일수 (기본 30)") parser.add_argument("--safety", type=float, default=0.9, help="증분 커버리지 안전계수 (기본 0.9)") parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() logging.basicConfig( level=logging.DEBUG if args.verbose else logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) settings = load_settings() now = datetime.strptime(args.assume_now, "%Y-%m-%d %H:%M:%S") if args.assume_now else datetime.now() symbols = list(settings.download_symbols) intervals = list(settings.download_intervals) store = CandleStore(settings.db_path) try: db_max = { sym: {iv: store.get_max_datetime(sym, iv) for iv in intervals} for sym in symbols } finally: store.close() plan = plan_backfill( db_max, now=now, intervals=intervals, batch_size=settings.candle_count, safety=args.safety, max_days=args.max_days, ) if plan.empty: logger.info("공백 없음 (증분 수집 범위 내) — 백필 생략") return 0 logger.warning("캔들 공백 감지 → 백필 계획: %s", plan.describe()) if args.dry_run: return 0 # 인터벌별 필요 일수가 다르므로 일수별로 묶어 실행 (요청 수 최소화) by_days: dict[int, dict[str, set[int]]] = {} for sym, m in plan.needs.items(): for iv, days in m.items(): by_days.setdefault(days, {}).setdefault(sym, set()).add(iv) python = sys.executable rc_all = 0 for days in sorted(by_days): sym_ivs = by_days[days] syms = ",".join(sorted(sym_ivs)) ivs = ",".join(str(i) for i in sorted({iv for s in sym_ivs.values() for iv in s})) cmd = [ python, str(ROOT / "scripts" / "00_download_candles.py"), "--full", "--days", str(days), "--symbols", syms, "--intervals", ivs, ] logger.warning("백필 실행: --full --days %s --symbols %s --intervals %s", days, syms, ivs) rc = subprocess.call(cmd, cwd=str(ROOT)) if rc != 0: logger.error("백필 실패 rc=%s (days=%s)", rc, days) rc_all = rc if rc_all == 0: logger.warning("백필 완료: %s", plan.describe()) return rc_all if __name__ == "__main__": raise SystemExit(main())