#!/usr/bin/env python3 """ 공유메모리 틱 배선 end-to-end 검증 — 동일 데이터로 dict vs SharedTicksMapping 백테 2회 대조. 거래가 확인된 실 파라미터(merged_params, 246거래)로 주간 백테를 두 번 돌린다: 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 momentum_backtest_common as mbc from kis_trader.backtest.param_search_momentum import _load_candles_for_search from kis_trader.backtest.momentum_backtest_common import resolve_momentum_universe from kis_trader.backtest.momentum_tick_loader import load_momentum_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_momentum_fast_20260705_030415.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) p["slot_money"] = float(slot_money) p["max_stocks"] = int(max_stocks) p["total_budget_krw"] = float(total_budget) p["portfolio_mode"] = True p["_backtest_period_start_key"] = start_key[:12] meta = {"start_key": start_key, "end_key": end_key} # db 없음 → 틱 재로딩 안 함 trades = mbc.run_momentum_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 = mbc.summarize_momentum_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 3_000_000) max_stocks = int(meta_j.get("max_stocks") or 3) total_budget = float(meta_j.get("total_budget_krw") or 9_000_000) rsi_period = int(engine_params.get("rsi_period", 3)) start_key = START.replace("-", "") + "0000" end_key = END.replace("-", "") + "2359" print("⏳ 캔들 로드...") codes_candles = _load_candles_for_search(START, END, rsi_period) print(f" 종목 {len(codes_candles):,}") print("⏳ 유니버스 로드...") history, src, n_bins, _iv, timing = resolve_momentum_universe( START.replace("-", ""), END.replace("-", ""), use_saved_history=True, strategy_id="MOMENTUM", ) universe = history print(f" 유니버스 슬롯 {len(universe or {}):,} (src={src})") print("⏳ ws_ticks 로드...") db = TradeDB() try: ticks_dict, tick_rows = load_momentum_ticks_by_code( db, start_key, end_key, set(codes_candles.keys()), ) finally: db.close() print(f" 틱 {tick_rows:,}건 / 종목 {len(ticks_dict):,}") # 거래를 더 많이 발생시켜 매도/진입/휩쏘 경로를 넓게 커버하는 변형들. variants = [ ("baseline(top)", {}), ("loose_entry", {"mom_rsi_min": 0, "mom_vol_mult": 1.0, "pattern_breakout": 1, "pattern_pullback": 1, "use_ema_filter": 0}), ("tight_sl", {"mom_rsi_min": 0, "mom_vol_mult": 1.0, "use_ema_filter": 0, "sl_pct": 0.7, "tp_pct": 2.0, "trail_pct": 0.5, "trail_arm_pct": 0.4}), ("wide_tp", {"mom_rsi_min": 0, "mom_vol_mult": 1.0, "use_ema_filter": 0, "sl_pct": 3.0, "tp_pct": 12.0, "tp_max_pct": 20.0, "shoulder_cut_pct": 0.5}), ] 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())