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