#!/usr/bin/env python3 """ BREAKOUT 공유메모리 틱 배선 end-to-end 검증 — 동일 데이터로 dict vs SharedTicksMapping 백테 2회 대조. 거래가 확인된 실 파라미터(merged_params)로 주간 백테를 두 번 돌린다: A) ticks_by_code = 원본 dict (기존 경로) B) ticks_by_code = SharedTicksMapping (공유메모리 컬럼 경로) 두 결과의 trades 리스트와 요약통계가 완전히 동일해야 한다(bit-identical). """ import json import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from database import TradeDB from kis_trader.backtest import breakout_backtest_common as bbc from kis_trader.backtest.param_search_breakout import _load_candles_for_search from kis_trader.backtest.breakout_backtest_common import resolve_breakout_universe from kis_trader.backtest.breakout_tick_loader import load_breakout_ticks_by_code from kis_trader.backtest.shared_ticks import build_shared_ticks START = "2026-06-28" END = "2026-07-05" JSON = "./kis_trader/backtest/results/search_breakout_fast_20260705_031411.json" def run_backtest(codes_candles, universe, ticks_by_code, engine_params, slot_money, max_stocks, total_budget, start_key, end_key): p = dict(engine_params) meta = {"start_key": start_key, "end_key": end_key} trades = bbc.run_breakout_backtest_web_aligned( codes_candles, p, universe, slot_money=slot_money, fee_rate=0.00015, sell_tax=0.0018, max_stocks=max_stocks, total_budget_krw=total_budget, ticks_by_code=ticks_by_code, orderbook_by_code={}, program_by_code={}, meta_out=meta, ) stats = bbc.summarize_breakout_trades(trades, total_budget_krw=total_budget, period_days=1) return trades, stats def norm_trades(trades): out = [] for t in trades: out.append(tuple(sorted( (k, (round(v, 6) if isinstance(v, float) else v)) for k, v in t.items() ))) return out def main(): meta_j = json.load(open(JSON)) r0 = meta_j["top"][0] engine_params = dict(r0["merged_params"]) slot_money = float(meta_j.get("slot_money") or 300_000) max_stocks = int(meta_j.get("max_stocks") or 20) total_budget = float(meta_j.get("total_budget_krw") or 6_000_000) lookback_min = int(engine_params.get("lookback_min", 1)) vol_window = int(engine_params.get("vol_window", 7)) start_key = START.replace("-", "") + "0000" end_key = END.replace("-", "") + "2359" print("⏳ 캔들 로드...") codes_candles = _load_candles_for_search( START, END, lookback_min, fixed={"lookback_min": lookback_min, "vol_window": vol_window}, ) print(f" 종목 {len(codes_candles):,}") print("⏳ 유니버스 로드...") universe, src, n_bins, _iv = resolve_breakout_universe( START.replace("-", ""), END.replace("-", ""), use_saved_history=True, strategy_id="BREAKOUT", ) print(f" 유니버스 슬롯 {len(universe or {}):,} (src={src})") print("⏳ ws_ticks 로드...") db = TradeDB() try: ticks_dict, tick_rows = load_breakout_ticks_by_code( db, start_key, end_key, set(codes_candles.keys()), ) finally: db.close() print(f" 틱 {tick_rows:,}건 / 종목 {len(ticks_dict):,}") # 거래를 넓게 커버하는 변형들 (진입/체결/휩쏘 경로 커버). _base_loose = { "vol_mult": 1.0, "min_turnover_1m_pct": 0.0, "cooldown_min": 0, "max_daily": 100, "prev_chg_min": -50.0, "prev_chg_max": 50.0, "time_start_hm": 900, "time_end_hm": 1520, "min_price": 0, } variants = [ ("baseline(top)", {}), ("loose", {"vol_mult": 1.0, "min_turnover_1m_pct": 0.0, "cooldown_min": 0, "max_daily": 50}), ("loose_wide", dict(_base_loose)), ("loose_slip", {**_base_loose, "intrabar_slippage_pct": 0.3}), ("loose_lb3", {**_base_loose, "lookback_min": 3}), ("tight_sl", {"vol_mult": 1.0, "sl_mode": "atr", "atr_sl_mult": 1.0, "trail_pct": 0.5}), ("wide", {"vol_mult": 1.0, "shoulder_cut_pct": 0.5, "trail_pct": 2.0}), ] print("▶ 공유메모리 store 빌드...") store = build_shared_ticks(ticks_dict) if store is None: print("❌ build_shared_ticks None — 검증 불가") return 1 all_ok = True total_trades_checked = 0 try: for name, override in variants: ep = dict(engine_params) ep.update(override) trades_a, stats_a = run_backtest(codes_candles, universe, ticks_dict, ep, slot_money, max_stocks, total_budget, start_key, end_key) shared_map = store.attach_mapping() trades_b, stats_b = run_backtest(codes_candles, universe, shared_map, ep, slot_money, max_stocks, total_budget, start_key, end_key) na, nb = norm_trades(trades_a), norm_trades(trades_b) ok = True if len(na) != len(nb): ok = False detail = f"거래수 {len(na)} vs {len(nb)}" else: diff = [i for i in range(len(na)) if na[i] != nb[i]] if diff: ok = False detail = f"내용불일치 {len(diff)}건 (idx {diff[0]})" print(" dict :", dict(na[diff[0]])) print(" shared:", dict(nb[diff[0]])) else: detail = "동일" for k in ("total_trades", "total_pnl", "win_rate", "pf", "mdd"): if stats_a.get(k) != stats_b.get(k): ok = False detail += f" | {k} {stats_a.get(k)}≠{stats_b.get(k)}" total_trades_checked += len(na) all_ok = all_ok and ok print(f" {'✅' if ok else '❌'} {name:16s} 거래 {len(na):3d} · 손익 {stats_a['total_pnl']:+,.0f} · {detail}") finally: store.unlink() print(f"\n대조한 총 거래 {total_trades_checked}건 (변형 {len(variants)}개)") print("🎉 bit-identical 통과" if all_ok else "🚨 불일치 발생") return 0 if all_ok else 2 if __name__ == "__main__": sys.exit(main())