변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
159 lines
5.9 KiB
Python
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())
|