Files
kis_bot/scripts/tail_live_bt_forensics.py
Your Name fc27e726f9 feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes):

구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함.

스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함.

코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함.

시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함.

기대 효과 (Impact):

이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
2026-07-17 01:09:09 +09:00

159 lines
5.9 KiB
Python

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