#!/usr/bin/env python3 """꼬리 Optuna JSON(#124·#179·mode) vs 동일 엔진 재평가 — DB 미변경. Usage: python3 -u scripts/tail_optuna_parity_reeval.py \\ --json kis_trader/backtest/results/optuna_tail_tpe_20260821_225356.json \\ --out logs/tail_optuna_parity_OUT.json """ from __future__ import annotations import argparse import json import time import traceback from pathlib import Path def _find_trial(d: dict, n: int): for key in ("results_all", "results", "results_gated", "results_stable"): for x in d.get(key) or []: if isinstance(x, dict) and x.get("optuna_trial_number") == n: return x return None def main() -> int: ap = argparse.ArgumentParser() ap.add_argument( "--json", default="kis_trader/backtest/results/optuna_tail_tpe_20260821_225356.json", ) ap.add_argument("--out", required=True) ap.add_argument("--trials", default="124,179", help="comma trial numbers") args = ap.parse_args() t0 = time.time() json_path = Path(args.json) out_path = Path(args.out) d = json.loads(json_path.read_text(encoding="utf-8")) start, end = d["start"], d["end"] slot = float(d["slot_money"]) max_stocks = int(d["max_stocks"]) budget = float(d["total_budget_krw"]) tf = int(d.get("timeframe") or 3) entry_mode = "align" for t in (d.get("results_all") or d.get("results") or []): em = (t.get("params") or {}).get("entry_mode") if em: entry_mode = str(em) break trial_ns = [int(x.strip()) for x in str(args.trials).split(",") if x.strip()] from kis_trader.backtest.param_search_optuna import prepare_tail_search_context from kis_trader.backtest.tail_param_search import evaluate_tail_param_combo print( f"prepare {start}~{end} slot={slot} max={max_stocks} budget={budget} " f"tf={tf} entry={entry_mode} OB=off hist=kiwoom", flush=True, ) ctx = prepare_tail_search_context( start, end, "tpe", timeframe=tf, slot_money=slot, max_stocks=max_stocks, total_budget_krw=budget, orderbook_filter="off", history_source="kiwoom", entry_mode=entry_mode, ) if ctx is None: print("prepare failed", flush=True) return 1 base = dict(ctx.base_params) base["_orderbook_filter_enabled"] = False for k in list(base.keys()): if "whipsaw" in k.lower() and isinstance(base[k], (bool, int)): base[k] = False if isinstance(base[k], bool) else 0 print( f"ctx ready candles={len(ctx.candles_by_code)} " f"univ_slots={len(ctx.universe_by_slot or {})} " f"ticks={'yes' if ctx.ticks_by_code else 'no'} " f"fee={ctx.fee_rate} tax={ctx.sell_tax}", flush=True, ) print( f"base tick_entry={base.get('backtest_use_tick_entry') or base.get('backtest_use_tick_db')} " f"tick_exit={base.get('backtest_use_tick_exit')} " f"ob={base.get('_orderbook_filter_enabled')} " f"pattern_hammer={base.get('pattern_hammer')}", flush=True, ) def run_one(label, combo, recorded): print(f"--- eval {label} ---", flush=True) r = evaluate_tail_param_combo( combo, base_params=base, candles_by_code=ctx.candles_by_code, fee_rate=ctx.fee_rate, sell_tax=ctx.sell_tax, 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, 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=ctx.log_verdict_by_code, include_trades=True, ) if r is None: out = { "label": label, "ok": False, "recorded": recorded, "reeval": None, "delta_pnl": None, "delta_trades": None, "same": False, } print(f" FAIL gates / no result", flush=True) return out re = { "total_pnl": float(r["total_pnl"]), "total_trades": int(r["total_trades"]), "win_rate": float(r["win_rate"]), "pf": float(r.get("pf") or 0), } rec_pnl = float(recorded.get("total_pnl") or 0) rec_tr = int(recorded.get("total_trades") or 0) delta_pnl = re["total_pnl"] - rec_pnl delta_tr = re["total_trades"] - rec_tr same = abs(delta_pnl) < 0.5 and delta_tr == 0 print( f" recorded PnL={rec_pnl:.0f} tr={rec_tr} | " f"reeval PnL={re['total_pnl']:.0f} tr={re['total_trades']} WR={re['win_rate']:.1f}% " f"| Δpnl={delta_pnl:.0f} Δtr={delta_tr} same={same}", flush=True, ) return { "label": label, "ok": True, "recorded": { "total_pnl": rec_pnl, "total_trades": rec_tr, "win_rate": recorded.get("win_rate"), "pf": recorded.get("pf"), }, "reeval": re, "delta_pnl": delta_pnl, "delta_trades": delta_tr, "same": same, "n_trades_exported": len(r.get("_trades") or []), } comparisons = [] try: for n in trial_ns: t = _find_trial(d, n) if not t: print(f"trial #{n} missing", flush=True) comparisons.append({"label": f"#{n}", "ok": False, "error": "missing"}) continue comparisons.append( run_one( f"#{n}", dict(t.get("params") or {}), { "total_pnl": t.get("total_pnl"), "total_trades": t.get("total_trades"), "win_rate": t.get("win_rate"), "pf": t.get("pf"), }, ) ) mc = d.get("mode_combo") or {} if mc.get("params"): bt = mc.get("backtest") or {} comparisons.append( run_one( "mode_combo", dict(mc["params"]), { "total_pnl": bt.get("total_pnl"), "total_trades": bt.get("total_trades"), "win_rate": bt.get("win_rate"), "pf": bt.get("pf"), }, ) ) except Exception: traceback.print_exc() return 1 payload = { "source_json": str(json_path), "start": start, "end": end, "slot_money": slot, "max_stocks": max_stocks, "total_budget_krw": budget, "orderbook_filter": "off", "history_source": "kiwoom", "entry_mode": entry_mode, "elapsed_sec": round(time.time() - t0, 1), "comparisons": comparisons, "all_same": all(c.get("same") for c in comparisons if c.get("ok")), } out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") print(f"wrote {out_path} all_same={payload['all_same']} elapsed={payload['elapsed_sec']}s", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())