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>
This commit is contained in:
191
kis_trader/scripts/verify_three_paths.py
Normal file
191
kis_trader/scripts/verify_three_paths.py
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
verify_three_paths.py — 실매 / param_search / 웹백테 가 '똑같은 엔진 파라미터'로
|
||||
도는지 검증한다.
|
||||
|
||||
목적
|
||||
----
|
||||
JS(웹) 입력값은 표시단위(%)로 받아 쿼리스트링으로 전송되고, 파이썬(api_backtest_*)
|
||||
에서 ÷100 등으로 엔진 비율(ratio)로 되돌린다. 이 왕복(ratio→표시→ratio)이
|
||||
손실 없이 카논(실매·param_search 가 쓰는 get_*_defaults_from_db) 과 100% 일치하는지
|
||||
수치로 확인한다. (소수점/정수 변환 차이 적발)
|
||||
|
||||
검증 구조
|
||||
---------
|
||||
- 실매(live) : 전략 객체가 get_*_defaults_from_db() 비율값을 그대로 사용.
|
||||
- param_search: base = get_*_defaults_from_db() (코드상 동일 함수 → 자동 일치).
|
||||
- 웹백테(web) : _*_ui_defaults_from_db() (표시%) → (JS는 숫자 그대로 통과)
|
||||
→ api_backtest_* 변환(÷100) → 엔진 비율.
|
||||
|
||||
따라서 'web 왕복 후 비율' == 'canonical 비율' 이면 세 경로가 동일하다.
|
||||
이 스크립트는 외부 서버/데이터 없이 변환만 재현해 비교한다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
# 허용 오차 — 부동소수 반올림(웹 표시 round(x*100,3)) 으로 생길 수 있는 미세 오차
|
||||
TOL = 1e-9
|
||||
|
||||
|
||||
def _fmt(v):
|
||||
if isinstance(v, float):
|
||||
return f"{v:.10g}"
|
||||
return str(v)
|
||||
|
||||
|
||||
def _cmp_rows(rows):
|
||||
"""rows: [(name, canonical, web, unit)] → 출력 + 불일치 수 반환."""
|
||||
bad = 0
|
||||
print(f" {'필드':28} {'canonical(실매/파서치)':>22} {'web 왕복후':>16} 판정")
|
||||
print(" " + "-" * 78)
|
||||
for name, can, web, unit in rows:
|
||||
if isinstance(can, (int, float)) and isinstance(web, (int, float)):
|
||||
ok = abs(float(can) - float(web)) <= TOL
|
||||
else:
|
||||
ok = str(can) == str(web)
|
||||
mark = "OK " if ok else "❌MISMATCH"
|
||||
if not ok:
|
||||
bad += 1
|
||||
print(f" {name:28} {_fmt(can):>22} {_fmt(web):>16} {mark} {unit}")
|
||||
return bad
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# 공통: 웹 표시% → 엔진 비율 (api_backtest_scalping 의 sl/tp 변환과 동일)
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
def _ui_pct_to_ratio(ui_pct) -> float:
|
||||
"""웹 입력(%) → 엔진 비율. api_backtest_scalping: float(x)/100 후 abs."""
|
||||
return abs(float(ui_pct) / 100.0)
|
||||
|
||||
|
||||
def verify_momentum() -> int:
|
||||
import backtest_web as bw
|
||||
import kis_trader.engine.momentum_engine as me
|
||||
import scalping_engine as se # noqa: F401 (웹이 쓰는 베이스 로더)
|
||||
|
||||
print("\n=== 모멘텀 (MOMENTUM) ===")
|
||||
can = me.get_momentum_defaults_from_db() # 실매 + param_search base (비율)
|
||||
ui = bw._momentum_ui_defaults_from_db(can) # 웹 표시값 (%)
|
||||
|
||||
# 웹 → 엔진 비율 재현 (api_backtest_scalping 변환)
|
||||
rows = [
|
||||
("sl_pct(손절)", can["sl_pct"], _ui_pct_to_ratio(ui["sl_pct"]), "비율"),
|
||||
("tp_pct(익절)", can["tp_pct"], _ui_pct_to_ratio(ui["tp_pct"]), "비율"),
|
||||
("tp_max_pct(익절상한)", can["tp_max_pct"], _ui_pct_to_ratio(ui["tp_max_pct"]), "비율"),
|
||||
("shoulder_min_high", can["shoulder_min_high"], _ui_pct_to_ratio(ui["shoulder_min_high"]), "비율"),
|
||||
("shoulder_cut_pct", can["shoulder_cut_pct"], _ui_pct_to_ratio(ui["shoulder_cut_pct"]), "비율"),
|
||||
# 정수/그대로 통과 필드
|
||||
("mom_rsi_min", can["mom_rsi_min"], float(ui["mom_rsi_min"]), "그대로"),
|
||||
("mom_rsi_max", can["mom_rsi_max"], float(ui["mom_rsi_max"]), "그대로"),
|
||||
("mom_vol_mult", can["mom_vol_mult"], float(ui["mom_vol_mult"]), "그대로"),
|
||||
("mom_vol_win", can["mom_vol_win"], int(float(ui["mom_vol_win"])), "정수"),
|
||||
("max_daily", can["max_daily"], int(float(ui["max_daily"])), "정수"),
|
||||
]
|
||||
return _cmp_rows(rows)
|
||||
|
||||
|
||||
def verify_breakout() -> int:
|
||||
import backtest_web as bw
|
||||
from kis_trader.strategies.breakout import breakout_ui_to_engine_params
|
||||
|
||||
print("\n=== 돌파 (BREAKOUT) ===")
|
||||
# 웹과 param_search 는 둘 다 breakout_ui_to_engine_params 사용 (동일 함수).
|
||||
ui = bw._bo_defaults_from_db()
|
||||
web_engine = bw._bo_ui_to_engine_params(ui) # 웹 경로 엔진값
|
||||
ps_engine = breakout_ui_to_engine_params(dict(ui)) # param_search 경로 (같은 함수)
|
||||
|
||||
# 실매(live) 가 읽는 DB 비율과도 일치하는지 — breakout strategy 기본 키
|
||||
from kis_trader.utils.env import get_strategy_env_dict
|
||||
env = get_strategy_env_dict("BREAKOUT")
|
||||
|
||||
def env_ratio(key, default):
|
||||
v = env.get(key)
|
||||
if v in (None, "", "None"):
|
||||
return float(default)
|
||||
return abs(float(v))
|
||||
|
||||
keys = [
|
||||
("sl_pct", "stop_loss_pct"),
|
||||
("tp_pct", "take_profit_pct"),
|
||||
("trail_pct", "trail_pct"),
|
||||
("shoulder_min_high_pct", "shoulder_min_high_pct"),
|
||||
("shoulder_cut_pct", "shoulder_cut_pct"),
|
||||
]
|
||||
bad = 0
|
||||
print(" [A] 웹 vs param_search (동일 함수여야 100% 일치)")
|
||||
web_keys = sorted(set(web_engine) & set(ps_engine))
|
||||
for k in web_keys:
|
||||
a, b = web_engine.get(k), ps_engine.get(k)
|
||||
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
||||
if abs(float(a) - float(b)) > TOL:
|
||||
print(f" ❌ {k}: web={a} ps={b}")
|
||||
bad += 1
|
||||
elif str(a) != str(b):
|
||||
print(f" ❌ {k}: web={a} ps={b}")
|
||||
bad += 1
|
||||
if bad == 0:
|
||||
print(f" OK — 공통 {len(web_keys)}개 키 전부 일치")
|
||||
|
||||
print(" [B] 웹 엔진비율 vs 실매 DB 비율")
|
||||
rows = []
|
||||
for eng_key, _ in keys:
|
||||
if eng_key not in web_engine:
|
||||
continue
|
||||
rows.append((eng_key, web_engine[eng_key], web_engine[eng_key], "비율(웹=엔진)"))
|
||||
# 실제 비교: web_engine 값이 DB 원본 비율과 같은지
|
||||
rows2 = []
|
||||
sl_dbf = env_ratio("BREAKOUT_STOP_LOSS_PCT", 0.02)
|
||||
rows2.append(("BREAKOUT_STOP_LOSS_PCT", sl_dbf, abs(float(web_engine.get("stop_loss_pct", web_engine.get("sl_pct", sl_dbf)))), "비율"))
|
||||
bad += _cmp_rows(rows2)
|
||||
return bad
|
||||
|
||||
|
||||
def verify_tail() -> int:
|
||||
import backtest_web as bw
|
||||
import kis_trader.engine.tail_engine as te
|
||||
|
||||
print("\n=== 꼬리 (TAIL/SHORT) ===")
|
||||
can = te.get_tail_defaults_from_db() # 실매 + param_search base
|
||||
ui = bw._tail_ui_defaults_from_db() # 웹 표시값
|
||||
|
||||
# 웹 표시(%) → 엔진 비율 재현 후 카논과 비교 (낙폭/손절/익절/어깨 계열)
|
||||
rows = [
|
||||
("sl_pct", can.get("sl_pct"), _ui_pct_to_ratio(ui["sl_pct"]), "비율"),
|
||||
("tp_pct", can.get("tp_pct"), _ui_pct_to_ratio(ui["tp_pct"]), "비율"),
|
||||
("shoulder_min_high", can.get("shoulder_min_high"), _ui_pct_to_ratio(ui["smin"]), "비율"),
|
||||
("shoulder_cut_pct", can.get("shoulder_cut_pct"), _ui_pct_to_ratio(ui["scut"]), "비율"),
|
||||
("rsi_threshold", can.get("rsi_threshold"), float(ui["rsi"]), "그대로"),
|
||||
("rsi_period", can.get("rsi_period"), int(float(ui["rsi_period"])), "정수"),
|
||||
("max_daily", can.get("max_daily"), int(float(ui["max_daily"])), "정수"),
|
||||
("stop_atr_mult", can.get("stop_atr_mult"), float(ui["stop_atr_mult"]), "그대로"),
|
||||
("target_atr_mult", can.get("target_atr_mult"), float(ui["target_atr_mult"]), "그대로"),
|
||||
]
|
||||
return _cmp_rows(rows)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
total_bad = 0
|
||||
for fn in (verify_momentum, verify_breakout, verify_tail):
|
||||
try:
|
||||
total_bad += fn()
|
||||
except Exception as e: # noqa: BLE001
|
||||
import traceback
|
||||
print(f"\n[ERROR] {fn.__name__}: {e}")
|
||||
traceback.print_exc()
|
||||
total_bad += 1
|
||||
print("\n" + "=" * 80)
|
||||
if total_bad == 0:
|
||||
print("✅ 검증 통과 — 실매 / param_search / 웹백테 가 동일한 엔진 파라미터로 돕니다.")
|
||||
else:
|
||||
print(f"❌ 불일치 {total_bad}건 — 위 MISMATCH 항목을 확인하세요.")
|
||||
return total_bad
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(0 if main() == 0 else 1)
|
||||
Reference in New Issue
Block a user