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 제거 븅신같은 초기설계 아예 제거
진입모드에 구멍메움
호가진입을 켜도 호가가 안들어올때 호가 안보고 그냥 사버림
This commit is contained in:
Your Name
2026-08-15 23:01:14 +09:00
parent 4a18ce2697
commit 36a3e2b4a1
94 changed files with 6368 additions and 1639 deletions

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""0814 filter_eval 매도벽 — ASK_MAX_MULT 2 vs 3 vs 완화 시 통과 건수 (조회 전용)."""
from __future__ import annotations
import sys
from pathlib import Path
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.utils.env import get_env_from_db, invalidate_merged_env_cache
def main() -> None:
invalidate_merged_env_cache()
db = TradeDB()
try:
cols = [dict(r)["Field"] for r in db.conn.execute("SHOW COLUMNS FROM ws_orderbook").fetchall()]
print("ws_orderbook cols ok", "eval_price" in cols, "ask_qty_l3" in cols)
slot = float(get_env_from_db("SLOT_MONEY_DEFAULT", "300000") or 300000)
print("SLOT", slot)
for k in (
"MOMENTUM_ORDERBOOK_ENTRY_ASK_MAX_MULT",
"BREAKOUT_ORDERBOOK_ENTRY_ASK_MAX_MULT",
"SCALP_ORDERBOOK_ENTRY_ASK_MAX_MULT",
"TAIL_ORDERBOOK_ENTRY_ASK_MAX_MULT",
):
print(k, repr(get_env_from_db(k, "")))
like = "20260814%"
br = db.conn.execute(
"SELECT strategy, reject_code, COUNT(*) AS n FROM ws_orderbook "
"WHERE source=%s AND snap_time LIKE %s GROUP BY strategy, reject_code",
("filter_eval", like),
).fetchall()
print("\n=== reject_code 0814 ===")
for r in br:
d = dict(r)
print(f" {d.get('strategy')} {d.get('reject_code')!r}: {d.get('n')}")
rows = [
dict(r)
for r in db.conn.execute(
"SELECT strategy, eval_price, ask_qty_l3, reject_code FROM ws_orderbook "
"WHERE source=%s AND snap_time LIKE %s AND eval_price > 0 AND ask_qty_l3 >= 0",
("filter_eval", like),
).fetchall()
]
print("rows with eval_price", len(rows))
mults = (2.0, 3.0, 8.0, 20.0, 50.0, 100.0)
by = {}
for row in rows:
sid = str(row.get("strategy") or "")
px = float(row.get("eval_price") or 0)
ask = int(row.get("ask_qty_l3") or 0)
if px <= 0:
continue
qty = max(1, int(slot / px))
rec = by.setdefault(sid, {"n": 0, "pass": {m: 0 for m in mults}})
rec["n"] += 1
for m in mults:
if ask <= int(qty * m):
rec["pass"][m] += 1
print("\n=== 매도벽만 단독 적용 시 통과율 (다른 탈락 무시) ===")
print("규칙: 매도3호가합 <= 필요주수 × 배수 이면 통과")
print("필요주수 = floor(슬롯/평가가)")
for sid, rec in sorted(by.items()):
n = rec["n"]
print(f"\n[{sid}] n={n}")
for m in mults:
p = rec["pass"][m]
pct = (100.0 * p / n) if n else 0
print(f" ×{m:g}: 통과 {p}/{n} ({pct:.1f}%)")
finally:
db.close()
if __name__ == "__main__":
main()

210
scratch/audit_ob_0814.py Normal file
View File

@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""2026-08-14 실체결 vs filter_eval 근접 통계 (인덱스 친화)."""
from __future__ import annotations
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional
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.utils.env import get_env_from_db, invalidate_merged_env_cache
def ts14(raw: Any) -> str:
s = str(raw or "").strip()
s = s.replace("-", "").replace(":", "").replace(" ", "").replace("T", "")
if len(s) < 8:
return ""
return (s + "000000")[:14]
def parse14(st: str) -> Optional[datetime]:
if len(st) < 14:
return None
try:
return datetime.strptime(st[:14], "%Y%m%d%H%M%S")
except ValueError:
return None
def main() -> None:
invalidate_merged_env_cache()
db = TradeDB()
try:
extra_keys = [
"WS_ORDERBOOK_TICK_MAX_AGE_SEC",
"SLOT_MONEY_DEFAULT",
"MOMENTUM_SLOT_MONEY",
"BREAKOUT_SLOT_MONEY",
"SCALP_SLOT_MONEY",
"TAIL_SLOT_MONEY",
"MOMENTUM_ORDERBOOK_ENTRY_ASK_MAX_MULT",
"BREAKOUT_ORDERBOOK_ENTRY_ASK_MAX_MULT",
"SCALP_ORDERBOOK_ENTRY_ASK_MAX_MULT",
"TAIL_ORDERBOOK_ENTRY_ASK_MAX_MULT",
"LIVE_OB_PROVIDER",
"WS_TRIGGER_EVAL_SAVE_ENABLED",
"MOMENTUM_ORDERBOOK_COLLECT_ENABLED",
"BREAKOUT_ORDERBOOK_COLLECT_ENABLED",
"KIWOOM_WS_ORDERBOOK_ENABLED",
"LS_WS_UH1_ENABLED",
"LS_CONDITION_ORDERBOOK",
]
print("=== extra env ===")
for k in extra_keys:
print(f" {k}={get_env_from_db(k, '')!r}")
day = "20260814"
like = day + "%"
trades = [
dict(r)
for r in db.conn.execute(
"SELECT id, code, name, strategy, buy_price, qty, buy_date, sell_date, sell_reason "
"FROM trade_history WHERE buy_date LIKE %s OR buy_date LIKE %s "
"ORDER BY buy_date",
("2026-08-14%", like),
).fetchall()
]
opens = [
dict(r)
for r in db.conn.execute(
"SELECT code, name, strategy, avg_buy_price AS buy_price, current_qty AS qty, buy_date "
"FROM active_trades WHERE buy_date LIKE %s OR buy_date LIKE %s",
("2026-08-14%", like),
).fetchall()
]
print(f"\nclosed buys 0814={len(trades)} open buys 0814={len(opens)}")
fe_n = db.conn.execute(
"SELECT COUNT(*) AS n FROM ws_orderbook WHERE source=%s AND snap_time LIKE %s",
("filter_eval", like),
).fetchone()
print("filter_eval 0814 n=", dict(fe_n)["n"])
rej_n = db.conn.execute(
"SELECT COUNT(*) AS n FROM ws_orderbook WHERE source=%s AND snap_time LIKE %s "
"AND reject_code IS NOT NULL AND reject_code <> %s",
("filter_eval", like, ""),
).fetchone()
print("filter_eval reject 0814 n=", dict(rej_n)["n"])
pass_n = db.conn.execute(
"SELECT COUNT(*) AS n FROM ws_orderbook WHERE source=%s AND snap_time LIKE %s "
"AND (reject_code IS NULL OR reject_code = %s)",
("filter_eval", like, ""),
).fetchone()
print("filter_eval pass 0814 n=", dict(pass_n)["n"])
by_st = db.conn.execute(
"SELECT strategy, "
"SUM(CASE WHEN reject_code IS NOT NULL AND reject_code <> %s THEN 1 ELSE 0 END) AS rej, "
"COUNT(*) AS n FROM ws_orderbook WHERE source=%s AND snap_time LIKE %s "
"GROUP BY strategy",
("", "filter_eval", like),
).fetchall()
print("filter_eval by strategy:")
for r in by_st:
d = dict(r)
print(f" {d.get('strategy')}: n={d.get('n')} rej={d.get('rej')}")
buckets = {
"fe_pm3": 0,
"fe_pm30": 0,
"fe_pm180": 0,
"fe_none180": 0,
"fe_rej_then_buy": 0,
"fe_pass_then_buy": 0,
"body_pm5": 0,
}
examples: List[str] = []
all_buys = list(trades) + list(opens)
for t in all_buys:
code = str(t.get("code") or "").strip()
b14 = ts14(t.get("buy_date"))
dt = parse14(b14)
if not dt:
continue
t0 = (dt - timedelta(seconds=180)).strftime("%Y%m%d%H%M%S")
t1 = (dt + timedelta(seconds=180)).strftime("%Y%m%d%H%M%S")
rows = [
dict(r)
for r in db.conn.execute(
"SELECT snap_time, reject_code, reject_msg, bid_qty_l3, ask_qty_l3, source, strategy "
"FROM ws_orderbook WHERE code=%s AND snap_time>=%s AND snap_time<=%s "
"AND source=%s ORDER BY snap_time",
(code, t0, t1, "filter_eval"),
).fetchall()
]
t0b = (dt - timedelta(seconds=5)).strftime("%Y%m%d%H%M%S")
t1b = (dt + timedelta(seconds=5)).strftime("%Y%m%d%H%M%S")
body5 = db.conn.execute(
"SELECT COUNT(*) AS n FROM ws_orderbook WHERE code=%s AND snap_time>=%s AND snap_time<=%s "
"AND source=%s",
(code, t0b, t1b, "kiwoom_0d"),
).fetchone()
if dict(body5)["n"] > 0:
buckets["body_pm5"] += 1
if not rows:
buckets["fe_none180"] += 1
if len(examples) < 12:
examples.append(
f"NO_EVAL {t.get('strategy')} {code} {t.get('name')} buy={b14} "
f"body±5s={dict(body5)['n']}"
)
continue
best = None
best_abs = 1e9
for row in rows:
dtr = parse14(str(row.get("snap_time") or ""))
if not dtr:
continue
ad = abs((dtr - dt).total_seconds())
if ad < best_abs:
best_abs = ad
best = row
if best is None:
buckets["fe_none180"] += 1
continue
if best_abs <= 3:
buckets["fe_pm3"] += 1
if best_abs <= 30:
buckets["fe_pm30"] += 1
buckets["fe_pm180"] += 1
rej = str(best.get("reject_code") or "").strip()
if rej:
buckets["fe_rej_then_buy"] += 1
examples.append(
f"REJ_BUY Δ={best_abs:.0f}s {t.get('strategy')} {code} {t.get('name')} "
f"buy={b14} eval={best.get('snap_time')} {rej} {best.get('reject_msg')} "
f"L3 {best.get('bid_qty_l3')}/{best.get('ask_qty_l3')}"
)
else:
buckets["fe_pass_then_buy"] += 1
print("\n=== 0814 매수 vs filter_eval ===")
print("buys total", len(all_buys))
for k, v in buckets.items():
print(f" {k}={v}")
print("\n=== 탈락기록 후 매수 / 평가없음 샘플 ===")
for e in examples:
print(" ", e)
print("\n=== 0814 매도 사유 ===")
reasons = db.conn.execute(
"SELECT sell_reason, COUNT(*) AS n FROM trade_history "
"WHERE sell_date LIKE %s OR sell_date LIKE %s GROUP BY sell_reason ORDER BY n DESC",
("2026-08-14%", like),
).fetchall()
for r in reasons:
d = dict(r)
print(f" {d.get('sell_reason')!r}: {d.get('n')}")
finally:
db.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,406 @@
#!/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()