#!/usr/bin/env python3 """실거래(06) 기동 전 설정·hybrid tier·규칙·한도 점검. LIVE_TRADING_ENABLED=1 필수.""" from __future__ import annotations import argparse 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_HYBRID_BOOTSTRAP_FIRES, 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.ops.hybrid_sim_execution import ( bootstrap_monitor_signals_from_outcomes, build_live_signal_history, plan_live_hit, ) from deepcoin.ops.live_trader import LiveTrader from deepcoin.ops.monitor import Monitor 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}") print(f" LIVE_HYBRID_BOOTSTRAP_FIRES={LIVE_HYBRID_BOOTSTRAP_FIRES}") 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 LIVE_TRADING_ENABLED: issues.append("LIVE_TRADING_ENABLED=0 — 실거래 기동 불가") if not GT_SIGNAL_CAUSAL: issues.append("GT_SIGNAL_CAUSAL=0 — 인과 GT·hybrid 정합 권장(1)") if not LIVE_HYBRID_BOOTSTRAP_FIRES: issues.append("LIVE_HYBRID_BOOTSTRAP_FIRES=0 — 시뮬 이력 부트스트랩 꺼짐") 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만 원 기준 원화 한도·알림 비율 점검.""" issues: list[str] = [] _print_header("1b. 초기 자금·비율 (운영)") 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), "LIVE_DAILY_LOSS_LIMIT_KRW": int(ic * 0.10), "LIVE_DAILY_KRW_MAX": ic, "LIVE_MAX_TRADES_PER_DAY": 15, } 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:,}") 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, {}), ("복리·과거leg+", ic * 2, ic / price * 0.5, 3.0, {0: 12.0}), ] for label, cash, qty, dd, completed in scenarios: hybrid_amt = _plan_with_dd( cash, qty, price, dd, enhanced=False, completed_leg_ret=completed ) conv_amt = _plan_with_dd( cash, qty, price, dd, enhanced=True, completed_leg_ret=completed ) 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_hybrid_bootstrap(df) -> list[str]: """fire_outcomes 부트스트랩 건수·plan_live_hit 동작.""" issues: list[str] = [] _print_header("2b. hybrid 부트스트랩 (sim_causal_hybrid)") boot = bootstrap_monitor_signals_from_outcomes() print(f" fire_outcomes monitor 발화: {len(boot)}건") if LIVE_HYBRID_BOOTSTRAP_FIRES and len(boot) < 100: issues.append(f"bootstrap 발화 {len(boot)}건 — 04_match_rules 재실행 필요") if df is None or df.empty or not boot: return issues hist = build_live_signal_history([]) last_buy = [s for s in boot if s["side"] == "buy"][-1:] if last_buy: plan = plan_live_hit(hist, last_buy[0], df, approved_buy_rules=None) print( f" 최근 매수 샘플 {last_buy[0]['dt']}: " f"ok={plan.ok} ₩{plan.amount_krw:,.0f} ({plan.message})" ) return issues def check_live_limits() -> None: """실거래 일한도 vs hybrid tier (plan_live_hit).""" _print_header("3. 실거래 한도 vs hybrid tier") 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]) hist = build_live_signal_history([]) dt = str(df.index[-1]) if df is not None and not df.empty else "2026-06-01 12:00:00" planned = plan_live_hit( hist, { "dt": dt, "rule_id": "buy_compound_tight", "side": "buy", "close": price, }, df, approved_buy_rules=None, ).amount_krw 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회 매수액이 일한도 초과 가능 — " "시뮬 대비 실거래 체결액이 작아질 수 있음" ) def check_live_eval() -> None: """현재 시점 규칙 발화.""" _print_header("4. 현재 발화 (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_live_once() -> None: """06 1회 실거래 루프 (발화 시 주문).""" _print_header("5. 06_execute_live --once") LiveTrader().run_once() def write_verification_report(issues: list[str], out_path: Path) -> None: """운영 점검 결과를 docs/05_ops에 기록.""" out_path.parent.mkdir(parents=True, exist_ok=True) status = "PASS" if not issues else "WARN" lines = [ "# Live 운영 점검", "", f"- 일시: {datetime.now():%Y-%m-%d %H:%M:%S}", f"- 결과: **{status}**", "", "## Check", "", f"- GT_SIGNAL_CAUSAL={GT_SIGNAL_CAUSAL}", f"- LIVE_TRADING_ENABLED={LIVE_TRADING_ENABLED}", "- 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", "", "```bash", "python scripts/06_execute_live.py", "```", "", "## Kill switch", "", "- 06 프로세스 중지", "- 빗썸 앱 수동 청산", "", ] ) out_path.write_text("\n".join(lines), encoding="utf-8") print(f"\n[저장] {out_path}") def main() -> int: """실거래 기동 전 점검.""" parser = argparse.ArgumentParser(description="06 실거래 점검") parser.add_argument( "--once", action="store_true", help="점검 후 06_execute_live 1회 실행 (실주문 가능)", ) args = parser.parse_args() print("[06_verify] 실거래 점검 시작") if not LIVE_TRADING_ENABLED: print("오류: LIVE_TRADING_ENABLED=0 — .env 에 1 설정 필요") return 1 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_hybrid_bootstrap(df)) check_live_limits() check_live_eval() if args.once: run_live_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}") return 1 print(" 운영 설정 PASS — 06_execute_live 기동 가능") return 0 if __name__ == "__main__": raise SystemExit(main())