ㅇ 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.
318 lines
11 KiB
Python
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()
|