#!/usr/bin/env python3 """ Phase C 슈퍼바이저: 06 dry-run 상시 + 매일 22:00 중간보고 + 금요일 22:00 최종 후 종료. 사용: python scripts/08_phase_c_supervisor.py python scripts/08_phase_c_supervisor.py --end-date 2026-06-05 --report-hour 22 """ from __future__ import annotations import argparse import runpy import signal import subprocess import sys import time from datetime import date, datetime, timedelta from pathlib import Path _ROOT = Path(__file__).resolve().parents[1] runpy.run_path(str(_ROOT / "scripts" / "_bootstrap.py")) from config import LIVE_TRADING_ENABLED # noqa: E402 from deepcoin.paths import ( # noqa: E402 PHASE_C_SUPERVISOR_LOG, PHASE_C_SUPERVISOR_PID, ) _DEFAULT_PY = "/Users/dsyoon/opt/anaconda3/envs/coin/bin/python" _REPORT_WINDOW_MIN = 5 def _log(msg: str) -> None: """슈퍼바이저 로그 (파일; nohup 시 stdout 중복 방지).""" line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}" PHASE_C_SUPERVISOR_LOG.parent.mkdir(parents=True, exist_ok=True) with PHASE_C_SUPERVISOR_LOG.open("a", encoding="utf-8") as f: f.write(line + "\n") def _run_script(py: str, name: str, *args: str) -> int: """scripts/ 하위 스크립트 실행.""" cmd = [py, str(_ROOT / "scripts" / name), *args] _log(f"실행: {' '.join(cmd)}") proc = subprocess.run(cmd, cwd=str(_ROOT), check=False) return proc.returncode def _in_report_window(now: datetime, hour: int) -> bool: """보고 시각(시) 직후 REPORT_WINDOW_MIN 분 이내.""" return now.hour == hour and now.minute < _REPORT_WINDOW_MIN def _stop_child(proc: subprocess.Popen[bytes] | None) -> None: """06 자식 프로세스 종료.""" if proc is None or proc.poll() is not None: return _log(f"06 종료 요청 pid={proc.pid}") proc.send_signal(signal.SIGTERM) try: proc.wait(timeout=15) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=5) _log("06 종료 완료") def _write_pid() -> None: """슈퍼바이저 PID 기록.""" PHASE_C_SUPERVISOR_PID.parent.mkdir(parents=True, exist_ok=True) PHASE_C_SUPERVISOR_PID.write_text(str(os_getpid()), encoding="utf-8") def os_getpid() -> int: """현재 PID.""" import os return os.getpid() def _daily_pipeline(py: str, *, final: bool) -> None: """다운로드 → verify → 07 보고.""" _run_script(py, "01_download.py") _run_script(py, "06_verify_live_dryrun.py") kind = "final" if final else "daily" _run_script(py, "07_phase_c_paper_report.py", "--kind", kind) def main() -> int: """ Phase C 슈퍼바이저 메인. Returns: 종료 코드 0=정상, 1=설정 오류. """ parser = argparse.ArgumentParser(description="Phase C dry-run 슈퍼바이저") parser.add_argument( "--end-date", type=lambda s: date.fromisoformat(s), default=date(2026, 6, 5), help="최종 보고·종료일 (금요일, ISO)", ) parser.add_argument("--report-hour", type=int, default=22, help="일일 보고 시각(시, 24h)") parser.add_argument( "--py", default=_DEFAULT_PY, help="Python 실행 파일 (coin conda)", ) args = parser.parse_args() if LIVE_TRADING_ENABLED: _log("오류: LIVE_TRADING_ENABLED=1 — Phase C는 0 이어야 합니다.") return 1 _write_pid() reported: set[date] = set() py = args.py end: date = args.end_date hour: int = args.report_hour _log( f"Phase C 슈퍼바이저 시작 · end={end} · 보고 {hour}:00 KST · LIVE=0" ) child = subprocess.Popen( [py, str(_ROOT / "scripts" / "06_execute_live.py")], cwd=str(_ROOT), ) _log(f"06 dry-run 기동 pid={child.pid}") try: while True: now = datetime.now() today = now.date() if child.poll() is not None: _log(f"06 비정상 종료 code={child.returncode} — 재기동") child = subprocess.Popen( [py, str(_ROOT / "scripts" / "06_execute_live.py")], cwd=str(_ROOT), ) if _in_report_window(now, hour) and today not in reported: if today <= end: reported.add(today) is_final = today == end _log( f"{'최종' if is_final else '중간'} 보고 시작 ({today})" ) _daily_pipeline(py, final=is_final) if is_final: _log("금요일 최종 보고 완료 — Phase C dry-run 종료") break if today > end and today not in reported: _log("종료일 경과 — 최종 보고(미실시 시) 후 종료") _daily_pipeline(py, final=True) break time.sleep(60) except KeyboardInterrupt: _log("KeyboardInterrupt — 종료") finally: _stop_child(child) if PHASE_C_SUPERVISOR_PID.is_file(): PHASE_C_SUPERVISOR_PID.unlink(missing_ok=True) _log("슈퍼바이저 종료") return 0 if __name__ == "__main__": sys.exit(main())