forked from animatedread/Warrior_EA
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()
|