feat: 운영 시작 시 누락 봉 증분 동기화 및 B-1 실거래 설정

05/06 시작 전 ops_sync로 지연 간격만 증분 보완하고, Phase B-1 live env·ncue 실행 래퍼를 반영한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-06-03 13:46:59 +01:00
parent 1fc560744d
commit a16c942be4
14 changed files with 301 additions and 14 deletions

View File

@@ -316,13 +316,21 @@ def download_symbol(
symbol: str,
interval: int,
months: int,
*,
verbose: bool = True,
skip_backfill: bool = False,
) -> None:
"""한 간격의 봉을 API로 받아 증분·백필 저장합니다."""
"""
한 간격의 봉을 API로 받아 증분·백필 저장합니다.
Args:
skip_backfill: True면 운영 증분만(백필 생략). 05/06 시작 동기화용.
"""
months = months_for_interval(interval, months)
label = interval_label(interval)
existing = get_row_count(symbol, interval)
if existing > 0:
if existing > 0 and not skip_backfill:
backfill_before_earliest(monitor, symbol, interval, months)
last_ts = get_last_timestamp(symbol, interval)
@@ -341,7 +349,7 @@ def download_symbol(
print(f" DB 기존 {existing}행 | API 목표 약 {target}")
data = monitor.get_coin_more_data(
symbol, interval, bong_count=target, verbose=True
symbol, interval, bong_count=target, verbose=verbose
)
if data is None or data.empty:
print(" -> API 데이터 없음")

191
deepcoin/data/ops_sync.py Normal file
View File

@@ -0,0 +1,191 @@
"""
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

View File

@@ -370,6 +370,9 @@ class LiveTrader(Monitor):
def run_once(self) -> None:
"""1회: 규칙 평가 → 시뮬 hybrid 체결 → 텔레그램."""
from deepcoin.data.ops_sync import ensure_ops_candles
ensure_ops_candles()
rules = load_monitor_rules()
print(
f"[06] {datetime.now():%Y-%m-%d %H:%M:%S} "

View File

@@ -29,6 +29,9 @@ class MonitorCoin(Monitor):
def monitor_wld(self) -> None:
"""전 봉 BB·일목·추세 및 규칙 발화를 출력합니다."""
from deepcoin.data.ops_sync import ensure_ops_candles
ensure_ops_candles()
print(
"[{}] {} ({})".format(
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),