변경 사항 ---- - _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>
153 lines
6.2 KiB
Python
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())
|