#!/usr/bin/env python3 """ 꼬리 실매 vs 백테 건별 forensics (C). 실매 trade_history(SHORT) 각 건에 대해: 유니버스 IN/OUT · 재편입 · 당일봉 entry_i · 웜업 후 align 신호 · 백테 체결 여부 사용: python3 -u scripts/tail_live_bt_forensics.py --date 2026-07-16 nohup python3 -u scripts/tail_live_bt_forensics.py --date 2026-07-16 \ > logs/tail_live_bt_forensics_20260716.log 2>&1 & """ from __future__ import annotations import argparse 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)) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--date", default="2026-07-16", help="YYYY-MM-DD") args = ap.parse_args() day = args.date.replace("-", "") day_dash = f"{day[:4]}-{day[4:6]}-{day[6:8]}" from database import TradeDB from kis_trader.engine import tail_engine as te from kis_trader.engine.tail_engine import ( _eval_live_align_lookback, _last_closed_bar_index, _universe_enter_minutes, ) from kis_trader.backtest import tail_backtest_common as tbc from kis_trader.backtest.universe_timeline import build_universe_timeline db = TradeDB() live = db.conn.execute( "SELECT code, name, buy_date, buy_price, sell_date, realized_pnl " "FROM trade_history WHERE strategy=%s AND buy_date LIKE %s " "ORDER BY buy_date", ("SHORT", f"{day_dash}%"), ).fetchall() print(f"=== 꼬리 forensics {day_dash} live={len(live)} ===") base = te.get_tail_defaults_from_db(db) universe, src, n_slots, _ = tbc.resolve_tail_universe( day, day, use_saved_history=True, strategy_id="SHORT", ) tl = build_universe_timeline( strategy_id="SHORT", start_ymd=day, end_ymd=day, debounce_sec=0, strict=False, ) start_key, end_key = day + "0000", day + "2359" candles_by_code, _, _ = tbc.load_tail_candles_by_code( db, start_key, end_key, int(base.get("timeframe") or 3), rsi_period=int(base.get("rsi_period") or 14), ) # REST 웜업 (유니버스 교집합) tbc.inject_tail_rest_warmup_memory( candles_by_code, start_key, timeframe=int(base.get("timeframe") or 3), universe_by_slot=universe, ) port = tbc.resolve_tail_portfolio_params( dict(db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone() or {}), base, ) row = db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone() fee, tax, _ = tbc.fee_and_slot_from_env_row(dict(row) if row else None) params = dict(base) tbc.merge_tail_portfolio_into_params(params, port) meta = {"db": db, "start_key": start_key, "end_key": end_key} bt_trades = tbc.run_tail_backtest_web_aligned( candles_by_code, params, universe, slot_money=float(port["slot_money"]), fee_rate=fee, sell_tax=tax, total_budget_krw=float(port["total_budget_krw"]), meta_out=meta, ) bt_by_code = {} for t in bt_trades: bt_by_code.setdefault(str(t.get("code")), []).append(t) enter_mins = _universe_enter_minutes(universe, tl, None) print(f"universe src={src} slots={n_slots} enter_minutes={len(enter_mins)}") print(f"warmup bars target={tbc.tail_backtest_candle_warmup_bars()} " f"rest={meta.get('skip_stats', {}).get('rest_warmup')}") print(f"BT trades={len(bt_trades)} pnl={sum(int(t.get('pnl') or 0) for t in bt_trades)}") print() for r in live: code = str(r["code"]) buy_ts = str(r["buy_date"]) buy_hm = buy_ts[11:16].replace(":", "") t12 = day + buy_hm name = r.get("name") or code print(f"── {code} {name} live {buy_ts} @{int(r['buy_price'])} pnl={r['realized_pnl']}") # transitions that day prev = False trans = [] for et_row in db.conn.execute( """SELECT event_time, MAX(code=%s) has_me FROM target_candidates_history WHERE strategy_id=%s AND event_time LIKE %s GROUP BY event_time ORDER BY event_time""", (code, "SHORT", f"{day_dash}%"), ).fetchall(): has = bool(et_row["has_me"]) if has != prev: trans.append((str(et_row["event_time"]), "IN" if has else "OUT")) prev = has print(f" transitions: {trans[:8]}{'...' if len(trans) > 8 else ''}") in_at_buy = False if tl is not None: codes = tl.codes_at(t12 + "00") or [] in_at_buy = code in codes print(f" universe@buy {t12}: {'IN' if in_at_buy else 'OUT'}") bars = candles_by_code.get(code) or [] n_prev = sum(1 for c in bars if str(c.get("candle_time") or "")[:8] < day) ei = _last_closed_bar_index(bars, t12, int(base.get("timeframe") or 3)) print(f" candles n={len(bars)} prev_day={n_prev} entry_i@buy={ei}", f"bar={bars[ei]['candle_time'] if ei >= 0 else None}") st = {"daily_cnt": 0, "last_exit_dt": None, "daily_pnl_krw": 0.0} if ei >= 19: rej, msg, sig = _eval_live_align_lookback( bars, ei, params, st, lookback=max(1, int(params.get("live_signal_lookback_bars") or 1)), ) print(f" align: reject={rej} msg={(msg or '')[:70]} " f"sig={bool(sig)} px={sig.get('entry_price') if sig else None}") else: print(f" align: SKIP entry_i={ei} < 19 (웜업 부족)") hits = bt_by_code.get(code) or [] if hits: for h in hits: print(f" BT hit: {h.get('entry_time')} @{h.get('entry')} " f"→ {h.get('exit_time')} pnl={h.get('pnl')}") else: print(" BT hit: NONE") print() db.close() return 0 if __name__ == "__main__": raise SystemExit(main())