Position Sizing¶
How portfolio weights are determined given a factor signal.
The Four Methods¶
Quant101 provides four sizing methods, all producing a weight DataFrame
with columns (date, ticker, weight):
Equal Weight¶
The baseline. No information beyond rank ordering is used.
Inverse Volatility¶
Lower-volatility stocks get higher weight. Risk-parity inspired — equalizes risk contribution rather than capital contribution.
Volatility Target¶
Each position is sized so its dollar volatility matches a target. Useful when you want a specific portfolio volatility profile.
Signal-Weighted¶
where:
- \(z_i\) = cross-sectional z-score of the factor signal (conviction)
- \(\sigma_i^2\) = rolling variance of historical returns (risk)
- Normalized to \(\sum|w_i| = \text{max\_leverage}\) (default 1.0)
High-conviction, low-risk stocks get larger positions.
The Kelly Lesson¶
Critical Bug: Why 'Half-Kelly' Was Wrong
This method was originally called "Half-Kelly sizing." It took three successive bug fixes and an ablation study to understand why that name (and mental model) was fundamentally misleading.
What Happened¶
We implemented \(w_i \propto \mu_i / \sigma_i^2\) (classic Kelly) for cross-sectional portfolio allocation. Results:
| Config | Sharpe |
|---|---|
| Equal-Weight + Daily | +0.814 (baseline) |
| Equal-Weight + Weekly | +0.417 |
| Signal-Weighted + Daily | +0.137 |
| Signal-Weighted + Weekly | −0.393 |
Signal-weighted sizing cost 0.68 Sharpe compared to equal-weight — the single largest source of degradation.
The Three Bugs¶
-
Normalization destroyed Kelly's answer. Normalizing \(\sum|w| = 1\) discards the leverage signal, which is the whole point of Kelly.
-
\(\mu_i\) from returns ignored the factor. Historical mean return over 60 days is pure noise (SNR ≈ 0.02). All configs produced identical Sharpe = 0.343 regardless of factor chosen.
-
Historical \(\mu\) is stealth momentum. A 60-day rolling mean injects momentum bias that conflicts with a mean-reversion signal like BBIBOLL.
Root Cause¶
Kelly criterion solves: "What fraction of my bankroll should I wager on a bet with known edge \(\mu\) and variance \(\sigma^2\)?"
This is for a single repeated bet with independent outcomes. We misused it for cross-sectional allocation across correlated assets.
| Kelly's Problem | Our Misuse | |
|---|---|---|
| Question | How much total capital to deploy? | How to split capital across N stocks? |
| Input | Edge and variance of one bet | Cross-sectional factor signal |
| Output | Portfolio leverage \(f^*\) | Relative stock weights |
| Regime | Independent repeated bets | Correlated concurrent positions |
The correct multi-asset generalization is multivariate Kelly:
where \(\Sigma\) is the covariance matrix. Our per-stock \(\mu_i / \sigma_i^2\) ignores correlations entirely.
Why \(z / \sigma^2\) Hurts Mean-Reversion¶
For BBIBOLL, the strongest alpha comes from high-volatility stocks that have deviated far from their bands. But \(w_i \propto z_i / \sigma_i^2\) penalizes large \(\sigma_i\) quadratically. The stocks with the highest conviction (\(|z_i|\)) also have the highest \(\sigma_i\) — the penalty overwhelms the signal.
Resolution¶
| Action | Status |
|---|---|
Renamed size_half_kelly → size_signal_weighted |
✅ |
| Docstring states "Not true Kelly" | ✅ |
| Formula kept as conviction × inverse-variance | ✅ |
| True Kelly (portfolio leverage) deferred to future module | Planned |
Takeaway¶
Interview One-Liner
"Kelly tells you how much to bet, not how to split the bet. Using scalar Kelly for cross-sectional allocation ignores correlations and concentrates into low-vol names — exactly the wrong thing for a volatility-driven alpha."
Full version: docs/latex/quant_lab.tex — Entry 4.