107 lines
6.2 KiB
Python
107 lines
6.2 KiB
Python
|
|
"""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)")
|