#!/usr/bin/env python3 """ verify_optuna_tpe_parity_checklist.py — 전전략 TPE 수정사항 스모크 검증 DB/실매 미변경. suggest 1회 + 축/게이트/TIME apply OFF 검사. .venv/bin/python scripts/verify_optuna_tpe_parity_checklist.py """ from __future__ import annotations import sys from pathlib import Path from typing import Any, Dict, List, Tuple ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) def _ok(msg: str) -> None: print(f" ✅ {msg}") def _fail(msg: str) -> None: print(f" ❌ {msg}") def _suggest_once(name: str, fn) -> Tuple[bool, Dict[str, Any], str]: import optuna import optuna.logging as ol ol.set_verbosity(ol.WARNING) study = optuna.create_study(direction="maximize") combo: Dict[str, Any] = {} pruned = False def objective(trial: optuna.Trial) -> float: nonlocal combo, pruned try: combo = fn(trial) except optuna.TrialPruned: pruned = True raise return 1.0 try: study.optimize(objective, n_trials=1, show_progress_bar=False) except Exception as exc: return False, {}, str(exc) if pruned or not combo: return False, {}, "pruned_or_empty" return True, combo, "" def main() -> int: fails: List[str] = [] print("=== Optuna TPE 전전략 수정사항 검증 ===") # 1) 게이트 기본값 print("\n[1] 탐색/사후 게이트") from kis_trader.backtest.optuna_common import ( OPTUNA_REPORT_MIN_PF_DEFAULT, OPTUNA_REPORT_MIN_WIN_RATE_DEFAULT, optuna_report_gate_defaults, optuna_search_gate_defaults, ) sw, sp, st = optuna_search_gate_defaults() if float(sw) != 0.0: fails.append(f"search min_win_rate={sw} (want 0)") _fail(f"탐색 min_win_rate={sw}") else: _ok("탐색 min_win_rate 기본 0") if float(sp) != 0.0: fails.append(f"search min_pf={sp}") _fail(f"탐색 min_pf={sp}") else: _ok("탐색 min_pf 기본 0") if int(st) != 1: fails.append(f"search min_trades={st}") _fail(f"탐색 min_trades={st}") else: _ok("탐색 min_trades 기본 1") rw, rp, rt = optuna_report_gate_defaults() if float(rw) != float(OPTUNA_REPORT_MIN_WIN_RATE_DEFAULT) or float(rp) != float(OPTUNA_REPORT_MIN_PF_DEFAULT): fails.append(f"report gates wr/pf={rw}/{rp}") _fail(f"report gates {rw}/{rp}") else: _ok(f"사후 게이트 WR≥{rw} PF≥{rp} min_trades≥{rt}") # 2) TIME apply OFF print("\n[2] session_env_patch / TIME apply") from kis_trader.backtest.backtest_portfolio_common import session_env_patch from kis_trader.utils.env import get_env_bool if get_env_bool("PARAM_SEARCH_APPLY_SESSION_TIME", False): fails.append("PARAM_SEARCH_APPLY_SESSION_TIME is ON") _fail("PARAM_SEARCH_APPLY_SESSION_TIME=ON (기본은 OFF여야 함)") else: _ok("PARAM_SEARCH_APPLY_SESSION_TIME 기본 OFF") for strat in ("MOMENTUM", "BREAKOUT", "SCALP", "TAIL"): patch = session_env_patch(strat, {"time_start_hm": 900, "time_end_hm": 1300}) if patch: fails.append(f"session_env_patch({strat})={patch}") _fail(f"{strat} session_env_patch 비어있지 않음: {patch}") else: _ok(f"{strat} session_env_patch → {{}}") # 3) TPE space: no time_*, skip_hts=false, ratchet numeric print("\n[3] TPE suggest 스모크 (전략별 1 trial)") from kis_trader.backtest.optuna_breakout_tpe_space import ( breakout_tpe_axis_keys, suggest_breakout_params_tpe, ) from kis_trader.backtest.optuna_momentum_tpe_space import ( momentum_tpe_axis_keys, suggest_momentum_params_tpe, ) from kis_trader.backtest.optuna_scalping_tpe_space import ( scalp_tpe_axis_keys, suggest_scalp_params_tpe, ) from kis_trader.backtest.optuna_tail_tpe_space import ( suggest_tail_params_tpe, tail_tpe_axis_keys, ) from kis_trader.backtest.optuna_tpe_common import ( RATCHET_TPE_AXIS_KEYS, format_ratchet_tiers_string, finalize_ratchet_combo, ) specs = [ ("momentum", suggest_momentum_params_tpe, momentum_tpe_axis_keys, True, ""), ("breakout", suggest_breakout_params_tpe, breakout_tpe_axis_keys, True, ""), ("tail", suggest_tail_params_tpe, tail_tpe_axis_keys, True, "off"), ("scalp", suggest_scalp_params_tpe, scalp_tpe_axis_keys, False, ""), ] for name, fn, keys_fn, has_ratchet, off_tok in specs: # 래칫 ON/OFF 둘 다 볼 수 있게 여러 번 seen_on = False seen_off = False last_combo: Dict[str, Any] = {} last_err = "" ok_any = False for _ in range(20): ok, combo, err = _suggest_once(name, fn) if not ok: last_err = err continue ok_any = True last_combo = combo if has_ratchet: if combo.get("ratchet_on") is False: seen_off = True else: seen_on = True else: break if seen_on and seen_off: break if not ok_any: fails.append(f"{name} suggest: {last_err}") _fail(f"{name} suggest 실패: {last_err}") continue combo = last_combo keys = keys_fn() time_keys = [k for k in keys if "time_" in k or k.endswith("_hm")] if time_keys: fails.append(f"{name} time axes={time_keys}") _fail(f"{name} 시간축 잔존: {time_keys}") else: _ok(f"{name} 시간축 없음") if combo.get("skip_hts_scan_dupes") is not False: fails.append(f"{name} skip_hts={combo.get('skip_hts_scan_dupes')}") _fail(f"{name} skip_hts_scan_dupes!=False") else: _ok(f"{name} skip_hts_scan_dupes=False") if has_ratchet: for rk in ("ratchet_on", "ratchet_n", "ratchet_tiers"): if rk not in combo: fails.append(f"{name} missing {rk}") _fail(f"{name} {rk} 누락") for rk in RATCHET_TPE_AXIS_KEYS: if rk not in keys: fails.append(f"{name} axis missing {rk}") _fail(f"{name} axis에 {rk} 없음") if not seen_off: fails.append(f"{name} OFF sample missing") _fail(f"{name} OFF 샘플 미관측") else: _ok(f"{name} OFF 샘플 확인") if not seen_on: fails.append(f"{name} ON sample missing") _fail(f"{name} ON 샘플 미관측") else: _ok(f"{name} ON 샘플 확인") # ON 고정 샘플로 문자열 형식 검사 on_combo = None for _ in range(24): ok, c2, _e = _suggest_once(name, fn) if ok and c2.get("ratchet_on"): on_combo = c2 break if on_combo is None and not seen_on: fails.append(f"{name} could not sample ON") _fail(f"{name} ON 샘플 확보 실패") elif on_combo: tiers = str(on_combo.get("ratchet_tiers") or "") if ":" not in tiers: fails.append(f"{name} ON tiers empty/bad={tiers!r}") _fail(f"{name} ON tiers={tiers!r}") else: _ok(f"{name} ON → ratchet_tiers={tiers!r}") fin = finalize_ratchet_combo(dict(on_combo), off_token=off_tok) if str(fin.get("ratchet_tiers")) != str(on_combo.get("ratchet_tiers")): fails.append(f"{name} finalize mismatch") _fail( f"{name} finalize {fin.get('ratchet_tiers')!r} " f"!= {on_combo.get('ratchet_tiers')!r}" ) else: _ok(f"{name} finalize 일치") off_combo = {"ratchet_on": False, "ratchet_n": 0} fin_off = finalize_ratchet_combo(off_combo, off_token=off_tok) if fin_off.get("ratchet_tiers") != off_tok: fails.append(f"{name} OFF finalize={fin_off.get('ratchet_tiers')!r}") _fail(f"{name} OFF finalize={fin_off.get('ratchet_tiers')!r}") else: _ok(f"{name} OFF finalize → {off_tok!r}") else: if "ratchet_tiers" in combo or "ratchet_on" in combo: fails.append(f"{name} unexpected ratchet") _fail(f"{name} 래칫 축이 있으면 안 됨") else: _ok(f"{name} 래칫 축 없음(의도)") _ok(f"{name} suggest OK keys={len(keys)} combo={len(combo)}") # 조립 헬퍼 단위 s = format_ratchet_tiers_string([(5.0, 2.0), (10.0, 1.0)]) if s != "5:2,10:1": fails.append(f"format={s}") _fail(f"format_ratchet={s}") else: _ok("format_ratchet_tiers_string") # 4) 브리핑 min_trades 문구 print("\n[4] 브리핑 min_trades") from kis_trader.backtest.optuna_briefing import build_rule_briefing md = build_rule_briefing( { "strategy": "momentum", "mode": "tpe", "start": "2026-07-20", "end": "2026-07-21", "backtest_days": 2, "optuna_n_trials_requested": 1, "optuna_trials_completed": 1, "min_win_rate": 0, "min_pf": 0, "min_trades": 1, "results_all": [], "results_gated": [], "report_gates": {"min_win_rate": 40, "min_pf": 1, "min_trades": 1}, }, ) if "min_trades" not in md or "최소 거래" not in md: fails.append("briefing missing min_trades expl") _fail("브리핑에 min_trades 설명 없음") else: _ok("브리핑에 min_trades 설명 포함") print("\n=== 결과 ===") if fails: print(f"FAIL {len(fails)}건") for f in fails: print(f" - {f}") return 1 print("ALL PASS") return 0 if __name__ == "__main__": raise SystemExit(main())