---
title: "4.2.2 Kelly criterion and volatility targeting"
description: "Full and fractional Kelly with estimation error, the cost of overbetting, and volatility targeting measured with its worst episode in the data."
url: "https://pythonparatrading.com/en/python-trading-course/level-4/kelly-criterion-and-volatility-targeting/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-4/kelly-criterion-and-volatility-targeting.png"
---

# 4.2.2 · Kelly criterion and volatility targeting: how much to risk

> Kelly says what fraction of capital maximizes long-term growth; volatility targeting keeps the risk of the position constant. This lesson derives both, applies them to the level's strategies and universe and measures them by what they cost: the growth lost by misestimating the mean, the drawdowns of full Kelly and the day when the volatility control had its highest exposure right before the blow.

- Course: [Level 4: Portfolios, risk, execution and live trading](https://pythonparatrading.com/en/python-trading-course/level-4/)
- Module: 4.2 · Risk management and position sizing
- Duration: 70 min
- Prerequisites: [2.1.4 · Volatility models: ARCH, GARCH, EGARCH and GJR with the arch library](https://pythonparatrading.com/en/python-trading-course/level-2/volatility-models-arch-garch-egarch-gjr/), [2.2.2 · Universe, signal, filter, entry, exit, sizing and risk management](https://pythonparatrading.com/en/python-trading-course/level-2/universe-signal-entry-exit-position-sizing/), [2.5.4 · Deflated Sharpe Ratio and Probability of Backtest Overfitting (PBO)](https://pythonparatrading.com/en/python-trading-course/level-2/deflated-sharpe-ratio-and-pbo/), [2.6.1 · CAGR, Sharpe, Sortino, Calmar, Omega, drawdown and Ulcer Index](https://pythonparatrading.com/en/python-trading-course/level-2/return-and-risk-metrics/), [4.2.1 · Risk per trade, stops and risk of ruin](https://pythonparatrading.com/en/python-trading-course/level-4/risk-per-trade-stops-and-risk-of-ruin/)

## By the end of this lesson

- Derive the Kelly fraction for a binary bet and in the continuous case (`f* = μ/σ²`) and explain why Kelly volatility equals the Sharpe ratio.
- Quantify the growth, volatility and drawdown probability of fractional Kelly, and the cost of betting twice Kelly.
- Measure how much growth is lost to estimation error in the mean and compute the fraction that compensates for it depending on the sample length.
- Implement volatility targeting without look-ahead, with a cap, a band and costs, and explain when it improves the Sharpe and when it does not.
- Present each rule with its worst episode in the data and detect look-ahead in an implementation with a negative `shift`.

## Kelly: how much to bet when you have an edge

Lesson 2.2.2 spread risk across markets with a hand-set volatility target, and 4.2.1 limits what is risked on each
trade. The question above them is missing: **how much total risk** is worth taking with a given edge. John L. Kelly
answered it in 1956 for a gambler who repeatedly bets a fraction $f$ of his capital: the one that maximizes the
**expected logarithmic growth**, which is the rate at which capital grows in the long run. For a bet that wins $b$
per unit with probability $p$ and loses the unit with $q = 1 - p$:

$$
g(f) = p\,\ln(1 + f b) + q\,\ln(1 - f), \qquad f^* = p - \frac{q}{b}.
$$

A coin with a 55% hit rate and an even-money payout gives $f^* = 0.10$: bet 10% of the capital on each toss.

```python
import numpy as np
import pandas as pd

p, b = 0.55, 1.0      # probability of winning and payout per unit
f_kelly = p - (1 - p) / b
print(f"Kelly fraction: {f_kelly:.2f}")


def g(f):
    """Expected logarithmic growth per bet."""
    return p * np.log1p(f * b) + (1 - p) * np.log1p(-f)


fracciones = [0.025, 0.05, 0.10, 0.15, 0.20, 0.30]
pd.Series({f: g(f) for f in fracciones}, name="g").round(5)
```
```output
Kelly fraction: 0.10
0.025    0.00219
0.050    0.00375
0.100    0.00501
0.150    0.00374
0.200   -0.00014
0.300   -0.01620
Name: g, dtype: float64
```

Growth per bet is maximal at $f = 0.10$ (0.00501 in logarithms) and almost symmetric around it: 0.05 and 0.15 give
the same (0.00375 and 0.00374). With 0.20, **twice Kelly**, growth is already negative (−0.00014): you play with an
edge and, even so, capital tends to shrink. With 0.30 the capital vanishes. An average does not show it; a thousand
repeated bets by 4,000 players do:

```python
rng = np.random.default_rng(7)
gana = rng.random((4000, 1000)) < p     # 4,000 paths of 1,000

filas = {}
for f in (0.05, 0.10, 0.20, 0.30):
    paso = np.where(gana, np.log1p(f * b), np.log1p(-f))
    lw = np.cumsum(paso, axis=1)                # log of wealth
    pico = np.maximum.accumulate(np.maximum(lw, 0), axis=1)
    caida = np.exp(lw - pico) - 1
    filas[f] = {"mediana_final": np.exp(np.median(lw[:, -1])),
                "p_pierde": (lw[:, -1] < 0).mean(),
                "p_mitad_inicial": (lw.min(axis=1)
                                    <= np.log(0.5)).mean(),
                "p_caida_50": (caida.min(axis=1) <= -0.5).mean(),
                "p_caida_80": (caida.min(axis=1) <= -0.8).mean()}
pd.DataFrame(filas).T.round(3)
```
```output
      mediana_final  p_pierde  p_mitad_inicial  p_caida_50  p_caida_80
0.05         42.632     0.009            0.116       0.854       0.064
0.10        149.660     0.061            0.483       1.000       0.850
0.20          0.871     0.514            0.911       1.000       1.000
0.30          0.000     0.954            0.997       1.000       1.000
```

With Kelly (0.10) the median player multiplies their capital by 150, but 6.1% end up losing, 48.3% touch half the
initial capital at some point and all of them suffer a 50% drawdown from a peak. With half Kelly (0.05) the median
multiplies by 43, only 0.9% lose, 11.6% touch half the initial capital and 6.4% suffer an 80% drawdown, versus 85%
with Kelly. With twice Kelly (0.20) half of the players lose money with a coin in their favor.

### In continuous time: the optimal volatility is the Sharpe ratio

For a strategy with expected excess return $\mu$ and volatility $\sigma$ (annual), leveraged $f$ times and rebalanced
often, the logarithmic growth of the excess is $g(f) = f\mu - \tfrac{1}{2}f^2\sigma^2$, a parabola. Its maximum:

$$
f^* = \frac{\mu}{\sigma^2}, \qquad g^* = \frac{1}{2}\,\mathrm{SR}^2, \qquad f^*\sigma = \frac{\mu}{\sigma} = \mathrm{SR}.
$$

The third equality is the most useful one: **at Kelly, account volatility equals the Sharpe ratio**. A strategy with
a Sharpe of 0.5 calls for 50% annual volatility, whatever its own volatility. With a fraction $c$ of Kelly, growth is
$g^*(2c - c^2)$ and volatility $c\cdot\mathrm{SR}$. The logarithm of capital is then a Brownian motion with drift
$\nu = \mathrm{SR}^2(c - c^2/2)$ and variance $s^2 = c^2\mathrm{SR}^2$, and the probability that it ever falls to a
fraction $x$ of the initial capital is $x^{2\nu/s^2} = x^{2/c - 1}$ (the probability of touching a lower barrier, with
known parameters and an infinite horizon):

```python
c = np.array([0.25, 0.5, 0.75, 1.0, 1.5, 2.0])
expo = 2 / c - 1          # P(ever falling to x) = x ** expo
pd.DataFrame({"crecimiento_vs_max": 2 * c - c**2,
              "vol_en_sharpes": c,
              "p_caer_a_la_mitad": 0.5 ** expo,
              "p_caer_al_20": 0.2 ** expo},
             index=pd.Index(c, name="fraccion")).round(3)
```
```output
          crecimiento_vs_max  vol_en_sharpes  p_caer_a_la_mitad  p_caer_al_20
fraccion
0.25                   0.438            0.25              0.008         0.000
0.50                   0.750            0.50              0.125         0.008
0.75                   0.938            0.75              0.315         0.068
1.00                   1.000            1.00              0.500         0.200
1.50                   0.750            1.50              0.794         0.585
2.00                   0.000            2.00              1.000         1.000
```

The header figure draws those two curves. Growth is flat near the maximum and the drawdown probability is not: going
from Kelly to half Kelly costs 25% of the growth and divides by four the probability of losing half (from 50% to
12.5%); a quarter Kelly keeps 44% of the growth with 0.8%. Above Kelly everything gets worse at once: with 1.5 times
Kelly growth is the same as with half Kelly (0.75) and the probability of losing half, 79%. The coin simulation
agrees with the formula (0.116 and 0.483 versus 0.125 and 0.5). The error is not symmetric: underbetting costs
little; overbetting, a lot.

## Kelly on the level's strategies

The ten strategies in `estrategias4_diarias.parquet` are simple daily returns in excess of the risk-free rate,
2006-2025, with a known true mean (`exceso_anual_esperado` in `estrategias4_verdad.csv`). The lesson leaves out
`degradada`, which stops working in 2019 and is the case of lesson 4.2.5. In addition to $\mu/\sigma^2$, the cell
computes the “exact” Kelly of the data (the $f$ that maximizes the mean of $\ln(1 + f r_t)$ with the actual daily
returns, tails included) and the leverage that ruins the account on the worst day of the sample:

```python
from scipy.optimize import minimize_scalar

est = pd.read_parquet("estrategias4_diarias.parquet")
est = est.set_index("fecha").drop(columns="degradada")
verdad = pd.read_csv("estrategias4_verdad.csv",
                     index_col="estrategia").reindex(est.columns)


def kelly_log(r):
    """f that maximizes the mean of log(1 + f·r) in the data."""
    tope = 0.999 / abs(r.min())          # beyond this, ruin
    res = minimize_scalar(lambda f: -np.log1p(f * r).mean(),
                          bounds=(0, tope), method="bounded")
    return res.x


mu, var = est.mean() * 252, est.var() * 252
tabla = pd.DataFrame({
    "mu": mu, "vol": np.sqrt(var),
    "kelly": mu / var,
    "kelly_verdad": verdad["exceso_anual_esperado"] / var,
    "kelly_log": est.apply(kelly_log),
    "ruina": 1 / est.min().abs()})
tabla.round(2)
```
```output
                     mu   vol  kelly  kelly_verdad  kelly_log  ruina
tendencia_futuros  0.06  0.11   4.97          4.30       4.94  20.25
carry_futuros      0.03  0.08   5.06          5.63       5.02  31.35
carry_divisas      0.05  0.09   5.81          3.81       5.73  21.78
venta_puts         0.04  0.12   2.56          2.82       2.54  15.25
pares              0.02  0.06   6.72          8.43       6.61  25.65
momentum_acciones  0.05  0.12   3.19          2.83       3.12  12.44
valor_acciones     0.02  0.09   3.01          3.06       3.01  31.49
venta_volatilidad  0.04  0.16   1.55          2.61       1.47   5.19
reversion_corto    0.03  0.07   6.41          5.61       6.36  24.95
```

Three readings:

- **The Kelly fractions are large**: from 1.5 to 6.7 times the capital for single strategies with volatilities from 6
  to 16%. It is the consequence of $f^*\sigma = \mathrm{SR}$: with a Sharpe of 0.4, Kelly calls for 40% volatility.
- **The sample mean drifts away from the true one** even with 20 years: in `carry_divisas` the sample Kelly is 5.81
  and the true one 3.81; in `venta_volatilidad`, 1.55 versus 2.61. With the sample mean you bet 50% too much on the
  first and 40% too little on the second.
- **Tails dominate in volatility selling.** Its exact Kelly (1.47) is smaller than the formula's, and the ruin
  leverage, 5.19 times, is only twice the true Kelly: the worst day of the sample loses 19.3%. In the rest, ruin lies
  at 12-31 times the capital and the formula and the exact Kelly almost coincide.

With nine weakly correlated strategies, the equal-weight portfolio has a higher Sharpe and a much lower volatility,
and Kelly shoots up:

```python
cartera = est.mean(axis=1)          # equal-weight, simple returns
k_cartera = cartera.mean() / cartera.var()
print(f"Portfolio Kelly: {k_cartera:.1f} times the capital")
print(f"Volatility at Kelly: {k_cartera * cartera.std() * 252**.5:.0%}")


def resultado(r):
    w = (1 + r).cumprod()
    años = len(r) / 252
    return {"cagr": w.iloc[-1] ** (1 / años) - 1
            if w.iloc[-1] > 0 else -1.0,
            "max_caida": (w / w.cummax() - 1).min(),
            "peor_dia": r.min(),
            "ruina": bool((w <= 0).any())}


fr = {c: resultado(c * k_cartera * cartera)
      for c in (0.125, 0.25, 0.5, 1.0, 2.0)}
print("Worst day:", cartera.idxmin().date())
pd.DataFrame(fr).T.astype(float).round(3)
```
```output
Portfolio Kelly: 18.8 times the capital
Volatility at Kelly: 84%
Worst day: 2018-02-05
        cagr  max_caida  peor_dia  ruina
0.125  0.086     -0.315    -0.053    0.0
0.250  0.167     -0.544    -0.106    0.0
0.500  0.303     -0.816    -0.213    0.0
1.000  0.417     -0.979    -0.426    0.0
2.000 -0.081     -1.000    -0.851    0.0
```

*Fig. — Equal-weight portfolio of the level's nine strategies at constant leverage, in fractions of its Kelly estimated on the whole sample (18.8 times capital), 2006-2025.* Bars by Kelly fraction with maximum drawdown on the left and compound annual return on the right. 1/8: CAGR +8.6%, drawdown −31.5%; 1/4: CAGR +16.7%, drawdown −54.4%; 1/2: CAGR +30.3%, drawdown −81.6%; Kelly: CAGR +41.7%, drawdown −97.9%; 2× Kelly: CAGR −8.1%, drawdown −100.0%. Full Kelly (highlighted) multiplies CAGR fivefold over one eighth, but its maximum drawdown nearly wipes out the account.

Kelly calls for 18.8 times the capital and 84% volatility. With the full-sample Kelly (which uses the 20-year mean
from the first day: look-ahead) the result is the theoretical one with a nuance: full Kelly gives the highest CAGR
(41.7%), but with a maximum drawdown of −97.9% and one day, February 5, 2018, on which it loses 42.6% of the capital.
One eighth of Kelly (2.3 times) gives 8.6% with a drawdown of −31.5%. Twice Kelly loses 8.1% per year and ends up
with a 100% drawdown (rounded). No fraction reaches strict ruin (the account does not go below zero because the
portfolio's worst day loses 2.3%), but a 98% drawdown is ruin for practical purposes.

With what was known each day (mean and variance since the start, with a three-year warm-up and no short positions),
the portfolio Kelly starts at zero in 2009, because the 2008 crisis left the sample mean negative, and rises to 5.9 in
2013, 14.6 in 2017 and 18.7 in 2025:

```python
mu_t = cartera.expanding(756).mean().shift(1)
var_t = cartera.expanding(756).var().shift(1)
k_t = (mu_t / var_t).clip(lower=0)       # no short positions
print(k_t.resample("YE").last().loc["2009":"2025":4].round(1))
pd.DataFrame({c: resultado((c * k_t * cartera).dropna())
              for c in (0.25, 0.5, 1.0)}).T.astype(float).round(3)
```
```output
fecha
2009-12-31     0.0
2013-12-31     5.9
2017-12-31    14.6
2021-12-31    16.2
2025-12-31    18.7
Freq: 4YE-DEC, dtype: float64
       cagr  max_caida  peor_dia  ruina
0.25  0.129     -0.346    -0.084    0.0
0.50  0.251     -0.581    -0.168    0.0
1.00  0.450     -0.839    -0.337    0.0
```

The CAGR of full Kelly comes out even higher (45.0%) because it reached the strategy late and with little leverage
in the 2009-2012 drawdowns; the maximum drawdown, −83.9%, and the worst day, −33.7%, are still those of an account
that nobody would keep. Calendar luck decides more here than the rule.

## Estimation error

Kelly assumes that $\mu$ and $\sigma$ are known. Volatility is estimated well with little data; the mean is not. With
$T$ years of data, the sample mean has a standard error of $\sigma/\sqrt{T}$, so the estimated Kelly is
$\hat f = f^*\,u$ with $u \sim N\!\left(1, \tfrac{1}{\mathrm{SR}^2 T}\right)$. Betting a fraction $c$ of the
estimated Kelly gives an expected growth, relative to that of the true Kelly, of

$$\frac{\mathrm{E}[g]}{g^*} = 2c - c^2\left(1 + \frac{1}{\mathrm{SR}^2 T}\right), \qquad
c^{\text{opt}} = \frac{\mathrm{SR}^2 T}{1 + \mathrm{SR}^2 T}.$$

The term $1/(\mathrm{SR}^2 T)$ is the relative variance of the estimated mean: a Sharpe of 0.4 measured with 5 years
has a t-statistic of $0.4\sqrt{5} = 0.89$, and the estimated Kelly can be anything between zero and twice the true
one. The cell checks it by simulation:

```python
rng = np.random.default_rng(11)
sr = 0.4                               # true annual Sharpe
filas = {}
for años in (3, 5, 10, 20):
    # estimated f / true f = estimated mean / true mean
    u = rng.normal(1, 1 / (sr * np.sqrt(años)), 200_000)
    for c in (0.25, 0.5, 0.75, 1.0):
        rel = 2 * c * u - (c * u) ** 2     # g / maximum g
        filas[(años, c)] = {"g_rel_media": rel.mean(),
                            "p_g_negativo": (rel < 0).mean()}
res = pd.DataFrame(filas).T
res.index.names = ["años", "fraccion"]
c_opt = {a: sr**2 * a / (1 + sr**2 * a) for a in (3, 5, 10, 20)}
print("Optimal fraction:", {a: round(v, 2) for a, v in c_opt.items()})
res.unstack("fraccion")["g_rel_media"].round(2)
```
```output
Optimal fraction: {3: 0.32, 5: 0.44, 10: 0.62, 20: 0.76}
fraccion  0.25  0.50  0.75  1.00
años
3         0.31  0.23 -0.23 -1.08
5         0.36  0.44  0.24 -0.24
10        0.40  0.59  0.59  0.38
20        0.42  0.67  0.76  0.69
```

*Fig. — Expected growth, as a % of what the true Kelly would give, when betting a fraction c of the Kelly estimated from 3, 5, 10 or 20 years of data on a Sharpe 0.4 strategy.* Four parabolas, one per sample length. With 20 years the maximum, 76%, is reached at c = 0.76; with 10 years, 62% at c = 0.62; with 5 years, 44% at c = 0.44; with 3 years, 32% at c = 0.32 (highlighted points). With 3 years of data, betting the full estimated Kelly gives negative expected growth.

With 20 years of data, the full estimated Kelly keeps 69% of the maximum growth and the best fraction is 0.76 (76%).
With 5 years, the full estimated Kelly has a negative expected growth (−24%) and the best fraction is 0.44. With 3
years, 0.32: the practical rule of “half Kelly” or “quarter Kelly” is not gratuitous caution, it is what the error in
the mean demands. And the formula is still optimistic for three reasons:

- The Sharpe of a backtest is usually inflated by selection among many variants (lesson 2.5.4): the mean from which
  $\hat f$ comes is not a random sample but the best of several.
- Fat tails and jumps (volatility selling) push the exact Kelly below $\mu/\sigma^2$.
- Portfolio correlations rise in crises (lesson 4.2.4): the volatility of the portfolio of the nine is not that of
  2006-2025 when it matters most.

## Volatility targeting: constant risk

**Volatility targeting** does not decide how much risk to take, but keeps it constant: it sets a target volatility
$\sigma^*$ and adjusts the exposure each day with the forecast volatility, computed only with data up to yesterday's
close:

$$w_t = \min\!\left(\frac{\sigma^*}{\hat\sigma_{t-1}},\; w_{\max}\right), \qquad
\hat\sigma^2_{t-1} = \text{EWMA of } r^2 \text{ up to } t-1.$$

```mermaid
flowchart LR
  R[Returns] --> V[EWMA vol. yesterday]
  V --> E[Target / vol.]
  E --> T[Cap and band]
  T --> O[Order at the close]
  O --> R
```

Applied to `RV_EEUU` (the universe's US equities, which mimic the total-return S&P 500) with a 15% target, a 20-session
half-life and a cap of 2 times:

```python
precios = pd.read_parquet("universo4_precios.parquet")
precios = precios.set_index("fecha")
fac = pd.read_parquet("universo4_factores_diarios.parquet")
fac = fac.set_index("fecha")
rend = precios / precios.shift(1).fillna(100) - 1  # base 100
exceso = rend.sub(fac["rf"], axis=0)


def vol_target(x, objetivo, vida_media=20, tope=2.0):
    """Exposure = target / YESTERDAY's EWMA vol., with a cap."""
    sig = np.sqrt((x**2).ewm(halflife=vida_media,
                             min_periods=60).mean() * 252)
    expo = (objetivo / sig.shift(1)).clip(upper=tope)
    return (expo * x).dropna(), expo


def metricas(r):
    w = (1 + r).cumprod()
    vol_año = r.groupby(r.index.year).std() * 252**0.5
    return {"exceso": r.mean() * 252, "vol": r.std() * 252**0.5,
            "sharpe": r.mean() / r.std() * 252**0.5,
            "max_caida": (w / w.cummax() - 1).min(),
            "peor_dia": r.min(),
            "vol_año_min": vol_año.min(),
            "vol_año_max": vol_año.max()}


rv = exceso["RV_EEUU"]
vt, expo = vol_target(rv, 0.15)
print(f"Mean exposure {expo.mean():.2f}, max {expo.max():.2f};"
      f" turnover {expo.diff().abs().sum() / (len(expo) / 252):.1f}"
      " times per year")
pd.DataFrame({"buy and hold": metricas(rv.loc[vt.index]),
              "vol. target 15%": metricas(vt)}).round(3)
```
```output
Mean exposure 0.95, max 1.84; turnover 4.4 times per year
             buy and hold  vol. target 15%
exceso              0.088            0.132
vol                 0.207            0.157
sharpe              0.427            0.837
max_caida          -0.609           -0.346
peor_dia           -0.115           -0.090
vol_año_min         0.110            0.125
vol_año_max         0.318            0.195
```

*Fig. — The universe's US equities (RV_EEUU) with and without a 15% volatility target: 63-session realised volatility and the control's exposure, 2006-2025.* Top, 63-session realised volatility at month end: the index's ranges from 8 to 55% (2020); the targeted version's (highlighted) stays mostly between 10 and 25%, with spikes at the start of each crisis, before exposure has come down. Bottom, month-end exposure: between 0.28 and 1.68 times (cap: 2), below 1 in crises.

The control does what it promises with risk: realized annual volatility moves between 12.5 and 19.5% instead of
between 11 and 31.8%, and the maximum drawdown drops from −60.9 to −34.6%. The figure shows why it is not exact: the
control's 63-session volatility jumps at the start of each crisis, when exposure is still that of the calm, and goes
back to 15% as soon as the EWMA catches up. Mean exposure is 0.95 and the maximum 1.84; turnover, 4.4 times the
capital per year. And the Sharpe goes from 0.43 to 0.84. That last figure calls for an explanation before believing
it.

## Why it improves the Sharpe, and when it does not

Reducing exposure when volatility rises improves the Sharpe only if the expected return **does not rise in
proportion** to volatility. If the mean were proportional to volatility (constant Sharpe), the control would change
nothing; if it were proportional to variance, it would make it worse. This is what Moreira and Muir (2017) say:
volatility-managed portfolios raise the Sharpe “because changes in volatility are not offset by proportional changes
in expected returns”. In the data it can be checked directly, grouping days by terciles of forecast volatility:

```python
pyg = pd.read_csv("riesgo4_pyg.csv", index_col="fecha",
                  parse_dates=True)["rendimiento"]


def por_tercil(x):
    sig = np.sqrt((x**2).ewm(halflife=20).mean()).shift(1)
    t = pd.qcut(sig, 3, labels=["low", "medium", "high"])
    t.name = "vol_prevista"
    grupos = x.groupby(t, observed=True)
    return pd.DataFrame({"media_anual": grupos.mean() * 252,
                         "vol_anual": grupos.std() * 252**0.5,
                         "sharpe": grupos.mean() / grupos.std()
                         * 252**0.5})


vv = est["venta_volatilidad"]
pd.concat({"GARCH book": por_tercil(pyg),
           "RV_EEUU": por_tercil(rv),
           "trend": por_tercil(est["tendencia_futuros"])}
          ).round(2)
```
```output
                         media_anual  vol_anual  sharpe
           vol_prevista
GARCH book low                  0.10       0.08    1.24
           medium               0.05       0.11    0.49
           high                 0.17       0.25    0.67
RV_EEUU    low                  0.14       0.13    1.05
           medium               0.26       0.16    1.60
           high                -0.13       0.29   -0.44
trend      low                  0.06       0.08    0.76
           medium               0.00       0.10    0.01
           high                 0.11       0.13    0.83
```

- **GARCH book** (`riesgo4_pyg.csv`, lesson 4.2.3): the true mean is constant and volatility varies. The measured
  Sharpe falls from 1.24 in the calm tercile to 0.67 in the high-volatility one (the means, 0.10 and 0.17, are noise
  around 7.6% per year). It is the textbook case in which the control helps.
- **`RV_EEUU`**: in the high-volatility tercile the mean is **−13%** per year. It is not a law of the market, it is
  the generator: the scripted crises have a negative drift and the calm a premium of around 20% per year, calibrated
  so that the 20-year mean is reasonable. The control wins mainly because it avoids the stretches that the generator
  made losers.
- **`tendencia_futuros`**: the high-volatility tercile has the best Sharpe (0.83), because the strategy wins in
  crises by construction. There the control cuts back precisely when it wins.

With the same control on several series (target equal to each one's volatility, cap of 3 times):

```python
series = {"GARCH book": pyg, "RV_EEUU": rv,
          "RF_T710": exceso["RF_T710"], "MP_ORO": exceso["MP_ORO"],
          "trend": est["tendencia_futuros"],
          "venta_vol": vv,
          "portfolio 9": cartera}
filas, fechas = {}, {}
for nombre, x in series.items():
    y, _ = vol_target(x, x.std() * 252**0.5, tope=3.0)
    a, d = metricas(x.loc[y.index]), metricas(y)
    filas[nombre] = {"sharpe_antes": a["sharpe"],
                     "sharpe_vt": d["sharpe"],
                     "peor_dia_antes": a["peor_dia"],
                     "peor_dia_vt": d["peor_dia"]}
    fechas[nombre] = str(y.idxmin().date())
tabla_vt = pd.DataFrame(filas).T.round(3)
tabla_vt["fecha_peor_vt"] = pd.Series(fechas)
tabla_vt
```
```output
             sharpe_antes  sharpe_vt  peor_dia_antes  peor_dia_vt fecha_peor_vt
GARCH book          0.626      0.842          -0.087       -0.066    2010-06-18
RV_EEUU             0.427      0.837          -0.115       -0.123    2018-10-03
RF_T710             0.117      0.134          -0.025       -0.026    2015-10-20
MP_ORO              0.711      0.701          -0.079       -0.045    2008-12-22
trend               0.472      0.536          -0.049       -0.039    2018-09-28
venta_vol           0.303      0.537          -0.193       -0.279    2018-02-05
portfolio 9         0.855      0.988          -0.023       -0.025    2018-02-05
```

In the GARCH book the Sharpe rises from 0.63 to 0.84 and the worst day improves. In bonds (`RF_T710`) and gold, the
control barely changes the Sharpe (0.12 to 0.13; 0.71 to 0.70). It is the pattern described by Harvey et al. (2018)
with more than 60 assets and daily data since 1926: the Sharpe improvement is limited to risk assets (equities and
credit) and is negligible in fixed income, currencies and commodities; the effect that is general is that it
**reduces the probability of extreme returns**. And Cederburg et al. (2020), with 103 equity strategies, find that the
volatility-managed versions do not systematically beat the originals and that, with versions implementable in real
time, they tend to have a lower Sharpe. In the course data, which have the relationship planted, the control looks
better than what you should expect in a real market.

<aside class="callout" data-kind="riesgo">

The average improvement is not the figure that matters for sizing. The `peor_dia_vt` column of the table says that
the control **worsens** the worst day in `RV_EEUU` (−11.5% to −12.3%), in volatility selling (−19.3% to −27.9%) and
in the portfolio of the nine. A volatility control raises exposure in the calm, and jumps arrive in the calm.

</aside>

## The worst episode and what it costs

The control's two worst days, with forecast volatility and the previous day's exposure:

```python
for nombre, x, ini, fin in [("venta_vol", vv, "2018-02-01",
                             "2018-02-07"),
                            ("RV_EEUU", rv, "2018-09-28",
                             "2018-10-05")]:
    _, e = vol_target(x, x.std() * 252**0.5, tope=3.0)
    sig = np.sqrt((x**2).ewm(halflife=20, min_periods=60).mean())
    t = pd.DataFrame({"rend": x, "sigmas": x / sig.shift(1),
                      "exposicion": e, "resultado": e * x})
    print(nombre)
    print(t.loc[ini:fin].round(3).to_string(), end="\n\n")
```
```output
venta_vol
             rend  sigmas  exposicion  resultado
fecha
2018-02-01 -0.004  -0.504       1.410     -0.005
2018-02-02 -0.004  -0.542       1.428     -0.006
2018-02-05 -0.193 -27.025       1.445     -0.279
2018-02-06 -0.002  -0.059       0.284     -0.001
2018-02-07  0.007   0.197       0.289      0.002

RV_EEUU
             rend  sigmas  exposicion  resultado
fecha
2018-09-28  0.006   0.498       1.107      0.006
2018-10-01 -0.024  -2.115       1.122     -0.027
2018-10-02  0.005   0.415       1.061      0.005
2018-10-03 -0.115  -9.505       1.076     -0.123
2018-10-04 -0.038  -1.572       0.535     -0.020
2018-10-05 -0.037  -1.476       0.522     -0.019
```

On February 5, 2018, volatility selling came from quiet months: exposure was 1.45 times. It loses 19.3%, **−27
standard deviations** of what the EWMA forecast, and the control turns it into −27.9%. The next day exposure drops to
0.28, with the loss already taken. On October 3, 2018, `RV_EEUU` falls 11.5% (−9.5 forecast standard deviations) with
an exposure of 1.08: −12.3%. In both cases the control reacts afterwards, which is all it can do with closing data.
Its protection works in crises that arrive slowly (2008, 2022) and not in jumps: for those there are the scenario
limits of lesson 4.2.4, the exposure cap and the kill switch of 4.2.5.

The control is not free either. Each exposure change is traded, and whatever goes above 1 has to be financed:

```python
liq = pd.read_csv("riesgo4_liquidez.csv", index_col="activo")
media_horq = liq.at["RV_EEUU", "horquilla_pb"] / 2 / 1e4
diferencial = 0.01        # assumption: rf + 1% on the borrowed amount


def con_costes(x, objetivo, banda=0.0):
    _, e = vol_target(x, objetivo)
    e = e.dropna()
    real = e.copy()
    for i in range(1, len(e)):                 # no-trade band
        prev = real.iloc[i - 1]
        if abs(e.iloc[i] / prev - 1) <= banda:
            real.iloc[i] = prev
    r = (real * x.loc[real.index]
         - real.diff().abs().fillna(0) * media_horq
         - (real - 1).clip(lower=0) * diferencial / 252)
    rot = real.diff().abs().sum() / (len(real) / 252)
    return {"sharpe": r.mean() / r.std() * 252**0.5,
            "rotacion_anual": rot,
            "coste_pb_año": (r.mean() - (real * x).mean())
            * -252 * 1e4}


pd.DataFrame({f"band {b:.0%}": con_costes(rv, 0.15, b)
              for b in (0.0, 0.10, 0.25)}).T.round(3)
```
```output
          sharpe  rotacion_anual  coste_pb_año
band 0%    0.829           4.440        11.989
band 10%   0.824           1.901         9.981
band 25%   0.746           1.200        10.160
```

With the half spread of the `RV_EEUU` ETF (0.435 bp, `riesgo4_liquidez.csv`) and an assumed financing spread of 1% on
the borrowed amount, the cost is about 12 bp per year and the Sharpe goes from 0.837 to 0.829. A 10% no-trade band
reduces turnover from 4.4 to 1.9 times per year almost at no cost in Sharpe (0.824); a 25% band tracks the target
worse and the Sharpe drops to 0.746. In a liquid asset the cost is small; in one with a 20 bp bid-ask spread, the
same turnover would cost about 44 bp per year in spread alone (lesson 2.4.7).

## The method from the site's research

The research “Volatility Targeting in Python” (link under “Resources”) proposes another rule on a portfolio of stocks
and long bonds: for each asset, $P = \hat\sigma_{100}/\sigma_{\text{hist}}$, the ratio between the 100-session
volatility and that of the whole series, and a weight $\max(1 - P, 0)$: full exposure when volatility is at its
minimum and zero when it exceeds the historical mean. It is a more aggressive volatility control than
$\sigma^*/\hat\sigma$, which exits the market instead of scaling down. Replicated with `RV_EEUU` and `RF_T20` (total
returns, as in the research):

```python
def metodo_1_menos_p(s, sin_anticipacion):
    """Weight = max(1 − 100-session vol / reference vol, 0)."""
    if sin_anticipacion:
        v = s.rolling(100).std().shift(1)
        ref = s.expanding(252).std().shift(1)
    else:                                   # as in the research
        v = s.rolling(100).std().shift(-2)
        ref = s.std()
    return (1 - v / ref).clip(lower=0) * s


def sharpe(r):
    r = r.dropna()
    return round(r.mean() / r.std() * 252**0.5, 3)


acc, bono = rend["RV_EEUU"], rend["RF_T20"]
print("fixed 60/40:      ", sharpe(0.6 * acc + 0.4 * bono))
print("1 − P, shift(-2): ", sharpe(metodo_1_menos_p(acc, False)
                                   + metodo_1_menos_p(bono, False)))
print("1 − P, shift(1):  ", sharpe(metodo_1_menos_p(acc, True)
                                   + metodo_1_menos_p(bono, True)))
```
```output
fixed 60/40:       0.619
1 − P, shift(-2):  1.18
1 − P, shift(1):   0.938
```

In the research's code the rolling volatility is shifted with `.shift(-2)`. In pandas a **negative** shift brings in
values from the future: row $t$ receives the volatility of the window ending at $t+2$, which includes the returns of
$t$, $t+1$ and $t+2$ that the rule is meant to weight. The reference, the volatility of the whole series, also uses
later data. With those two look-aheads the Sharpe is 1.18; computing volatility up to yesterday (`shift(1)`) and the
reference only with the past, 0.94. Both versions beat the fixed 60/40 (0.62) on these data, because of the planted
relationship between volatility and return seen above, but the difference between them, 0.24 of Sharpe, is
look-ahead and could not be collected. It is the same error that lesson 2.5.1 looks for in any backtest: the rule
must be computed with what was known when deciding.

## A complete sizing rule

Kelly and the volatility control combine: Kelly gives the volatility that can be justified (a fraction of the Sharpe
that is truly expected, not the backtest one) and the control maintains it. The limits from the neighboring lessons
cap it from above:

```mermaid
flowchart TB
  M[Expected Sharpe] --> F[Fraction c]
  F --> S[Target vol.]
  S --> V[Vol. control]
  V --> X[Cap and stress]
  X --> K[Kill switch]
  K --> P[Position]
```

| Step | What it decides | Cost or worst case in this lesson |
|---|---|---|
| Expected Sharpe | The backtest one, discounted for selection (2.5.4) and regime | A mean estimated with 5 years can be worth zero |
| Fraction $c$ | $\mathrm{SR}^2T/(1+\mathrm{SR}^2T)$ at most; less with tails | Full Kelly: a −97.9% drawdown in the portfolio of the nine |
| Target volatility | $c \cdot \mathrm{SR}$ | With a Sharpe of 0.8 and $c = 1/4$, 20%: already high for a personal account |
| Volatility control | Exposure = target / yesterday's volatility | Jumps after the calm: −27.9% in one day in volatility selling |
| Cap and stress | Maximum exposure and maximum loss per scenario (4.2.4) | The cap limits the exposure with which the jump arrives |
| Kill switch | Cut off if the strategy stops working (4.2.5) | A Kelly estimated with old data leverages a dead strategy |

In practice, the target volatility of a personal account is usually set well below Kelly: the one you can stand
without changing plans in the worst reasonable drawdown, which is a psychological and financial number before a
statistical one. Kelly serves above all as a **ceiling**: above it there is nothing to gain.

## Exercises

1. **Kelly on a strategy that degrades.** Estimate the Kelly of `degradada` with data up to 2019-06-30 and apply half
   Kelly, with that figure fixed, from 2019-07-01 onwards. Compute the CAGR and the maximum drawdown of both periods.
   What does this say about the order of the steps in the previous table?

**Solution.**

```python
deg = pd.read_parquet("estrategias4_diarias.parquet")
deg = deg.set_index("fecha")["degradada"]
antes, despues = deg[:"2019-06-30"], deg["2019-07-01":]
k_deg = antes.mean() / antes.var()
print(f"Kelly estimated up to 2019-06: {k_deg:.1f}")
pd.DataFrame({"before": resultado(0.5 * k_deg * antes),
              "after": resultado(0.5 * k_deg * despues)}
             ).T.astype(float).round(3)
```
```output
Kelly estimated up to 2019-06: 16.8
         cagr  max_caida  peor_dia  ruina
before  0.873     -0.830    -0.339    0.0
after  -0.440     -0.997    -0.520    0.0
```

With thirteen and a half years of a strategy with a Sharpe of 1.30, the estimated Kelly is 16.8 times and half Kelly
(8.4 times) gives a CAGR of 87.3% before the cutoff, already with a drawdown of −83%. Afterwards, the same exposure on
a strategy without an edge loses 44% per year, with a drawdown of −99.7% and a −52% day: the volatility stays and the
mean disappears. Kelly multiplies the edge it is given; if the edge no longer exists, it multiplies the noise. That is
why the kill switch (lesson 4.2.5) comes before any size optimization and the estimate of the mean should be reviewed
with recent data.

2. **The EWMA half-life.** Apply the volatility control to `venta_volatilidad` with half-lives of 5, 20 and 60
   sessions (target equal to its volatility, cap of 3). Compare the Sharpe, the maximum drawdown and the worst day. Is
   there a half-life that avoids February 5, 2018?

**Solution.**

```python
obj = vv.std() * 252**0.5
pd.DataFrame({vm: metricas(vol_target(vv, obj, vm, 3.0)[0])
              for vm in (5, 20, 60)}).T[
    ["sharpe", "max_caida", "peor_dia"]].round(3)
```
```output
    sharpe  max_caida  peor_dia
5    0.550     -0.509    -0.470
20   0.537     -0.404    -0.279
60   0.445     -0.382    -0.249
```

No half-life avoids the jump: the worst day is always February 5, 2018. With 5 sessions it is much worse (−47.0%):
the short half-life forgets the last crisis sooner, so after a few weeks of calm the exposure is the highest. With 60
sessions the worst day is −24.9% and the maximum drawdown the smallest (−38.2%), in exchange for a lower Sharpe
(0.445 versus 0.537), because it reacts late and stays with low exposure for longer after each crisis. A one-day jump
can only be controlled with the size you hold before it happens: exposure cap and scenario limits.

3. **A short, brilliant backtest.** A system has a Sharpe of 1.5 in a 2-year backtest and its author wants to trade
   half Kelly. What fraction of the estimated Kelly does the estimation error formula recommend? What else would you
   discount and why?

**Solution.**

With $\mathrm{SR} = 1.5$ and $T = 2$, $\mathrm{SR}^2T = 4.5$ and $c^{\text{opt}} = 4.5/5.5 \approx 0.82$: the
formula, taken literally, would allow more than half Kelly. But it assumes that the Sharpe of 1.5 is the truth,
measured without bias. A 2-year backtest chosen among several variants has an inflated Sharpe (lesson 2.5.4). If the
true Sharpe were 0.5, the Kelly estimated with 1.5 would be three times the true one, and half the estimated Kelly
would be 1.5 times the true Kelly: above the growth maximum, with 75% of the possible growth and a 79% probability of
ever falling to half (table of fractions). In addition, two years contain no crisis, so volatility is underestimated
and the correlations are those of the calm. The reasonable thing is to set the target volatility with a prudent
expected Sharpe (not the backtest one), start with paper trading (lesson 4.5.3) and increase the size only when the
strategy confirms live.

## Self-assessment

1. A strategy has an expected excess return of 6% per year and a volatility of 12%. What leverage does Kelly give and what volatility would the account have?

   - A. 4.2 times the capital and 50% volatility, which is the Sharpe ratio (0.5)
   - B. 0.5 times the capital and 6% volatility
   - C. 1 times the capital: Kelly never uses leverage
   - D. 12 times the capital

   *Answer A: 4.2 times the capital and 50% volatility, which is the Sharpe ratio (0.5).* f* = μ/σ² = 0.06/0.0144 ≈ 4.2. The account volatility is f*·σ = μ/σ = Sharpe = 50%. With fat tails and an estimated mean, nobody trades that: hence the fractions.

2. With half Kelly, what share of the maximum growth is kept and what is the probability of ever falling to half the initial capital (continuous case, known parameters)?

   - A. 50% of the growth and a 50% probability
   - B. 75% of the growth and a 12.5% probability, versus 50% for full Kelly
   - C. 100% of the growth and no probability
   - D. 25% of the growth and a 1% probability

   *Answer B: 75% of the growth and a 12.5% probability, versus 50% for full Kelly.* Relative growth is 2c − c² = 0.75 and the probability of touching x is x raised to 2/c − 1: 0.5³ = 0.125. The coin simulation gives 0.116 with a thousand bets.

3. A strategy with a Sharpe of 0.4 has been measured with 5 years of data. If you bet the full estimated Kelly, what growth do you expect relative to that of the true Kelly?

   - A. 100%: the estimate is unbiased
   - B. 75%
   - C. 50%
   - D. Negative (−24%): the error in the mean makes the estimated Kelly often exceed twice the true one

   *Answer D: Negative (−24%): the error in the mean makes the estimated Kelly often exceed twice the true one.* With 5 years, estimated f / true f has a standard deviation of 1/(0.4·√5) ≈ 1.1. Expected growth is 2c − c²(1 + 1/(SR²T)) = 2 − 2.25 < 0. The fraction that maximizes it is 0.44.

4. In the data, volatility targeting raises the Sharpe of `RV_EEUU` from 0.43 to 0.84. Why should you not expect the same in a real market?

   - A. Because the data have errors
   - B. Because the control uses future information
   - C. Because in the generator the high-volatility stretches have a negative drift and calm periods a high premium (≈ 20% per year): the control wins mainly because of that planted relationship; in the literature the improvement is small or does not survive out of sample
   - D. Because real equities do not have time-varying volatility

   *Answer C: Because in the generator the high-volatility stretches have a negative drift and calm periods a high premium (≈ 20% per year): the control wins mainly because of that planted relationship; in the literature the improvement is small or does not survive out of sample.* The control improves the Sharpe when the expected return does not rise in proportion to volatility. In `RV_EEUU` the high-volatility tercile returns −13% per year by construction. Harvey et al. (2018) find an improvement in risk assets and almost none in bonds, currencies and commodities; Cederburg et al. (2020), that out of sample it does not systematically beat the unmanaged portfolio.

5. On February 5, 2018, volatility selling loses 19.3% without the control and 27.9% with volatility targeting. What happened?

   - A. A data error
   - B. The control sold at the worst moment
   - C. Forecast volatility was low after months of calm, so exposure was 1.45 times; a jump of −27 forecast standard deviations leaves no time to reduce it
   - D. The exposure cap did not work

   *Answer C: Forecast volatility was low after months of calm, so exposure was 1.45 times; a jump of −27 forecast standard deviations leaves no time to reduce it.* The control reacts to past volatility. A jump without warning arrives with exposure at its maximum and the control amplifies it. The next day exposure drops to 0.28, when the loss has already been taken.

## Key points

- Kelly maximizes long-term growth with `f* = μ/σ²`; at Kelly, account volatility is the Sharpe ratio. For the portfolio of the nine strategies that is 18.8 times the capital and 84% volatility.
- Betting above Kelly is a bad deal: at 2 times Kelly growth is zero with all the risk. Below it, little growth and a lot of drawdown are given up: half Kelly keeps 75% of the growth with half the volatility.
- The mean is poorly estimated: with a sample of T years, the fraction that maximizes expected growth is SR²T/(1 + SR²T) of the estimated Kelly (0.44 with 5 years and a Sharpe of 0.4), and less if the backtest Sharpe is inflated.
- Volatility targeting keeps risk stable and reduces the maximum drawdown, but it only raises the Sharpe if return does not grow with volatility; in the data that relationship is planted and in the literature it is fragile.
- Its worst episode is the jump after the calm: high exposure and a larger loss than without the control (−27.9% versus −19.3% in volatility selling).
- A sizing rule is computed with what was known each day: a `shift(-2)` in volatility raises the Sharpe of the 1 − P method from 0.94 to 1.18 without the rule being better.

## Resources

### Research

- [Volatility Targeting in Python](https://pythonparatrading.com/en/volatility-targeting-2/) · Research-0004 · 7 min

### Books

- [Kelly, J. L. (1956): A New Interpretation of Information Rate. Bell System Technical Journal 35(4), 917-926](https://doi.org/10.1002/j.1538-7305.1956.tb03809.x) · doi.org
- [Thorp, E. O. (2006): The Kelly Criterion in Blackjack, Sports Betting, and the Stock Market. Handbook of Asset and Liability Management, ch. 9](https://doi.org/10.1016/S1872-0978(06)01009-X) · doi.org
- [Moreira, A. and Muir, T. (2017): Volatility-Managed Portfolios. The Journal of Finance 72(4), 1611-1644](https://doi.org/10.1111/jofi.12513) · doi.org
- [Harvey, C. R. et al. (2018): The Impact of Volatility Targeting. The Journal of Portfolio Management 45(1), 14-33](https://doi.org/10.3905/jpm.2018.45.1.014) · doi.org
- [Cederburg, S. et al. (2020): On the performance of volatility-managed portfolios. Journal of Financial Economics 138(1), 95-117](https://doi.org/10.1016/j.jfineco.2020.04.015) · doi.org

### Data

- [Level 4 data: daily returns of ten strategies (Parquet)](https://pythonparatrading.com/curso/datos/nivel-4/estrategias4_diarias.parquet) · Download · estrategias4_diarias.parquet

Previous lesson: [4.2.1 · Risk per trade, stops and risk of ruin](https://pythonparatrading.com/en/python-trading-course/level-4/risk-per-trade-stops-and-risk-of-ruin/)

Next lesson: [4.2.3 · VaR, Expected Shortfall and VaR backtesting](https://pythonparatrading.com/en/python-trading-course/level-4/var-expected-shortfall-and-var-backtesting/)
