Files
kis_trader/_verify_shared_e2e_breakout.py
Hwang 61c72a8a4c feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항
----
- _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가
- _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가
- _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가

영향
----
- 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임
- 기존 기능에 대한 영향 없음

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 01:27:00 +09:00

157 lines
6.2 KiB
Python

#!/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())