The Intuition

Mean-Variance Optimization, introduced by Harry Markowitz (1952), is the mathematical foundation of modern portfolio theory. Applied as a dynamic single-asset strategy, it determines the theoretically optimal position size at each point in time given estimates of expected return (mu) and variance (sigma²). Unlike binary long/short signals, it produces a continuously variable position that shrinks as uncertainty increases and grows as the risk-adjusted return improves.

The Merton (1969) intertemporal consumption model shows that the optimal risky asset allocation for a CRRA investor is exactly w* = mu / (sigma² × risk_aversion). This is the formula we implement. With risk_aversion = 1, the position is the unconstrained Kelly fraction — the growth-optimal bet size. Higher risk aversion shrinks the position, acting as a conservative multiplier on the Kelly fraction.

The key advantage over a binary signal: position sizing is proportional to the signal strength. When mu is large relative to sigma², the strategy takes a large long position. When mu and sigma² are both large (high-momentum but also high-volatility), the position is moderated by the risk. This volatility-targeting property means the strategy naturally de-risks during volatile periods — unlike fixed-size strategies that maintain full exposure through market turbulence.

Key assumptions: (1) The rolling estimates of mu and sigma² are unbiased predictors of future return and variance. In practice, expected return estimation is notoriously noisy — the signal-to-noise ratio in mu estimates is very low at daily to weekly horizons. (2) Returns are approximately normally distributed — fat tails mean the Gaussian variance understates true risk. (3) The position is continuously rebalanced — in reality, transaction costs create a "no-trade zone" around the optimal position.

The strategy's practical failure mode is estimation error in mu: rolling sample means of returns are extremely noisy. A 60-day rolling mean of daily returns has a standard error of approximately sigma / sqrt(60), which is typically larger than the true expected daily return. This noise creates excessive position turnover and whipsaw losses. Practitioners often use shrinkage estimators (e.g., James-Stein) or factor-based expected return models to reduce estimation error.

The Math

Read this as a compact model summary: what the signal sees, what it ignores, and where fragility can creep in.

mu(t)     = mean(r[t-n:t]) × 252          [annualised return]
sigma2(t) = var(r[t-n:t]) × 252           [annualised variance]

w*(t) = mu(t) / (sigma2(t) × risk_aversion)

Position(t) = clip(w*(t), -1, +1)

Parameters

ParameterTypeDefaultDescription
window int 60 Rolling window for estimating μ and σ
risk_aversion float 1.0 Risk aversion coefficient (higher = smaller positions). Applied to a scale-normalised Merton weight, so it changes average exposure across roughly 0.25-8 instead of being absorbed by the leverage cap.
scale_window int 252 Trailing window used to normalise the mu/sigma^2 ratio to an O(1) scale. Backward-looking only.
max_leverage float 1.0 Absolute cap on the resulting position size.

Source Code

def run(ticker: str, start: str, end: str, **params) -> dict:
    window = int(params.get("window", 60))
    risk_aversion = max(1e-6, float(params.get("risk_aversion", 1.0)))
    scale_window = max(window, int(params.get("scale_window", 252)))
    max_leverage = max(1e-6, float(params.get("max_leverage", 1.0)))
    df = fetch_ohlcv(ticker, start, end)

    log_ret = np.log(df["Close"] / df["Close"].shift(1))
    mu = log_ret.rolling(window).mean() * 252
    sigma2 = (log_ret.rolling(window).std() ** 2) * 252

    # Merton/Markowitz optimal weight, mu / (gamma * sigma^2).
    merton = mu / sigma2.replace(0, np.nan)

    # Rescaling. Raw mu/sigma^2 off a short rolling window is enormous and wildly
    # dispersed (annualising a 60-day drift routinely gives |mu/sigma^2| in the
    # tens), so `.clip(-1, 1)` bound on almost every bar and the strategy
    # collapsed to sign(mu) — risk_aversion had no effect whatsoever.
    # Normalising by the trailing median magnitude of the same ratio puts the
    # signal on a stable O(1) scale, so risk_aversion genuinely modulates average
    # exposure across its documented range instead of being swallowed by the clip.
    # The normaliser is backward-looking (rolling median of realised values), so
    # it introduces no look-ahead.
    scale = merton.abs().rolling(scale_window, min_periods=window).median()
    scale = scale.replace(0, np.nan)

    raw_pos = merton / (scale * risk_aversion)
    pos = raw_pos.clip(-max_leverage, max_leverage).fillna(0.0)

    return run_backtest(df, pos, ticker=ticker, start=start, end=end,
                        strategy=METADATA["slug"],
                        params={
                            "window": window,
                            "risk_aversion": risk_aversion,
                            "scale_window": scale_window,
                            "max_leverage": max_leverage,
                        },
                        **resolve_friction(params))

Further Reading

  • Markowitz, H. (1952). Portfolio Selection. Journal of Finance, 7(1), 77–91.
  • Merton, R. (1969). Lifetime Portfolio Selection Under Uncertainty: The Continuous-Time Case. Review of Economics and Statistics, 51(3), 247–257.
  • Grinold, R. & Kahn, R. (2000). Active Portfolio Management, 2nd ed. McGraw-Hill.
  • Chan, E. (2013). Algorithmic Trading, Ch. 5. Wiley.

When It Works / When It Fails

Works
  • Instruments with stable estimated mean and variance
  • Portfolios where risk-budgeting adds measurable value
  • When Gaussian return assumptions approximately hold
Fails
  • High-uncertainty environments with noisy mean estimates
  • Fat-tailed return distributions (Gaussian assumption violated)
  • Regime shifts where parameters move faster than the window

Regime Fit

Bull (all vol)
1.0 / 0.85 / 0.55 across Calm, Normal, Stressed. MV stays invested but scales back in stressed environments.
Transition (all vol)
0.75 / 0.55 / 0.25. More conservative than binary signals; allocates less but stays in the market.
Bear (all vol)
0.65 / 0.5 / 0.25. Remarkably stays partially invested even in bear regimes — an all-weather characteristic.
Stressed vol (any regime)
Max 0.55, floor 0.25. Exposure never fully cuts to zero; MV scales continuously rather than flipping binary.

Compared to Alternatives

vs Volatility Scaled Momentum
VSM uses momentum sign + vol scaling; MV uses both return estimate AND variance in the position calculation. MV is more complete but more parameter-sensitive.
vs TSMOM
TSMOM allocates binary ±1 based on return sign; MV allocates continuously based on risk/return ratio. MV provides more granular position sizing.
vs Bollinger
Bollinger is a directional mean-reversion signal; MV is a position-sizing framework. They operate at different layers — MV sizes a position, Bollinger generates the direction signal.
Run This Strategy →