#!/usr/bin/env python3 """Phase A: live_trader dry-run·sim_causal_hybrid(06) 정합·한도 점검.""" from __future__ import annotations import runpy from datetime import datetime from pathlib import Path runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py")) from config import ( # noqa: E402 CHART_LOOKBACK_DAYS, GT_INITIAL_CASH_KRW, GT_SIGNAL_CAUSAL, LIVE_COOLDOWN_MIN, LIVE_DAILY_KRW_MAX, LIVE_DAILY_LOSS_LIMIT_KRW, LIVE_MAX_TRADES_PER_DAY, LIVE_ORDER_KRW, LIVE_TRADING_ENABLED, MATCH_LIVE_CACHE_SEC, MATCH_PRIMARY_INTERVAL, MONITOR_ALERT_KRW_AMOUNT, MONITOR_LOOP_SLEEP_SEC, SIM_PRIMARY_SIZING, SYMBOL, TRADING_FEE_RATE, ) from deepcoin.data.mtf_bb import load_frames_from_db from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params from deepcoin.matching.live_eval import evaluate_live_rules from deepcoin.ground_truth.causal_gt_hybrid import _close_series_from_df, hybrid_tier_scale from deepcoin.ground_truth.gt_model import leg_entry_weights from deepcoin.matching.position_sizing import compute_buy_amount_krw from deepcoin.matching.load_rules import load_monitor_rules from deepcoin.matching.live_sizing import LivePositionState, live_sizing_enabled from deepcoin.ops.hybrid_sim_execution import replay_paper_portfolio from deepcoin.ops.live_trader import LiveTrader from deepcoin.ops.monitor import Monitor from deepcoin.ops.paper_portfolio import PaperPortfolio from deepcoin.matching.position_sizing import load_ev_wf_approved_rule_ids def _plan_with_dd( cash: float, qty: float, price: float, dd_pct: float, *, enhanced: bool, completed_leg_ret: dict[int, float] | None = None, leg_id: int = 1, ) -> float: """drawdown %를 고정한 tier 매수 원화 (검증용).""" weights = leg_entry_weights([price]) trade: dict = {"leg_id": leg_id, "drawdown_pct": dd_pct} dd_params = load_hybrid_dd_params() scale = hybrid_tier_scale( trade, completed_leg_ret=completed_leg_ret or {}, enhanced=enhanced, dd_large_pct=dd_params.get("dd_large_pct"), dd_medium_pct=dd_params.get("dd_medium_pct"), ) return compute_buy_amount_krw( cash, qty, price, weights[0], weights[0], asset_pct_scale=scale, fee_rate=TRADING_FEE_RATE, ignore_weight_split=bool(trade.get("conviction_buy")), ) def _print_header(title: str) -> None: print(f"\n=== {title} ===") def check_config() -> list[str]: """필수 설정 확인. 문제 목록 반환.""" issues: list[str] = [] _print_header("1. 설정") print(f" SYMBOL={SYMBOL}") print(f" GT_SIGNAL_CAUSAL={GT_SIGNAL_CAUSAL} (live_sizing={live_sizing_enabled()})") print(f" LIVE_TRADING_ENABLED={LIVE_TRADING_ENABLED}") print(f" SIM_PRIMARY_SIZING={SIM_PRIMARY_SIZING}") dd = load_hybrid_dd_params() print(f" hybrid DD: large={dd.get('dd_large_pct')}% medium={dd.get('dd_medium_pct')}%") print( f" LIVE 한도: daily_max={LIVE_DAILY_KRW_MAX:,} " f"max_trades={LIVE_MAX_TRADES_PER_DAY} " f"loss_limit={LIVE_DAILY_LOSS_LIMIT_KRW:,} " f"cooldown={LIVE_COOLDOWN_MIN}min" ) print( f" 06 루프: sleep={MONITOR_LOOP_SLEEP_SEC}s · " f"live_eval_cache={MATCH_LIVE_CACHE_SEC}s · bar={MATCH_PRIMARY_INTERVAL}m" ) print(" 체결 엔진: sim_causal_hybrid (hybrid_sim_execution)") print(f" GT_INITIAL_CASH_KRW=₩{GT_INITIAL_CASH_KRW:,}") rules = load_monitor_rules() print(f" monitor_rules={[r['rule_id'] for r in rules]}") if not GT_SIGNAL_CAUSAL: issues.append("GT_SIGNAL_CAUSAL=0 — hybrid live sizing 비활성") if not LIVE_TRADING_ENABLED: issues.append("LIVE_TRADING_ENABLED=0 — 실전 운영 시 1 필요") if len(rules) != 2: issues.append(f"monitor_rules {len(rules)}개 (기대 2)") expected = {"buy_compound_tight", "sell_mtf_cross_all_tf"} got = {r["rule_id"] for r in rules} if got != expected: issues.append(f"monitor_rules 불일치: {got}") return issues def check_capital_alignment() -> list[str]: """ 초기 자금 40만 원 기준 원화 한도·알림 비율 점검. LIVE_TRADING_ENABLED=1 → Phase B-1(운영), 0 → Phase C(dry-run). Returns: 불일치 시 이슈 문자열 목록. """ issues: list[str] = [] phase = "B-1 실전" if LIVE_TRADING_ENABLED else "C dry-run" _print_header(f"1b. 초기 자금·비율 (40만 원, {phase})") ic = int(GT_INITIAL_CASH_KRW) expected: dict[str, int] = { "GT_INITIAL_CASH_KRW": 400_000, "MONITOR_ALERT_KRW_AMOUNT": int(ic * 0.10), "LIVE_ORDER_KRW": int(ic * 0.10), } if LIVE_TRADING_ENABLED: expected["LIVE_DAILY_LOSS_LIMIT_KRW"] = int(ic * 0.10) expected["LIVE_DAILY_KRW_MAX"] = ic expected["LIVE_MAX_TRADES_PER_DAY"] = 15 else: expected["LIVE_DAILY_LOSS_LIMIT_KRW"] = int(ic * 0.05) expected["LIVE_DAILY_KRW_MAX"] = int(ic * 10) expected["LIVE_MAX_TRADES_PER_DAY"] = 999 actual = { "GT_INITIAL_CASH_KRW": ic, "MONITOR_ALERT_KRW_AMOUNT": int(MONITOR_ALERT_KRW_AMOUNT), "LIVE_ORDER_KRW": int(LIVE_ORDER_KRW), "LIVE_DAILY_LOSS_LIMIT_KRW": int(LIVE_DAILY_LOSS_LIMIT_KRW), "LIVE_DAILY_KRW_MAX": int(LIVE_DAILY_KRW_MAX), "LIVE_MAX_TRADES_PER_DAY": int(LIVE_MAX_TRADES_PER_DAY), } for key, exp in expected.items(): got = actual[key] ok = got == exp mark = "OK" if ok else "WARN" print(f" [{mark}] {key}={got:,} (기대 {exp:,})") if not ok: issues.append(f"{key}={got:,} ≠ 기대 {exp:,}") if not LIVE_TRADING_ENABLED: paper = PaperPortfolio.load() if int(paper.cash_krw) != ic and paper.qty < 1e-12: issues.append( f"paper 현금 ₩{paper.cash_krw:,.0f} ≠ 초기 ₩{ic:,} (보유 없을 때)" ) elif int(getattr(paper, "initial_cash_krw", 0) or paper.cash_krw) != ic: print(f" [INFO] paper 운용 중 (cash=₩{paper.cash_krw:,.0f})") else: print(" [INFO] LIVE=1 — paper_portfolio 검사 생략 (실계좌·live_signal_history 사용)") return issues def check_tier_sizing(df) -> list[str]: """hybrid vs conviction tier 금액 비교 (enhanced=False가 primary).""" issues: list[str] = [] _print_header("2. hybrid tier 사이징 (시나리오)") price = 487.0 ic = int(GT_INITIAL_CASH_KRW) scenarios = [ ("신규·소형DD(1%)", ic, 0.0, 1.0, {}), ("신규·대형DD(6%)", ic, 0.0, 6.0, {}), ("복리·과거large leg", ic * 5, 2000.0, 3.0, {"completed_leg_ret": {1: 25.0}}), ] for label, cash, qty, dd, extra in scenarios: hybrid_amt = _plan_with_dd( cash, qty, price, dd, enhanced=False, completed_leg_ret=extra.get("completed_leg_ret"), ) conv_amt = _plan_with_dd( cash, qty, price, dd, enhanced=True, completed_leg_ret=extra.get("completed_leg_ret"), ) print( f" [{label}] cash={cash:,} hybrid={hybrid_amt:,} " f"conviction={conv_amt:,} (conviction 배포 금지)" ) if hybrid_amt <= 0: issues.append(f"hybrid 금액 0: {label}") return issues def check_paper_replay(df) -> list[str]: """paper signal_history → 시뮬 replay 잔고 일치.""" issues: list[str] = [] _print_header("3. paper 시뮬 replay") paper = PaperPortfolio.load() hist = paper.signal_history print(f" signal_history={len(hist)} · saved cash=₩{paper.cash_krw:,.0f} qty={paper.qty:.4f}") if not hist: print(" (이력 없음 — 신규 dry-run)") return issues approved = load_ev_wf_approved_rule_ids() replayed, _ = replay_paper_portfolio(hist, df, approved_buy_rules=approved) cash_diff = abs(replayed.cash_krw - paper.cash_krw) qty_diff = abs(replayed.qty - paper.qty) print( f" replay cash=₩{replayed.cash_krw:,.0f} qty={replayed.qty:.4f} " f"(Δcash={cash_diff:,.0f} Δqty={qty_diff:.6f})" ) if cash_diff > 1.0 or qty_diff > 1e-6: issues.append( "paper_portfolio.json 과 sim replay 불일치 — " "signal_history 갱신 후 06 --once 1회 권장" ) return issues def check_live_limits() -> None: """시뮬 대비 실거래 일한도 영향 안내.""" _print_header("4. 실거래 한도 vs hybrid tier") st = LivePositionState() mon = Monitor(cooldown_file=None) frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS) df = frames.get(MATCH_PRIMARY_INTERVAL) price = 487.0 if df is not None and not df.empty: close_s = _close_series_from_df(df) if not close_s.empty: price = float(close_s.iloc[-1]) planned = st.plan_buy_amount_krw( str(df.index[-1]) if df is not None and not df.empty else "2026-06-01 12:00:00", price, float(GT_INITIAL_CASH_KRW), 0.0, df, enhanced=False, fee_rate=TRADING_FEE_RATE, ) trader = LiveTrader() ok, reason = trader._can_trade("buy_compound_tight", planned) print(f" 현재가={price:,.0f} · hybrid planned={planned:,}") print(f" LIVE_DAILY_KRW_MAX={LIVE_DAILY_KRW_MAX:,} → _can_trade={ok} ({reason or 'OK'})") if planned > LIVE_DAILY_KRW_MAX: print( " WARN: hybrid 1회 매수액이 일한도 초과 가능 — " "파일럿은 의도적 제한, 시뮬(+1121%)과 괴리 발생" ) def check_live_eval() -> None: """현재 시점 규칙 발화.""" _print_header("5. 현재 발화 (live_eval)") fired = evaluate_live_rules(force_refresh=True) if not fired: print(" 발화 없음 (정상 — 신호 대기)") return for hit in fired: print(f" {hit['side']} {hit['rule_id']} @ {hit['dt']} close={hit['close']}") def run_dryrun_once() -> None: """06 1회 dry-run.""" _print_header("6. 06_execute_live --once (dry-run)") LiveTrader().run_once() def write_verification_report(issues: list[str], out_path: Path) -> None: """Phase A 결과를 docs/05_ops에 기록.""" out_path.parent.mkdir(parents=True, exist_ok=True) status = "PASS" if not issues else "WARN" lines = [ "# Live Phase A — dry-run 검증", "", f"- 일시: {datetime.now():%Y-%m-%d %H:%M:%S}", f"- 결과: **{status}**", "", "## Plan (목적)", "", "- 06 dry-run/live 체결이 `hybrid_sim_execution`(sim_causal_hybrid)과 정합인지 확인", "- conviction tier(`enhanced=True`) 미사용 확인", "- 실거래 한도가 hybrid tier와 어떻게 상호작용하는지 기록", "", "## Do (실행)", "", "```bash", "python scripts/06_verify_live_dryrun.py", "python scripts/06_execute_live.py --once", "```", "", "## Check (점검 결과)", "", f"- GT_SIGNAL_CAUSAL={GT_SIGNAL_CAUSAL}", f"- LIVE_TRADING_ENABLED={LIVE_TRADING_ENABLED}", f"- monitor_rules: buy_compound_tight, sell_mtf_cross_all_tf", f"- hybrid DD: {load_hybrid_dd_params()}", "", ] if issues: lines.append("### 이슈") lines.append("") for i in issues: lines.append(f"- {i}") lines.append("") lines.extend( [ "## Act (다음 단계)", "", "1. `05_run_monitor.py` 1~2일 병행 (알림만)", "2. `.env` 파일럿 한도 확정 후 `LIVE_TRADING_ENABLED=1`", "3. 1~2주 실계좌 PnL·슬리피지 기록 (본 문서 갱신)", "", "## Kill switch", "", "- `LIVE_TRADING_ENABLED=0` + 06 프로세스 중지", "- 빗썸 앱 수동 청산", "", ] ) out_path.write_text("\n".join(lines), encoding="utf-8") print(f"\n[저장] {out_path}") def main() -> int: """Phase A 검증 실행.""" print("[06_verify] Phase A dry-run 검증 시작") issues = check_config() issues.extend(check_capital_alignment()) mon = Monitor(cooldown_file=None) frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS) df = frames.get(MATCH_PRIMARY_INTERVAL) if df is None or df.empty: issues.append("3m OHLC 없음 — 01_download 필요") else: issues.extend(check_tier_sizing(df)) issues.extend(check_paper_replay(df)) check_live_limits() check_live_eval() run_dryrun_once() out = ( Path(__file__).resolve().parents[1] / "docs" / "05_ops" / f"live_verification_{datetime.now():%Y%m%d}.md" ) write_verification_report(issues, out) _print_header("요약") if issues: for i in issues: print(f" WARN: {i}") print(" → 이슈 확인 후 Phase B(소액 파일럿) 진행") return 1 print(" 운영 설정 PASS — 06_execute_live 실전 기동 가능") return 0 if __name__ == "__main__": raise SystemExit(main())