Files
kis_bot/kis_trader/scripts/compare_momentum_scan_memo.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

234 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
모멘텀 백테 — baseline vs 스캔 eval 메모이즈 동일성·속도 비교 (프로덕션 코드 수정 없음).
제안 최적화: 틱·호가 없을 때 eval_momentum_buy_at_index 를
(code, signal_idx, daily_cnt, last_exit_dt) 키로 캐시 → 10초 스캔큐 내 중복 연산 제거.
유니버스·포트폴리오·청산 루프는 그대로 유지 → 결과 불변 전제 검증용.
사용:
cd /home/hoon/kis_bot
python3 kis_trader/scripts/compare_momentum_scan_memo.py
python3 kis_trader/scripts/compare_momentum_scan_memo.py --start 2026-06-22 --end 2026-06-26
"""
from __future__ import annotations
import argparse
import copy
import json
import sys
import time
from typing import Any, Dict, List, Optional, Tuple
sys.path.insert(0, "/home/hoon/kis_bot")
from kis_trader.backtest import momentum_backtest_common as mbc
from kis_trader.backtest import momentum_portfolio_backtest as mpb
from kis_trader.backtest.momentum_backtest_common import resolve_momentum_universe
from kis_trader.backtest.param_search_momentum import _load_candles_for_search, _ui_to_engine_params
_orig_eval = mpb.eval_momentum_buy_at_index
_eval_cache: Dict[Tuple, Tuple] = {}
_cache_hits = 0
_cache_miss = 0
def _state_key(state: Dict[str, Any]) -> Tuple:
led = state.get("last_exit_dt")
led_s = led.isoformat() if led is not None else ""
return (int(state.get("daily_cnt", 0) or 0), led_s)
def _params_eval_key(params: Dict[str, Any]) -> Tuple:
"""매수신호에 영향 주는 엔진 파라미터 + indicator cache 객체 id."""
ic = params.get("_indicator_cache")
keys = (
"rsi_period", "mom_rsi_min", "mom_rsi_max", "time_start_hm", "mom_time_end_hm",
"time_end_hm", "cooldown_min", "max_daily", "max_daily_chg", "min_price",
"use_defense_filters", "use_high_chase_filter", "use_daily_range_filter",
"use_ema_filter", "use_rsi_max_filter", "ema_fast_period", "ema_slow_period",
"high_chase_thr", "mom_vol_mult", "mom_vol_win", "pattern_breakout",
"pattern_pullback", "chase_lookback_min", "pullback_lookback_min",
"pullback_min_pct", "pullback_max_pct", "mom_max_from_open_pct",
"mom_min_from_open_pct", "skip_hts_scan_dupes",
"_ob_max_spread_pct", "_ob_min_bid_ask_ratio", "_ob_ask_max_mult",
"_backtest_orderbook_snapshot", "_backtest_program_snapshot",
"_backtest_log_orderbook_verdict",
)
parts: List[Any] = [id(ic)]
for k in keys:
v = params.get(k)
if isinstance(v, dict):
parts.append(id(v))
elif isinstance(v, (list, tuple)):
parts.append(tuple(v) if len(v) < 8 else id(v))
else:
parts.append(v)
return tuple(parts)
def _memo_eval(
candles: List[Dict],
i: int,
params: Dict[str, Any],
state: Dict[str, Any],
) -> Tuple[Optional[str], Optional[str], Optional[Dict[str, Any]]]:
global _cache_hits, _cache_miss
key = (id(candles), int(i), _state_key(state), _params_eval_key(params))
if key in _eval_cache:
_cache_hits += 1
return _eval_cache[key]
r = _orig_eval(candles, i, params, state)
_eval_cache[key] = r
_cache_miss += 1
return r
def _install_memo(enabled: bool) -> None:
global _cache_hits, _cache_miss
_cache_hits = 0
_cache_miss = 0
_eval_cache.clear()
if enabled:
mpb.eval_momentum_buy_at_index = _memo_eval
else:
mpb.eval_momentum_buy_at_index = _orig_eval
def _trade_key(t: Dict[str, Any]) -> Tuple:
return (
str(t.get("code") or ""),
str(t.get("buy_time") or t.get("entry_time") or ""),
str(t.get("sell_time") or t.get("exit_time") or ""),
)
def _norm_trade(t: Dict[str, Any]) -> Dict[str, Any]:
return {
"code": str(t.get("code") or ""),
"buy_time": str(t.get("buy_time") or t.get("entry_time") or ""),
"sell_time": str(t.get("sell_time") or t.get("exit_time") or ""),
"buy_price": round(float(t.get("buy_price") or t.get("entry") or 0), 4),
"sell_price": round(float(t.get("sell_price") or t.get("exit") or 0), 4),
"qty": int(t.get("qty") or 0),
"pnl": int(round(float(t.get("pnl") or 0))),
"sell_reason": str(t.get("sell_reason") or ""),
"hold_min": round(float(t.get("hold_min") or 0), 1),
}
def _run_once(
cc: Dict,
eng: Dict,
univ: Optional[Dict],
meta: Dict,
*,
use_memo: bool,
) -> Tuple[List[Dict], Dict[str, Any], float, Dict[str, int]]:
_install_memo(use_memo)
t0 = time.time()
tr = mbc.run_momentum_backtest_web_aligned(
cc, copy.deepcopy(eng), univ,
slot_money=300000, fee_rate=0.00015, sell_tax=0.0018,
max_stocks=20, total_budget_krw=6000000,
meta_out=copy.deepcopy(meta),
)
elapsed = time.time() - t0
stats = mbc.summarize_momentum_trades(
tr, total_budget_krw=6000000, period_days=5,
)
cache_info = {"hits": _cache_hits, "misses": _cache_miss}
return tr, stats, elapsed, cache_info
def _compare_trades(base: List[Dict], opt: List[Dict]) -> Dict[str, Any]:
bn = [_norm_trade(t) for t in sorted(base, key=_trade_key)]
on = [_norm_trade(t) for t in sorted(opt, key=_trade_key)]
out: Dict[str, Any] = {
"baseline_count": len(bn),
"memo_count": len(on),
"identical": bn == on,
"diff_samples": [],
}
if bn == on:
return out
n = max(len(bn), len(on))
for i in range(n):
b = bn[i] if i < len(bn) else None
o = on[i] if i < len(on) else None
if b != o:
out["diff_samples"].append({"idx": i, "baseline": b, "memo": o})
if len(out["diff_samples"]) >= 10:
break
return out
def _load_params_from_json() -> Dict[str, Any]:
path = "kis_trader/backtest/results/search_momentum_fast_20260627_015211.json"
with open(path, encoding="utf-8") as f:
return json.load(f)["top"][0]["merged_params"]
def main() -> int:
parser = argparse.ArgumentParser(description="모멘텀 baseline vs scan-eval 메모이즈 비교")
parser.add_argument("--start", default="2026-06-22")
parser.add_argument("--end", default="2026-06-26")
args = parser.parse_args()
sk = args.start.replace("-", "") + "0000"
ek = args.end.replace("-", "") + "2359"
print(f"기간: {args.start} ~ {args.end}")
print("=" * 70)
t_load = time.time()
cc = _load_candles_for_search(args.start, args.end, 3)
univ, src, n_slots, _, _ = resolve_momentum_universe(
sk[:8], ek[:8], use_saved_history=True,
)
mp = _load_params_from_json()
eng = _ui_to_engine_params(mp)
eng.update({
"slot_money": 300000,
"max_stocks": 20,
"total_budget_krw": 6000000,
"portfolio_mode": True,
})
meta = {"start_key": sk, "end_key": ek}
print(f"로드 {time.time() - t_load:.1f}s | 종목 {len(cc)} | 유니버스 {src} {n_slots}슬롯")
print("파라미터: search_momentum_fast 1위 merged (EMA OFF, vol×3, sl1.5%)")
print("=" * 70)
print("\n[1/2] BASELINE (현재 코드 그대로)")
tr_b, st_b, el_b, _ = _run_once(cc, eng, univ, meta, use_memo=False)
print(f" 시간 {el_b:.1f}s | 거래 {st_b['total_trades']} | 손익 {int(st_b['total_pnl']):+,} | "
f"PF {st_b['pf']} | 승률 {st_b['win_rate']}%")
print("\n[2/2] MEMO (eval_momentum_buy_at_index 메모이즈 — 프로덕션 미적용, 스크립트만)")
tr_m, st_m, el_m, cache = _run_once(cc, eng, univ, meta, use_memo=True)
print(f" 시간 {el_m:.1f}s | 거래 {st_m['total_trades']} | 손익 {int(st_m['total_pnl']):+,} | "
f"PF {st_m['pf']} | 승률 {st_m['win_rate']}%")
print(f" eval 캐시 hit {cache['hits']:,} / miss {cache['misses']:,} "
f"(hit률 {100.0 * cache['hits'] / max(1, cache['hits'] + cache['misses']):.1f}%)")
cmp = _compare_trades(tr_b, tr_m)
speedup = el_b / el_m if el_m > 0 else 0.0
print("\n" + "=" * 70)
print("비교 결과")
print(f" 거래건수: baseline {cmp['baseline_count']} vs memo {cmp['memo_count']}")
print(f" 손익: baseline {int(st_b['total_pnl']):+,} vs memo {int(st_m['total_pnl']):+,}")
print(f" PF: baseline {st_b['pf']} vs memo {st_m['pf']}")
print(f" 속도: baseline {el_b:.1f}s → memo {el_m:.1f}s ({speedup:.2f}x)")
print(f" 거래내역 동일: {'✅ YES' if cmp['identical'] else '❌ NO'}")
if not cmp["identical"]:
print("\n ⚠️ 차이 샘플 (최대 10건):")
for d in cmp["diff_samples"]:
print(f" #{d['idx']}")
print(f" baseline: {d['baseline']}")
print(f" memo: {d['memo']}")
print("=" * 70)
return 0 if cmp["identical"] else 1
if __name__ == "__main__":
raise SystemExit(main())