"""Slippage stress test for any ORB strategy. Runs the same backtest at increasing slippage levels to find the break-even slippage point. Useful for validating that a strategy's edge survives realistic transaction costs. Usage: python -m apps.intraday_bt.scripts.run_slippage_ladder \\ --config configs/intraday/strategies/orb_pullback_v1.yaml \\ --days 200 \\ --bps 5,10,15,25 """ from __future__ import annotations import argparse import asyncio from pathlib import Path from apps.intraday_bt.composite import _build_sleeve_config from apps.intraday_bt.run import load_config, run as run_sleeve async def _main(args: argparse.Namespace) -> None: bps_values = [float(x.strip()) for x in args.bps.split(",")] base_config = load_config(args.config) days = args.days capital = args.capital name = Path(args.config).stem print(f"\nSlippage ladder: {name} ({days}d, ${capital:,.0f})") print(f"{'BPS':>6} {'Return':>9} {'Sharpe':>7} {'MaxDD':>8} {'WR':>7} {'Trades':>7}") print("-" * 52) baseline_return = None for bps in bps_values: sleeve = _build_sleeve_config(base_config, capital, days) # Override slippage_bps params = sleeve.orb_strategy if params is not None: params_dict = params.model_dump() params_dict["slippage_bps"] = bps from libs.intraday.domain import ORBStrategyParams sleeve_config_dict = sleeve.model_dump() sleeve_config_dict["orb_strategy"] = params_dict from libs.intraday.domain import IntradayConfig sleeve = IntradayConfig(**sleeve_config_dict) _, metrics, _, _ = await run_sleeve(sleeve) ret = (metrics.total_return_pct or 0) * 100 sharpe = metrics.sharpe_ratio or 0.0 dd = (metrics.max_drawdown_pct or 0) * 100 wr = (metrics.win_rate or 0) * 100 trades = metrics.total_trades or 0 if baseline_return is None: baseline_return = ret degradation = ret - baseline_return deg_str = f"({degradation:+.1f}pp)" if bps != bps_values[0] else " " print(f"{bps:>6.0f} {ret:>+8.2f}% {sharpe:>7.3f} {dd:>+7.2f}% {wr:>6.1f}% {trades:>7} {deg_str}") # Find break-even (last bps where return > 0) print() print(f" Run with --bps ladder to find break-even slippage.") def main() -> None: parser = argparse.ArgumentParser(description="Slippage ladder stress test") parser.add_argument("--config", required=True) parser.add_argument("--days", type=int, default=200) parser.add_argument("--capital", type=float, default=10_000.0) parser.add_argument("--bps", default="5,10,15,25", help="Comma-separated slippage bps values (default: 5,10,15,25)") args = parser.parse_args() asyncio.run(_main(args)) if __name__ == "__main__": main()