""" THE OUTSTANDING VALIDATION: does the vol-gated dip-z survive a bear market? The H4 result (STRATEGY.md) is built on 2021-2026, which contains no 2008 and no 2020. This family's losses are concentrated in bear ONSETS, so a sample without one proves very little about the drawdown that matters. Broker intraday history does not reach back far enough, so the bear test runs on D1, where the indices go back to 2008. The D1 rule is the same shape as the H4 one; it is a proxy, and the point is not to re-measure the edge but to see what the drawdown does when the market actually falls. Reported per calendar year, because an average over 18 years hides exactly the thing being looked for. """ from __future__ import annotations import sys import numpy as np sys.path.insert(0, __file__.rsplit("\\", 1)[0] if "\\" in __file__ else ".") import backtest as bt # noqa: E402 from run_screen import zscore_entries # noqa: E402 from vol_filter_test import vol_pctile # noqa: E402 STOP_ATR = 3.0 PROP_DD = 0.05 IDX = ["SP500", "NAS100", "US30", "DAX40"] def trades_for(symbols, period, gated, max_bars=10): out = [] for s in symbols: try: d = bt.load(s, period) except Exception: # noqa: BLE001 continue e, xma = zscore_entries(d, 20, -1.5, 0) if gated: e = e & (np.nan_to_num(vol_pctile(d), nan=-1) >= 0.50) tr = bt.simulate(d, e, side=1, exit_ma=xma, max_bars=max_bars, stop_atr=STOP_ATR) tr = bt.add_r(tr, d, stop_atr=STOP_ATR) for t in tr: t["symbol"] = s t["exit_t"] = d["ts"][t["exit_i"]] out += tr out.sort(key=lambda t: t["exit_t"]) return out def curve_stats(trades, risk): eq = [1.0] for t in trades: eq.append(eq[-1] * (1.0 + risk * t["r"])) eq = np.array(eq) peak = np.maximum.accumulate(eq) dd = (peak - eq) / peak return eq, dd def per_year(trades, risk): """Year-by-year return and the worst drawdown inside that year.""" years = sorted({int(str(t["exit_t"])[:4]) for t in trades}) rows = [] for y in years: sub = [t for t in trades if int(str(t["exit_t"])[:4]) == y] if not sub: continue eq, dd = curve_stats(sub, risk) rows.append((y, len(sub), eq[-1] - 1.0, dd.max(), float(np.mean([t["ret"] for t in sub])) * 1e4)) return rows if __name__ == "__main__": risk = 0.0025 print("=== D1, 4 indices, dip-z (z<=-1.5, exit SMA20, 10 bars, stop 3ATR), " f"risk {risk:.2%} ===\n") for gated in (False, True): tag = "VOL-GATED (pct>=0.50)" if gated else "UNGATED" tr = trades_for(IDX, "PERIOD_D1", gated) if not tr: print(f"{tag}: no trades / no data") continue eq, dd = curve_stats(tr, risk) t0 = min(t["t"] for t in tr) t1 = max(t["exit_t"] for t in tr) yrs = (t1 - t0) / np.timedelta64(365, "D") total = eq[-1] - 1.0 rdd = total / dd.max() if dd.max() > 1e-9 else np.nan print(f"--- {tag} --- {str(t0)[:10]} .. {str(t1)[:10]} " f"n={len(tr)} {len(tr)/max(yrs*12,1e-9):.1f}/mo " f"total {total:.1%} CAGR {eq[-1]**(1/max(yrs,1e-9))-1:.1%} " f"maxDD {dd.max():.1%} ret/DD {rdd:.2f}") print(f"{'year':>6}{'n':>5}{'return':>9}{'maxDD':>8}{'bp/trade':>10}") for y, n, r, d_, bp in per_year(tr, risk): mark = " <-- bear" if y in (2008, 2011, 2015, 2018, 2020, 2022) else "" print(f"{y:>6}{n:>5}{r:>9.2%}{d_:>8.2%}{bp:>10.1f}{mark}") print()