#!/usr/bin/env python3 """ 이전 모멘텀 fine #1 파라미터 고정 × 래칫만 A/B (apply 없음). 기준 JSON: optuna_momentum_fine_20260716_014654.json (best ~+83k, 래칫 축 없음) 기간: 2026-07-15 / 포트 120만 / orderbook off """ from __future__ import annotations import json import os import sys import time from datetime import datetime ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if ROOT not in sys.path: sys.path.insert(0, ROOT) os.environ.setdefault("MOMENTUM_BACKTEST_REST_WARMUP", "1") os.environ.setdefault("MOMENTUM_BACKTEST_REST_WARMUP_BARS", "700") os.environ.setdefault("MOMENTUM_BACKTEST_REST_SLEEP_SEC", "0.25") from kis_trader.backtest.optuna_common import announce_optuna_json_path from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo from kis_trader.utils.env import get_env_from_db # 래칫 후보: OFF + 실매 + fine 격자 대표 + 최근 Optuna 선호 RATCHET_CASES = [ ("OFF", ""), ("LIVE", "5:2,10:1.5"), ("OPTUNA_TOP", "2:1.5,5:1"), ("MID", "2:1,5:0.8,8:0.6"), ] BASE_JSON = os.path.join( ROOT, "kis_trader/backtest/results/optuna_momentum_fine_20260716_014654.json", ) def main() -> int: with open(BASE_JSON, encoding="utf-8") as f: src = json.load(f) base_combo = dict(src["results"][0]["params"]) start = str(src.get("start") or "2026-07-15") end = str(src.get("end") or start) print("=" * 72, flush=True) print("모멘텀 래칫 A/B | 이전 fine#1 고정 | apply 없음", flush=True) print(f"기준 JSON: {BASE_JSON}", flush=True) print(f"기간: {start} ~ {end}", flush=True) print(f"고정 params: {json.dumps(base_combo, ensure_ascii=False)}", flush=True) print( f"DB MOMENTUM_RATCHET_TIERS(참고): {get_env_from_db('MOMENTUM_RATCHET_TIERS', '')!r}", flush=True, ) print("=" * 72, flush=True) t0 = time.time() ctx = prepare_momentum_search_context( start, end, "fine", orderbook_filter="off", ) if ctx is None: print("❌ context 준비 실패", flush=True) return 1 print( f"✅ context OK | {time.time() - t0:.1f}s | " f"slot={ctx.slot_money:,.0f} max={ctx.max_stocks} budget={ctx.total_budget_krw:,.0f}", flush=True, ) rows = [] for label, ratchet in RATCHET_CASES: combo = dict(base_combo) combo["ratchet_tiers"] = ratchet print("-" * 72, flush=True) print(f"▶ 케이스 {label} | ratchet_tiers={ratchet!r}", flush=True) t1 = time.time() result = evaluate_momentum_param_combo( combo, base_fixed=ctx.base_fixed, grid_keys=list(ctx.grid_keys) + ["ratchet_tiers"], 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=ctx.log_verdict_by_code, start_key=ctx.start_key, end_key=ctx.end_key, ) elapsed = time.time() - t1 if result is None: row = { "label": label, "ratchet_tiers": ratchet, "ok": False, "elapsed_sec": round(elapsed, 2), "note": "evaluate None", } print(f" ❌ None ({elapsed:.1f}s)", flush=True) else: row = { "label": label, "ratchet_tiers": ratchet, "ok": True, "elapsed_sec": round(elapsed, 2), "total_pnl": float(result.get("total_pnl") or 0), "total_trades": int(result.get("total_trades") or 0), "win_rate": float(result.get("win_rate") or 0), "pf": float(result.get("pf") or 0) if result.get("pf") is not None else None, } print( f" ✅ pnl={row['total_pnl']:,.0f} | trades={row['total_trades']} | " f"wr={row['win_rate']:.1f}% | pf={row['pf']} | {elapsed:.1f}s", flush=True, ) rows.append(row) ok_rows = [r for r in rows if r.get("ok")] ok_rows.sort(key=lambda r: (-float(r["total_pnl"]), -int(r["total_trades"]))) print("=" * 72, flush=True) print("📊 A/B 결과 (PnL 내림차순)", flush=True) for i, r in enumerate(ok_rows, 1): print( f" {i}. [{r['label']}] ratchet={r['ratchet_tiers']!r} | " f"pnl={r['total_pnl']:,.0f} | trades={r['total_trades']} | wr={r['win_rate']:.1f}%", flush=True, ) if len(ok_rows) >= 2: best, worst = ok_rows[0], ok_rows[-1] print( f"Δ best−worst = {best['total_pnl'] - worst['total_pnl']:+,.0f}원 " f"({best['label']} vs {worst['label']})", flush=True, ) off = next((r for r in ok_rows if r["label"] == "OFF"), None) live = next((r for r in ok_rows if r["label"] == "LIVE"), None) if off and live: print( f"Δ OFF−LIVE = {off['total_pnl'] - live['total_pnl']:+,.0f}원 " f"(OFF {off['total_pnl']:,.0f} / LIVE {live['total_pnl']:,.0f})", flush=True, ) print("=" * 72, flush=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") out_dir = os.path.join(ROOT, "kis_trader/backtest/results") os.makedirs(out_dir, exist_ok=True) out_path = os.path.join(out_dir, f"momentum_ratchet_ab_{ts}.json") out = { "kind": "momentum_ratchet_ab", "apply": False, "base_json": BASE_JSON, "base_pnl_reported": src["results"][0].get("total_pnl"), "base_params": base_combo, "start": start, "end": end, "slot_money": int(ctx.slot_money), "max_stocks": int(ctx.max_stocks), "total_budget_krw": int(ctx.total_budget_krw), "cases": rows, "ranked": ok_rows, "elapsed_sec": round(time.time() - t0, 1), } with open(out_path, "w", encoding="utf-8") as f: json.dump(out, f, indent=2, ensure_ascii=False) announce_optuna_json_path( out_path, strategy="momentum", mode="ratchet_ab", note="래칫 A/B 최종 JSON", ) return 0 if __name__ == "__main__": raise SystemExit(main())