운영 백테스트(+1,873,140%)과 live/paper 체결 규칙을 맞추고, 캔들 증분 sync· tail 신호 갱신·일일 체결 상한·슬리피지를 반영한다. docs/live 차트 생성 스크립트와 .env.example·README를 갱신한다. Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
#!/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 deepcoin.config import load_settings
|
|
from deepcoin.evaluation.causal_sim import normalize_signals_for_sim
|
|
from deepcoin.ground_truth.pnl import simulate_gt_signals_pnl
|
|
from deepcoin.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"],
|
|
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())
|