| """
|
| 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)
|