#!/usr/bin/env python3 """최근 실체결 매수/매도 시각 vs 호가 스냅·필터 판정 대조 (조회 전용).""" from __future__ import annotations import json import sys from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List, Optional, Tuple ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from database import TradeDB from kis_trader.engine.orderbook_env import load_orderbook_threshold_cfg, orderbook_filter_enabled from kis_trader.engine.orderbook_filter import _evaluate_orderbook_verdict from kis_trader.utils.env import get_env_from_db, invalidate_merged_env_cache from kis_trader.ws.orderbook_cache import orderbook_snapshot_from_storage # 최근 거래일(금) 포함 며칠 LOOKBACK_PREFIX = "2026081" # 2026-08-10~14 장일 묶음 (LIKE 바인딩) DAY_START = "20260810" def ts14(raw: Any) -> str: if raw is None: return "" s = str(raw).strip() if not s: return "" s = s.replace("-", "").replace(":", "").replace(" ", "").replace("T", "").replace(".", "") if len(s) < 8: return "" return (s + "000000")[:14] def parse14(st: str) -> Optional[datetime]: st = (st or "").strip() if len(st) < 12: return None try: return datetime.strptime(st[:14], "%Y%m%d%H%M%S") except ValueError: return None def strat_canon(s: str) -> str: u = (s or "").strip().upper() if u in ("SHORT", "TAIL_CATCH", "TAIL"): return "TAIL" if u in ("BO", "BREAKOUT"): return "BREAKOUT" if u in ("MOM", "MOMENTUM"): return "MOMENTUM" if u in ("SCALP", "SCALPING", "REVERSAL"): return "SCALP" return u def cols(db: TradeDB, table: str) -> List[str]: rows = db.conn.execute(f"SHOW COLUMNS FROM {table}").fetchall() out = [] for r in rows: d = dict(r) if not isinstance(r, dict) else r out.append(d.get("Field") or d.get("field") or list(d.values())[0]) return out def nearest_ob( db: TradeDB, table: str, have: List[str], code: str, t: datetime, window_sec: int, source: Optional[str] = None, ) -> Optional[Dict[str, Any]]: t0 = (t - timedelta(seconds=window_sec)).strftime("%Y%m%d%H%M%S") t1 = (t + timedelta(seconds=window_sec)).strftime("%Y%m%d%H%M%S") extra = "" params: List[Any] = [code, t0, t1] if source and "source" in have: extra = " AND source = %s" params.append(source) sel = [ c for c in ( "id", "code", "snap_time", "recv_ts", "best_bid", "best_ask", "total_bid_qty", "total_ask_qty", "bid_qty_l3", "ask_qty_l3", "levels_json", "source", "strategy", "reject_code", "reject_msg", "eval_price", ) if c in have ] sql = ( f"SELECT {', '.join(sel)} FROM {table} " f"WHERE code = %s AND snap_time >= %s AND snap_time <= %s{extra} " f"ORDER BY snap_time" ) rows = [dict(r) for r in db.conn.execute(sql, tuple(params)).fetchall()] if not rows: return None best = None best_dt = None for row in rows: dt = parse14(ts14(row.get("snap_time"))) if dt is None: continue if best is None or abs((dt - t).total_seconds()) < abs((best_dt - t).total_seconds()): best = row best_dt = dt if best is None: return None best["_delta_sec"] = (best_dt - t).total_seconds() return best def ratio_l3(row: Dict[str, Any]) -> Optional[float]: b = float(row.get("bid_qty_l3") or 0) a = float(row.get("ask_qty_l3") or 0) if a <= 0: return None return b / a def spread_pct(row: Dict[str, Any]) -> Optional[float]: bb = float(row.get("best_bid") or 0) ba = float(row.get("best_ask") or 0) if bb <= 0 or ba <= 0: return None mid = (bb + ba) / 2.0 return (ba - bb) / mid * 100.0 def env_flag(snap_json: Any, key: str) -> str: if not snap_json: return "" try: d = json.loads(snap_json) if isinstance(snap_json, str) else snap_json except (TypeError, ValueError, json.JSONDecodeError): return "" if not isinstance(d, dict): return "" for k in (key, key.lower()): if k in d: return str(d.get(k) or "") # nested env env = d.get("env") if isinstance(d.get("env"), dict) else {} return str(env.get(key) or "") def main() -> None: invalidate_merged_env_cache() db = TradeDB() try: print("=== SHOW COLUMNS ===") th_cols = cols(db, "trade_history") at_cols = cols(db, "active_trades") wo_cols = cols(db, "ws_orderbook") print("trade_history:", th_cols) print("active_trades:", at_cols) print("ws_orderbook:", wo_cols) ls_cols: List[str] = [] try: ls_cols = cols(db, "ls_ws_orderbook") print("ls_ws_orderbook:", ls_cols) except Exception as e: print("ls_ws_orderbook SHOW failed:", e) print("\n=== 현재 DB 호가 스위치 ===") keys = [] for pfx in ("MOMENTUM", "BREAKOUT", "SCALP", "TAIL"): keys.extend( [ f"{pfx}_ORDERBOOK_FILTER_ENABLED", f"{pfx}_ORDERBOOK_MAX_SPREAD_PCT", f"{pfx}_ORDERBOOK_MIN_BID_ASK_RATIO", f"{pfx}_ORDERBOOK_ENTRY_BID_LEVELS", f"{pfx}_EXIT_OB_ENABLED", f"{pfx}_STOP_OB_ENABLED", ] ) keys.extend( [ "ORDERBOOK_FILTER_ENABLED", "WS_ORDERBOOK_COLLECT_ENABLED", "WS_ORDERBOOK_SAVE_ENABLED", "KIWOOM_WS_ORDERBOOK_ENABLED", "LS_WS_ORDERBOOK_SAVE", "SELL_USE_ORDERBOOK_ON_PROFIT", ] ) for k in keys: print(f" {k}={get_env_from_db(k, '')!r}") for sid in ("MOMENTUM", "BREAKOUT", "SCALP", "TAIL"): print(f" orderbook_filter_enabled({sid})={orderbook_filter_enabled(sid)}") print("\n=== 호가 테이블 건수 (LIKE 바인딩) ===") for tbl, have in (("ws_orderbook", wo_cols), ("ls_ws_orderbook", ls_cols)): if not have: continue n = db.conn.execute( f"SELECT COUNT(*) AS n FROM {tbl} WHERE snap_time LIKE %s", (LOOKBACK_PREFIX + "%",), ).fetchone() print(f" {tbl} snap_time LIKE {LOOKBACK_PREFIX}% : {dict(n)['n']}") if "source" in have: srcs = db.conn.execute( f"SELECT source, COUNT(*) AS n FROM {tbl} WHERE snap_time LIKE %s GROUP BY source", (LOOKBACK_PREFIX + "%",), ).fetchall() for r in srcs: d = dict(r) print(f" source={d.get('source')!r} n={d.get('n')}") print("\n=== 실체결 trade_history (buy_date >= 20260810) ===") th_sel = [ c for c in ( "id", "code", "name", "strategy", "buy_price", "sell_price", "qty", "profit_rate", "buy_date", "sell_date", "sell_reason", "env_snapshot", ) if c in th_cols ] trades = [ dict(r) for r in db.conn.execute( f"SELECT {', '.join(th_sel)} FROM trade_history " f"WHERE buy_date LIKE %s OR sell_date LIKE %s OR buy_date LIKE %s OR sell_date LIKE %s " f"ORDER BY id DESC LIMIT 80", ("2026-08-1%", "2026-08-1%", "2026081%", "2026081%"), ).fetchall() ] # 날짜 포맷이 다를 수 있어 추가 필터 filtered: List[Dict[str, Any]] = [] for t in trades: b = ts14(t.get("buy_date")) s = ts14(t.get("sell_date")) if (b and b >= DAY_START) or (s and s >= DAY_START): filtered.append(t) print(f" rows matched query={len(trades)} after date filter={len(filtered)}") print("\n=== 미청산 active_trades ===") at_sel = [ c for c in ("code", "name", "strategy", "avg_buy_price", "current_qty", "buy_date", "status") if c in at_cols ] actives = [dict(r) for r in db.conn.execute(f"SELECT {', '.join(at_sel)} FROM active_trades").fetchall()] for a in actives: print( f" {a.get('strategy')} {a.get('code')} {a.get('name')} " f"buy={a.get('buy_date')} qty={a.get('current_qty')} st={a.get('status')}" ) events: List[Tuple[str, Dict[str, Any]]] = [] for t in filtered: events.append(("BUY", t)) events.append(("SELL", t)) for a in actives: events.append(("BUY_OPEN", a)) print("\n=== 체결↔호가 매칭 (매수 ±180s filter_eval 우선, 본체 ±15분) ===") summary = { "buy_n": 0, "sell_n": 0, "filter_on_would_reject": 0, "filter_eval_reject_but_bought": 0, "no_filter_eval": 0, "timing_gt_3s": 0, "timing_gt_30s": 0, "snap_none_would_pass": 0, } details: List[str] = [] for side, trade in events: code = str(trade.get("code") or "").strip() sid = strat_canon(str(trade.get("strategy") or "")) if side == "SELL": t14 = ts14(trade.get("sell_date")) else: t14 = ts14(trade.get("buy_date")) dt = parse14(t14) if not dt: continue if t14 < DAY_START: continue if side.startswith("BUY"): summary["buy_n"] += 1 else: summary["sell_n"] += 1 filt_on = orderbook_filter_enabled(sid) if sid in ("MOMENTUM", "BREAKOUT", "SCALP", "TAIL") else False env_k = f"{sid}_ORDERBOOK_FILTER_ENABLED" snap_flag = env_flag(trade.get("env_snapshot"), env_k) fe = nearest_ob(db, "ws_orderbook", wo_cols, code, dt, 180, "filter_eval") body_kw = nearest_ob(db, "ws_orderbook", wo_cols, code, dt, 900, "kiwoom_0d") if body_kw is None: body_kw = nearest_ob(db, "ws_orderbook", wo_cols, code, dt, 900, None) body_ls = None if ls_cols: body_ls = nearest_ob(db, "ls_ws_orderbook", ls_cols, code, dt, 900, None) body = body_ls if sid == "BREAKOUT" and body_ls else (body_kw or body_ls) judge = fe or body recon = None recon_msg = "" if judge: try: snap = orderbook_snapshot_from_storage(judge) recon, recon_msg = _evaluate_orderbook_verdict( snap, sid, {}, current_price=float(trade.get("buy_price") or trade.get("avg_buy_price") or 0) ) except Exception as e: recon_msg = f"eval_err:{e}" fe_rej = (fe or {}).get("reject_code") if fe else None dlt_fe = fe.get("_delta_sec") if fe else None dlt_body = body.get("_delta_sec") if body else None if fe and abs(float(dlt_fe)) > 3: summary["timing_gt_3s"] += 1 if (dlt_body is not None) and abs(float(dlt_body)) > 30: summary["timing_gt_30s"] += 1 if side.startswith("BUY") and not fe: summary["no_filter_eval"] += 1 if side.startswith("BUY") and recon: summary["filter_on_would_reject"] += 1 if filt_on: summary["filter_eval_reject_but_bought"] += 1 if side.startswith("BUY") and judge is None: summary["snap_none_would_pass"] += 1 spr = spread_pct(judge) if judge else None rat = ratio_l3(judge) if judge else None line = ( f"[{side}] {sid} {code} {trade.get('name','')} t={t14} " f"px={trade.get('buy_price') or trade.get('avg_buy_price') or trade.get('sell_price')} " f"qty={trade.get('qty') or trade.get('current_qty')} " f"reason={trade.get('sell_reason','') if side=='SELL' else ''} " f"DB필터ON={filt_on} env_snap={snap_flag!r}\n" f" filter_eval={'YES' if fe else 'NO'}" f" Δ={dlt_fe}s rej={fe_rej!r} msg={(fe or {}).get('reject_msg')!r}\n" f" body_src={(body or {}).get('source') if body else None} Δ={dlt_body}s " f"bidL3={(judge or {}).get('bid_qty_l3')} askL3={(judge or {}).get('ask_qty_l3')} " f"spread={spr} ratio={None if rat is None else round(rat,3)}\n" f" 재계산판정={recon!r} {recon_msg}" ) details.append(line) print(line) print("\n=== 요약 ===") for k, v in summary.items(): print(f" {k}={v}") print("\n=== filter_eval 탈락인데 같은 종목 매수가 있는지 (버그 후보) ===") if "reject_code" in wo_cols: rej_rows = [ dict(r) for r in db.conn.execute( "SELECT code, snap_time, strategy, reject_code, reject_msg, eval_price " "FROM ws_orderbook WHERE source = %s AND snap_time LIKE %s " "AND reject_code IS NOT NULL AND reject_code <> %s " "ORDER BY snap_time DESC LIMIT 40", ("filter_eval", LOOKBACK_PREFIX + "%", ""), ).fetchall() ] print(f" filter_eval reject rows (최근40)={len(rej_rows)}") for r in rej_rows[:15]: print( f" {r.get('snap_time')} {r.get('strategy')} {r.get('code')} " f"{r.get('reject_code')} {r.get('reject_msg')}" ) else: print(" reject_code 컬럼 없음") finally: db.close() if __name__ == "__main__": main()