""" 05/06 운영 시작 전 coins.db 누락 봉을 증분 보완합니다. 마지막 저장 시각이 현재보다 OPS_SYNC_MAX_LAG_BARS 이상 뒤처진 간격만 01_download.download_symbol 과 동일 경로로 API 수집합니다. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime import pandas as pd from config import ( COIN_NAME, DB_PATH, DOWNLOAD_MONTHS, OPS_SYNC_MAX_LAG_BARS, OPS_SYNC_ON_START, SYMBOL, ) from deepcoin.data.downloader import ( download_jobs, download_symbol, get_last_timestamp, get_row_count, interval_label, ) from deepcoin.ops.monitor import Monitor # 프로세스당 1회 상세 로그(루프마다 ensure 호출 시 스팸 방지) _logged_fresh_once: bool = False @dataclass class OpsSyncResult: """운영 전 DB 동기화 결과.""" synced: list[int] = field(default_factory=list) skipped_fresh: list[int] = field(default_factory=list) errors: dict[int, str] = field(default_factory=dict) disabled: bool = False @property def ok(self) -> bool: """치명적 오류 없이 완료.""" return not self.errors or len(self.synced) > 0 or len(self.skipped_fresh) > 0 def lag_minutes_since_last( last_ts: pd.Timestamp | None, interval_minutes: int ) -> float | None: """ 마지막 봉 시각 대비 경과 분. Returns: last_ts 없으면 None. """ if last_ts is None: return None now = pd.Timestamp.now() last = pd.Timestamp(last_ts) if last.tzinfo is not None and now.tzinfo is None: last = last.tz_localize(None) return max(0.0, (now - last).total_seconds() / 60.0) def is_interval_stale( symbol: str, interval: int, *, max_lag_bars: int = OPS_SYNC_MAX_LAG_BARS, db_path: str = DB_PATH, ) -> tuple[bool, str]: """ 간격별 DB가 운영에 필요한 최신성을 만족하는지 판단. Returns: (stale 여부, 사유 문자열) """ rows = get_row_count(symbol, interval, db_path=db_path) last = get_last_timestamp(symbol, interval, db_path=db_path) if rows == 0 or last is None: return True, "데이터 없음" lag = lag_minutes_since_last(last, interval) if lag is None: return True, "마지막 시각 없음" threshold = float(interval * max_lag_bars) if lag > threshold: return True, f"지연 {lag:.0f}분 > 허용 {threshold:.0f}분" return False, f"최신 (마지막 {last.strftime('%Y-%m-%d %H:%M:%S')})" def list_stale_intervals( symbol: str = SYMBOL, *, max_lag_bars: int = OPS_SYNC_MAX_LAG_BARS, db_path: str = DB_PATH, ) -> list[tuple[int, str, str]]: """ 갱신이 필요한 (interval, label, reason) 목록. Returns: download_jobs 순서와 동일하게 stale 항목만. """ out: list[tuple[int, str, str]] = [] for interval, label in download_jobs(): stale, reason = is_interval_stale( symbol, interval, max_lag_bars=max_lag_bars, db_path=db_path ) if stale: out.append((interval, label, reason)) return out def ensure_ops_candles( symbol: str = SYMBOL, months: int | None = None, *, force: bool = False, verbose_download: bool = False, ) -> OpsSyncResult: """ 05/06 실행 직전 누락·지연 봉을 coins.db에 증분 적재합니다. Args: symbol: 코인 코드. months: 보관 개월( None 이면 DOWNLOAD_MONTHS ). force: True면 최신 간격도 전부 재수집. verbose_download: True면 download_symbol API 진행 로그 출력. Returns: OpsSyncResult """ global _logged_fresh_once result = OpsSyncResult() if not OPS_SYNC_ON_START and not force: result.disabled = True print("[ops_sync] OPS_SYNC_ON_START=0 — 건너뜀") return result months = months or DOWNLOAD_MONTHS jobs = download_jobs() stale = list_stale_intervals(symbol) if not force else [ (iv, lb, "force") for iv, lb in jobs ] fresh = [ iv for iv, _ in jobs if iv not in {s[0] for s in stale} ] result.skipped_fresh = fresh if not stale: if not _logged_fresh_once or force: print( f"[ops_sync] {COIN_NAME} ({symbol}) DB 최신 · " f"간격 {len(fresh)}개 (증분 다운로드 없음)" ) for iv in fresh: last = get_last_timestamp(symbol, iv) if last is not None: print(f" [{interval_label(iv)}] ~ {last}") _logged_fresh_once = True return result print( f"[ops_sync] {COIN_NAME} ({symbol}) 누락 보완 · " f"갱신 {len(stale)}개 / 최신 {len(fresh)}개 (백필 생략·증분만)" ) monitor = Monitor(cooldown_file=None) started = datetime.now() for interval, label, reason in stale: print(f"\n[ops_sync] --- {label} --- ({reason})") try: download_symbol( monitor, symbol, interval, months, verbose=verbose_download, skip_backfill=True, ) result.synced.append(interval) except Exception as exc: result.errors[interval] = str(exc) print(f" [ops_sync] 오류 interval={interval}: {exc}") elapsed = datetime.now() - started _logged_fresh_once = True print(f"\n[ops_sync] 완료 (소요: {elapsed}) · 갱신={result.synced} 오류={len(result.errors)}") return result