40만 원 기준 시뮬·dry-run 정합 및 hybrid 체결 엔진 통합.
초기 자금 GT_INITIAL_CASH_KRW=400000과 원화 한도 비율(알림·LIVE_ORDER·일한도·손실한도)을 맞추고, dry-run/live 체결을 sim_causal_hybrid(replay)와 동일 경로로 통합한다. 시뮬 리포트 갱신, Phase C 슈퍼바이저·매수매도 리허설 스크립트를 추가한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
173
scripts/08_phase_c_supervisor.py
Normal file
173
scripts/08_phase_c_supervisor.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user