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

132 lines
5.7 KiB
Python

"""
kis_trader/scripts/approx_param_search_from_log.py
==================================================
log_backfill(판정 로그 백필) 기반 **근사 파람서치** — 완화 방향 1차 추정 전용.
⚠️ 한계 (정직):
- 매 판정마다 "처음 걸린 검사 1개"의 수치만 있다.
- 따라서 임계값을 **완화**했을 때 "그 검사를 통과로 전환하는 건수의 상한"만 안다.
- 전환 후 다음 검사(미관측) 통과 여부는 모른다 → 실제 매수 증가는 이보다 적다.
- 임계 **강화** 방향은 통과(시그널) 건에 수치가 없어 계산 불가.
→ 정밀 파람서치는 filter_eval(호가 본체)이 쌓인 뒤 별도로.
사용:
python -m kis_trader.scripts.approx_param_search_from_log --ymd 20260626 --strategy MOMENTUM
"""
from __future__ import annotations
import argparse
import re
from collections import Counter
from typing import Any, Dict, List, Optional
from kis_trader.database.db_manager import get_db
LOG_BACKFILL_SOURCE = "log_backfill"
_RATIO_RE = re.compile(r"잔량\(\d+호가\)\s+([\d.]+)\s+<\s+([\d.]+)")
_SPREAD_RE = re.compile(r"스프레드\s+([\d.]+)%\s+>\s+([\d.]+)%")
_WALL_RE = re.compile(r"\s+(\d+)주\s+>\s+허용\s+(\d+)주")
def _load_rows(db, ymd: str, strategy: str) -> List[Dict[str, Any]]:
sql = (
"SELECT snap_time, code, strategy, reject_code, reject_msg "
"FROM ws_orderbook WHERE source=%s AND snap_time LIKE %s"
)
params: List[Any] = [LOG_BACKFILL_SOURCE, ymd + "%"]
strat = (strategy or "").strip().upper()
if strat in ("TAIL", "SHORT"):
sql += " AND strategy IN ('TAIL','SHORT')"
elif strat:
sql += " AND strategy=%s"
params.append(strat)
sql += " ORDER BY snap_time"
return [dict(r) for r in db.conn.execute(sql, tuple(params)).fetchall()]
def run(ymd: str, strategy: str, *, ask_mult_default: float = 3.0) -> None:
db = get_db()
rows = _load_rows(db, ymd, strategy)
if not rows:
print(f"⚠️ log_backfill 데이터 없음 (ymd={ymd}, strategy={strategy}). 먼저 백필 실행.")
return
reason_cnt: Counter = Counter()
passes = 0
ratios: List[float] = [] # 호가수급: 실제 ratio
spreads: List[float] = [] # 호가스프레드: 실제 spread%
walls: List[tuple] = [] # 매도벽: (ask3, allow)
cur_ratio_thr = 0.85
cur_spread_thr = 0.45
for r in rows:
rc = r.get("reject_code")
msg = r.get("reject_msg") or ""
if not rc:
passes += 1
continue
reason_cnt[rc] += 1
if rc == "탈락-호가수급":
m = _RATIO_RE.search(msg)
if m:
ratios.append(float(m.group(1)))
cur_ratio_thr = float(m.group(2))
elif rc == "탈락-호가스프레드":
m = _SPREAD_RE.search(msg)
if m:
spreads.append(float(m.group(1)))
cur_spread_thr = float(m.group(2))
elif rc == "탈락-매도벽":
m = _WALL_RE.search(msg)
if m:
walls.append((int(m.group(1)), int(m.group(2))))
total = len(rows)
print(f"\n===== 근사 파람서치 (ymd={ymd}, strategy={strategy or 'ALL'}) =====")
print(f"호가필터 판정 총 {total}건 | 통과(시그널) {passes} | 거절 {total - passes}")
print("거절 사유 분포:", dict(reason_cnt))
# ── 1) 호가수급(min_bid_ask_ratio) 완화 스윕 ───────────────
print(f"\n[호가수급] 현재 임계 min_bid_ask_ratio = {cur_ratio_thr}")
print(" 임계 낮추면 '호가수급 거절→통과 전환(상한)':")
for thr in (0.85, 0.7, 0.5, 0.35, 0.2, 0.0):
flip = sum(1 for x in ratios if x >= thr)
print(f" ratio>={thr:<4}{flip:>4}/{len(ratios)} 전환")
# ── 2) 스프레드(max_spread_pct) 완화 스윕 ──────────────────
print(f"\n[스프레드] 현재 임계 max_spread_pct = {cur_spread_thr}%")
print(" 임계 높이면 '스프레드 거절→통과 전환(상한)':")
for thr in (0.45, 0.6, 0.8, 1.0, 1.5, 2.0):
flip = sum(1 for x in spreads if x <= thr)
print(f" spread<={thr:<4}% → {flip:>4}/{len(spreads)} 전환")
# ── 3) 매도벽(entry_ask_max_mult) 완화 스윕 ────────────────
print(f"\n[매도벽] 현재 entry_ask_max_mult = {ask_mult_default} (허용=주문수량×배수)")
print(" 배수 높이면 '매도벽 거절→통과 전환(상한)':")
for new_mult in (3.0, 5.0, 8.0, 12.0, 20.0, 50.0):
flip = 0
for ask3, allow in walls:
qty_need = allow / ask_mult_default if ask_mult_default > 0 else 0
new_allow = qty_need * new_mult
if ask3 <= new_allow:
flip += 1
print(f" mult={new_mult:<5}{flip:>4}/{len(walls)} 전환")
print("\n※ 모든 수치는 '해당 검사 통과 전환 상한'이다. 다음 검사(미관측) 통과 여부는")
print(" 포함하지 않으므로 실제 매수 증가는 이보다 적다. 정밀치는 filter_eval 누적 후.")
def main(argv: Optional[List[str]] = None) -> int:
ap = argparse.ArgumentParser(description="log_backfill 기반 근사 파람서치(완화방향)")
ap.add_argument("--ymd", required=True, help="거래일 YYYYMMDD")
ap.add_argument("--strategy", default="MOMENTUM", help="전략 (기본 MOMENTUM)")
ap.add_argument("--ask-mult-default", type=float, default=3.0,
help="당시 entry_ask_max_mult (허용 역산용, 기본 3.0)")
args = ap.parse_args(argv)
run(args.ymd, args.strategy, ask_mult_default=args.ask_mult_default)
return 0
if __name__ == "__main__":
raise SystemExit(main())