---
title: "3.3.2 Currency carry, momentum and value; baskets"
description: "Carry from forwards, its crash risk episode by episode, momentum and value with the real exchange rate, and a basket of all three with costs."
url: "https://pythonparatrading.com/en/python-trading-course/level-3/currency-carry-momentum-value/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-3/currency-carry-momentum-value.png"
---

# 3.3.2 · Carry and crash risk, momentum and value in currencies; baskets

> Lesson 2.3.4 measured currency carry and its crashes. This one goes further: it extracts carry from forwards and separates the covered parity basis, decomposes the premium by portfolios, measures each crash against the planted one, builds momentum and value with the real exchange rate and combines them in a basket net of costs. The data are twelve fictitious currencies with the three effects planted and a control with none.

- Course: [Level 3: Derivatives: futures, spreads, forex and options](https://pythonparatrading.com/en/python-trading-course/level-3/)
- Module: 3.3 · Advanced forex
- Duration: 75 min
- Prerequisites: [1.2.4 · Forex: pairs, pips, lots, rollover and CFDs](https://pythonparatrading.com/en/python-trading-course/level-1/forex-pairs-pips-lots-and-rollover/), [2.3.1 · Trend following and momentum](https://pythonparatrading.com/en/python-trading-course/level-2/trend-following-and-momentum/), [2.3.4 · Carry in currencies, futures and bonds; seasonality and calendar effects](https://pythonparatrading.com/en/python-trading-course/level-2/carry-seasonality-calendar-effects/), [2.5.6 · Parameter sensitivity: plateaus versus peaks, robustness across assets and periods](https://pythonparatrading.com/en/python-trading-course/level-2/parameter-sensitivity-and-robustness/), [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/)

## By the end of this lesson

- Compute the rate differential implied by the 1-month forward and separate the covered parity basis, and check that the basis does not predict returns.
- Decompose the carry premium into interest and exchange rate with portfolios sorted by differential, and relate it to the dollar and HML factors of the literature.
- Measure carry crash risk episode by episode against the planted one, its relationship with global currency volatility and what scaling by volatility does (and does not do).
- Build momentum (cross-sectional and time-series) and value with the real exchange rate without look-ahead bias, and measure their fragility to the chosen window.
- Combine carry, momentum and value in a basket with past-volatility weights and per-currency costs, and judge it by subperiods and drawdowns, not only by the Sharpe.

## Twelve currencies and what they have planted

Lesson 2.3.4 defined carry, built the classic three-versus-three portfolio with next-day execution, estimated the Fama
regression and looked at the crashes. Here all of that is taken as known and we work with a richer dataset: **twelve
fictitious currencies** against the USD, on business days from 2006 to 2025, each with its exchange rate, its
one-month forward, its deposit rates and its monthly CPI. Each currency mimics the profile of a real one: the evro
(`QEV`) the euro, the Nara yen (`QYN`) the yen, the Brava real (`QBR`) the Brazilian real. The data are synthetic and
three effects are planted with known parameters:

| Effect | What it does in the data | Planted parameter |
|---|---|---|
| Carry | Long the currency earns 80% of the rate differential (incomplete uncovered parity) | θ = 0.20 |
| Crash | In eight stress episodes, each currency falls k · (its differential − the mean) | k from 0.6 to 3.0 |
| Momentum | Annual drift in the direction of the excess return over 63 sessions | λ = 0.15 volatilities |
| Value | Each currency's own component reverts to zero | Half-life of 3 years |
| Control | `sorpresas_macro`: a series that does not enter any equation | No effect |

There is also a common dollar factor, a risk factor that lifts high-yield currencies and lowers safe havens (`QYN`,
`QFR`) in crises, and a covered parity basis in five currencies. Everything is in
`public/curso/datos/nivel-3/README.md` and in `fx3_verdad.csv`. The lesson measures each effect as if it did not know
it and compares.

```mermaid
flowchart TB
  D[fx3_diario] --> C[Carry]
  D --> M[Momentum]
  I[fx3_ipc_mensual] --> V[Value]
  D --> V
  C --> R[Month-end ranks]
  M --> R
  V --> R
  R --> P[3 long, 3 short]
  P --> E[Scale by vol]
  E --> K[Net basket]
```

## Carry in the forwards and the covered parity basis

A currency manager does not usually borrow in one currency to deposit in another: they buy forward. The one-month
forward already contains the rate differential, by **covered interest parity** (lesson 1.2.4):

$$
F = S \cdot \frac{1 + i_{\text{USD}}\,\tau}{1 + i\,\tau}
\qquad\Longrightarrow\qquad
i_{\text{USD}}^{\text{impl}} = \frac{(1 + i\,\tau)\,F/S - 1}{\tau}
$$

with $S$ and $F$ in USD per unit of the currency, $i$ the currency's rate and $\tau = 30/360$. If parity held, the USD
rate implied by the forward would be the deposit rate. Since 2008 it does not hold in real currencies: Du, Tepper and
Verdelhan (2018) document persistent deviations (the **basis**) in G10 currencies, which they attribute to arbitraging
them consuming bank balance sheet. In the data there is a planted basis in five currencies. It is recovered with the
exact formula; the usual log approximation, $\ln(S/F)/\tau$, leaves several basis points of error in the high-yield
ones, where $\ln(1 + i\,\tau)$ no longer resembles $i\,\tau$.

```python
import numpy as np
import pandas as pd
import statsmodels.api as sm

fx = pd.read_parquet("fx3_diario.parquet")

def tabla(col):
    return fx.pivot(index="fecha", columns="divisa", values=col)

X = tabla("usd_por_unidad")
i_div = tabla("tipo_deposito_1m")
i_usd = fx.groupby("fecha")["tipo_deposito_1m_usd"].first()
dif = i_div.sub(i_usd, axis=0)                  # % per year
cotiza_usd = fx.groupby("divisa")["par"].first().str.startswith("USD")
F = tabla("forward_1m")
F.loc[:, cotiza_usd] = 1 / F.loc[:, cotiza_usd]  # USD per unit
TAU = 30 / 360
i_usd_impl = ((1 + i_div / 100 * TAU) * F / X - 1) / TAU * 100
base_pb = 100 * i_usd_impl.sub(i_usd, axis=0)
base_pb.median().round(1).sort_values().to_frame("base_pb").T
```
```output
divisa    QYN   QEV  QFR  QLB  QSK  QBR  QCD  QAD  QNK  QMP  QZD  QZR
base_pb -30.3 -15.4 -9.9 -5.4 -5.4 -0.0  0.0 -0.0 -0.0 -0.0 -0.0 -0.0
```

The five planted bases come out almost exact: −30 bp in `QYN`, −15 in `QEV`, −10 in `QFR` and −5 in `QLB` and `QSK`
(the planted figures in calm periods; in stress episodes they widen, and the median ignores them). The rest, zero. A
negative basis means that getting dollars forward with that currency is more expensive than borrowing them: the USD
rate implied by the forward is below the deposit rate, and whoever sells the currency forward to hedge pays that
difference (lesson 3.3.3).

Does the basis matter for carry? For ranking currencies, hardly at all: 30 bp versus differentials of several points.
Does it predict anything? In the model it does not enter any return equation: it is a control. The regression of the
next month's excess return on the basis, with errors clustered by month (lesson 2.1.1), confirms it:

```python
dias = X.index.to_series().diff().dt.days
interes = dif.shift(1).div(100).mul(dias / 360, axis=0)
d_spot = np.log(X).diff()
r = d_spot + interes                       # excess return
fin = X.index.to_series().groupby(X.index.to_period("M")).max()

sig_mes = r.groupby(r.index.to_period("M")).sum().shift(-1)
sig_mes.index = fin.values                 # the month after each month-end
panel = pd.DataFrame({"y": 1200 * sig_mes.stack(),   # % per year
                      "base": base_pb.loc[fin].stack(),
                      "dif": dif.loc[fin].stack()}).dropna()
grupos = panel.index.get_level_values(0).astype(str)
for xs in (["base"], ["dif", "base"]):
    m = sm.OLS(panel["y"], sm.add_constant(panel[xs])).fit(
        cov_type="cluster", cov_kwds={"groups": grupos})
    print(xs, "t =", m.tvalues[xs].round(2).to_dict())
```
```output
['base'] t = {'base': 0.3}
['dif', 'base'] t = {'dif': 1.75, 'base': -0.39}
```

On its own, the basis has a t of 0.30; with the differential in the regression, −0.39. The differential has a t of
1.75: not even carry, which is planted, is significant currency by currency and month by month. It is the same power
problem as the Fama regression of lesson 2.3.4. The useful measure is the portfolio.

## The premium, portfolio by portfolio

Lustig, Roussanov and Verdelhan (2011) sort currencies by their rate differential (or their forward discount) into
portfolios and document two factors: the **dollar** (the average of all currencies against the USD) and the
**currency HML** (the high-yield portfolio minus the low-yield one). With twelve currencies you get four portfolios of
three. At each month-end they are sorted by differential and held from the next session:

```python
puesto = dif.loc[fin].rank(axis=1)
grupo = ((puesto - 1) // 3 + 1).reindex(X.index, method="ffill")
grupo = grupo.shift(1)                      # from the next session
filas = {}
for g in range(1, 5):
    en_g = grupo == g
    filas[f"C{g}"] = {
        "dif_medio": dif.where(en_g).mean(axis=1).mean(),
        "interes": 260 * 100 * interes.where(en_g).mean(axis=1).mean(),
        "spot": 260 * 100 * d_spot.where(en_g).mean(axis=1).mean(),
        "total": 260 * 100 * r.where(en_g).mean(axis=1).mean(),
    }
pd.DataFrame(filas).T.round(2)
```
```output
    dif_medio  interes  spot  total
C1      -1.40    -1.41  1.39  -0.02
C2      -0.29    -0.29  0.35   0.06
C3       1.24     1.26 -0.69   0.57
C4       6.18     6.25 -0.31   5.95
```

This is the header figure. The highest-yield portfolio (`C4`, 6.18 points over the USD on average) collects 6.25% a
year in interest and loses only 0.31% on the exchange rate: it keeps a 5.95% excess return. The lowest-yield one
(`C1`) pays 1.41% in interest and recovers it almost entirely on the exchange rate (+1.39%): its total is zero.
Uncovered parity predicts that the exchange rate offsets the differential in all of them; it does in the low-yield
ones and not in the high-yield ones, where the model collects 80% of the differential and also loses the crashes. That
`C4` only loses 0.31% a year on the spot includes the crashes: they are lost all at once, but between one and the next
the high-yield currency does not even depreciate as much as parity says.

The difference `C4 − C1` is the currency HML, and with twelve currencies in four groups of three it is exactly the
three-versus-three carry portfolio. The average of all of them against the USD is the dollar factor:

```python
dolar = r.mean(axis=1)
hml = (r.where(grupo == 4).mean(axis=1)
       - r.where(grupo == 1).mean(axis=1)).dropna()

def sharpe(x, n=260):
    return x.mean() / x.std() * np.sqrt(n)

pd.DataFrame({"media_pct": [260 * 100 * dolar.mean(),
                            260 * 100 * hml.mean()],
              "sharpe": [sharpe(dolar), sharpe(hml)]},
             index=["dolar", "hml"]).round(2)
```
```output
       media_pct  sharpe
dolar       1.50    0.20
hml         5.97    0.52
```

The dollar factor returns 1.50% a year with a Sharpe of 0.20: the average of the twelve currencies includes the
high-yield ones and collects some carry. The HML, 5.97% with a Sharpe of 0.52. In the literature, the dollar explains
most of the common variation of currencies and the HML the difference in average return between portfolios; in the
data, by construction, the dollar factor is a bounded AR(1) with no drift and almost all of the premium is in the HML.

## Crash risk, episode by episode

The three-versus-three carry portfolio, with the money market convention (360 days) and 260 business days for
annualizing:

```python
def cartera(senal, n=3):
    """Long the n with the highest signal and short the n lowest,
    signal at month-end, position from the next session."""
    s = senal.loc[fin]
    k = s.notna().sum(axis=1)
    pos = s.rank(axis=1)
    w = (pos.gt(k - n, axis=0).astype(float)
         - (pos <= n).astype(float)) / n
    w = w.where(k >= 2 * n, 0.0, axis=0)
    w = w.reindex(X.index, method="ffill").shift(1)
    vivo = w.abs().sum(axis=1) > 0
    return (w * r).sum(axis=1)[vivo], w[vivo]

def resumen(x, n=260):
    m = x.resample("ME").sum()
    curva = x.cumsum()
    return pd.Series({
        "sharpe": x.mean() / x.std() * np.sqrt(n),
        "media_pct": 100 * x.mean() * n,
        "vol_pct": 100 * x.std() * np.sqrt(n),
        "asim_mensual": m.skew(),
        "max_dd_pct": 100 * (curva - curva.cummax()).min(),
        "peor_mes_pct": 100 * m.min(),
    })

carry, w_carry = cartera(dif)
print(resumen(carry).round(2).to_string())
mensual = carry.resample("ME").sum()
print((100 * mensual.nsmallest(4)).round(1).to_string())
```
```output
sharpe           0.52
media_pct        5.97
vol_pct         11.50
asim_mensual    -1.49
max_dd_pct     -51.80
peor_mes_pct   -27.61
fecha
2008-10-31   -27.6
2010-05-31    -8.7
2020-03-31    -8.3
2014-05-31    -8.1
```

A Sharpe of 0.52, 5.97% a year with 11.50% volatility, a monthly skewness of −1.49, a maximum drawdown of 51.8% (in
log) and a worst month of −27.6%, October 2008. The data's README gives 0.55 and 6.2% because it annualizes with 252
days and accrues interest with 365: the conventions move the second decimal of the Sharpe, not the conclusion. The
other three worst months (May 2010, March 2020, May 2014) are much smaller: carry risk is concentrated in one episode.

### Each episode, against the planted one

In each stress episode the generator makes each currency fall $k \cdot (\Delta i_j - \overline{\Delta i})$ in log,
spread over the episode. A cross-sectional regression of each currency's fall in the episode on its differential
relative to the mean, at the start of the episode, should give a slope of $-k$:

```python
tramos = pd.DataFrame({
    "desde": ["2008-09-29", "2010-05-03", "2011-08-01", "2013-05-22",
              "2015-08-11", "2018-04-16", "2020-02-24", "2024-07-31"],
    "hasta": ["2008-10-27", "2010-05-25", "2011-09-23", "2013-06-24",
              "2015-08-25", "2018-09-07", "2020-03-23", "2024-08-05"],
    "k": [3.0, 0.8, 1.2, 1.0, 0.8, 0.6, 2.5, 0.6]})
filas = []
for t in tramos.itertuples():
    antes = X.index[X.index < t.desde][-1]
    caida = np.log(X.loc[t.hasta] / X.loc[antes])
    x = (dif.loc[antes] - dif.loc[antes].mean()) / 100
    fit = sm.OLS(caida, sm.add_constant(x)).fit()
    filas.append({"desde": t.desde[:7],
                  "sesiones": len(X.loc[t.desde:t.hasta]),
                  "k": t.k, "pendiente": fit.params.iloc[1],
                  "error": fit.bse.iloc[1],
                  "carry_pct": 100 * carry[t.desde:t.hasta].sum()})
pd.DataFrame(filas).round(2)
```
```output
     desde  sesiones    k  pendiente  error  carry_pct
0  2008-09        21  3.0      -3.14   0.17     -28.41
1  2010-05        17  0.8      -1.12   0.09      -7.98
2  2011-08        40  1.2      -1.22   0.32      -8.28
3  2013-05        24  1.0      -0.17   0.16       0.64
4  2015-08        11  0.8      -0.81   0.12      -7.33
5  2018-04       105  0.6       2.62   0.96      28.18
6  2020-02        21  2.5      -2.38   0.22     -11.38
7  2024-07         4  0.6      -0.60   0.07      -4.46
```

*Fig. — Carry portfolio result in each planted stress window, with its k and the measured cross-sectional slope b (should be −k).* Vertical bars per stress window. 2008 (k 3.0, b −3.14): −28.4%, highlighted. 2010 (k 0.8, b −1.12): −8.0%. 2011 (k 1.2, b −1.22): −8.3%. 2013 (k 1.0, b −0.17): +0.6%. 2015 (k 0.8, b −0.81): −7.3%. 2018 (k 0.6, b +2.62): +28.2%. 2020 (k 2.5, b −2.38): −11.4%. 2024 (k 0.6, b −0.60): −4.5%. The large short windows are recovered; the long, small 2018 one is not.

The two large, short episodes are recovered well: October 2008 (k = 3.0) gives −3.14 with an error of 0.17, and the
pandemic (k = 2.5), −2.38 with 0.22. In them the portfolio loses 28.4% and 11.4%. The small episodes are recovered
worse or not at all: 2013 (k = 1.0) gives −0.17, and 2018 (k = 0.6) gives **+2.62**: the portfolio **gains** 28.2% in
a planted stress episode. That episode lasts 105 sessions and the planted fall, 0.6 times a differential of about 5
points, is about 3%; in those five months the high-yield currencies appreciated 20–30% because of the other components
of the model (the risk factor, momentum, noise). The general lesson: a small effect spread over a long period is not
seen in that period; a large, concentrated effect is. And a reading of real data that says “in episode X carry did not
fall, so crash risk is a myth” makes the same mistake.

### Global currency volatility

Menkhoff, Sarno, Schmeling and Schrimpf (2012) relate carry returns to **innovations in global foreign exchange
volatility**: high-yield currencies lose when volatility rises unexpectedly, and low-yield ones act as a hedge. Their
measure is the average of the absolute daily changes of all currencies. In the data, the volatility of the risk factor
doubles in stress episodes, so the relationship should appear:

```python
vol_global = (d_spot.abs().mean(axis=1).resample("ME").mean()
              * np.sqrt(260))
ar1 = sm.OLS(vol_global, sm.add_constant(vol_global.shift(1)),
             missing="drop").fit()
sorpresa = ar1.resid                    # what the AR(1) did not expect
j = pd.concat({"carry": 100 * mensual, "sorpresa": 100 * sorpresa},
              axis=1).dropna()
m = sm.OLS(j["carry"], sm.add_constant(j["sorpresa"])).fit(
    cov_type="HAC", cov_kwds={"maxlags": 3})
print("correlation:", round(j.corr().iloc[0, 1], 2))
print("slope:", round(m.params["sorpresa"], 2),
      " t:", round(m.tvalues["sorpresa"], 2))
```
```output
correlation: -0.21
slope: -0.6  t: -1.37
```

The correlation is −0.21, with the sign found in the literature: carry loses in the months when global volatility rises
more than expected. But the slope, with errors robust to autocorrelation (HAC), has a t of −1.37: with 240 months, it
is not significant. The risk is there, in the data and in the model, but concentrated in a few months; a regression
over all months dilutes it.

### Scaling by volatility: what it does and what it does not

The usual answer to crash risk is to scale the position by the inverse of its recent volatility (lesson 2.2.2 and the
site's volatility targeting research). With a 10% annual target and the volatility of the previous 63 sessions, capped
at 2 times:

```python
vol_63 = carry.rolling(63).std().shift(1) * np.sqrt(260)
escala = (0.10 / vol_63).clip(upper=2.0)
carry_esc = (carry * escala).dropna()
pd.DataFrame({"carry": resumen(carry[carry_esc.index]),
              "escalado": resumen(carry_esc)}).round(2)
```
```output
              carry  escalado
sharpe         0.51      0.42
media_pct      5.82      4.41
vol_pct       11.52     10.43
asim_mensual  -1.49     -1.53
max_dd_pct   -51.80    -45.34
peor_mes_pct -27.61    -23.72
```

Scaling reduces the maximum drawdown from 51.8% to 45.3% and the worst month from 27.6% to 23.7%, but it lowers the
Sharpe from 0.51 to 0.42 and leaves the skewness the same (−1.53). It does not protect against the first blow because
carry crashes come after calm periods: in September 2008 the volatility of the previous three months was low and the
position was at its maximum size. It helps with what comes afterwards, when volatility has already risen. Insurance
against the first blow is something else: a size that survives it, or out-of-the-money options on the high-yield
currencies, which cost premium (modules 3.4 and 3.5).

```mermaid
flowchart TB
  A[Calm: low vol] --> B[Maximum position]
  B --> C[Crash: first month]
  C --> D[Vol rises]
  D --> E[Scaling reduces]
  E --> F[Smaller loss]
```

## Momentum: cross-sectional and time-series

Currency momentum buys those that have risen most (in excess of the deposit) over the last months and sells those that
have risen least. Menkhoff, Sarno, Schmeling and Schrimpf (2012, in the *Journal of Financial Economics*) document it
in currencies with a distinctive feature: it is stronger in currencies with high costs that are hard to arbitrage. In
the data it is planted as a drift of λ = 0.15 volatilities a year in the direction of the excess return of the
previous 63 sessions. Two versions: **cross-sectional** (three versus three by rank, like carry) and **time-series**
(each currency long or short according to its own sign, lesson 2.3.1).

```python
exceso_63 = r.rolling(63).sum()
mom, w_mom = cartera(exceso_63)
ts_w = (np.sign(exceso_63.loc[fin]) / 12).reindex(
    X.index, method="ffill").shift(1)
tsmom = (ts_w * r).sum(axis=1)[ts_w.abs().sum(axis=1) > 0]
print(pd.DataFrame({"transversal": resumen(mom),
                    "serial": resumen(tsmom)}).round(2).to_string())

# next month's return in annual volatilities
vol = d_spot.rolling(63).std() * np.sqrt(260)
y = r.rolling(21).sum().shift(-21) * 12 / vol
pan = pd.DataFrame({"y": y.loc[fin].stack(),
                    "senal": np.sign(exceso_63).loc[fin].stack()}
                   ).dropna()
m = sm.OLS(pan["y"], sm.add_constant(pan["senal"])).fit(
    cov_type="cluster",
    cov_kwds={"groups": pan.index.get_level_values(0).astype(str)})
print(f"lambda = {m.params['senal']:.3f} "
      f"(standard error {m.bse['senal']:.3f}; planted 0.15)")
```
```output
              transversal  serial
sharpe               0.20    0.16
media_pct            1.69    0.79
vol_pct              8.60    5.05
asim_mensual        -0.24   -0.03
max_dd_pct         -44.83  -28.49
peor_mes_pct       -11.64   -5.77
lambda = 0.060 (standard error 0.109; planted 0.15)
```

The cross-sectional portfolio has a Sharpe of 0.20 and the time-series one, 0.16. Both have a skewness close to zero,
without carry's crash profile; but the worst month of cross-sectional momentum is also October 2008 (−11.6%): in a
crash the currencies that had been rising were the high-yield ones. The direct estimate of the planted parameter gives
λ = 0.060 with a standard error of 0.109: the interval includes the planted 0.15 and zero. With twelve currencies and
240 months, an effect of 0.15 volatilities a year is below what can be measured precisely, even if it is there.

## Value: the real exchange rate

The value signal in currencies compares the exchange rate with an equilibrium value. The simplest is **purchasing
power parity** (PPP): in the long run, the real exchange rate, $q = \ln X + \ln(\text{CPI}_{\text{currency}} /
\text{CPI}_{\text{USD}})$, should revert to its mean. A currency whose real rate has risen a lot over five years is
expensive; one that has fallen, cheap. Asness, Moskowitz and Pedersen (2013) use that signal (minus the change in the
real rate over five years) in currencies, together with value in other asset classes. Menkhoff, Sarno, Schmeling and
Schrimpf (2017) refine the measure by adjusting for productivity and other fundamentals[^1].

Two look-ahead details. A month's CPI is published with a delay: at the end of month $t$ only that of month $t-1$ is
known, and that is what is used. And the signal needs five years of history, so value starts in 2011.

```python
ipc = pd.read_csv("fx3_ipc_mensual.csv", parse_dates=["mes"])
ipc = ipc.set_index("mes")
rel = ipc[X.columns].div(ipc["USD"], axis=0).shift(1)  # previous month
rel.index = rel.index.to_period("M")
q_real = np.log(X.loc[fin]) + np.log(rel.loc[fin.index].to_numpy())

ventanas = {}
for meses in (36, 48, 60, 72):
    senal = -(q_real - q_real.shift(meses)).reindex(X.index)
    v, _ = cartera(senal)
    ventanas[meses] = resumen(v)[["sharpe", "media_pct",
                                  "max_dd_pct"]]
val, w_val = cartera(-(q_real - q_real.shift(60)).reindex(X.index))
pd.DataFrame(ventanas).T.round(2)
```
```output
    sharpe  media_pct  max_dd_pct
36    0.05       0.40      -35.41
48    0.34       2.99      -28.72
60    0.18       1.66      -30.46
72    0.60       5.19      -27.12
```

*Fig. — Sharpe of the currency value portfolio by the window of the real exchange rate change.* Horizontal bars: 36 months 0.05; 48 months 0.34; 60 months 0.18 (highlighted, the window used); 72 months 0.60; 1,260 sessions, the data README's, 0.31. A shaded band marks ±1 standard error (0.26) around 0.18, from 0 to 0.44: all but the 72-month window fall inside.

With 60 months, the window of Asness, Moskowitz and Pedersen, value has a Sharpe of 0.18. With 48, 0.34; with 36,
0.05; with 72, 0.60. The data's README, with 1,260 sessions (about 58 months) and annualizing with 252 days, gives
0.30. Each window starts on a different date and leaves out different years, and with about fifteen years of signal
the standard error of a Sharpe is around $1/\sqrt{15} \approx 0.26$. The four figures are compatible with a real but
weak premium; choosing the 72-month one because it comes out best would be overfitting (lessons 2.5.4 and 2.5.6).

Does the real rate recover the planted three-year half-life? The planted value is each currency's own component,
without the dollar factor; the real rate mixes it with the dollar factor (five-year half-life), the carry drift and
inflation differences:

```python
vida = {}
for c in q_real:
    z = q_real[c].dropna()
    z = z - z.mean()
    ar = sm.OLS(z.diff().iloc[1:], z.shift(1).iloc[1:]).fit()
    vida[c] = np.log(2) / -ar.params.iloc[0] / 12      # years
pd.Series(vida).round(1).sort_values().to_frame("vida_anos").T
```
```output
           QLB  QFR  QSK  QEV  QCD  QAD  QZD  QNK  QZR  QMP  QYN  QBR
vida_anos  0.9  1.1  1.1  1.2  1.2  1.7  1.8  2.2  2.5  3.2  4.9  5.1
```

Half-lives range from 0.9 years (`QLB`) to 5.1 (`QBR`), with a median of 1.75: almost all below the planted three and
with a huge dispersion. The short ones are short because the real rate carries noise that reverts quickly (that of the
CPI, the momentum moves), and the AR(1) mixes the two speeds. The longest are those of currencies with extreme
differentials (`QBR` very high, `QYN` very low): the carry drift, which moves the real rate year after year in one
direction, looks like persistence. It is the same problem PPP has in real data, where estimates of the half-life of
real exchange rate deviations are several years and very imprecise: a currency value signal works with a slow
reversion measured with a lot of noise.

### The control

`sorpresas_macro` is an AR(1) series per currency that does not enter any equation. A three-versus-three portfolio
with it as the signal should earn nothing:

```python
sorpresas = tabla("sorpresas_macro")
control, _ = cartera(sorpresas)
resumen(control).round(2).to_frame("control").T
```
```output
         sharpe  media_pct  vol_pct  asim_mensual  max_dd_pct  peor_mes_pct
control     0.1       0.71     6.99          0.45      -28.69         -5.17
```

A Sharpe of 0.10: 0.71% a year with a maximum drawdown of 28.7%. With no planted effect, twenty years of a currency
portfolio return something by chance. It is the yardstick for momentum (0.20) and value (0.18): they are above it, but
not far. Carry (0.52) does stand apart.

## Costs: turnover decides

The three signals change at very different rates. Carry depends on central bank rates, which move little; 63-session
momentum changes ranking almost every month. With a cost per side of 3 bp of notional in G10 currencies and 10 bp in
emerging ones (`QMP`, `QBR`, `QZR`), a reasonable assumption for an institutional client that will have to be checked
against your own prices (lessons 2.4.7 and 3.3.1):

```python
coste_lado = pd.Series(3e-4, index=X.columns)
coste_lado[["QMP", "QBR", "QZR"]] = 10e-4
brutas = {"carry": (carry, w_carry), "momentum": (mom, w_mom),
          "value": (val, w_val)}
netas, filas = {}, {}
for nom, (bruta, w) in brutas.items():
    giro = w.diff().abs().fillna(w.abs())
    coste = (giro * coste_lado).sum(axis=1)
    netas[nom] = bruta - coste
    anos = len(w) / 260
    filas[nom] = {"giro_anual": giro.sum(axis=1).sum() / anos,
                  "coste_pct": 100 * 260 * coste.mean(),
                  "sharpe_bruto": resumen(bruta)["sharpe"],
                  "sharpe_neto": resumen(netas[nom])["sharpe"]}
pd.DataFrame(filas).T.round(2)
```
```output
          giro_anual  coste_pct  sharpe_bruto  sharpe_neto
carry           1.60       0.06          0.52         0.51
momentum       20.38       1.02          0.20         0.08
value           5.69       0.26          0.18         0.15
```

Carry turns over 1.6 times a year and pays 0.06% a year: its Sharpe goes from 0.52 to 0.51. Value, 5.7 times and
0.26% (from 0.18 to 0.15). Momentum turns over **20 times a year** and pays 1.02% a year, more than half of its gross
return: its Sharpe drops from 0.20 to 0.08, below the control. A momentum premium that looks small but real in a
backtest without costs comes to nothing with reasonable costs; in emerging currencies, with wider bid-ask spreads,
worse.

## A basket of carry, momentum and value

The three premia have little to do with each other:

```python
N = pd.DataFrame(netas).dropna()
N.resample("ME").sum().corr().round(2)
```
```output
          carry  momentum  value
carry      1.00      0.12   0.02
momentum   0.12      1.00  -0.23
value      0.02     -0.23   1.00
```

A correlation of 0.12 between carry and momentum, 0.02 between carry and value, and −0.23 between momentum and value.
The negative correlation between value and momentum is the one Asness, Moskowitz and Pedersen (2013) document across
all asset classes: a currency that has risen a lot (high momentum) has become more expensive (low value). The basket
combines them with the same risk: each strategy is scaled to 10% volatility with its standard deviation over the
previous 252 sessions (not that of the whole sample, which would be look-ahead bias) and the basket is the average of
the three.

```python
vol_ant = N.rolling(252, min_periods=126).std().shift(1)
escaladas = (N * 0.10 / (vol_ant * np.sqrt(260))).dropna()
cesta = escaladas.mean(axis=1)
tabla_cesta = pd.DataFrame({c: resumen(escaladas[c])
                            for c in escaladas})
tabla_cesta["cesta"] = resumen(cesta)
print("from", cesta.index[0].date())
tabla_cesta.round(2)
```
```output
from 2011-08-24
              carry  momentum  value  cesta
sharpe         0.82      0.32   0.31   0.91
media_pct      8.38      3.24   3.16   4.93
vol_pct       10.16     10.25  10.30   5.39
asim_mensual   0.28      0.09  -0.09  -0.02
max_dd_pct   -24.67    -35.12 -23.63 -12.31
peor_mes_pct  -8.79    -10.30  -6.98  -4.78
```

*Fig. — Carry, momentum and value net of costs, each scaled to 10% volatility with past data, and the basket of the three (August 2011 to 2025).* Four lines of monthly cumulative return since August 2011. Carry rises unevenly to 121%, with a flat stretch from 2016 to 2020. Momentum and value end at 47% and 46% with long drawdowns. The basket, highlighted, rises more steadily to 71%, with half the volatility of each strategy and a maximum drawdown of 12%.

Since August 2011 (when all three have at least half a year of history to estimate their volatility), the basket has
a Sharpe of 0.91 with 5.39% volatility and a maximum drawdown of 12.3%. Scaled carry alone, over the same period, 0.82
and a 24.7% drawdown; momentum, 0.32; value, 0.31. The basket barely improves the Sharpe of carry, because since 2011
carry has been the best of the three and has not had an October 2008; where it shows is in the maximum drawdown, which
is halved, and in the skewness, which becomes almost zero.

```python
periodos = [("2011", "2015"), ("2016", "2020"), ("2021", "2025")]
pd.DataFrame({f"{a}-{b}": {**{c: resumen(escaladas[c][a:b])["sharpe"]
                              for c in escaladas},
                           "cesta": resumen(cesta[a:b])["sharpe"]}
              for a, b in periodos}).round(2)
```
```output
          2011-2015  2016-2020  2021-2025
carry          1.12       0.18       1.23
momentum       0.04       0.31       0.55
value          0.77       0.19       0.03
cesta          0.95       0.53       1.28
```

No strategy wins in every subperiod. Carry has a Sharpe of 0.18 in 2016-2020 and 1.23 in 2021-2025; value, 0.77 in
2011-2015 and 0.03 in 2021-2025; momentum, 0.04 at the beginning and 0.55 at the end. The basket has the most stable
Sharpe: 0.95, 0.53 and 1.28. That stability, more than a higher average Sharpe, is what you buy by diversifying across
premia that do not depend on the same thing.

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

The basket cannot be measured in 2008, because value needs five years of history. Its 12% maximum drawdown does not
include carry's worst episode. A basket of carry and momentum, which can be measured from 2006, shows what would have
happened (exercise 1). And all of this is synthetic data with planted effects: the Sharpes are not evidence that the
basket works in real currencies.

</aside>

## What a currency basket demands in live trading

- **Size by the worst episode, not by volatility.** Carry has a volatility of 11% and a drawdown of 52%. The basket
  reduces the second, but does not eliminate it.
- **Per-currency costs.** Momentum dies from costs; in emerging currencies the bid-ask spread is several times that of
  the G10, and in crashes it widens further (lesson 3.3.1).
- **Windows and rebalancing dates.** Value changes from 0.05 to 0.60 with the window. Any parameter that cannot be
  justified before looking at the result is a degree of freedom that inflates the backtest (lesson 2.5.4).
- **Look-ahead bias.** CPI with publication lag, signal at the month-end close and position from the next session,
  volatility weights with past data. All three mistakes show up in published backtests.
- **Implementation.** Positions are taken with currency forwards or swaps, not with deposits: carry is collected in the
  forward points, basis included. Lesson 3.3.3 covers execution and hedging the currency risk of a portfolio.

## Exercises

1. Build a basket of only net carry and momentum, which can be measured from 2006, with the same past-volatility
   scaling. Compare with scaled carry alone: Sharpe, maximum drawdown and result in October 2008.

**Solution.**

```python
dos = pd.DataFrame({k: netas[k] for k in ("carry", "momentum")})
dos = dos.dropna()
v2 = dos.rolling(252, min_periods=126).std().shift(1)
esc2 = (dos * 0.10 / (v2 * np.sqrt(260))).dropna()
cesta2 = esc2.mean(axis=1)
oct08 = {c: 100 * esc2[c]["2008-10"].sum() for c in esc2}
oct08["cesta"] = 100 * cesta2["2008-10"].sum()
t = pd.DataFrame({"carry": resumen(esc2["carry"]),
                  "cesta": resumen(cesta2)}).T
t["oct_2008_pct"] = [oct08["carry"], oct08["cesta"]]
t.round(2)
```
```output
       sharpe  media_pct  vol_pct  asim_mensual  max_dd_pct  peor_mes_pct  oct_2008_pct
carry    0.53       5.48    10.32         -1.93      -46.06        -27.05        -27.05
cesta    0.37       3.01     8.06         -2.08      -43.39        -20.84        -20.84
```

The two-strategy basket starts in September 2006 (half a year of history for the volatility) and includes
October 2008. Its Sharpe is **worse** than that of carry alone (0.37 versus 0.53): momentum net of costs barely returns
anything and takes up half of the risk. In October 2008 it loses 20.8% versus carry's 27.1%, because only half is in
carry, but momentum also loses that month: in a crash, the currencies that had been rising are the high-yield ones.
The maximum drawdown barely changes (−43.4% versus −46.1%) and the skewness gets worse (−2.08). Diversifying with a
weak premium that is correlated in the worst month does not protect: the 12% drawdown of the three-strategy basket,
measured from 2011, says nothing about 2008.

2. The dollar factor mixes the carry premium with USD exposure. Build a carry portfolio “against the dollar”: long the
   three with the highest differential, no shorts. Compare its correlation with the dollar factor and with the
   three-versus-three portfolio.

**Solution.**

```python
puesto_d = dif.loc[fin].rank(axis=1)
w_d = ((puesto_d > 9).astype(float) / 3).reindex(
    X.index, method="ffill").shift(1)
carry_usd = (w_d * r).sum(axis=1)[w_d.sum(axis=1) > 0]
comp = pd.DataFrame({"carry_usd": carry_usd, "carry_3x3": carry,
                     "dolar": dolar}).dropna()
print(comp.resample("ME").sum().corr().round(2).to_string())
resumen(carry_usd).round(2).to_frame("carry_usd").T
```
```output
           carry_usd  carry_3x3  dolar
carry_usd       1.00       0.77   0.80
carry_3x3       0.77       1.00   0.28
dolar           0.80       0.28   1.00
           sharpe  media_pct  vol_pct  asim_mensual  max_dd_pct  peor_mes_pct
carry_usd    0.51       5.95    11.59          0.12      -53.44        -14.65
```

The portfolio long only the high-yield currencies has a monthly correlation of 0.80 with the dollar factor and 0.77
with the three-versus-three carry, which in turn has only 0.28 with the dollar. Its return is almost the same (5.95% a
year, Sharpe of 0.51) and its maximum drawdown somewhat larger (53.4%), but its worst month is −14.7%, not −27.6%, and
its skewness is positive (+0.12). The difference is in the shorts: in a crash, the low-yield currencies are safe
havens that rise, and the three-versus-three portfolio loses on both sides; the long one against the USD only on one,
in exchange for carrying dollar risk all the time. That is why studies separate the dollar factor from the HML: they
are two different risks with two different tail profiles.

3. A colleague proposes the 72-month window for value because it gives a Sharpe of 0.60. Which three checks would you
   ask for before accepting it?

**Solution.**

First, that the window be justified before looking at the result: the planted three-year half-life or the five-year
convention of the literature, not the Sharpe. Second, that the result be stable around the chosen window (66 and 78
months should give something similar) and across subperiods; a figure that jumps from 0.34 to 0.18 and to 0.60 in
neighboring windows is noise. Third, that it be corrected for the number of windows tried: the deflated Sharpe ratio
of lesson 2.5.4 with four trials and about fifteen years of data leaves 0.60 far from conclusive. And a fourth, a
specific one: the 72-month window starts in 2012, a year after the 60-month one, and it is not the same sample.

[^1]: Menkhoff, Sarno, Schmeling and Schrimpf (2017), [Currency Value](https://doi.org/10.1093/rfs/hhw067), *Review of Financial Studies* 30(2).

## Self-assessment

1. In the data, the covered parity basis of the QYN is −30 bp. What does it mean for a carry strategy that uses forward points as its signal?

   - A. That the QYN carry is 30 bp better than deposits say
   - B. That there is a risk-free arbitrage of 30 bp a year
   - C. That the differential implied by the forward departs from that of deposits by 30 bp; it ranks the currencies almost the same and, in the data, the basis predicts nothing (t = −0.39 with the differential in the regression)
   - D. That the forward is computed wrong

   *Answer C: That the differential implied by the forward departs from that of deposits by 30 bp; it ranks the currencies almost the same and, in the data, the basis predicts nothing (t = −0.39 with the differential in the regression).* The basis is the difference between the USD rate implied by the forward and the deposit rate. It has existed since 2008 in real currencies (Du, Tepper and Verdelhan, 2018) because arbitraging it consumes bank balance sheet. For carry it barely changes the ranking of currencies, and in this data it is a control: it does not enter any return equation.

2. The three-versus-three carry portfolio has a Sharpe of 0.52 since 2006; scaled by volatility and net of costs, 0.82 since August 2011. What mainly explains the difference?

   - A. That the short period does not include the October 2008 crash, the worst month (−27.6%): the carry Sharpe depends on whether or not the sample has a large crash
   - B. That the strategy improved over time
   - C. That costs went down
   - D. A calculation error

   *Answer A: That the short period does not include the October 2008 crash, the worst month (−27.6%): the carry Sharpe depends on whether or not the sample has a large crash.* Carry earns steadily and gives it back in a few months. A sample without its worst episode gives a positive skewness (+0.28) and a much higher Sharpe. That is why carry is always presented with its worst episode, not just with its Sharpe.

3. The measured slope of the 2018 stress episode is +2.62, with k = 0.6 planted (high-yield currencies should fall). What is happening?

   - A. The generator has a bug
   - B. The episode lasts 105 sessions and the planted fall, about 0.6 × 5 points ≈ 3%, is buried under 20–30% moves of the high-yield currencies from other sources; the short and large episodes (2008, 2020) are recovered well
   - C. That in 2018 carry had no risk
   - D. That 2018 should be removed from the sample

   *Answer B: The episode lasts 105 sessions and the planted fall, about 0.6 × 5 points ≈ 3%, is buried under 20–30% moves of the high-yield currencies from other sources; the short and large episodes (2008, 2020) are recovered well.* A small effect spread over five months has a very low signal-to-noise ratio. In 2008 (k = 3.0) −3.14 is measured with an error of 0.17, and in 2020 (k = 2.5), −2.38. What is not seen in one episode does not stop existing: it is seen by aggregating many.

4. Scaling the carry portfolio by its volatility over the previous 63 days reduces the maximum drawdown from 51.8% to 45.3%, but the worst month only goes from −27.6% to −23.7%. Why does it not protect more?

   - A. Because the scaling uses future volatility
   - B. Because the scaling increases costs
   - C. Because volatility does not change in crises
   - D. Because carry crashes come after calm periods: past volatility was low and the position was at its maximum size when the fall began

   *Answer D: Because carry crashes come after calm periods: past volatility was low and the position was at its maximum size when the fall began.* Scaling reacts to volatility that has already happened. It helps in long crises, in which volatility rises and the position is reduced in time for the rest, but not with the first blow. Insurance against the first blow is another instrument (options, modules 3.4 and 3.5) or a size that survives it.

5. The value signal with a 36-month window gives a Sharpe of 0.05; with 48, 0.34; with 60, 0.18; with 72, 0.60. What do you do?

   - A. Use 72 months
   - B. Discard value
   - C. Use last year's window
   - D. Average or set the window for prior reasons (the planted half-life is 3 years) and treat the dispersion as the uncertainty of the method, not as a parameter to optimize

   *Answer D: Average or set the window for prior reasons (the planted half-life is 3 years) and treat the dispersion as the uncertainty of the method, not as a parameter to optimize.* With 12 currencies and about 15 years of signal, the Sharpe of a particular window has a standard error of around 0.25. Choosing the best of four is overfitting (lessons 2.5.4 and 2.5.6). The dispersion across windows is the useful information: the value in this data is real but weak.

## Key points

- Carry can be read from forwards: the implied differential matches that of deposits except for the covered parity basis, which in the data is recovered exactly (−30 bp in the QYN, −15 in the QEV) and does not predict returns.
- Sorted by differential, the portfolios earn almost everything through interest: the highest-yield one collects 6.25% a year and loses 0.31% on the exchange rate. Its difference with the lowest-yield one is the currency HML: the 3-versus-3 carry.
- Carry loses suddenly in stress episodes: −28.4% in October 2008, with a measured slope of −3.14 versus the planted k = 3.0. Small, long episodes cannot be distinguished from noise.
- Momentum and value are weaker and more fragile premia: cross-sectional momentum (Sharpe 0.20 gross) is left with almost nothing after costs (0.08) because of its turnover of 20 times a year, and value changes from 0.05 to 0.60 depending on the window.
- The basket of the three with past-volatility weights and costs has a Sharpe of 0.91 and a maximum drawdown of 12% since 2011, versus 0.82 and 25% for carry alone, scaled the same way, over the same period: its advantage lies in the drawdowns and in each factor's bad subperiods, not in the Sharpe.
- The data are synthetic and the effects are planted: the figures show the methods and their traps, they do not prove that these strategies work in real markets; that is what the cited literature is for.

## Resources

### Research

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

### Documentation

- [Lustig, Roussanov and Verdelhan (2011): Common Risk Factors in Currency Markets, Review of Financial Studies 24(11)](https://doi.org/10.1093/rfs/hhr068) · doi.org
- [Menkhoff, Sarno, Schmeling and Schrimpf (2012): Carry Trades and Global Foreign Exchange Volatility, Journal of Finance 67(2)](https://doi.org/10.1111/j.1540-6261.2012.01728.x) · doi.org
- [Menkhoff, Sarno, Schmeling and Schrimpf (2012): Currency momentum strategies, Journal of Financial Economics 106(3)](https://doi.org/10.1016/j.jfineco.2012.06.009) · doi.org
- [Asness, Moskowitz and Pedersen (2013): Value and Momentum Everywhere, Journal of Finance 68(3)](https://doi.org/10.1111/jofi.12021) · doi.org
- [Du, Tepper and Verdelhan (2018): Deviations from Covered Interest Rate Parity, Journal of Finance 73(3)](https://doi.org/10.1111/jofi.12620) · doi.org

### Data

- [Level 3 data: twelve currencies with forwards, rates and CPI (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/fx3_diario.parquet) · Download · fx3_diario.parquet

Previous lesson: [3.3.1 · FX microstructure: OTC, ECN, last look and sessions](https://pythonparatrading.com/en/python-trading-course/level-3/fx-microstructure-otc-ecn-last-look/)

Next lesson: [3.3.3 · Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5](https://pythonparatrading.com/en/python-trading-course/level-3/currency-risk-and-fx-execution/)
