Files
kis_bot/scripts/_diag_scalp_live_bt_20260716.py
Your Name 61bec4bd1d feat: Add DART strategy and related configurations
ㅇ
Changes:
- Introduced the DART strategy to the trading system, including its configuration and integration into the existing framework.
- Updated the database schema to include DART-specific tables for disclosures and watchlists.
- Enhanced the backtesting and parameter search functionalities to support the DART strategy.
- Implemented new rules for browser verification and API interactions to ensure compliance with the updated DART strategy.

Impact:
- These additions expand the trading capabilities of the system, allowing for more comprehensive analysis and execution of DART-related strategies, while maintaining system integrity and performance.
2026-07-21 07:50:24 +09:00

318 lines
11 KiB
Python

#!/usr/bin/env python3
"""SCALP 2026-07-16 실매↔웹BT 진입 괴리 진단 (adhoc)."""
from __future__ import annotations
from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple
from database import TradeDB
from kis_trader.backtest.scalping_backtest_common import resolve_scalp_universe
from kis_trader.engine.scalping_engine import (
_apply_buy_state_filters,
_eval_scalp_buy_at_index,
get_scalping_defaults_from_db,
)
def _z(code: Any) -> str:
s = str(code or "").strip()
return s.zfill(6) if s.isdigit() else s
def _hm_from_buy(buy_date: str) -> Tuple[str, str]:
"""'2026-07-16 09:39:03' → day=20260716, entry_key≈202607160939"""
raw = str(buy_date or "").strip().replace("-", "").replace(":", "").replace(" ", "")
day = raw[:8]
hm = raw[8:12] if len(raw) >= 12 else ""
return day, day + hm
def load_candles(db: TradeDB, code: str, day: str) -> List[Dict]:
rows = db.conn.execute(
"""SELECT candle_time, open, high, low, close, volume, is_confirmed
FROM ws_candles
WHERE code=%s AND timeframe=1 AND candle_time LIKE %s
ORDER BY candle_time""",
(code, day + "%"),
).fetchall()
out = []
for r in rows:
d = dict(r)
d["candle_time"] = str(d["candle_time"])
out.append(d)
return out
def find_index(candles: List[Dict], key12: str) -> Optional[int]:
k = str(key12)[:12]
for i, c in enumerate(candles):
if str(c["candle_time"])[:12] == k:
return i
return None
def nearest_index(candles: List[Dict], key12: str) -> Optional[int]:
k = str(key12)[:12]
best = None
best_d = 10**9
for i, c in enumerate(candles):
ct = str(c["candle_time"])[:12]
if len(ct) < 12:
continue
try:
d = abs(int(ct) - int(k))
except ValueError:
continue
if d < best_d:
best_d = d
best = i
return best
def eval_at(candles: List[Dict], i: int, params: Dict) -> Tuple[str, str]:
if i is None or i < 1:
return "no_idx", "봉인덱스 없음"
state = {"daily_cnt": 0, "last_exit_dt": None}
st = _apply_buy_state_filters(candles, i, params, state)
if st[2] is None:
return "state", str(st[0] or st[1] or "state_reject")
rej, msg, sig = _eval_scalp_buy_at_index(candles, i, params)
if rej:
return str(rej), str(msg or "")
if not sig:
return "no_sig", "신호없음"
rsi = sig.get("rsi")
return "PASS", "rsi=%s mode=%s" % (rsi, sig.get("entry_mode"))
def slot_has(univ: Dict[str, List[str]], slot: str, code: str) -> bool:
if not univ:
return False
return code in (univ.get(slot) or [])
def first_slots(univ: Dict[str, List[str]], code: str, day: str, limit: int = 8) -> List[str]:
out = []
for sk in sorted(univ.keys()):
if not sk.startswith(day):
continue
if code in (univ.get(sk) or []):
out.append(sk)
if len(out) >= limit:
break
return out
def main() -> None:
db = TradeDB()
live_rows = db.conn.execute(
"""SELECT code, name, buy_date, sell_date, buy_price, sell_reason, realized_pnl
FROM trade_history WHERE strategy=%s AND buy_date LIKE %s
ORDER BY buy_date""",
("SCALP", "2026-07-16%"),
).fetchall()
active_rows = db.conn.execute(
"""SELECT code, name, buy_date, avg_buy_price
FROM active_trades WHERE strategy=%s AND buy_date LIKE %s""",
("SCALP", "2026-07-16%"),
).fetchall()
# 사용자 웹 BT 9건 (스크린샷)
bt_user = [
("001130", "2026-07-16 14:55:00"),
("067830", "2026-07-16 14:25:00"),
("035000", "2026-07-16 09:55:00"),
("047770", "2026-07-16 14:27:00"),
("226400", "2026-07-16 12:36:00"),
("330350", "2026-07-16 12:42:00"),
("460930", "2026-07-16 09:21:00"),
("007540", "2026-07-16 09:36:00"),
("439090", "2026-07-16 09:52:00"),
]
bt_codes = {_z(c) for c, _ in bt_user}
live_codes = {_z(r["code"]) for r in list(live_rows) + list(active_rows)}
params = get_scalping_defaults_from_db()
params.update({
"rsi_period": 3,
"rsi_oversold": 23.0,
"rsi_overbought": 75.0,
"sl_pct": 0.035,
"tp_pct": 0.03,
"tp_max_pct": 0.04,
"drop_rate": 0.01,
"vol_mult": 1.5,
"use_defense_filters": False,
"use_macd_cross": False,
"skip_hts_scan_dupes": False,
"require_reversal_candle": False,
"min_price": 6000.0,
"high_chase_thr": 0.99,
"max_daily_chg": 50.0,
"cooldown_min": 5,
"time_start_hm": 900,
"time_end_hm": 1530,
"max_daily": 100,
})
univ, usrc, nslots, _ = resolve_scalp_universe(
"2026-07-16", "2026-07-16", use_saved_history=True, strategy_id="SCALP",
)
univ = univ or {}
print("UNIVERSE", usrc, "slots", nslots)
print("\n" + "=" * 72)
print("A) 실매 ONLY — 왜 BT가 못 샀나 (매수시각 기준 신호봉=진입직전봉)")
print("=" * 72)
for r in live_rows:
code = _z(r["code"])
if code in bt_codes:
tag = "BOTH"
else:
tag = "LIVE_ONLY"
day, entry_key = _hm_from_buy(r["buy_date"])
candles = load_candles(db, code, day)
# ALIGN: 진입봉=entry_key, 신호봉=직전 확정봉
entry_i = find_index(candles, entry_key)
if entry_i is None:
entry_i = nearest_index(candles, entry_key)
signal_i = (entry_i - 1) if entry_i is not None and entry_i >= 1 else None
slots = first_slots(univ, code, day, 5)
in_entry_slot = slot_has(univ, entry_key, code) if entry_key else False
sig_key = str(candles[signal_i]["candle_time"])[:12] if signal_i is not None else ""
in_sig_slot = slot_has(univ, sig_key, code) if sig_key else False
status, detail = ("no_candle", "분봉0")
if signal_i is not None:
status, detail = eval_at(candles, signal_i, params)
# vol detail if reject
vol_info = ""
if signal_i is not None and candles:
c = candles[signal_i]
vols = [float(x.get("volume") or 0) for x in candles]
win = max(1, min(20, signal_i))
avg = sum(vols[signal_i - win : signal_i]) / win if win else 0
vol = vols[signal_i]
vol_info = "vol=%.0f avg20=%.0f need>=%.0f" % (
vol, avg, avg * float(params["vol_mult"]),
)
print(
f"\n[{tag}] {code} {r['name']} live_buy={r['buy_date']} @{r['buy_price']}"
)
print(f" candles={len(candles)} entry_key={entry_key} signal_i={signal_i} sig_key={sig_key}")
print(f" universe: in_entry_slot={in_entry_slot} in_sig_slot={in_sig_slot} first_slots={slots}")
print(f" TRIGGER@signal: {status} | {detail} | {vol_info}")
# also scan morning for first PASS in BT conditions
first_pass = None
for i in range(1, len(candles)):
st, det = eval_at(candles, i, params)
if st == "PASS":
ck = str(candles[i]["candle_time"])[:12]
if slot_has(univ, ck, code) or not univ:
first_pass = (ck, det)
break
print(f" first PASS+univ day: {first_pass}")
print("\n" + "=" * 72)
print("B) BT ONLY — 실매는 왜 안 샀나 (BT 매수시각 기준)")
print("=" * 72)
for code, buy_t in bt_user:
code = _z(code)
if code in live_codes:
continue
day, entry_key = _hm_from_buy(buy_t)
candles = load_candles(db, code, day)
entry_i = find_index(candles, entry_key)
signal_i = (entry_i - 1) if entry_i is not None and entry_i >= 1 else None
sig_key = str(candles[signal_i]["candle_time"])[:12] if signal_i is not None else ""
status, detail = eval_at(candles, signal_i, params) if signal_i is not None else ("no", "")
slots = first_slots(univ, code, day, 5)
# history presence count
n_hist = db.conn.execute(
"SELECT COUNT(*) n FROM target_candidates_history WHERE strategy_id=%s AND code=%s AND slot_key LIKE %s",
("SCALP", code, day + "%"),
).fetchone()["n"]
print(f"\n[BT_ONLY] {code} bt_buy={buy_t}")
print(f" hist_rows={n_hist} first_slots={slots}")
print(f" TRIGGER@BT_signal {sig_key}: {status} | {detail}")
print(f" in_sig_slot={slot_has(univ, sig_key, code)} in_entry_slot={slot_has(univ, entry_key, code)}")
print("\n" + "=" * 72)
print("C) BOTH — 시각 차이")
print("=" * 72)
live_by = {_z(r["code"]): r for r in live_rows}
for code, buy_t in bt_user:
code = _z(code)
if code not in live_by:
continue
lr = live_by[code]
print(f" {code}: LIVE {lr['buy_date']} @{lr['buy_price']} | BT {buy_t}")
# vol_mult ON/OFF sensitivity for LIVE_ONLY
print("\n" + "=" * 72)
print("D) LIVE_ONLY — vol_mult=0 이면 PASS 되나?")
print("=" * 72)
p0 = dict(params)
p0["vol_mult"] = 0.0
for r in live_rows:
code = _z(r["code"])
if code in bt_codes:
continue
day, entry_key = _hm_from_buy(r["buy_date"])
candles = load_candles(db, code, day)
entry_i = find_index(candles, entry_key) or nearest_index(candles, entry_key)
signal_i = (entry_i - 1) if entry_i and entry_i >= 1 else None
s1, d1 = eval_at(candles, signal_i, params) if signal_i is not None else ("?", "")
s0, d0 = eval_at(candles, signal_i, p0) if signal_i is not None else ("?", "")
print(f" {code}: vol1.5={s1}({d1[:40]}) | vol0={s0}({d0[:40]})")
# min_price with defense OFF should not block — confirm
print("\n" + "=" * 72)
print("E) 슬롯 경쟁 가설 — 실매 매수 시각에 BT 후보가 몇 개?")
print("=" * 72)
# for each live buy minute, count how many codes PASS in that signal slot among univ
for r in live_rows[:5]:
day, entry_key = _hm_from_buy(r["buy_date"])
# signal approx entry-1min
try:
sig_num = int(entry_key) - 1
sig_key = str(sig_num)
except Exception:
sig_key = entry_key
# fix minute borrow
if entry_key.endswith("00"):
# 0900 -> 0859 not valid; use find
pass
hh = int(entry_key[8:10])
mm = int(entry_key[10:12])
if mm == 0:
hh -= 1
mm = 59
else:
mm -= 1
sig_key = "%s%02d%02d" % (day, hh, mm)
codes_in = list(univ.get(sig_key) or [])
passes = []
for c in codes_in[:80]:
candles = load_candles(db, _z(c), day)
si = find_index(candles, sig_key)
if si is None:
continue
st, det = eval_at(candles, si, params)
if st == "PASS":
passes.append((_z(c), det))
print(
f" live={_z(r['code'])} @{r['buy_date']} sig={sig_key} "
f"univ={len(codes_in)} PASS={len(passes)} sample={passes[:6]}"
)
db.close()
if __name__ == "__main__":
main()