#!/usr/bin/env python3 """fractal_swing 현실적 백테스트 — 슬리피지·일일체결 상한 시나리오.""" from __future__ import annotations import argparse import json import logging import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src" if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) from bithumb.config import load_settings from bithumb.evaluation.causal_sim import normalize_signals_for_sim from bithumb.ground_truth.pnl import simulate_gt_signals_pnl from bithumb.operations.signal_pipeline import ( _signals_in_lookback, generate_raw_signals, load_ops_candles, ) def _configure_logging(verbose: bool) -> None: level = logging.DEBUG if verbose else logging.INFO logging.basicConfig( level=level, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) def main() -> int: """fractal_swing 슬리피지 시나리오 백테스트.""" parser = argparse.ArgumentParser(description="fractal_swing 현실적 3년 백테스트") parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() _configure_logging(args.verbose) settings = load_settings() df = load_ops_candles(settings) gen = generate_raw_signals(settings, df=df, use_cache=True) end = gen["data_end"] price = gen["last_price"] scoped = _signals_in_lookback(gen["raw_signals"], end, settings.gt_sim_lookback_days) normalized = normalize_signals_for_sim(scoped) scenarios = [ {"label": "stage2_ideal", "slippage_rate": 0.0, "daily_max_trades": None}, {"label": "ops_default", "slippage_rate": settings.ops_slippage_rate, "daily_max_trades": settings.ops_daily_max_trades}, {"label": "slippage_0.1pct", "slippage_rate": 0.001, "daily_max_trades": settings.ops_daily_max_trades}, {"label": "slippage_0.1pct_no_cap", "slippage_rate": 0.001, "daily_max_trades": None}, ] rows = [] for sc in scenarios: sim = simulate_gt_signals_pnl( signals=normalized, initial_cash_krw=settings.gt_initial_cash_krw, fee_rate=settings.gt_trading_fee_rate, min_order_krw=settings.ops_min_order_krw, slippage_rate=sc["slippage_rate"], daily_max_trades=sc["daily_max_trades"], buy_cash_pct=settings.ops_buy_cash_pct, sell_coin_pct=settings.ops_sell_coin_pct, sim_lookback_days=settings.gt_sim_lookback_days, data_end=end, last_mark_price=price, ) buys = sim["buys_executed"] rows.append({ **sc, "return_pct": sim["total_return_pct"], "final_equity_krw": sim["final_equity_krw"], "buys_executed": buys, "sells_executed": sim["sells_executed"], "buys_skipped": sim["buys_skipped"], "daily_buys_avg": round(buys / settings.gt_sim_lookback_days, 2), }) report = { "technique_id": settings.ops_technique_id, "symbol": settings.symbol, "sim_lookback_days": settings.gt_sim_lookback_days, "initial_cash_krw": settings.gt_initial_cash_krw, "signals_in_period": len(scoped), "scenarios": rows, } out = Path("docs/spot/3_operations/fractal_realistic_backtest.json") out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") print("\n=== fractal_swing 현실적 백테스트 (3년) ===") for row in rows: print( f"{row['label']}: {row['return_pct']}% " f"(슬리피지 {row['slippage_rate']}, 일상한 {row['daily_max_trades']}) " f"매수 {row['buys_executed']} (일 {row['daily_buys_avg']})" ) print(f"\nJSON: {out}") return 0 if __name__ == "__main__": raise SystemExit(main())