"""Does the drift edge survive FINANCING - and can it be harvested inside a prop drawdown limit? Drift is the only positive, significant, cost-surviving result this project has produced (SP500 +12.25%/yr t 2.84, XAUUSD +10.24%/yr t 2.88). But every one of those numbers was computed charging SPREAD ONLY, and drift is a HOLD strategy - so the cost that decides it is not the spread, it is the overnight financing on the notional, every night, for years. a 4-hour barrier trade pays the spread once and roughly zero financing a 4-month hold pays the spread once and financing 120 times That is why a result can be real in R-multiples and worthless in an account. Long CFD financing runs about SOFR + 2-3%, so against a ~12% gross drift it is not a haircut, it is most of the edge. This prices it properly instead of assuming it away. WHAT IS MODELLED ---------------- price return from the validated M1 bid/ask books, so the series is the same one every other result in this project used spread paid once on entry and once on exit - negligible over a long hold, included anyway so the comparison is honest at short holds too financing annual % of NOTIONAL, charged daily on the levered position. Swept, because it is broker-specific and MT5 keeps NO history of it, so it cannot be recovered from data and must come from the symbol spec. leverage applied to notional, so financing scales with it. This is the whole point: leverage multiplies the gross edge AND the financing equally, so it cannot improve the ratio - it only buys return at the price of drawdown. WHAT IS NOT MODELLED, AND WHY IT MATTERS ---------------------------------------- Cash-index CFDs usually apply a dividend adjustment (credited to longs, ~1.3%/yr on the S&P). That would OFFSET part of the financing and is not in the price series, so the SP500 net figures here are pessimistic by roughly that much. Gold has no dividend and no such offset. Stated rather than silently assumed either way. """ import numpy as np, sys sys.stdout.reconfigure(encoding='utf-8', errors='replace') import book, fills SYMS = ('SP500', 'XAUUSD', 'EURUSD', 'USDJPY') TRADING_DAYS = 252 #--- Prop rules modelled. Set to a common shape rather than any one firm's small print; the #--- conclusion below is not sensitive to a percentage point either way, and the script sweeps #--- the two that matter. CHECK THESE AGAINST YOUR ACTUAL PROGRAMME before acting on it. TARGET, MAXLOSS, DAILY, HORIZON, N_PATH, BLOCK = 0.08, 0.06, 0.03, 252, 20000, 20 def first_passage(r, ann_financing, lev, rel_spread, rng): """-> P(hit target), P(breach), P(neither within the horizon). Block bootstrap rather than iid resampling: daily returns cluster in volatility, and iid draws would break exactly the clustering that produces the losing streaks a drawdown rule is designed to catch - which would make every prop rule look far easier to survive. """ nb = HORIZON // BLOCK + 1 starts = rng.integers(0, len(r) - BLOCK, size=(N_PATH, nb)) idx = (starts[:, :, None] + np.arange(BLOCK)[None, None, :]).reshape(N_PATH, -1)[:, :HORIZON] paths = r[idx] daily_fin = ann_financing / TRADING_DAYS step = lev * (paths - daily_fin) - 2.0 * rel_spread * lev / TRADING_DAYS eq = np.cumprod(np.maximum(1.0 + step, 1e-9), axis=1) hit_t = eq >= (1.0 + TARGET) #--- total loss measured from the STARTING balance, the usual prop "max loss" rule hit_l = (eq <= (1.0 - MAXLOSS)) | (step <= -DAILY) first_t = np.where(hit_t.any(1), hit_t.argmax(1), HORIZON + 1) first_l = np.where(hit_l.any(1), hit_l.argmax(1), HORIZON + 1) win = first_t < first_l lose = first_l < first_t return float(win.mean()), float(lose.mean()), float((~win & ~lose).mean()) def daily(sym): """Daily mid closes and the median relative spread, from the validated book.""" f = book.frame(sym, 'D1') c = f.c r = np.diff(np.log(c)) rel_spread = float(np.median(f.spread / f.c)) return f.t[1:], r, rel_spread def stats(r, ann_financing=0.0, lev=1.0, rel_spread=0.0, turns_per_year=1.0): """Equity path of a levered long, financed daily. -> dict of the numbers that decide it. Financing is charged on NOTIONAL (lev x equity), which is what a broker actually does, so the cost scales with leverage exactly as the return does. """ daily_fin = ann_financing / TRADING_DAYS #--- compounding on the equity, not additive on the notional: a drawdown reduces the #--- position and therefore the financing, which is how a real account behaves step = 1.0 + lev * (r - daily_fin) step = np.maximum(step, 1e-9) # a wipeout is absorbing, not negative eq = np.cumprod(step) #--- round-trip spread, amortised over the year at the stated turnover eq *= np.exp(-turns_per_year * 2.0 * rel_spread * lev * np.arange(len(eq)) / TRADING_DAYS) yrs = len(r) / TRADING_DAYS cagr = eq[-1] ** (1.0 / yrs) - 1.0 vol = np.std(lev * r) * np.sqrt(TRADING_DAYS) peak = np.maximum.accumulate(eq) dd = 1.0 - eq / peak #--- the prop-relevant statistic is not the average day, it is the WORST day worst_day = float(np.min(lev * r)) return dict(cagr=cagr, vol=vol, sharpe=(cagr / vol if vol > 0 else 0.0), maxdd=float(dd.max()), worst_day=worst_day, eq=eq, dd=dd, yrs=yrs) if __name__ == '__main__': syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS) print("=== 1. GROSS DRIFT, unlevered, financing 0 - reproducing the known result ===") print(f" {'sym':>7}{'years':>7}{'CAGR':>9}{'vol':>8}{'Sharpe':>8}{'maxDD':>8}" f"{'worst day':>11}{'spread':>9}") D = {} for s in syms: t, r, sp = daily(s) D[s] = (t, r, sp) k = stats(r, 0.0, 1.0, sp) print(f" {s:>7}{k['yrs']:>7.1f}{100*k['cagr']:>8.2f}%{100*k['vol']:>7.1f}%" f"{k['sharpe']:>8.2f}{100*k['maxdd']:>7.1f}%{100*k['worst_day']:>10.2f}%" f"{1e4*sp:>8.1f}bp") print("\n=== 2. THE QUESTION THAT DECIDES IT: net CAGR vs financing rate ===") print(" unlevered. Typical long CFD financing is SOFR + 2-3%, so look at the 7-8% column.") fins = (0.0, 0.03, 0.05, 0.075, 0.10) print(f" {'sym':>7}" + "".join(f"{100*f:>11.1f}%" for f in fins) + f"{'breakeven':>12}") for s in syms: t, r, sp = D[s] row = f" {s:>7}" for fin in fins: row += f"{100*stats(r, fin, 1.0, sp)['cagr']:>11.2f}%" #--- the financing rate at which the whole edge is gone lo, hi = 0.0, 0.50 for _ in range(40): mid = 0.5 * (lo + hi) if stats(r, mid, 1.0, sp)['cagr'] > 0: lo = mid else: hi = mid row += f"{100*lo:>11.2f}%" print(row) print(" 'breakeven' = the financing rate at which net CAGR hits zero. If that is below") print(" what your broker charges, the edge does not exist in your account.") print("\n=== 3. THE PROP QUESTION: reach the target BEFORE breaching a limit? ===") print(f" Not 'survive the worst 25-year drawdown' - an evaluation is weeks, so this is a") print(f" FIRST-PASSAGE problem. Block bootstrap (20-day blocks, so volatility clustering") print(f" and autocorrelation survive) of the real daily returns, {N_PATH:,} paths each.") print(f" Rules modelled: +{100*TARGET:.0f}% target, -{100*MAXLOSS:.0f}% total loss from") print(f" start, -{100*DAILY:.0f}% daily loss, {HORIZON} trading days.\n") print(f" {'sym':>7}{'fin':>6}{'lev':>6}{'CAGR':>9}{'P(target)':>11}{'P(breach)':>11}" f"{'P(timeout)':>12}{'edge vs coin':>14}") rng = np.random.default_rng(7) for s in ('SP500', 'XAUUSD'): t, r, sp = D[s] for fin in (0.05, 0.075): for lev in (1.0, 2.0, 3.0, 5.0): res = first_passage(r, fin, lev, sp, rng) #--- a fair-coin benchmark: risking the same amount with NO edge would hit #--- +8 before -6 with probability 6/(6+8). Beating that is the whole claim. coin = MAXLOSS / (MAXLOSS + TARGET) print(f" {s:>7}{100*fin:>5.1f}%{lev:>6.1f}" f"{100*stats(r, fin, lev, sp)['cagr']:>8.2f}%" f"{100*res[0]:>10.1f}%{100*res[1]:>10.1f}%{100*res[2]:>11.1f}%" f"{100*(res[0]-coin):>13.1f}pp") print("\n 'edge vs coin' compares to a no-skill bet with the same barriers, which reaches") print(" +8% before -6% with probability 6/14 = 42.9%. That is the number to beat, NOT 50%.")