refactor: GT·시뮬·운영 3축 정리 및 hybrid 실거래 정합
Phase C/dry-run·미사용 모듈·재생성 HTML을 제거하고, 운영 체결을 sim_causal_hybrid와 동일한 hybrid 로직으로 통합한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
345
scripts/06_verify_live.py
Normal file
345
scripts/06_verify_live.py
Normal file
@@ -0,0 +1,345 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user