#!/usr/bin/env python3 """read-only 감시 + 불일치 시 조치 tick / loop 재시작.""" from __future__ import annotations import argparse import logging import sys 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 from bithumb.operations.ops_lock import ops_tick_lock from bithumb.operations.watch_ops import ( inspect_ops_watch, inspect_vol_watch, is_vol_breakout_ops, remediate_ops_watch, remediate_vol_watch, ) def _configure_logging(verbose: bool) -> None: level = logging.DEBUG if verbose else logging.INFO logging.basicConfig( level=level, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) def main() -> int: """CLI 진입점.""" parser = argparse.ArgumentParser(description="운영 read-only 감시 + 조치") parser.add_argument( "--dry-run", action="store_true", help="점검·알림만 (tick/재시작 없음)", ) parser.add_argument( "--inspect-only", action="store_true", help="stdout 출력만 (텔레그램·조치 없음)", ) parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() _configure_logging(args.verbose) settings = load_settings() watch_lock = settings.ops_tick_lock_path if watch_lock is not None: watch_lock = watch_lock.parent / "ops.watch.lock" if watch_lock is not None: with ops_tick_lock(watch_lock, blocking=False) as acquired: if not acquired: print("watch already running — skip") return 0 return _run_watch(settings, args) return _run_watch(settings, args) def _run_watch(settings, args) -> int: vol_mode = is_vol_breakout_ops(settings) report = inspect_vol_watch(settings) if vol_mode else inspect_ops_watch(settings) print("=== ops watch ===") if vol_mode: print("mode: vol_breakout") print(f"checked_at: {report.checked_at}") print(f"ledger_pending: {report.ledger_pending}") print(f"executable_pending: {report.executable_pending}") print(f"tick_age_sec: {report.tick_age_sec}") print(f"loop_running: {report.loop_running}") for issue in report.issues: print(f" [{issue.severity}] {issue.kind}: {issue.message}") if args.inspect_only: return 1 if report.issues else 0 if not report.issues: print("OK — 조치 없음") return 0 result = ( remediate_vol_watch(settings, report, dry_run=args.dry_run) if vol_mode else remediate_ops_watch(settings, report, dry_run=args.dry_run) ) print("\n=== remediation ===") for action in result.actions: print(f" action: {action}") for msg in result.messages: print(f" {msg}") return 0 if __name__ == "__main__": raise SystemExit(main())