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>
135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
"""
| |
Portfolio test: the recovered dip-z config across the instruments that showed
| |
an edge over the long-only control, on ONE equity curve.
| |
| |
WHY A PORTFOLIO IS THE ANSWER TO THE PROP LIMIT
| |
-----------------------------------------------
| |
Per symbol at 1% risk the strategy clears cadence and beats its control but
| |
breaks the 5% drawdown limit. Drawdown scales ~linearly with risk per trade
| |
while ret/DD does not, so the fix is sizing, not signal. Spreading the same
| |
risk budget across several instruments then buys back some return, but ONLY to
| |
the extent the drawdowns are not simultaneous -- and four equity indices are
| |
highly correlated, so that benefit must be measured, never assumed.
| |
| |
HONESTY NOTE ON THE EQUITY CURVE: each trade's R is applied at its EXIT, in
| |
chronological order. With concurrent positions this understates the true
| |
intra-trade drawdown, because two open losers are not marked to market
| |
together. The reported maxDD is therefore a FLOOR, not a ceiling -- treat the
| |
5% test as necessary, not sufficient.
| |
"""
| |
| |
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
| |
| |
STOP_ATR = 3.0
| |
PROP_DD = 0.05
| |
| |
| |
def collect(symbols, period, lo=None, hi=None, max_bars=10):
| |
"""All trades across symbols, chronological, with R attached."""
| |
out = []
| |
for s in symbols:
| |
d = bt.load(s, period)
| |
e, xma = zscore_entries(d, 20, -1.5, 0)
| |
ts = d["ts"]
| |
m = np.ones(len(ts), bool)
| |
if lo is not None:
| |
m &= ts >= np.datetime64(lo)
| |
if hi is not None:
| |
m &= ts < np.datetime64(hi)
| |
tr = bt.simulate(d, e & m, 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(trades, risk, max_concurrent=None):
| |
"""Equity curve; optionally refuse entries beyond `max_concurrent` open."""
| |
if max_concurrent is not None:
| |
kept, open_until = [], []
| |
for t in sorted(trades, key=lambda x: x["t"]):
| |
open_until = [u for u in open_until if u > t["t"]]
| |
if len(open_until) >= max_concurrent:
| |
continue
| |
open_until.append(t["exit_t"])
| |
kept.append(t)
| |
trades = sorted(kept, key=lambda x: x["exit_t"])
| |
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 trades, eq, dd.max()
| |
| |
| |
def report(tag, trades, risk, max_concurrent=None):
| |
if not trades:
| |
print(f"{tag:<26} no trades")
| |
return None
| |
kept, eq, maxdd = curve(trades, risk, max_concurrent)
| |
t0 = min(t["t"] for t in kept)
| |
t1 = max(t["exit_t"] for t in kept)
| |
years = (t1 - t0) / np.timedelta64(365, "D")
| |
total = eq[-1] - 1.0
| |
cagr = eq[-1] ** (1 / max(years, 1e-9)) - 1.0
| |
permo = len(kept) / max(years * 12, 1e-9)
| |
rets = np.array([t["ret"] for t in kept])
| |
rmult = np.array([t["r"] for t in kept])
| |
ret_dd = total / maxdd if maxdd > 1e-9 else np.nan
| |
flags = []
| |
if permo < 2.0:
| |
flags.append("cadence")
| |
if maxdd > PROP_DD:
| |
flags.append("maxDD")
| |
if not (ret_dd >= 2.0):
| |
flags.append("ret/DD")
| |
print(f"{tag:<26}{len(kept):>6}{permo:>7.1f}{rets.mean()*1e4:>9.1f}"
| |
f"{rmult.mean():>8.3f}{total:>9.1%}{cagr:>8.1%}{maxdd:>8.1%}{ret_dd:>8.2f}"
| |
f" {'PASS' if not flags else 'fail:' + ','.join(flags)}")
| |
return dict(n=len(kept), permo=permo, total=total, cagr=cagr, maxdd=maxdd, ret_dd=ret_dd)
| |
| |
| |
HDR = (f"{'portfolio':<26}{'n':>6}{'/mo':>7}{'exp_bp':>9}{'meanR':>8}"
| |
f"{'total':>9}{'CAGR':>8}{'maxDD':>8}{'ret/DD':>8} screen")
| |
| |
IDX = ["SP500", "NAS100", "US30", "DAX40"]
| |
ALL7 = IDX + ["EURUSD", "USDJPY", "XAUUSD"]
| |
SPLIT = "2024-01-01"
| |
| |
if __name__ == "__main__":
| |
period = "PERIOD_H4"
| |
print("=== 4-INDEX PORTFOLIO, recovered dip-z config, H4, long only ===")
| |
print("(equity applies each trade's R at exit; concurrent DD is understated)\n")
| |
| |
full = collect(IDX, period)
| |
print(HDR)
| |
for risk in (0.010, 0.0075, 0.005, 0.0035, 0.0025):
| |
report(f"4 idx, risk {risk:.2%}", full, risk)
| |
print()
| |
print("--- risk 0.5%, capped concurrency (correlated indices draw down together) ---")
| |
print(HDR)
| |
for mc in (4, 3, 2, 1):
| |
report(f"4 idx, 0.50%, max {mc} open", full, 0.005, max_concurrent=mc)
| |
print()
| |
print("--- IS / OOS at the sizing that passes ---")
| |
print(HDR)
| |
report("4 idx IS (..2024)", collect(IDX, period, None, SPLIT), 0.005)
| |
report("4 idx OOS (2024..)", collect(IDX, period, SPLIT, None), 0.005)
| |
print()
| |
print("--- all 7 symbols (incl. the three with no measured edge) ---")
| |
print(HDR)
| |
report("7 sym, risk 0.50%", collect(ALL7, period), 0.005)
| |
report("7 sym IS (..2024)", collect(ALL7, period, None, SPLIT), 0.005)
| |
report("7 sym OOS (2024..)", collect(ALL7, period, SPLIT, None), 0.005)
|