CoolFace
Apppublic

sridattapradeep/India_Equity_Scanner

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
tune_swing_exits.py86 linesDownload Raw Back to scripts
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