Phase C→B 배포 문서·dry-run 검증을 추가하고 운영 env를 동기화한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
xavis
2026-06-01 17:04:07 +09:00
parent d385456867
commit 3cbfa40aab
12 changed files with 830 additions and 12 deletions

View File

@@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""Phase A: live_trader dry-run·hybrid tier 사이징·한도 점검."""
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_SIGNAL_CAUSAL,
LIVE_COOLDOWN_MIN,
LIVE_DAILY_KRW_MAX,
LIVE_DAILY_LOSS_LIMIT_KRW,
LIVE_MAX_TRADES_PER_DAY,
LIVE_TRADING_ENABLED,
MATCH_PRIMARY_INTERVAL,
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.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} (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"
)
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_tier_sizing(df) -> list[str]:
"""hybrid vs conviction tier 금액 비교 (enhanced=False가 primary)."""
issues: list[str] = []
_print_header("2. hybrid tier 사이징 (시나리오)")
price = 487.0
scenarios = [
("신규·소형DD(1%)", 1_000_000, 0.0, 1.0, {}),
("신규·대형DD(6%)", 1_000_000, 0.0, 6.0, {}),
("복리·과거large leg", 5_000_000, 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_live_limits() -> None:
"""시뮬 대비 실거래 일한도 영향 안내."""
_print_header("3. 실거래 한도 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,
1_000_000,
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("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_dryrun_once() -> None:
"""06 1회 dry-run."""
_print_header("5. 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 (목적)",
"",
"- hybrid primary(`enhanced=False`) live_trader 경로가 시뮬과 정합인지 확인",
"- 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()
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))
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())