---
title: "2.6.1 Return and risk metrics"
description: "CAGR, volatility, Sharpe with its standard error, Sortino, Calmar, Omega, drawdown, duration and Ulcer Index, from scratch and against empyrical-reloaded."
url: "https://pythonparatrading.com/en/python-trading-course/level-2/return-and-risk-metrics/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-2/return-and-risk-metrics.png"
---

# 2.6.1 · CAGR, Sharpe, Sortino, Calmar, Omega, drawdown and Ulcer Index

> An equity curve is summarized in a few figures, and each one hides a convention: what counts as a year, which risk-free rate is subtracted, which part of the distribution is looked at. This lesson implements them from scratch on three strategies with very different return shapes and checks that they match empyrical-reloaded.

- Course: [Level 2: Systematic strategies and rigorous backtesting](https://pythonparatrading.com/en/python-trading-course/level-2/)
- Module: 2.6 · Performance metrics
- Duration: 65 min
- Prerequisites: [0.1.2 · Realistic expectations: costs, time and risk of loss](https://pythonparatrading.com/en/python-trading-course/level-0/realistic-trading-expectations-costs-risk/), [0.7.1 · Simple and log returns, compound interest and descriptive statistics](https://pythonparatrading.com/en/python-trading-course/level-0/returns-and-descriptive-statistics/), [1.6.1 · Returns, volatility, rolling correlations and drawdown](https://pythonparatrading.com/en/python-trading-course/level-1/returns-volatility-correlation-and-drawdown/)

## By the end of this lesson

- Compute CAGR, volatility, Sharpe, Sortino, Calmar and Omega from daily returns, with the right annualization convention for each market (252 sessions, 261 business days or calendar dates).
- Subtract the risk-free rate when the series is a total return and not subtract it when it is already an excess return, as in futures or funded currency positions.
- Estimate the standard error of a Sharpe with Lo's formula (2002), its version with skewness and kurtosis, and the correction for autocorrelation when annualizing.
- Measure drawdown in depth and in duration and summarize it with the Ulcer Index and the Martin ratio.
- Explain why a carry with negative skewness and an index can have a similar Sharpe and very different risks.

## Three ways of making money

A backtest returns a series of returns, and almost everything that is said about a strategy comes from summarizing it
in a few figures. Each figure looks at a different aspect of the series, and to see what each one looks at it pays to
measure them on strategies that make money in different ways. This lesson builds three on the level 2 data:

- **IDA index.** Long the continuous future of a fictitious equity index, bought and held. It is the benchmark: it
  earns the equity premium and suffers all its crises.
- **TSMOM on futures.** Time-series momentum on the 24 markets of `futuros2_continuos.parquet`: long if the market has
  risen over the last 252 sessions, short if it has fallen, with each position sized for 40% annual volatility divided
  by 24, as in Moskowitz, Ooi and Pedersen (2012)[^1]. Its typical shape: many small losses when there is no trend and
  large gains when there is one.
- **Currency carry.** At each month-end, long the three currencies with the highest interest rate and short the three
  with the lowest in `fx2_carry.parquet`. It collects the rate differential almost every day and gives it back all at
  once in the crashes.

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

The data are synthetic and both effects are planted with known parameters (README of the level 2 data): the expected
return of each future carries a momentum term with a Sharpe of about 0.15 per market (the indices, in addition, the
equity premium with the crises of the script), and each currency earns on average 70% of its rate differential and
loses it in five crashes on fixed dates. That these strategies make money here says nothing about real markets. For
real evidence, the literature: Moskowitz, Ooi and Pedersen (2012) for time-series momentum and Brunnermeier, Nagel and
Pedersen (2008)[^2] for the crash risk of carry.

</aside>

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

# 1) Futures: long IDA and time-series momentum (TSMOM)
fut = pd.read_parquet("futuros2_continuos.parquet")
px = fut.pivot(index="fecha", columns="mercado",
               values="cierre_ajustado")
r_fut = px.pct_change()
vol = r_fut.ewm(span=60, min_periods=60).std() * np.sqrt(252)
senal = np.sign(px / px.shift(252) - 1)
peso = (senal * 0.40 / px.shape[1] / vol).shift(1)
tsmom = (peso * r_fut).sum(axis=1, min_count=1)

# 2) Currency carry: long the 3 with the highest rate, short 3
fx = pd.read_parquet("fx2_carry.parquet")
tabla = lambda c: fx.pivot(index="fecha", columns="divisa", values=c)
spot, dif = tabla("spot"), tabla("tipo_divisa") - tabla("tipo_usd")
dias = spot.index.to_series().diff().dt.days
interes = dif.shift(1) / 100 * dias.to_numpy()[:, None] / 360
r_fx = spot.pct_change() + interes    # spot + accrued differential
fin_mes = dif.index.to_series().groupby(dif.index.to_period("M")).max()
rango = dif.loc[fin_mes].rank(axis=1)
w_fx = ((rango >= 8).astype(float) - (rango <= 3).astype(float)) / 3
w_fx = w_fx.reindex(dif.index).ffill().shift(1)
carry = (w_fx * r_fx).sum(axis=1, min_count=1)

inicio = "2005-02-01"
rets = {"indice": r_fut["IDA"].loc[inicio:],
        "tsmom": tsmom.loc[inicio:],
        "carry": carry.loc[inicio:]}

def describe(r):
    anos = (r.index[-1] - r.index[0]).days / 365.25
    m = (1 + r).resample("ME").prod() - 1
    return {"sesiones": len(r), "por_ano": len(r) / anos,
            "asim_diaria": r.skew(), "asim_mensual": m.skew(),
            "curtosis_mensual": m.kurt()}

(pd.DataFrame({k: describe(r) for k, r in rets.items()}).T
 .astype({"sesiones": int}).round(2))
```
```output
        sesiones  por_ano  asim_diaria  asim_mensual  curtosis_mensual
indice      5263   251.68         0.12         -2.15             15.90
tsmom       5263   251.68         0.02          0.16             -0.07
carry       5457   260.95        -0.71         -1.38              7.39
```

All three start on February 1, 2005, the first day with carry weights: comparing metrics from different periods is
the first mistake of any strategy table, because each period brings its own crises. They are stored in a dictionary
and not in a DataFrame because they do not share a calendar: the futures trade on NYSE sessions (251.7 a year) and
the currencies on business days (261 a year, with no holidays). In a DataFrame, NYSE holidays would be gaps in the
carry.

The table already shows the difference in shape. The carry's monthly skewness is −1.38 and its excess kurtosis, 7.39:
a long left tail. The index's is even more extreme (−2.15 and 15.90), but because of a single month: March 2020, with
−48.3% (the February-March crisis of the script, which the index shares with the level's stocks). Without that month,
the index's stays at −0.46, the same as the carry's without its worst month (February 2009). TSMOM is the only one
with positive monthly skewness (0.16) and thin tails (excess kurtosis −0.07). The header figure shows it in the
cumulative capital: the index falls in 2008 and in 2020 and recovers in about three years each time; the carry rises
smoothly until 2008, falls sharply and takes a decade to recover.

```mermaid
flowchart TB
  R[Returns] --> A[Growth]
  R --> V[Dispersion]
  R --> T[Path]
  A --> CAGR[CAGR]
  V --> S[Sharpe, Sortino]
  V --> O[Omega]
  T --> D[DD, Calmar, Ulcer]
```

The metrics in this lesson fall into three families. **Growth** metrics say how much the capital has grown.
**Dispersion** metrics compare the mean with the variability of returns and do not depend on their order: shuffling
the days does not change a Sharpe. **Path** metrics depend on the order: the same set of days, ordered with the losses
together, gives a much worse drawdown. To see the risk of a strategy you need all three.

## Return: the CAGR and what a year is

The compound annual growth rate is the constant growth that takes the initial capital to the final one:

$$
\text{CAGR} = \left(\frac{V_T}{V_0}\right)^{365.25 / D} - 1
$$

with $D$ the calendar days between the first and the last date. Lesson 0.7.1 already computed it this way. Libraries
do it differently: they count years as sessions divided by 252.

```python
def cagr_fechas(r):
    anos = (r.index[-1] - r.index[0]).days / 365.25
    return (1 + r).prod() ** (1 / anos) - 1

def cagr_sesiones(r, n=252):
    return (1 + r).prod() ** (n / len(r)) - 1

pd.DataFrame({k: {"por_fechas": cagr_fechas(r),
                  "por_sesiones_252": cagr_sesiones(r),
                  "media_x252": r.mean() * 252}
              for k, r in rets.items()}).T.round(4)
```
```output
        por_fechas  por_sesiones_252  media_x252
indice      0.0971            0.0972      0.1134
tsmom       0.1239            0.1241      0.1244
carry       0.0389            0.0375      0.0409
```

In the futures both calculations match, because the NYSE has 251.7 sessions a year. In the carry they do not: 5,457
business days divided by 252 are 21.7 “years”, when the calendar only has 20.9, and the same growth spread over more
years gives a CAGR of 3.75% instead of 3.89%. Fourteen basis points a year, because of a convention. In crypto, which
trades 365 days, the error would be much larger.

The last column is the daily mean times 252, the arithmetic “annual return”. It is always higher than the CAGR, and
the difference grows with volatility: it is the $\sigma^2/2$ term from lesson 0.7.1. In the index, with 20%
volatility, the mean times 252 (11.34%) exceeds the CAGR (9.71%) by 1.6 points. When a report says “annual return”,
you have to ask which of the two.

```mermaid
flowchart TB
  Q{What is annualized?} -- growth --> F[Years by dates]
  Q -- mean or vol --> N{Market?}
  N -- US equities --> B[252 sessions]
  N -- currencies --> D[261 days]
  N -- crypto --> C[365 days]
```

## Volatility and the Sharpe ratio

Annual volatility is the daily standard deviation times the square root of the sessions in the year. The **Sharpe
ratio** divides the mean return in excess of the risk-free rate by its standard deviation[^3]:

$$
SR = \frac{\overline{r - r_f}}{\sigma(r - r_f)}\,\sqrt{n}
$$

with $n$ the observations per year. Both are annualized with $\sqrt{n}$ assuming independent returns; further below we
see what happens when they are not.

```python
def vol_anual(r, n=252):
    return r.std() * np.sqrt(n)

def sharpe(r, rf=0.0, n=252):
    exceso = r - rf
    return exceso.mean() / exceso.std() * np.sqrt(n)

def sesiones_por_ano(r):
    return len(r) / ((r.index[-1] - r.index[0]).days / 365.25)

pd.DataFrame({k: {"vol": vol_anual(r), "sharpe_252": sharpe(r),
                  "sharpe_real": sharpe(r, n=sesiones_por_ano(r))}
              for k, r in rets.items()}).T.round(3)
```
```output
          vol  sharpe_252  sharpe_real
indice  0.203       0.559        0.559
tsmom   0.121       1.026        1.025
carry   0.091       0.451        0.459
```

With 252 or with the actual sessions, the futures Sharpes barely change (from 1.026 to 1.025 in TSMOM). The carry's
goes from 0.451 to 0.459: annualizing by 252 a series with 261 days a year understates it by 1.8%
($\sqrt{261/252}$). It is small, but it is a systematic bias against currencies when they are compared with stocks in
the same table.

The risk-free rate needs more care. A future does not require paying out the notional: a margin is deposited and the
rest of the capital can sit in T-bills earning the risk-free rate. That is why the return of a future is already an
**excess** over the risk-free rate, and the same happens with a currency position funded in dollars, which earns the
rate differential. On those series, the Sharpe is computed without subtracting anything. A stock portfolio, on the
other hand, gives a **total** return, and each day's risk-free rate has to be subtracted. The typical mistake is
mixing the two:

```python
tl = pd.read_csv("tipo_libre2.csv", parse_dates=["fecha"],
                 index_col="fecha")["tipo_3m"]
rf = (tl / 100 / 252).reindex(rets["tsmom"].index).ffill()
total = rets["tsmom"] + rf      # the collateral earns the risk-free rate

print(f"Average risk-free rate: {rf.mean() * 252:.2%} annual")
print(f"Sharpe of total without subtracting rf: {sharpe(total):.3f}")
print(f"Sharpe of total subtracting rf:         {sharpe(total, rf):.3f}")
print(f"Sharpe of the excess (the series):      {sharpe(rets['tsmom']):.3f}")
```
```output
Average risk-free rate: 1.79% annual
Sharpe of total without subtracting rf: 1.173
Sharpe of total subtracting rf:         1.026
Sharpe of the excess (the series):      1.026
```

If the interest on the collateral is added to TSMOM (the synthetic risk-free rate of `tipo_libre2.csv`, 1.79% annual
on average) and the Sharpe is computed without subtracting it, you get 1.173 instead of 1.026. Subtracting each day's
rate takes you back exactly to the excess series. The error grows with the level of rates: with T-bills at 5%, a
strategy that does nothing has a huge “Sharpe”, because its volatility is almost zero.

## How far can you trust a Sharpe

A measured Sharpe is an estimate, and it has a standard error. Lo (2002)[^4] derives it for independent, normal
returns:

$$
SE(\widehat{SR}) \approx \sqrt{\frac{1 + \tfrac{1}{2}\,SR^2}{T}}
$$

with $SR$ and $T$ at the same frequency (for example, monthly Sharpe and number of months). Since the monthly Sharpe is
small, the numerator is almost 1 and the error of the annual Sharpe is around $1/\sqrt{A}$, with $A$ the years of
data: with 21 years, 0.22. Lesson 0.1.2 already used that approximation to compute how many years are needed to tell
a Sharpe apart from chance.

Two corrections matter here. The first, the shape of the distribution: with skewness $\gamma_3$ and kurtosis
$\gamma_4$ (not excess), the error is[^5]

$$
SE(\widehat{SR}) \approx \sqrt{\frac{1 - \gamma_3\,SR + \tfrac{\gamma_4 - 1}{4}\,SR^2}{T - 1}}
$$

and negative skewness increases it. The second, autocorrelation: multiplying a monthly Sharpe by $\sqrt{12}$ assumes
independent months. Lo (2002) gives the correct factor,

$$
SR_{\text{annual}} = \eta(q)\,SR_{\text{monthly}}, \qquad
\eta(q) = \frac{q}{\sqrt{q + 2\sum_{k=1}^{q-1}(q-k)\,\rho_k}}
$$

with $q = 12$ and $\rho_k$ the autocorrelation of monthly returns at lag $k$. With positive autocorrelation,
$\eta(12)$ is smaller than $\sqrt{12} \approx 3.464$ and the annual Sharpe goes down.

```python
from scipy import stats

def error_sharpe(r):
    m = (1 + r).resample("ME").prod() - 1
    T, sr = len(m), m.mean() / m.std()
    g3, g4 = stats.skew(m), stats.kurtosis(m, fisher=False)
    iid = np.sqrt((1 + sr**2 / 2) / T)
    no_normal = np.sqrt((1 - g3 * sr + (g4 - 1) / 4 * sr**2)
                        / (T - 1))
    rho = [m.autocorr(k) for k in range(1, 12)]
    eta = 12 / np.sqrt(12 + 2 * sum((12 - k) * rho[k - 1]
                                    for k in range(1, 12)))
    return {"meses": T, "sr_mensual_x_raiz12": sr * np.sqrt(12),
            "se_iid": iid * np.sqrt(12),
            "se_no_normal": no_normal * np.sqrt(12),
            "eta": eta, "sr_anual_lo": sr * eta}

pd.DataFrame({k: error_sharpe(r) for k, r in rets.items()}).T.round(3)
```
```output
        meses  sr_mensual_x_raiz12  se_iid  se_no_normal    eta  sr_anual_lo
indice  251.0                0.549   0.220         0.264  4.242        0.672
tsmom   251.0                1.033   0.223         0.219  3.717        1.109
carry   251.0                0.409   0.219         0.239  2.935        0.347
```

*Fig. — Annual Sharpe of the three strategies with a ± 2 standard error interval (skewness and kurtosis formula, 251 months); the thin mark is the Sharpe annualised with Lo's (2002) autocorrelation correction.* Three horizontal intervals. Futures TSMOM, highlighted: Sharpe 1.03 with an interval from 0.60 to 1.47, the only one far from zero; its autocorrelation-corrected Sharpe rises to 1.11. IDA index: 0.55, from 0.02 to 1.08, the widest because of the March 2020 fall; corrected for autocorrelation it rises to 0.67, because falls are followed by rebounds. FX carry: 0.41, from −0.07 to 0.89, the only one that includes zero; its autocorrelation-corrected Sharpe falls to 0.35.

With 251 months, the three standard errors are between 0.219 and 0.264. The shape of the distribution raises two of
them: the index's, from 0.220 to 0.264, because of March 2020 (skewness −2.15, kurtosis 15.9), and the carry's, from
0.219 to 0.239. Autocorrelation sets the three apart. The carry's $\eta$ is 2.935, well below 3.464: the carry's bad
months come in a row (the crashes of the script last one to four months), and its corrected annual Sharpe drops from
0.41 to 0.35. The index's is 4.242, above: its autocorrelations at lags 3 to 10 are negative, because each drop is
followed by a rebound, and its corrected annual Sharpe rises from 0.55 to 0.67. TSMOM's is 3.717 (it rises from 1.03
to 1.11). The same correction lowers or raises a Sharpe depending on the series.

The figure shows the ± 2 standard error intervals. Only TSMOM's stays far from zero (from 0.60 to 1.47). The index's
grazes it (from 0.02 to 1.08) and the carry's includes it (from −0.07 to 0.89). And that is with the carry having a
planted effect in the data: 21 years of a Sharpe of 0.4 are not enough to tell it apart from chance with this rule.
When, in addition, many variants have been tried before settling on one, the bar rises; that is the Deflated Sharpe
of lesson 2.5.4.

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

The monthly metrics in this section start from the same daily series, aggregated with `resample("ME")`: the
annualized monthly Sharpe (0.409 in the carry) is not the annualized daily one (0.451). Neither is “the” Sharpe:
always report the calculation frequency, and prefer the monthly one for standard errors, because the daily one carries
more autocorrelation and more kurtosis.

</aside>

## Sortino, Calmar and Omega

The Sharpe penalizes rises and falls alike. Sortino and Price (1994)[^6] propose dividing only by the **downside
deviation** with respect to a target $\tau$:

$$
\text{Sortino} = \frac{\overline{r - \tau}}{\sqrt{\tfrac{1}{N}\sum_{t} \min(r_t - \tau,\ 0)^2}}\;\sqrt{n}
$$

The sum runs over all periods ($N$), not only over the negative ones: days above the target count as zero. It is the
convention of empyrical and quantstats. If the distribution were symmetric, the downside deviation would be
$\sigma/\sqrt{2}$ and the Sortino, $\sqrt{2} \approx 1.41$ times the Sharpe. The ratio between the two measures the
shape:

```python
def sortino(r, n=252, objetivo=0.0):
    exceso = r - objetivo
    abajo = np.sqrt((exceso.clip(upper=0) ** 2).mean())
    return exceso.mean() / abajo * np.sqrt(n)

def mensual(r):
    return (1 + r).resample("ME").prod() - 1

pd.DataFrame({k: {"sharpe": sharpe(r), "sortino": sortino(r),
                  "cociente": sortino(r) / sharpe(r),
                  "cociente_mensual": sortino(mensual(r), 12)
                                      / sharpe(mensual(r), n=12)}
              for k, r in rets.items()}).T.round(3)
```
```output
        sharpe  sortino  cociente  cociente_mensual
indice   0.559    0.805     1.441             1.350
tsmom    1.026    1.527     1.489             1.901
carry    0.451    0.634     1.405             1.391
```

On daily data, the three ratios are between 1.41 and 1.49, close to $\sqrt{2}$: a day does not have time to show the
shape of the strategy. On monthly data it appears: TSMOM reaches 1.90, because its good months are larger than its bad
ones; the carry drops to 1.39 and the index to 1.35, because their worst months (the crashes and the crises) are
larger than their best. The Sortino captures skewness, but only if it is measured at a frequency at which skewness
exists.

The **Calmar** divides the CAGR by the maximum drawdown in absolute value. It is the figure an investor in a futures
fund looks at the most: how much it earns per point of the worst drop. The **Omega** of Keating and Shadwick
(2002)[^7] compares the whole mass of the distribution above a threshold $L$ with the mass below:

$$
\Omega(L) = \frac{\sum_t (r_t - L)^{+}}{\sum_t (L - r_t)^{+}}
$$

An Omega greater than 1 means that, in total, more is gained above the threshold than is lost below it.

```python
def omega(r, umbral=0.0):
    d = r - umbral
    return d.clip(lower=0).sum() / -d.clip(upper=0).sum()

u5 = 1.05 ** (1 / 252) - 1      # 5% annual, per session
pd.DataFrame({k: {"diaria_0": omega(r), "diaria_5%": omega(r, u5),
                  "mensual_0": omega(mensual(r))}
              for k, r in rets.items()}).T.round(3)
```
```output
        diaria_0  diaria_5%  mensual_0
indice     1.115      1.064      1.609
tsmom      1.183      1.108      2.153
carry      1.079      0.985      1.390
```

Omega depends a lot on the frequency and the threshold. On daily data, with a zero threshold, the three are between
1.08 and 1.18: each day the mean is tiny compared with the dispersion. With a threshold of 5% annual (converted to its
per-session equivalent, as empyrical does), the carry drops below 1: it does not beat that target. On monthly data,
TSMOM (2.15) pulls away from the index and the carry (1.61 and 1.39). An Omega without its threshold and its frequency
cannot be compared with another.

## Drawdown: depth, duration and Ulcer Index

Each day's drawdown is the drop in capital from its previous peak (lesson 1.6.1). There is a detail when computing it
from returns: the capital starts at 1 **before** the first day. If the first return is negative, the maximum of
`cumprod()` starts below 1 and that first day does not count as a drop; `clip(lower=1.0)` corrects it. quantstats adds
a dummy date with the initial value for the same purpose.

```python
def drawdown(r):
    capital = (1 + r).cumprod()
    pico = capital.cummax().clip(lower=1.0)   # we start at 1
    return capital / pico - 1

def episodios(r):
    dd = drawdown(r)
    bajo = dd < 0
    grupo = (~bajo).cumsum()[bajo]
    filas = []
    for _, e in dd[bajo].groupby(grupo):
        sig = dd.index.searchsorted(e.index[-1], side="right")
        filas.append({
            "inicio": e.index[0].date(), "minimo": e.idxmin().date(),
            "recupera": dd.index[sig].date() if sig < len(dd) else None,
            "profundidad": e.min(), "sesiones": len(e),
            "anos": ((dd.index[min(sig, len(dd) - 1)] - e.index[0])
                     .days / 365.25)})
    return pd.DataFrame(filas)

ep_carry = episodios(rets["carry"])
ep_carry.sort_values("profundidad").head(3).round(3)
```
```output
         inicio      minimo    recupera  profundidad  sesiones   anos
49   2008-08-29  2011-03-31  2018-06-15       -0.479      2555  9.793
68   2019-07-29  2020-03-31  2022-04-13       -0.235       707  2.708
109  2025-03-17  2025-07-03        None       -0.108       208  0.791
```

The carry's worst episode starts on August 29, 2008, bottoms out on March 31, 2011 at −47.9% and does not recover its
peak until June 15, 2018: 2,555 sessions, 9.8 years underwater. The 2008 crash leaves the drop at −24.7% in December;
the 2010 and 2011 crashes double it. For an investor, depth is half the damage: the other half is a decade without new
highs.

Peter Martin's **Ulcer Index** (1987)[^8] combines both dimensions in one figure: the square root of the mean of the
squared drawdowns,

$$
UI = \sqrt{\frac{1}{N}\sum_{t=1}^{N} DD_t^{2}}
$$

A deep and long drawdown weighs much more than an equally deep but brief one, because it is added up over many days.
The **Martin ratio** (Ulcer Performance Index) divides the annual excess return by the Ulcer Index, as the Sharpe
divides by the standard deviation.

```python
def ulcer(r):
    return np.sqrt((drawdown(r) ** 2).mean())

def martin(r, rf_anual=0.0):
    return (cagr_fechas(r) - rf_anual) / ulcer(r)

def resumen_dd(r):
    e = episodios(r)
    return {"dd_max": e["profundidad"].min(),
            "sesiones_max": e["sesiones"].max(),
            "anos_max": e["anos"].max(),
            "bajo_el_agua": (drawdown(r) < 0).mean(),
            "ulcer": ulcer(r), "martin": martin(r),
            "calmar": cagr_fechas(r) / -drawdown(r).min()}

pd.DataFrame({k: resumen_dd(r) for k, r in rets.items()}).T.round(3)
```
```output
        dd_max  sesiones_max  anos_max  bajo_el_agua  ulcer  martin  calmar
indice  -0.622         959.0     3.817         0.921  0.178   0.545   0.156
tsmom   -0.194         675.0     2.683         0.914  0.064   1.931   0.639
carry   -0.479        2555.0     9.793         0.958  0.213   0.182   0.081
```

*Fig. — Anatomy of a drawdown: FX carry from 2005 to 2025, with its maximum depth, the length of the worst episode and the Ulcer Index.* Weekly drawdown area of FX carry. Until August 2008 it barely goes below 5%; the crash takes it to −24.7% in December and the fall continues to the maximum, highlighted, of −47.9% in March 2011. A bracket above marks the whole episode, from August 2008 to June 2018: 9.8 years under water. A dashed line at −21.3% marks the Ulcer Index, the root mean square of drawdowns. Afterwards there is another fall of −23.5% in 2020.

The figure draws the anatomy of the carry's worst drawdown: the maximum depth in amber, the bracket of the 9.8 years
underwater and the Ulcer Index, 21.3%, as a line: it is the “typical” drawdown level in root mean square. All three
strategies spend more than 91% of the sessions below their previous peak; what sets them apart is how much and for
how long.

The index falls more than the carry: its maximum drawdown is −62.2% (from November 20, 2019 to May 19, 2020), and the
2008 one, −56.0% (from October 2007 to April 2009). But it recovers from both in a little over three years: in
September 2023 (3.8 years) and in November 2010 (3.1 years). The carry, with a smaller drop, spends 9.8 years
underwater. Depth and duration are different risks and both have to be reported.

*Fig. — Four metrics of the three strategies, February 2005 to 2025: FX carry comes close to the index on Sharpe, but its Calmar is half and its Ulcer higher.* Four panels of horizontal bars, one per metric, with index, TSMOM and FX carry. Sharpe: 0.56, 1.03 and 0.45. Sortino: 0.81, 1.53 and 0.63. Calmar: 0.16, 0.64 and 0.08, highlighted. Ulcer Index: 17.8%, 6.4% and 21.3%. TSMOM wins on all four; carry comes close to the index on Sharpe, but its Calmar is half the index's and its Ulcer higher, with less than half the volatility.

The comparison in the figure sums up the lesson. In Sharpe, the index and the carry are close (0.56 and 0.45). In
Calmar, the carry has half (0.08 against 0.16), and in Ulcer, more (21.3% against 17.8%), with less than half the
index's volatility (9.1% against 20.3%): its maximum drawdown is equivalent to 5.3 times its annual volatility, and
the index's, to 3.1. It is the signature of a strategy with negative skewness and losses that chain together: the
standard deviation makes it look safe until the day of the crash. TSMOM wins on all four metrics; in these synthetic
data, because the momentum term is planted in the expected return of each future.

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

Maximum drawdown grows with the length of the sample: the longer it is, the more likely it is to find a worse crisis.
In the carry, the 2005 to 2010 one is −44.3% and that of the whole sample, −47.9%. A three-year Calmar and a
twenty-year one cannot be compared, and a short backtest that includes no crash of the kind its strategy is exposed to
promises a Calmar that does not exist.

</aside>

## Against empyrical-reloaded

empyrical-reloaded is the metrics library used by Zipline Reloaded and pyfolio-reloaded (code in its repository,
Apache 2.0). Comparing each of your own functions with its counterpart is the cheapest test that the formulas are
correctly implemented:

```python
import empyrical as ep

def comparar(r):
    return pd.Series({
        "sharpe": sharpe(r) - ep.sharpe_ratio(r),
        "sortino": sortino(r) - ep.sortino_ratio(r),
        "omega": omega(r) - ep.omega_ratio(r),
        "dd_max": drawdown(r).min() - ep.max_drawdown(r),
        "vol": vol_anual(r) - ep.annual_volatility(r),
        "cagr_252": cagr_sesiones(r) - ep.cagr(r),
        "cagr_fechas": cagr_fechas(r) - ep.cagr(r),
    })

dif_max = pd.DataFrame({k: comparar(r) for k, r in rets.items()})
dif_max.abs().max(axis=1)
```
```output
sharpe         1.665335e-15
sortino        2.886580e-15
omega          2.220446e-16
dd_max         5.551115e-17
vol            0.000000e+00
cagr_252       0.000000e+00
cagr_fechas    1.358127e-03
dtype: float64
```

Sharpe, Sortino, Omega, maximum drawdown and volatility match up to floating-point rounding error ($10^{-15}$). So
does our own CAGR with 252 sessions. The CAGR by dates differs by 0.0014, the carry difference from the CAGR section:
empyrical is not wrong, it uses another convention (`annualization=261` in its functions would change it). Ulcer,
duration and the Martin ratio are not in empyrical; quantstats does have an `ulcer_index`, and lesson 2.6.3 compares
it with this one.

## Which metric for which return shape

- **For a symmetric strategy with thin tails**, Sharpe and Sortino tell almost the same story and the Sharpe is the
  standard for comparing. A stock portfolio almost always is one, but not in its crises: the index in this lesson has
  a monthly skewness of −2.15 because of a single month.
- **For a strategy with negative skewness** (carry, volatility selling, reversal with distant stops), the Sharpe is
  optimistic. Look at Calmar, Ulcer, drawdown duration and the monthly Sortino; and distrust any backtest that does not
  include a crash.
- **For a strategy with positive skewness** (trend), the Sharpe is conservative and what weighs is the duration of the
  flat stretches: the 2.7 years of TSMOM's worst episode are the moment when a system gets abandoned.
- **Always**: the same period for all, the frequency and the annualization convention in plain view, the risk-free
  rate only where it applies and the standard error next to each Sharpe.

## Exercises

1. Compute the carry's Calmar with data up to December 31, 2007 and with the whole sample. What would someone
   evaluating it at the beginning of 2008 have concluded?

**Solution.**

```python
for fin in ("2007-12-31", "2025-12-31"):
    r = rets["carry"].loc[:fin]
    dd = drawdown(r).min()
    print(f"up to {fin}: CAGR {cagr_fechas(r):.2%}, "
          f"max DD {dd:.1%}, Calmar {cagr_fechas(r) / -dd:.2f}, "
          f"Sharpe {sharpe(r, n=261):.2f}")
```
```output
up to 2007-12-31: CAGR 11.09%, max DD -9.1%, Calmar 1.22, Sharpe 1.30
up to 2025-12-31: CAGR 3.89%, max DD -47.9%, Calmar 0.08, Sharpe 0.46
```

Up to 2007, the carry had a CAGR of 11.09%, a maximum drawdown of −9.1%, a Calmar of 1.22 and a Sharpe of 1.30: an
excellent strategy in any table. With the whole sample, the Calmar drops to 0.08 and the Sharpe to 0.46. Nothing in
the previous three years announced the crash: negative skewness is only seen when it happens. That is why a carry
backtest has to include crisis periods, and why the Calmar of a short sample with no crash says nothing.

2. Martin recommends computing the Ulcer Index with weekly data. Compare it with the daily one for the three
   strategies, taking the last capital value of each week.

**Solution.**

```python
def ulcer_semanal(r):
    capital = (1 + r).cumprod().resample("W-FRI").last()
    dd = capital / capital.cummax().clip(lower=1.0) - 1
    return np.sqrt((dd ** 2).mean())

pd.DataFrame({k: {"diario": ulcer(r), "semanal": ulcer_semanal(r)}
              for k, r in rets.items()}).T.round(4)
```
```output
        diario  semanal
indice  0.1781   0.1719
tsmom   0.0642   0.0615
carry   0.2133   0.2117
```

With weekly data the Ulcer Index drops very little (from 17.81% to 17.19% in the index, from 6.42% to 6.15% in TSMOM,
from 21.33% to 21.17% in the carry): as Martin says, it is almost independent of the frequency, unlike the standard
deviation, which has to be annualized. It drops a little because the Friday close skips some midweek lows.

3. A manager presents a Sharpe of 1.2 computed with 36 months of monthly returns multiplied by √12. Which four
   questions would you ask before believing it?

**Solution.**

First, whether the risk-free rate was subtracted and which one: with rates at 4 or 5%, not subtracting it greatly
inflates the Sharpe of a low-volatility strategy. Second, what its standard error is: with 3 years,
$1/\sqrt{3} \approx 0.58$, so the ± 2 standard error interval goes from roughly 0.05 to 2.35 and the figure is
compatible with a mediocre strategy. Third, whether the monthly returns are autocorrelated: a portfolio with illiquid
assets or assets valued with a delay has positive autocorrelation, and Lo's $\eta(12)$ factor lowers the annual
Sharpe. Fourth, what shape the distribution has and what happened in the worst month: if the strategy sells
volatility or collects carry, 36 months without a crash give a high Sharpe that negative skewness will refute; ask
for the maximum drawdown, its duration and the monthly Sortino.

[^1]: Tobias J. Moskowitz, Yao Hua Ooi and Lasse Heje Pedersen, “Time series momentum”, Journal of Financial Economics
    104(2), 228-250 (2012), <https://doi.org/10.1016/j.jfineco.2011.11.003>.
[^2]: Markus K. Brunnermeier, Stefan Nagel and Lasse H. Pedersen, “Carry Trades and Currency Crashes”, NBER
    Macroeconomics Annual 23, 313-348 (2008), <https://doi.org/10.1086/593088>.
[^3]: William F. Sharpe, “The Sharpe Ratio”, The Journal of Portfolio Management 21(1), 49-58 (1994): the “ex post”
    version is the mean of the difference between the fund's return and the benchmark's (here, the risk-free rate)
    divided by its standard deviation.
[^4]: Andrew W. Lo, “The Statistics of Sharpe Ratios”, Financial Analysts Journal 58(4), 36-52 (2002).
[^5]: The version with skewness and kurtosis is due to Mertens (2002); it is reproduced in David H. Bailey and Marcos
    López de Prado, “The Sharpe ratio efficient frontier”, The Journal of Risk 15(2), 3-44 (2012),
    <https://doi.org/10.21314/JOR.2012.255>, the basis of the Deflated Sharpe of lesson 2.5.4.
[^6]: Frank A. Sortino and Lee N. Price, “Performance Measurement in a Downside Risk Framework”, The Journal of
    Investing 3(3), 59-64 (1994).
[^7]: Con Keating and William F. Shadwick, “A Universal Performance Measure”, The Journal of Performance Measurement
    6(3) (2002).
[^8]: Peter G. Martin, “Ulcer Index: An Alternative Approach to the Measurement of Investment Risk & Risk-Adjusted
    Performance”, <https://www.tangotools.com/ui/ui.htm>: the author developed it in 1987 and published it in 1989; he
    defines the UPI with the annualized total return minus that of the risk-free asset.

## Self-assessment

1. The currency carry trades 261 days a year. Why is its CAGR computed with `empyrical.cagr` (3.75%) lower than the one computed with the dates (3.89%)?

   - A. Because empyrical subtracts the risk-free rate
   - B. Because empyrical counts years as sessions divided by 252, and 5,457 business days are 21.7 “years” of 252 when the calendar only has 20.9
   - C. Because empyrical uses log returns
   - D. Because the series has gaps on holidays

   *Answer B: Because empyrical counts years as sessions divided by 252, and 5,457 business days are 21.7 “years” of 252 when the calendar only has 20.9.* The same total growth spread over more years gives a lower rate. For the CAGR, always count years with the dates; to annualize means and volatilities, use the market's actual sessions per year.

2. TSMOM on futures has a Sharpe of 1.026. If you add the interest earned by the collateral and compute the Sharpe without subtracting the risk-free rate, you get 1.173. Which one is correct?

   - A. 1.026: the Sharpe measures the excess over the risk-free rate, and the futures series is already an excess return; on the total return the risk-free rate has to be subtracted, and you are back to 1.026
   - B. 1.173, because it is the return actually earned
   - C. The average of the two
   - D. Neither: with futures the Sharpe makes no sense

   *Answer A: 1.026: the Sharpe measures the excess over the risk-free rate, and the futures series is already an excess return; on the total return the risk-free rate has to be subtracted, and you are back to 1.026.* Without subtracting the risk-free rate, any strategy that leaves the money in T-bills seems to have an edge. With an average rate of 1.79%, the error adds 0.15 to TSMOM's Sharpe.

3. With 251 months of data, the standard error of an annual Sharpe is about 0.22. What does that imply for the currency carry, with an annualized monthly Sharpe of 0.41?

   - A. That its edge is proven at 95%
   - B. That the standard error does not depend on the years of data
   - C. That its ± 2 standard error interval goes from −0.07 to 0.89: with 21 years of data, that Sharpe cannot be told apart from zero
   - D. That the true Sharpe is exactly 0.41

   *Answer C: That its ± 2 standard error interval goes from −0.07 to 0.89: with 21 years of data, that Sharpe cannot be told apart from zero.* The standard error of the annual Sharpe is of the order of 1/√years. With its negative skewness and its kurtosis, the carry's rises to 0.239: the interval includes zero, even though the effect is planted in the data.

4. The IDA index and the FX carry have a similar Sharpe (0.56 and 0.45). Why is the carry's Calmar half as large?

   - A. Because the carry has more volatility
   - B. Because the Calmar penalizes currency markets
   - C. Because of a calculation error
   - D. Because the carry earns 3.9% a year and, with a volatility of 9%, falls 47.9% in its worst episode, 5.3 times its volatility: its losses come in crashes that the standard deviation does not anticipate; the index falls more (62.2%), but earns 9.7% a year

   *Answer D: Because the carry earns 3.9% a year and, with a volatility of 9%, falls 47.9% in its worst episode, 5.3 times its volatility: its losses come in crashes that the standard deviation does not anticipate; the index falls more (62.2%), but earns 9.7% a year.* The Sharpe only sees dispersion. Both have negative monthly skewness, but the index rebounds after each crisis and the carry chains bad months together (its Lo η is 2.94, below √12). The Calmar, the Ulcer (21.3% against 17.8%) and the drawdown duration (9.8 years against 3.8) do capture that path.

5. What does the Ulcer Index measure?

   - A. The square root of the mean of the squared drawdowns: it grows with the depth and with the time spent underwater
   - B. The worst daily loss
   - C. The standard deviation of the negative returns
   - D. The number of drawdown episodes

   *Answer A: The square root of the mean of the squared drawdowns: it grows with the depth and with the time spent underwater.* A deep but brief drawdown and a moderate but very long one can give the same Ulcer. It is the metric closest to what someone following the strategy feels; Martin proposes dividing the annual excess return by it (Martin ratio or UPI).

## Key points

- Each metric brings a convention: years by dates for the CAGR; actual sessions per year (252 in equities, 261 in currencies) for means and volatilities; and a risk-free rate that is subtracted only if the series is not already an excess return.
- A Sharpe is an estimate with an error: about 1/√years units. With 21 years, ± 0.44 at two standard errors; negative skewness and autocorrelation make it worse.
- Sharpe and Sortino look at dispersion; Calmar, duration and Ulcer look at the path. A carry with negative skewness comes close in Sharpe to an index (0.45 and 0.56), has half the Calmar and more Ulcer with less than half the volatility.
- Drawdown has depth and duration: the index's is deeper (−62.2%) and recovers in 3.8 years; the carry's (−47.9%) lasts 9.8.
- Implemented from scratch, Sharpe, Sortino, Omega, volatility and drawdown match empyrical-reloaded up to rounding; the CAGR only matches if years are counted the way it does.
- The data are synthetic and the effects are planted: these figures teach how to measure, they do not prove that TSMOM or the carry work in real markets.

## Resources

### Research

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

### Documentation

- [Peter G. Martin: Ulcer Index and Ulcer Performance Index (definition and algorithm)](https://www.tangotools.com/ui/ui.htm) · tangotools.com
- [empyrical-reloaded: risk and performance metrics (source code)](https://github.com/stefan-jansen/empyrical-reloaded) · github.com

### Books

- [Andrew W. Lo, “The Statistics of Sharpe Ratios”, Financial Analysts Journal 58(4), 36-52 (2002)](https://doi.org/10.2469/faj.v58.n4.2453) · doi.org
- [William F. Sharpe, “The Sharpe Ratio”, The Journal of Portfolio Management 21(1), 49-58 (1994)](https://doi.org/10.3905/jpm.1994.409501) · doi.org
- [Frank A. Sortino and Lee N. Price, “Performance Measurement in a Downside Risk Framework”, The Journal of Investing 3(3), 59-64 (1994)](https://doi.org/10.3905/joi.3.3.59) · doi.org

### Data

- [Level 2 data: currency carry, 2005-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/fx2_carry.parquet) · Download · fx2_carry.parquet

Previous lesson: [2.5.8 · “Honest backtest” checklist before moving to paper trading](https://pythonparatrading.com/en/python-trading-course/level-2/honest-backtest-checklist/)

Next lesson: [2.6.2 · Win rate, payoff, expectancy, alpha, beta, information ratio, turnover and capacity](https://pythonparatrading.com/en/python-trading-course/level-2/trade-metrics-alpha-beta-capacity/)
