M/08 — Robust portfolio construction
Universe-saturation of minimum-variance portfolios
How OOS volatility, MaxDD and CVaR-95 of a min-variance portfolio scale with the strategy-universe size N — across Ledoit–Wolf, Huber-robust and sample covariance estimators.
The mathematics
Given an N × T matrix of strategy returns R (each column a bar, each row a strategy) on a training window, the minimum-variance portfolio with full investment is
At large N, the conditioning of Σ̂ dominates portfolio behaviour on unseen data: the smallest eigenvalues of the sample covariance are biased downward (Marchenko–Pastur distortion at q = N/T), so Σ̂⁻¹ blows up the corresponding directions and the resulting w* chases noise. Three remedies are compared here.
Ledoit–Wolf shrinkage
F is a low-parameter target (constant correlation or scalar identity); the Ledoit–Wolf 2004 closed form gives a consistent estimator of α* from the data itself.
Huber-style robust M-estimator
with ψ Huber's bounded influence function. The estimator is iterated to convergence; on roughly Gaussian centered returns it coincides numerically with the sample covariance, which explains why the LW and Huber curves below are visually indistinguishable.
Saturation
Under independence, OOS portfolio variance scales as O(N⁻¹). With correlated factor structure the rate is slower and an effective number of independent directions caps the gain. The empirical saturation point N* is the universe size at which adding strategies stops moving the OOS metric materially.
Empirical setup
We run a deep walk-forward analysis on 10 USDT crypto pairs at 30-minute resolution. The largest universe is AVAX with N = 49,068 candidate strategies on T_train = 1,240 bars (q = N/T ≈ 39.6 — far above the Marchenko–Pastur regime). For each anchor N ∈ {25, 100, 500, 2,000} we sample N strategies, fit each estimator on the training window, build w*, and evaluate vol_oos, MaxDD_oos and CVaR-95_oos on the held-out window.
Concretely, on AVAX (LW) the OOS volatility falls from σ ≈ 149.5 at N = 25 to σ ≈ 116.7 at N = 100, σ ≈ 84.5 at N = 500 and σ ≈ 70.4 at N = 2,000 — a 53% reduction with two-thirds of it captured by N = 500. CVaR-95 follows the same shape, dropping from −18.8 to −7.1. On BTC (LW) the curve is steeper still: σ goes from 87.0 (N = 25) to 25.6 (N = 2,000).
Method comparison
At small N the three estimators differ visibly: sample covariance has lower OOS volatility (e.g. AVAX N = 500: σ_sample ≈ 73.0 vs σ_LW ≈ 84.5), because with full rank still attainable the unbiased sample estimate beats the shrunk one. As N grows, q = N/T crosses the conditioning threshold and the sample inverse becomes unstable; LW and Huber overtake on every asset for N ≳ 5,000. MaxDD is more idiosyncratic: sample sometimes wins on MaxDD even at large N (AVAX N = 2,000: MaxDD_sample ≈ −222 vs MaxDD_LW ≈ −311), which we attribute to the sample portfolio concentrating in a few well-conditioned eigen-directions during the test window.
Why this matters
The practical takeaway is to budget your strategy-mining effort against diminishing returns. Past the N* of an asset, additional strategies add operational cost without buying OOS risk reduction; below it, the portfolio is volatility-inefficient. The exported curves let a desk pick the smallest universe consistent with its risk target, and the LW/Huber convergence means the cheap shrinkage estimator is sufficient — the heavy robust pipeline rarely earns its compute budget on this data.
Reproducibility
DaruFinance / strategy-robust-portfolio
Python — open source reference implementation
Minimal invocation
import json
from strategy_robust_portfolio import build_min_variance, sweep_universe
# returns_train: T x N strategy returns matrix (in-sample)
# returns_test: T' x N strategy returns matrix (out-of-sample)
weights = build_min_variance(
returns_train,
method="lw", # one of {"lw", "robust", "sample"}
rho=1e-3, # correlation regulariser added to the diagonal
)
# Saturation sweep over universe size N
curve = sweep_universe(
returns_train, returns_test,
n_grid=[25, 100, 500, 2000, 10000, 49068],
methods=["lw", "robust", "sample"],
seed=0,
)
print(json.dumps(curve.summary(), indent=2))
References
- [1]Ledoit, O. & Wolf, M. (2004). A well-conditioned estimator for large-dimensional covariance matrices. Journal of Multivariate Analysis 88(2), 365–411.
- [2]Maronna, R. A. (1976). Robust M-estimators of multivariate location and scatter. The Annals of Statistics 4(1), 51–67.
- [3]Rockafellar, R. T. & Uryasev, S. (2000). Optimization of conditional value-at-risk. Journal of Risk 2(3), 21–41.
- [4]Bun, J., Bouchaud, J.-P. & Potters, M. (2017). Cleaning large correlation matrices: tools from random matrix theory. Physics Reports 666, 1–109.