forked from mnbvc188199/Warrior_EA
Recovered live config (z20 <= -1.5, exit SMA20 / 10 bars, 3xATR) plus a Garman-Klass vol-regime gate. Expectancy is monotone in the vol regime in IS, OOS and full sample, 4/4 indices; the gate reverses on USDJPY/XAUUSD. D1 2008-2026 survives 2008/2020/2022 (maxDD 2.8%, ret/DD 7.74); the gate halves trades, so it belongs on H4, never D1. reconcile.py matches the EA to the backtest trade by trade; combine_charts.py rebuilds the account curve from per-chart tester runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
"""
| |
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()
|