Files
Bithumb/scripts/06_verify_live_dryrun.py
dsyoon c6888c9228 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>
2026-06-03 11:31:24 +09:00

363 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 LIVE_TRADING_ENABLED:
issues.append("LIVE_TRADING_ENABLED=1 — dry-run 점검 시 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만 원 기준 원화 한도·알림 비율 점검 (100만 시대 ×0.4).
Returns:
불일치 시 이슈 문자열 목록.
"""
issues: list[str] = []
_print_header("1b. 초기 자금·비율 (40만 원)")
ic = int(GT_INITIAL_CASH_KRW)
expected = {
"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.05),
"LIVE_DAILY_KRW_MAX": int(ic * 10),
}
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),
}
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:,}")
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})")
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(" Phase A PASS — Phase B(소액 LIVE_TRADING_ENABLED=1) 준비 완료")
return 0
if __name__ == "__main__":
raise SystemExit(main())