""" 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())