#!/usr/bin/env python3 """Optuna mode 타점 vs mode+100호가 vs mode+100+휩쏘 — 실엔진 백테 비교. 후처리 표의 「필터후 +23,337」은 체결 재시뮬이고, 이 스크립트는 mode params 로 포트폴리오 백테를 다시 돌린다. """ from __future__ import annotations import json import sys from datetime import datetime from pathlib import Path from typing import Any, Dict, Optional ROOT = Path("/home/hoon/kis_bot") sys.path.insert(0, str(ROOT)) JSON_PATH = ROOT / "kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json" OUT_PATH = ROOT / "kis_trader/backtest/results" / ( f"compare_mom_mode_ob100_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" ) def _pack(label: str, rec: Optional[Dict[str, Any]], note: str = "") -> Dict[str, Any]: if not isinstance(rec, dict): return {"label": label, "ok": False, "note": note or "evaluate None"} return { "label": label, "ok": True, "total_pnl": rec.get("total_pnl"), "total_trades": rec.get("total_trades"), "win_rate": rec.get("win_rate"), "pf": rec.get("pf"), "mdd": rec.get("mdd"), "stability_score": rec.get("stability_score"), "n_losing_days": rec.get("n_losing_days"), "worst_day_pnl": rec.get("worst_day_pnl"), "note": note, } def main() -> int: print(f"[1] load {JSON_PATH}", flush=True) data = json.loads(JSON_PATH.read_text(encoding="utf-8")) mc = data.get("mode_combo") or {} mode_params = dict(mc.get("params") or {}) if not mode_params: print("❌ mode_combo.params 없음", flush=True) return 1 mode_a = next( (a for a in ((data.get("postprocess_topn") or {}).get("postprocess_by_anchor") or []) if a.get("role") == "mode"), None, ) if not mode_a: print("❌ mode 후처리 앵커 없음", flush=True) return 1 combos = ((mode_a.get("orderbook") or {}).get("combos") or {}) e = combos.get("e") or combos.get("100") or {} if not e.get("ok"): print("❌ 100방(e) 없음", flush=True) return 1 ep = dict(e.get("params") or {}) ws = e.get("whipsaw") if isinstance(e.get("whipsaw"), dict) else None if not (ws and ws.get("ok")): ws = mode_a.get("whipsaw") or {} wp = dict((ws or {}).get("params") or {}) if (ws or {}).get("ok") else {} start, end = str(data["start"]), str(data["end"]) slot = float(data.get("slot_money") or 300000) ms = int(data.get("max_stocks") or 8) bud = float(data.get("total_budget_krw") or 1200000) grid_keys = list(data.get("grid_keys") or []) print( f"[2] prepare momentum {start}~{end} OB로드=on slot={slot} max={ms} bud={bud}", flush=True, ) print(f" 100방: spread≤{ep.get('orderbook_max_spread_pct')} ratio≥{ep.get('orderbook_min_bid_ask_ratio')} ask×≤{ep.get('orderbook_entry_ask_max_mult')}", flush=True) print(f" 휩쏘: {wp}", flush=True) print( f" 후처리참고 필터후 OB {((e.get('recommended_stats') or {}))} / 휩쏘 {((ws or {}).get('recommended_stats') or {})}", flush=True, ) from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo ctx = prepare_momentum_search_context( start, end, str(data.get("mode") or "tpe"), slot_money=slot, max_stocks=ms, total_budget_krw=bud, orderbook_filter="on", # 호가·틱 로드 (필터 ON/OFF는 콤보에서 덮어씀) history_source="kiwoom", market="KR", ) if ctx is None: print("❌ prepare 실패", flush=True) return 1 # prepare 가 base_fixed 에 필터 ON 을 넣어도, 케이스별로 명시 덮어씀 base = dict(ctx.base_fixed) def _eval(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]: return evaluate_momentum_param_combo( combo, base_fixed=base, grid_keys=grid_keys or list(ctx.grid_keys), codes_candles=ctx.codes_candles, min_trades=1, min_win_rate=0.0, min_pf=0.0, universe_by_slot=ctx.universe_by_slot, slot_money=ctx.slot_money, max_stocks=ctx.max_stocks, total_budget_krw=ctx.total_budget_krw, fee_rate=ctx.fee_rate, sell_tax=ctx.sell_tax, period_days=ctx.period_days, cache_holder=ctx.cache_holder, ticks_by_code=ctx.ticks_by_code, orderbook_by_code=ctx.orderbook_by_code, program_by_code=ctx.program_by_code, log_verdict_by_code=getattr(ctx, "log_verdict_by_code", None), start_key=ctx.start_key, end_key=ctx.end_key, include_trades=False, ) cases = [] # A) 타점만 (호가·휩쏘 OFF) a = dict(mode_params) a["_orderbook_filter_enabled"] = False a["whipsaw_enabled"] = False print("[3A] mode 타점만 (호가OFF·휩쏘OFF)", flush=True) cases.append(_pack("A_mode_only", _eval(a), "호가OFF·휩쏘OFF")) # B) +100 진입호가 b = dict(mode_params) b["_orderbook_filter_enabled"] = True b["_ob_max_spread_pct"] = float(ep["orderbook_max_spread_pct"]) b["_ob_min_bid_ask_ratio"] = float(ep["orderbook_min_bid_ask_ratio"]) if ep.get("orderbook_entry_ask_max_mult") is not None: b["_ob_ask_max_mult"] = float(ep["orderbook_entry_ask_max_mult"]) b["whipsaw_enabled"] = False print("[3B] mode + 100진입호가 (휩쏘OFF)", flush=True) cases.append(_pack("B_mode_ob100", _eval(b), "호가100 ON · 휩쏘OFF")) # C) +100 + 휩쏘 c = dict(b) c["whipsaw_enabled"] = True if wp.get("whipsaw_subbar_sec") is not None: c["whipsaw_subbar_sec"] = int(wp["whipsaw_subbar_sec"]) if wp.get("whipsaw_lookback_sec") is not None: c["whipsaw_lookback_sec"] = int(wp["whipsaw_lookback_sec"]) if wp.get("whipsaw_dip_pct") is not None: c["whipsaw_dip_pct"] = float(wp["whipsaw_dip_pct"]) print("[3C] mode + 100진입호가 + 휩쏘", flush=True) cases.append(_pack("C_mode_ob100_whip", _eval(c), "호가100 ON · 휩쏘ON")) # D) 웹 폼 방금 끝난 결과(참고) web_path = ROOT / "kis_trader/backtest/results/momentum_bt_20260818_20260821_20260823_152820.json" web_row = {"label": "D_web_form_done", "ok": False, "note": "파일없음"} if web_path.is_file(): wj = json.loads(web_path.read_text(encoding="utf-8")) sm = wj.get("summary") or wj web_row = { "label": "D_web_form_done", "ok": True, "total_pnl": sm.get("total_pnl"), "total_trades": sm.get("total_trades"), "win_rate": sm.get("win_rate"), "pf": sm.get("profit_factor") or sm.get("pf"), "mdd": sm.get("max_drawdown") or sm.get("mdd"), "note": "웹 폼(다른 호가/휩쏘 DB값) 방금 완료 — Optuna 100방과 다름", } cases.append(web_row) # E) Optuna JSON mode 실측 + 후처리 표 숫자(참고) bt = mc.get("backtest") or {} cases.append({ "label": "E_optuna_mode_recorded", "ok": True, "total_pnl": bt.get("total_pnl"), "total_trades": bt.get("total_trades"), "win_rate": bt.get("win_rate"), "pf": bt.get("pf"), "note": "Optuna JSON mode_combo.backtest (호가탐색 전 타점 실측)", }) cases.append({ "label": "F_post_ob100_filter_sim", "ok": True, "total_pnl": (e.get("recommended_stats") or {}).get("pnl"), "total_trades": (e.get("recommended_stats") or {}).get("count"), "win_rate": (e.get("recommended_stats") or {}).get("win_rate"), "note": "후처리 필터시뮬(체결 재필터) — 엔진 재백테 아님", }) cases.append({ "label": "G_post_ob100_whip_filter_sim", "ok": bool(ws and ws.get("ok")), "total_pnl": ((ws or {}).get("recommended_stats") or {}).get("pnl"), "total_trades": ((ws or {}).get("recommended_stats") or {}).get("count"), "win_rate": ((ws or {}).get("recommended_stats") or {}).get("win_rate"), "note": "후처리 휩쏘 필터시뮬 — 엔진 재백테 아님", }) out = { "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "source_json": str(JSON_PATH), "period": f"{start}~{end}", "ob100": ep, "whip": wp, "cases": cases, } OUT_PATH.write_text(json.dumps(out, indent=2, ensure_ascii=False), encoding="utf-8") print("\n===== 비교 =====", flush=True) for row in cases: print( f"{row['label']:28s} ok={row.get('ok')} " f"trades={row.get('total_trades')} WR={row.get('win_rate')} " f"PF={row.get('pf')} PnL={row.get('total_pnl')} " f"| {row.get('note','')}", flush=True, ) print(f"\n💾 {OUT_PATH}", flush=True) print("OK", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())