#!/usr/bin/env python3 """꼬리 청산 A/B — baseline vs 래칫 vs 시간컷 (fast 5위 base, 2026-05).""" from __future__ import annotations import json from datetime import datetime from database import TradeDB from kis_trader.backtest import tail_backtest_common as tbc from kis_trader.engine import tail_engine as te START = "2026-05-01" END = "2026-05-31" TF = 3 SCENARIOS = [ ("baseline_DB(래칫ON·어깨0.5%·트레일OFF)", {}), ("어깨2%+trail1.5/1.5_래칫ON", { "shoulder_min_high": 0.02, "trail_pct": 0.015, "trail_arm_pct": 0.015, }), ("어깨2%+trail1.5/1.5_래칫OFF", { "ratchet_tiers": "", "shoulder_min_high": 0.02, "trail_pct": 0.015, "trail_arm_pct": 0.015, }), ("어깨2%만_트레일OFF_래칫ON", { "shoulder_min_high": 0.02, "trail_pct": 0.0, "trail_arm_pct": 0.0, }), ("어깨2%만_트레일OFF_래칫OFF", { "ratchet_tiers": "", "shoulder_min_high": 0.02, "trail_pct": 0.0, "trail_arm_pct": 0.0, }), ] def _load_base_params() -> dict: """DB 최신값 그대로 — fast 파라서치·실매와 동일.""" d = te.get_tail_defaults_from_db() d["portfolio_mode"] = True d["force_eod_exit"] = False d.setdefault("max_hold_bars", 0) return d def main() -> None: db = TradeDB() try: start_key, end_key, start_ymd, end_ymd = tbc.date_keys(START, END) candles_by_code, _, _ = tbc.load_tail_candles_by_code( db, start_key, end_key, TF, rsi_period=14, ) universe_by_slot, _, _, _ = tbc.resolve_tail_universe( start_ymd, end_ymd, use_saved_history=True, ) snap = db.get_merged_env_snapshot() fee_rate, sell_tax, _ = tbc.fee_and_slot_from_env_row(snap) base = _load_base_params() period_days = max(1, (datetime.strptime(END, "%Y-%m-%d") - datetime.strptime(START, "%Y-%m-%d")).days + 1) slot = float(base.get("slot_money") or 200000) mxs = int(base.get("max_stocks") or 20) tb = float(base.get("total_budget_krw") or mxs * slot) rows = [] for label, patch in SCENARIOS: params = dict(base) params.update(patch) meta: dict = {} trades = tbc.run_tail_backtest_web_aligned( candles_by_code, params, universe_by_slot, slot_money=slot, fee_rate=fee_rate, sell_tax=sell_tax, max_stocks=mxs, total_budget_krw=tb, meta_out=meta, ) stats = tbc.summarize_tail_trades( trades, total_budget_krw=tb, period_days=period_days, ) reasons: dict = {} for t in trades: k = str(t.get("sell_reason") or t.get("reason") or "?") reasons[k] = reasons.get(k, 0) + 1 rows.append({ "scenario": label, "total_trades": stats["total_trades"], "win_rate": stats["win_rate"], "total_pnl": stats["total_pnl"], "pf": stats["pf"], "avg_hold_min": stats["avg_hold_min"], "sell_reasons": reasons, "patch": patch, }) print( f"{label:32} | 거래 {stats['total_trades']:4} | " f"승률 {stats['win_rate']:5.1f}% | PnL {stats['total_pnl']:>9,} | " f"PF {stats['pf']:.2f} | 보유 {stats['avg_hold_min']:.0f}분 | {reasons}" ) out = { "start": START, "end": END, "timeframe": TF, "base": {k: base.get(k) for k in ( "min_drop_rate", "shoulder_min_high", "shoulder_cut_pct", "ratchet_tiers", "trail_pct", "trail_arm_pct", "stop_atr_mult", "target_atr_mult", "max_daily_change", "entry_mode", "slot_money", "max_stocks", "total_budget_krw", )}, "scenarios": rows, } path = f"/home/hoon/kis_bot/kis_trader/backtest/results/tail_exit_ab_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(path, "w", encoding="utf-8") as f: json.dump(out, f, ensure_ascii=False, indent=2) print(f"\n💾 저장: {path}") finally: db.close() if __name__ == "__main__": main()