ответвлён от animatedread/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 строки
5,1 КиБ
Python
135 строки
5,1 КиБ
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)
|