Files
kis_bot/_verify_shared_e2e.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

153 lines
6.2 KiB
Python

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