sridattapradeep/India_Equity_Scanner
0
1"""2tune_swing_exits.py — A/B exit rules for the swing screener over real data.3 4Fetches the 3-year universe window once, builds the template-entry panel once,5then replays every exit-rule configuration over the identical trade entries so6the comparison is apples-to-apples. For robustness, each config also reports7avg R split by date halves (a rule that only works in one half is curve-fit).8 9Run: venv\\Scripts\\python scripts\\tune_swing_exits.py10"""11from __future__ import annotations12 13import sys14import time15from pathlib import Path16 17import pandas as pd18 19sys.path.insert(0, str(Path(__file__).resolve().parents[1]))20 21from backtest import _template_panel, simulate_swing_trades # noqa: E40222from scanner import ( # noqa: E40223 BENCHMARK_SYMBOL, NIFTY_500_UNIVERSE, _fetch_ohlcv, _fetch_ohlcv_batch,24)25 26CONFIGS = [27 # label kwargs28 *[(f"stop {s}A / target {t}R",29 dict(stop_atr=s, target_r=t, max_hold=40))30 for s in (1.5, 2.0, 2.5, 3.0) for t in (1.5, 2.0, 2.5, 3.0)],31 ("stop 2.0A / tgt 2.5R / BE@1R", dict(stop_atr=2.0, target_r=2.5, max_hold=40, breakeven_at_r=1.0)),32 ("stop 2.5A / tgt 2.5R / BE@1R", dict(stop_atr=2.5, target_r=2.5, max_hold=40, breakeven_at_r=1.0)),33 ("stop 2.0A / tgt 3.0R / BE@1R", dict(stop_atr=2.0, target_r=3.0, max_hold=40, breakeven_at_r=1.0)),34 ("chandelier 2.5A (no target)", dict(stop_atr=2.5, target_r=None, max_hold=60, trail_atr=2.5)),35 ("chandelier 3.0A (no target)", dict(stop_atr=3.0, target_r=None, max_hold=60, trail_atr=3.0)),36 ("chandelier 3.5A (no target)", dict(stop_atr=3.5, target_r=None, max_hold=60, trail_atr=3.5)),37]38 39 40def fmt(v, d=2):41 return "—" if v is None else f"{v:.{d}f}"42 43 44def main() -> int:45 t0 = time.time()46 print(f"fetching 3y for {len(NIFTY_500_UNIVERSE)} symbols…", flush=True)47 dfs = _fetch_ohlcv_batch(NIFTY_500_UNIVERSE, period="3y")48 _fetch_ohlcv(BENCHMARK_SYMBOL, period="3y") # warm, not needed for swing49 print(f"fetched {len(dfs)} symbols in {time.time()-t0:.0f}s", flush=True)50 51 panel = _template_panel(dfs)52 entries = panel["entries"]53 print(f"entry signals: {int(entries.to_numpy().sum())}\n", flush=True)54 55 rows = []56 for label, kw in CONFIGS:57 s = simulate_swing_trades(dfs, entries, collect_trades=True, **kw)58 trades = s.pop("trades", [])59 # date-half stability60 h1_avg = h2_avg = None61 if trades:62 tdf = pd.DataFrame(trades)63 mid = tdf["entry_date"].sort_values().iloc[len(tdf) // 2]64 h1 = tdf[tdf["entry_date"] <= mid]["r"]65 h2 = tdf[tdf["entry_date"] > mid]["r"]66 h1_avg = round(float(h1.mean()), 3) if len(h1) else None67 h2_avg = round(float(h2.mean()), 3) if len(h2) else None68 rows.append({"config": label, **s, "h1_avg_r": h1_avg, "h2_avg_r": h2_avg})69 70 rows.sort(key=lambda r: (r["avg_r"] if r["avg_r"] is not None else -9), reverse=True)71 hdr = (f"{'config':<32} {'n':>5} {'win%':>6} {'avgR':>7} {'totR':>8} {'PF':>6} "72 f"{'tgt%':>6} {'stop%':>6} {'to%':>5} {'hold':>5} {'H1avgR':>7} {'H2avgR':>7}")73 print(hdr)74 print("-" * len(hdr))75 for r in rows:76 print(f"{r['config']:<32} {r['trades_closed']:>5} {fmt(r['win_rate'],1):>6} "77 f"{fmt(r['avg_r']):>7} {fmt(r['total_r'],1):>8} {fmt(r['profit_factor']):>6} "78 f"{fmt(r['target_pct'],0):>6} {fmt(r['stop_pct'],0):>6} {fmt(r['timeout_pct'],0):>5} "79 f"{fmt(r['avg_hold_bars'],0):>5} {fmt(r['h1_avg_r'],3):>7} {fmt(r['h2_avg_r'],3):>7}")80 print("\nNote: avg R is per unit of INITIAL risk; configs share identical entries.")81 return 082 83 84if __name__ == "__main__":85 sys.exit(main())86 