Files
kis_bot/scratch/audit_orderbook_trade_timing.py
Your Name 36a3e2b4a1 feat: Enhance trading system with new permanent subscription features and order book management
Changes:
- Added a new API endpoint for managing permanent subscriptions, allowing users to enable or disable subscriptions dynamically.
- Implemented a function to fill candle data from Kiwoom, ensuring that only relevant data is inserted into the database.
- Introduced a mechanism to handle master subscription states, improving the management of subscription statuses.
- Updated the database schema to include new fields for managing subscription states and order book filtering.

Impact:
- These enhancements improve the flexibility and reliability of the trading system, allowing for better management of subscriptions and order book data, while reducing the risk of data inconsistencies.

히스토리 align 제거 븅신같은 초기설계 아예 제거
진입모드에 구멍메움
호가진입을 켜도 호가가 안들어올때 호가 안보고 그냥 사버림
2026-08-15 23:01:14 +09:00

407 lines
14 KiB
Python

#!/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()