"""What does the PORTFOLIO framing actually require? (the standalone-CAGR critique is wrong) A component of an uncorrelated portfolio must not be judged on its own CAGR. Diversification cuts variance by sqrt(N), and on a prop account you are constrained by DRAWDOWN rather than by capital, so a lower-variance portfolio can be levered back up to the same drawdown budget. Return therefore scales by ~sqrt(N) while the risk limit is held fixed. A 2.4%/yr component is not automatically useless; ten of them can be a 10%/yr business. That is arithmetic and it is correct. What it does NOT do is create mean where there is none: portfolio mean = mean of the component means (diversification does nothing to it) portfolio sd = component sd / sqrt(N) (this is the whole benefit) So the portfolio approach AMPLIFIES the average edge of the generator. If the generator's true average OOS edge is positive, this works and works well. If it is zero, sqrt(N) leverage applied to zero is zero, and every component still pays its own spread and commission N times over. => The unit that has to be validated is THE GENERATOR, not any single strategy. That is a cheap and decisive test, and it is the same family-wise logic every other result in this project was held to - a single strategy with 53 OOS trades has almost no power, but 40 strategies x 53 trades is 2,120 OOS trades and their MEAN is measurable. """ import numpy as np, sys sys.stdout.reconfigure(encoding='utf-8', errors='replace') #--- measured from the SQX trade list (research/sqx_audit.py) PER_TRADE_MEAN = 12.91 PER_TRADE_SD = 95.0 TRADES_PER_YEAR = 10.7 BALANCE = 5000.0 OOS_MEAN, OOS_SD, OOS_N = 11.07, 94.8, 53 MAXDD_PCT = 5.93 DD_LIMIT = 8.0 TARGET_RET = 10.0 # a profit target worth the effort, %/yr def sharpe(mean_pt, sd_pt, n_yr): return (mean_pt * n_yr) / (sd_pt * np.sqrt(n_yr)) if __name__ == '__main__': ann_mean = PER_TRADE_MEAN * TRADES_PER_YEAR / BALANCE * 100 ann_sd = PER_TRADE_SD * np.sqrt(TRADES_PER_YEAR) / BALANCE * 100 sh = ann_mean / ann_sd print("=== 1. THE COMPONENT, ON PORTFOLIO TERMS ===") print(f" annual return {ann_mean:+.2f}% annual sd {ann_sd:.2f}% Sharpe {sh:.2f}") print(f" maxDD {MAXDD_PCT:.2f}% -> headroom to the {DD_LIMIT:.0f}% limit: " f"{DD_LIMIT/MAXDD_PCT:.2f}x leverage available standalone") print(f" levered standalone: {ann_mean*DD_LIMIT/MAXDD_PCT:+.2f}%/yr at the DD limit\n") print("=== 2. WHAT N UNCORRELATED COPIES BUY (drawdown held at the limit) ===") print(f" {'N':>4}{'port sd':>10}{'port maxDD':>12}{'leverage':>10}" f"{'return @limit':>15}{'Sharpe':>9}") for N in (1, 2, 5, 10, 20, 40): p_sd = ann_sd / np.sqrt(N) p_dd = MAXDD_PCT / np.sqrt(N) # scales with vol, first-order lev = DD_LIMIT / p_dd print(f" {N:>4}{p_sd:>10.2f}{p_dd:>12.2f}{lev:>10.2f}" f"{ann_mean*lev:>+15.2f}{sh*np.sqrt(N):>9.2f}") N_need = (TARGET_RET / (ann_mean * DD_LIMIT / MAXDD_PCT)) ** 2 print(f"\n -> {TARGET_RET:.0f}%/yr at an {DD_LIMIT:.0f}% DD limit needs N ~ {N_need:.0f} " f"genuinely uncorrelated components of this quality.") print(" That is achievable. The portfolio framing is sound - IF the edge is real.\n") print("=== 3. THE SAME ARITHMETIC WHEN THE TRUE EDGE IS ZERO ===") print(" Diversification divides the SD by sqrt(N). It multiplies the MEAN by 1.") print(" Leverage L applied to a portfolio of true-zero-mean components:") for N in (10, 40): p_dd = MAXDD_PCT / np.sqrt(N) lev = DD_LIMIT / p_dd cost_mult = N * lev print(f" N={N:<3} leverage {lev:.2f}x -> expected return 0.00%/yr, and spread+commission " f"is paid {cost_mult:.0f}x the single-strategy rate") print(" -> the failure mode is not 'flat'. Levered noise pays N x L rounds of cost.\n") print("=== 4. SO TEST THE GENERATOR, NOT THE STRATEGY ===") se1 = OOS_SD / np.sqrt(OOS_N) print(f" one strategy: {OOS_N} OOS trades, se {se1:.2f}, observed mean {OOS_MEAN:+.2f} " f"-> t {OOS_MEAN/se1:+.2f}") print(f" detectable edge at 80% power, single strategy: {2.8*se1:+.2f} per trade " f"({100*2.8*se1*TRADES_PER_YEAR/BALANCE:.1f}%/yr) - far above what any real edge looks like") print(" pooled across a generated population, same OOS windows:") print(f" {'strategies':>11}{'OOS trades':>12}{'se of mean':>12}{'detectable edge':>17}") for K in (5, 10, 20, 40, 80): n = K * OOS_N se = OOS_SD / np.sqrt(n) print(f" {K:>11}{n:>12}{se:>12.2f}{2.8*se:>+17.2f}") print("\n -> ~40 strategies gives se ~2.1, so a true average edge of +6/trade shows at t~3.") print(" THE TEST: export every candidate the generator produces (not only the ones that") print(" passed its own filters - that is the selection step), pool their OOS trades, and") print(" ask whether the MEAN is positive. A generator whose population mean is ~0 produces") print(" portfolios of noise no matter how uncorrelated the components are.\n") print("=== 5. THE CORRELATION THAT ACTUALLY MATTERS ===") print(" Everything above assumes correlation ~0 IN THE TAIL. Two standard reasons it is not:") print(" * correlations estimated over a full history are dominated by quiet periods;") print(" strategies on one instrument converge in a drawdown, which is the only time the") print(" 4%/day rule is at risk. Measure correlation CONDITIONAL on the worst decile of days.") print(" * strategies sharing an instrument, a timeframe and an indicator family are not") print(" independent draws whatever the sample correlation says. Diversify the MECHANISM") print(" and the instrument, not just the parameter set.") print(" For a daily-loss rule, simultaneous losses are the only exposure that counts:") for N in (10, 20, 40): for rho in (0.0, 0.2, 0.5): eff = N / (1 + (N - 1) * rho) # effective independent count print(f" N={N:<3} rho={rho:<4} -> effective N {eff:5.1f}, " f"sqrt(N) benefit {np.sqrt(eff):.2f}x (vs {np.sqrt(N):.2f}x if truly independent)")