---
title: "2.3.4 Carry, seasonality and calendar effects"
description: "Carry in currencies, futures and bonds with next-day execution and costs, its crashes and the Fama regression; calendar effects as a control."
url: "https://pythonparatrading.com/en/python-trading-course/level-2/carry-seasonality-calendar-effects/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-2/carry-seasonality-calendar-effects.png"
---

# 2.3.4 · Carry in currencies, futures and bonds; seasonality and calendar effects

> Carry is what a position earns if prices do not move: a currency's interest rate differential, the slope of a future's curve, a bond's coupon and its roll down the curve. This lesson measures it on the level's synthetic data, where the premium is planted with its true value, turns it into portfolios with next-day execution and costs, shows why it crashes all at once and tests calendar effects on series where there are none.

- Course: [Level 2: Systematic strategies and rigorous backtesting](https://pythonparatrading.com/en/python-trading-course/level-2/)
- Module: 2.3 · Strategy families
- Duration: 70 min
- Prerequisites: [1.2.3 · Futures: specifications, margin, roll and contango](https://pythonparatrading.com/en/python-trading-course/level-1/futures-contract-specs-margin-and-roll/), [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/), [1.6.3 · Seasonality: day of the week, month, turn of the month and intraday sessions](https://pythonparatrading.com/en/python-trading-course/level-1/seasonality-and-calendar-effects/), [2.1.1 · Hypothesis tests, p-values, bootstrap and regression with robust standard errors](https://pythonparatrading.com/en/python-trading-course/level-2/hypothesis-testing-bootstrap-robust-errors/)

## By the end of this lesson

- Define the carry of a currency, of a future and of a bond, and compute it with each market's data.
- Build a currency carry portfolio and a futures carry portfolio with the signal at the close, the position from the next session and per-side costs.
- Estimate the Fama regression and the slope of futures carry with errors clustered by date, and compare them with the known answer.
- Measure the crash risk of carry: skewness, worst months and losses in the stress periods.
- Test calendar effects on series with none planted and recognize the false positives.

## What carry is

The **carry** of a position is what it returns if prices do not move. Koijen, Moskowitz, Pedersen and Vrugt (2018)
define it this way for any asset and split it into two parts: the expected return if nothing changes (the carry) and
the price change, which nobody knows in advance. The definition is useful because carry **is known today**: it is a
signal without estimation, without parameters to optimize and with no possible look-ahead.

```mermaid
flowchart TB
  C[Carry] --> FX[Currencies]
  C --> FU[Futures]
  C --> BO[Bonds]
  FX --> D1[Rate differential]
  FU --> D2[Curve slope]
  BO --> D3[Coupon and roll-down]
```

| Asset | Carry | What has to happen to collect it |
|---|---|---|
| Currency (long against the USD) | Currency's rate − USD rate | The exchange rate does not depreciate as much as the differential |
| Future (long) | Curve slope: ln(F₁ / F₂) / (τ₂ − τ₁) | The curve does not change and the contract converges toward spot |
| Bond (long, financed) | Yield − financing rate + roll-down | The yield curve does not move |

Theory says that carry should earn nothing: if a currency pays more interest, the market should expect it to
depreciate by the same amount (**uncovered interest rate parity**), and if a future is in backwardation, spot should
fall until it offsets it. What is observed in the literature is that this offset is only partial, and that carry earns
a premium with a specific risk: it loses all at once in crises. Brunnermeier, Nagel and Pedersen (2008) document that
high-yield currencies have negative skewness, linked to the unwinding of positions in stress episodes.

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

The data in this lesson is **synthetic** and the carry premium is **planted** with a known value: currencies earn 70%
of their differential, futures return half of the exogenous part of their carry and a tenth of the part that is mean
reversion, and there are five crashes with fixed dates. They serve to check whether the methods recover what was put
in. No figure here is evidence that carry works in real markets: that is what the cited literature is for.

</aside>

## Currency carry: the interest rate differential

`fx2_carry.parquet` has ten fictitious currencies against the USD, on business days from 2005 to 2025: the price in USD
of one unit (`spot`) and the short-term rates of the currency and of the USD, in % a year. Whoever is long a currency
(buys it with borrowed dollars and deposits it) earns its change plus the interest rate differential, with the
money-market 360-day convention:

$$
r_{t} = \Delta \ln S_{t} + \left(i_{t-1} - i^{\ast}_{t-1}\right)\frac{d_t}{360}
$$

with $S$ the spot, $i$ the currency's rate, $i^{\ast}$ the USD rate and $d_t$ the calendar days since the previous
session (3 on Mondays). The differential that counts is the previous day's: it is the one that was known when the
position was opened.

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

fx = pd.read_parquet("fx2_carry.parquet")
spot = fx.pivot(index="fecha", columns="divisa", values="spot")
dif = (fx.pivot(index="fecha", columns="divisa", values="tipo_divisa")
       - fx.pivot(index="fecha", columns="divisa", values="tipo_usd"))
dias = spot.index.to_series().diff().dt.days
d_spot = np.log(spot).diff()
interes = dif.shift(1).div(100).mul(dias / 360, axis=0)
r_fx = d_spot + interes

anual = pd.DataFrame({
    "diferencial_pct": dif.mean(),
    "interes_pct": 100 * interes.mean() * 260,
    "spot_pct": 100 * d_spot.mean() * 260,
    "total_pct": 100 * r_fx.mean() * 260,
})
anual.round(2)
```
```output
        diferencial_pct  interes_pct  spot_pct  total_pct
divisa
ALV                7.32         7.40      0.43       7.83
BOR                5.10         5.16     -5.45      -0.29
CAS                2.92         2.95     -0.81       2.14
DUN                2.93         2.96      0.35       3.31
EST                1.20         1.21     -0.34       0.88
FOR                0.59         0.60     -2.82      -2.23
GAL               -0.41        -0.41      2.09       1.68
HUR               -1.06        -1.07     -0.19      -1.26
ISL               -0.81        -0.81      2.43       1.61
JAR               -1.77        -1.79      2.54       0.75
```

The `interes_pct` column is the differential collected (somewhat larger than the mean differential because of the
360-day convention) and `spot_pct`, how much the exchange rate moved. `ALV`, the one with the highest rate (7.3 points
above the USD), collected 7.40% a year in interest and also appreciated 0.43%: 7.83% a year being long. `BOR`, the
second, collected 5.16% and lost 5.45% on the spot, almost all of it in a single month (February 2009, below). The
low-yield ones (`GAL`, `ISL`, `JAR`) paid interest and appreciated. The average pattern is that of the known answer,
but currency by currency the noise of the exchange rate, 10% to 15% a year, is much larger than the differential.

The **Fama regression** (1984) is the classic way to test uncovered parity. Fama used the forward premium, which by
covered parity is the interest rate differential; here the differential is used directly: the change in the exchange
rate over the next month on today's differential.

$$
\Delta \ln S_{t+1} = a + b\,\left(i_t - i^{\ast}_t\right) + \varepsilon_{t+1}
$$

Under parity, $b = -1$: the currency depreciates by as much as it pays in excess. If the exchange rate were a random
walk, $b = 0$ and the carry would be collected in full. In the data, the known answer is $b = -0.3$ outside the
crashes, which add losses proportional to the differential. With ten currencies that share the same months, the
residuals of the same month are correlated: the standard error has to be clustered by date (lesson 2.1.1).

```python
import statsmodels.api as sm

fin_mes = (spot.index.to_series()
           .groupby(spot.index.to_period("M")).max())
s_mes = np.log(spot.loc[fin_mes])
x_mes = dif.loc[fin_mes] / 100 / 12      # differential for the month
y_mes = s_mes.diff().shift(-1)           # change over the next month

panel = pd.DataFrame({"y": y_mes.stack(), "x": x_mes.stack()}).dropna()
meses = panel.index.get_level_values("fecha").to_period("M")
fama = sm.OLS(panel["y"], sm.add_constant(panel["x"])).fit(
    cov_type="cluster", cov_kwds={"groups": meses.astype(str)})
b, eb = fama.params["x"], fama.bse["x"]
print(f"b = {b:.2f}  (standard error {eb:.2f})")
print(f"95% CI: [{b - 1.96 * eb:.2f}, {b + 1.96 * eb:.2f}]")

crash = meses.isin(pd.PeriodIndex(
    ["2008-09", "2008-10", "2008-11", "2008-12", "2010-05",
     "2011-08", "2011-09", "2015-08", "2015-09", "2020-02",
     "2020-03"], freq="M") - 1)   # x at month end, y from the next one
sin = sm.OLS(panel["y"][~crash], sm.add_constant(panel["x"][~crash]))
sin = sin.fit(cov_type="cluster",
              cov_kwds={"groups": meses[~crash].astype(str)})
print(f"without the crash months: b = {sin.params['x']:.2f} "
      f"(standard error {sin.bse['x']:.2f})")
```
```output
b = -0.53  (standard error 0.36)
95% CI: [-1.24, 0.18]
without the crash months: b = -0.09 (standard error 0.34)
```

The slope is −0.53, and its 95% interval goes from −1.24 to +0.18: it includes uncovered parity (−1), the random walk
(0) and the known answer (−0.3). Without the crash months, the slope rises to −0.09 with a standard error of 0.34: a few
months move the estimate by almost half a unit. It is the general lesson of the level applied to a famous regression:
the method is correct, but with ten currencies and 251 months it does not have the power to separate the hypotheses
that matter. That is why the return of a carry portfolio, which aggregates all the currencies and all the months into a
single series, is a more direct measure of the premium.

## A carry portfolio with next-day execution

The classic strategy ranks the currencies by differential and buys the high-yield ones against the low-yield ones.
Here, with ten currencies: at the close of the last day of each month they are ranked by differential, the three with
the highest rate are bought and the three with the lowest rate are sold, with a third of the capital in each. The
position enters in the **next session** (`shift(1)` on the daily weights) and is held until the next month end. Each
change in weight pays a cost of 3 basis points per side on the notional traded, a reasonable spread for liquid
currencies for an institutional client; for high-yield emerging-market currencies it is usually larger.

```python
puesto = dif.loc[fin_mes].rank(axis=1)
pesos_mes = ((puesto >= 8).astype(float)
             - (puesto <= 3).astype(float)) / 3
# signal at the month-end close, position from the next session
pesos = pesos_mes.reindex(spot.index, method="ffill").shift(1)

COSTE = 3 / 1e4                                   # per side
giro = pesos.diff().abs().sum(axis=1).fillna(pesos.abs().sum(axis=1))
bruto = (pesos * r_fx).sum(axis=1)
neto = (bruto - COSTE * giro)[pesos.abs().sum(axis=1) > 0]

def resumen(r, n=260):
    eq = (1 + r).cumprod()
    mensual = r.resample("ME").sum()
    return pd.Series({
        "rent_anual_pct": 100 * r.mean() * n,
        "vol_anual_pct": 100 * r.std() * np.sqrt(n),
        "sharpe": r.mean() / r.std() * np.sqrt(n),
        "asimetria_mensual": mensual.skew(),
        "peor_mes_pct": 100 * mensual.min(),
        "max_dd_pct": 100 * (eq / eq.cummax() - 1).min(),
    })

pd.DataFrame({"bruto": resumen(bruto[neto.index]),
              "neto": resumen(neto)}).round(2)
```
```output
                   bruto   neto
rent_anual_pct      3.56   3.50
vol_anual_pct       9.24   9.24
sharpe              0.38   0.38
asimetria_mensual  -1.85  -1.85
peor_mes_pct      -22.72 -22.72
max_dd_pct        -49.61 -49.68
```

Net of costs, the portfolio returns 3.50% a year with a volatility of 9.24%: a Sharpe of 0.38. Costs hardly matter
(from 3.56% to 3.50%), because the ranking by rates changes little from one month to the next and very little is
traded. What does matter are the other three rows: a **monthly skewness of −1.85**, a worst month of −22.7% and a
maximum drawdown of 49.7%. With a volatility of 9% and normal returns, a drawdown of half the capital would be almost
impossible; carry has one because its losses are not normal.

```python
partes = pd.DataFrame({
    "interes": (pesos * interes).sum(axis=1),
    "divisa": (pesos * d_spot).sum(axis=1),
    "costes": -COSTE * giro,
}).loc[neto.index]
(100 * partes.mean() * 260).round(2)
```
```output
interes    6.56
divisa    -3.00
costes    -0.05
dtype: float64
```

The portfolio collects 6.56% a year of differential and loses 3.00% on the exchange rates: it keeps somewhat more than
half of the carry. The known answer (the spot takes 30% of the differential, plus the crashes) predicts something
similar. Costs, 0.05 points a year, are irrelevant at this frequency.

## Carry crashes

The figure at the top is the curve of the net portfolio, with the generator's five crash periods shaded. The planted
mechanism is the one the literature describes: in a flight to quality, high-yield currencies fall and low-yield ones,
which act as a safe haven, rise. The portfolio is long the former and short the latter, so it loses on both sides at
once.

```python
crashes = {"2008 Q4": ("2008-09-15", "2008-12-31"),
           "2010 May": ("2010-05-03", "2010-05-31"),
           "2011 Aug-Sep": ("2011-08-01", "2011-09-30"),
           "2015 Aug-Sep": ("2015-08-17", "2015-09-30"),
           "2020 Feb-Mar": ("2020-02-24", "2020-03-31")}
en_crash = {k: 100 * neto[a:b].sum() for k, (a, b) in crashes.items()}
print(pd.Series(en_crash).round(1).to_string())
print()
mensual = neto.resample("ME").sum()
print((100 * mensual.nsmallest(5)).round(1).to_string())
```
```output
2008 Q4        -23.4
2010 May        -7.6
2011 Aug-Sep    -8.0
2015 Aug-Sep    -4.4
2020 Feb-Mar   -15.6

fecha
2009-02-28   -22.7
2020-03-31   -13.2
2008-11-30    -8.6
2022-08-31    -8.3
2010-05-31    -7.6
```

In the last quarter of 2008 the portfolio loses 23.4%, and in February and March 2020 15.6%: two episodes that each
take away several years of differential. The worst month, however, is not in any planted period: February 2009, with
−22.7%, is the 37% fall of `BOR` in one month, a tail draw from the Student's t innovations of the volatility model. It
is a useful reminder: the tail risk of a portfolio concentrated in a few currencies is not limited to the crashes you
know about, and a backtest with the stress periods removed “because they were exceptional” overestimates what can be
earned.

```mermaid
flowchart TB
  E[Market stress] --> V[Risk selling]
  V --> A[High yield falls]
  V --> B[Safe havens rise]
  A --> P[Carry loses]
  B --> P
  P --> L[Forced unwind]
  L --> A
```

The loop in the diagram is what makes carry crashes abrupt: losses force positions to be reduced, and the selling
pushes the same currencies even lower. For a system, the practical consequence is that the Sharpe and the volatility
do not describe the risk: you have to look at the skewness, the worst month and the losses in specific episodes, and
size the position to survive them (lesson 2.2.2).

## Futures carry: the slope of the curve

In a future, the carry is the slope of the curve between the contract held and the next one (lesson 1.2.3). If the
first trades above the second (backwardation), the carry is positive: with a still curve, the contract held rises
toward spot as it approaches expiry. `futuros2_carry.parquet` brings it computed for the 24 markets:

$$
c_t = \frac{\ln\left(F_{1,t} / F_{2,t}\right)}{\tau_2 - \tau_1}
$$

```python
cont = pd.read_parquet("futuros2_continuos.parquet")
carry = pd.read_parquet("futuros2_carry.parquet")
spec = pd.read_csv("futuros2_especificaciones.csv",
                   index_col="mercado")

px = cont.pivot(index="fecha", columns="mercado",
                values="cierre_ajustado")
c_anual = carry.pivot(index="fecha", columns="mercado",
                      values="carry_anual")
r_fut = np.log(px).diff()
(100 * c_anual.mean()).groupby(spec["sector"]).agg(
    ["mean", "min", "max"]).round(1)
```
```output
                mean   min  max
sector
agricolas       -3.6  -5.4 -1.4
bonos            1.1   0.3  2.2
divisas          0.6  -1.3  1.9
energia         -2.4 -10.3  4.0
metales         -2.7  -4.7 -0.6
renta_variable  -0.0  -0.1  0.1
```

Mean carry depends on the sector. Bonds have positive carry (1.1% on average), larger the longer the maturity;
agriculturals and metals, negative (−3.6% and −2.7%), like the contango from storage and financing of many
commodities; energy goes from −10.3% for gas to +4.0% for heavy crude; the indices, almost zero, because their carry in
the generator is 2% minus the risk-free rate, which on average is close to 2%. Ranking the 24 markets by carry is partly
ranking sectors, and partly something that changes over time: a commodity's carry moves with its price.

In this data carry has two parts, and they do not pay the same. Commodity, currency and bond prices revert toward a
reference level (an anchor that grows slowly), as in Schwartz's (1997) one-factor model[^1]. When spot is well above
the anchor, the curve goes into backwardation because the market already expects it to fall; when it is well below, into
contango. That part of the carry, the **reversion** part, is “fair”: it discounts an expected move in spot and barely
predicts the future's return. The other, the **exogenous** part (storage, financing, hedging pressure), is the one that
carries the premium. The generator's known answer, in annual terms, is:

$$
E[r] = 0.5\,c^{\text{exo}} + 0.1\,c^{\text{rev}} + 0.15\,\sigma\,\operatorname{sign}\left(r_{252}\right)
$$

plus the equity premium in the indices and a fall in bonds in 2022. The last term is a trend premium: the market's
volatility times the sign of its return over the previous 252 sessions. Only the sum $c = c^{\text{exo}} +
c^{\text{rev}}$ is observed, and the reversion part is correlated with the trend: a market that has been rising for a
year is above its anchor, in backwardation and with a positive trend. A regression on carry alone mixes the three
things; with trend at the same time, it at least separates the last one.

```python
fin_f = px.index.to_series().groupby(px.index.to_period("M")).max()
r_sig = np.log(px.loc[fin_f]).diff().shift(-1) * 12
sigma = r_fut.std() * np.sqrt(252)          # σ of each market
tend = sigma * np.sign(np.log(px).diff(252)).loc[fin_f]
datos_c = pd.DataFrame({"y": r_sig.stack(),
                        "carry": c_anual.loc[fin_f].stack(),
                        "tend": tend.stack()}).dropna()
grupos = datos_c.index.get_level_values("fecha").astype(str)
for x in (["carry"], ["carry", "tend"]):
    m = sm.OLS(datos_c["y"], sm.add_constant(datos_c[x])).fit(
        cov_type="cluster", cov_kwds={"groups": grupos})
    print(", ".join(f"{k} {m.params[k]:.2f} (se {m.bse[k]:.2f})"
                    for k in x))
print(f"n = {len(datos_c)}; carry-trend correlation: "
      f"{datos_c['carry'].corr(datos_c['tend']):.2f}")
```
```output
carry 0.27 (se 0.13)
carry 0.17 (se 0.14), tend 0.11 (se 0.08)
n = 7176; carry-trend correlation: 0.49
```

On its own, the carry slope is 0.27 with a standard error of 0.13: neither the 0.5 of the exogenous part nor the 0.1 of
the reversion part, but a mix of the two. With trend in the regression, carry drops to 0.17 and trend comes out at 0.11
(truth: 0.15). Part of what the simple regression attributed to carry was momentum, because the two variables have a
correlation of 0.49 at month end. The data README gives 0.26, 0.15 and 0.12 with the volatility from its table and
another annualization: the same conclusion. The $\sigma$ in the cell is each market's whole-sample volatility, a
constant scale factor like the one in the table; it is fine for measuring, but for trading you would have to use that
of the previous window.

The standard errors are clustered by month because the 24 markets share their surprises, and they are large: with
carry and trend together, neither coefficient reaches two standard errors. The 7,176 market-months look like a lot,
but they are 299 months of 24 correlated markets. And the regression cannot separate the two parts of carry, because it
only sees the sum: the 0.5 planted in the exogenous part is not recovered with a regression on observed carry.

*Fig. — Month-end carry and next-month return in the 24 futures, in twenty carry groups.* Scatter of twenty points: each is the mean of about 360 market-months grouped by carry, from -39% to +28% a year on the horizontal axis, against their annualised next-month return. The points rise from left to right with a lot of noise. A highlighted line marks the regression fit, slope 0.27; a steeper dashed one the 0.5 slope that would appear if all carry were the exogenous part, the one that earns the premium in the generator.

The figure groups the observations into twenty buckets by carry and plots the mean next-month return of each one, with
the fitted line and, dashed, the one with slope 0.5 that would be seen if all the carry were exogenous. The relationship
is visible, but so is its noise: each point is the mean of about 360 market-months and even so it sits several
percentage points away from the line.

### A futures carry portfolio with per-tick costs

The portfolio buys the eight markets with the highest carry and sells the eight with the lowest, with a weight inverse
to their 63-session volatility so that each position contributes a similar risk (2.5% a year). It is rebuilt at the
close of each month and enters in the next session. Costs come from the specifications: `coste_tipico_ticks` ticks of
slippage plus the commission, per contract and side, divided by the contract's notional (price × multiplier). In
addition, each **roll** (the change of contract) pays two sides on the whole position.

```python
precio = cont.pivot(index="fecha", columns="mercado", values="cierre")
contrato = cont.pivot(index="fecha", columns="mercado",
                      values="contrato")
coste_pb = ((spec["coste_tipico_ticks"] * spec["valor_tick"]
             + spec["comision_usd"])
            / (precio * spec["multiplicador"]) * 1e4)
coste_pb.median().sort_values().round(1).iloc[[0, 1, 2, -3, -2, -1]]
```
```output
mercado
IDB     0.7
DIA     0.8
BOA     0.9
AGA    10.9
AGB    11.3
MEB    13.2
dtype: float64
```

In the financial markets, one side costs less than one basis point of notional: 0.7 in the `IDB` index, 0.8 in the
`DIA` currency and 0.9 in the 2-year bond `BOA`. In the contracts whose tick and commission weigh more relative to the
notional, the cost is multiplied by more than ten: 10.9 basis points in corn `AGA`, 11.3 in wheat `AGB` and 13.2 in
silver `MEB`. Moreover, a cost measured in ticks depends on the price: if a contract's price halves, its cost in
percentage terms doubles. It is a detail that a backtest with a fixed cost in basis points does not see (lesson 2.4.7).

```python
vol = r_fut.rolling(63).std() * np.sqrt(252)
puesto_f = c_anual.loc[fin_f].rank(axis=1)
n_f = puesto_f.notna().sum(axis=1)
lado = ((puesto_f > n_f.to_numpy()[:, None] - 8).astype(float)
        - (puesto_f <= 8).astype(float))
pesos_f = (lado * 0.025 / vol.loc[fin_f]).fillna(0)
pesos_f = pesos_f.reindex(px.index, method="ffill").shift(1)

roll = contrato.ne(contrato.shift(1)) & contrato.shift(1).notna()
coste = (pesos_f.diff().abs() * coste_pb
         + 2 * pesos_f.abs() * coste_pb * roll) / 1e4
bruto_f = (pesos_f * r_fut).sum(axis=1)["2001":]
neto_f = bruto_f - coste.sum(axis=1)["2001":]
pd.DataFrame({"bruto": resumen(bruto_f, 252),
              "neto": resumen(neto_f, 252)}).round(2)
```
```output
                   bruto   neto
rent_anual_pct      7.36   5.71
vol_anual_pct      11.64  11.64
sharpe              0.63   0.49
asimetria_mensual   0.01   0.01
peor_mes_pct      -12.67 -12.78
max_dd_pct        -32.03 -33.71
```

Gross, the portfolio returns 7.36% a year with a Sharpe of 0.63; net, 5.71% and 0.49. Costs, 1.65 points a year, take
somewhat more than a fifth of the premium: the monthly rebalance changes few positions, but each roll pays two sides on
the whole position, and in the expensive markets of the previous cell that adds up. Unlike currency carry, the monthly
skewness is almost zero (0.01): in these futures, the high- and low-carry markets do not all fall at once in the
script's crises. The maximum drawdown is 33.7%, and the result is not stable over time:

```python
periodos = [("2001", "2009"), ("2010", "2019"), ("2020", "2025")]
sh = {f"{a}-{b}": resumen(neto_f[a:b], 252)["sharpe"]
      for a, b in periodos}
print(pd.Series(sh).round(2).to_dict())
sector = spec["sector"].reindex(px.columns)
peso_22 = pesos_f.loc["2022"].mean().groupby(sector).sum()
res_22 = (pesos_f * r_fut).loc["2022"].sum().groupby(sector).sum()
pd.DataFrame({"peso_medio_2022": peso_22,
              "resultado_2022_pct": 100 * res_22}).round(2)
```
```output
{'2001-2009': 0.79, '2010-2019': 0.58, '2020-2025': -0.06}
                peso_medio_2022  resultado_2022_pct
sector
agricolas                 -0.08                0.50
bonos                      1.35              -23.90
divisas                   -0.15                2.23
energia                    0.03               -4.42
metales                   -0.21               -1.56
renta_variable             0.06                0.31
```

The net Sharpe is 0.79 in 2001-2009, 0.58 in 2010-2019 and −0.06 in 2020-2025. The last period has a clear culprit: in
2022 the portfolio is long bonds with an average weight of 135% of capital, and bonds lose 23.9 points that year (the
generator plants a fall in bonds in 2022, like the one in real markets). Nobody chose that weight: it comes from
weighting by inverse volatility. A 2-year bond with a volatility of 2% receives a notional about seventeen times larger
than a crude with 35% to contribute the same risk in calm conditions, and when bonds fall together, as in 2022, the
risk that was supposed to be spread out was concentrated. The data README measures a somewhat different portfolio
(half the markets against the other half, weights of 10% over volatility) and finds the same: a Sharpe of 0.45 over
the whole sample and negative in 2020-2025.

The portfolio, moreover, is not a pure bet on carry. With the “fair” reversion inside the signal and a correlation of
0.49 with trend, part of what it earns is disguised momentum, and part of what it pays is reversion that the curve had
already discounted. Knowing which premium is being collected matters for combining it with other strategies (lesson
2.3.1).

## Bonds: carry and roll-down

In a financed bond, carry has two parts. The first is the yield minus the rate at which the position is financed. The
second, the **roll-down**: with an upward-sloping curve, a 10-year bond is a 9-year bond after one year, which is priced
with a lower yield, and its price rises even though the curve has not moved. For a horizon $h$ (in years), with a
still curve and modified duration $D$:

$$
\text{carry} \approx (y_T - r)\,h + D_{T-h}\,\left(y_T - y_{T-h}\right)
$$

The level's bond futures do not come with a yield curve, so the calculation is done with an example curve: a 10-year
bond with a coupon equal to its yield, valued today and one year from now with the same curve.

```python
plazos = np.array([1, 2, 5, 7, 9, 10, 30])
curva = np.array([3.50, 3.55, 3.80, 3.98, 4.16, 4.25, 4.70]) / 100
r_fin = 0.034                         # one-year financing

def precio_bono(cupon, rend, anos):
    t = np.arange(1, anos + 1)
    return (cupon / (1 + rend) ** t).sum() + 1 / (1 + rend) ** anos

y10 = np.interp(10, plazos, curva)
y9 = np.interp(9, plazos, curva)
hoy = precio_bono(y10, y10, 10)                  # = 1 (at par)
en_un_ano = precio_bono(y10, y9, 9)              # same curve
total = (en_un_ano + y10) / hoy - 1
print(f"coupon {100 * y10:.2f}%, roll-down "
      f"{100 * (en_un_ano / hoy - 1):.2f}%, "
      f"minus financing {100 * r_fin:.2f}%")
print(f"total one-year carry: {100 * (total - r_fin):.2f}%")
```
```output
coupon 4.25%, roll-down 0.66%, minus financing 3.40%
total one-year carry: 1.51%
```

The bond collects its 4.25% coupon and gains 0.66% in price as it rolls from 10 to 9 years; financed at 3.40%, the
total one-year carry is 1.51%. The formula's approximation gives the same: (4.25 − 3.40)% for one year, plus a duration
of about 7.4 times the 9 basis points that separate the 10- and 9-year yields.

*Fig. — Roll-down of a 10-year bond: with an unchanged curve, in one year it is priced at the 9-year yield (schematic).* Example yield curve, from 3.50% at 1 year to 4.70% at 30, upward sloping. A highlighted point marks the bond today, at 10 years and 4.25%; another, the same bond in one year, at 9 years and 4.16%. The segment between them is the roll-down, a 0.66% price gain. A dashed horizontal line marks funding at 3.40%.

The figure draws it on the example curve: the bond starts at the 10-year maturity and, with a still curve, ends the year
at the 9-year one, with a lower yield. The steeper the segment it rolls down, the larger the roll-down, and the longer
the duration, the more price each basis point gives. A bond carry strategy chooses maturities with the highest carry
plus roll-down per unit of duration. In bond futures that carry is already in the slope of the futures curve: it is what
the `bonos` rows of the carry by sector measure.

## Seasonality and calendar effects: the control

Lesson 1.6.3 measured the classic calendar effects (day of the week, month, turn of the month) and showed why almost
none holds up. Here they serve as a **control**: the level 2 generator plants no calendar effect at all in the equity
indices. Anything that shows up is chance or an artifact of the data. The five index futures (`IDA` to `IDE`) are
tested, comparing each day with the rest, not with zero:

```python
from scipy import stats

indices = ["IDA", "IDB", "IDC", "IDD", "IDE"]
ri = r_fut[indices].dropna()
dia = ri.index.dayofweek
filas = []
for m in indices:
    for d, nombre in enumerate(["Mon", "Tue", "Wed", "Thu", "Fri"]):
        t, p = stats.ttest_ind(ri[m][dia == d], ri[m][dia != d],
                               equal_var=False)
        filas.append({"indice": m, "dia": nombre, "t": t, "p": p})
dow = pd.DataFrame(filas)
print("p < 0.05:", (dow["p"] < 0.05).sum(), "of", len(dow),
      "| expected by chance:", 0.05 * len(dow))
print(dow.nsmallest(3, "p").round(3).to_string(index=False))
print("mean correlation between indices:",
      round(ri.corr().to_numpy()[np.triu_indices(5, 1)].mean(), 2))
```
```output
p < 0.05: 1 of 25 | expected by chance: 1.25
indice dia      t     p
   IDD Thu  2.132 0.033
   IDE Thu  1.571 0.116
   IDD Mon -1.402 0.161
mean correlation between indices: 0.81
```

One of the 25 tests has p < 0.05 (`IDD` on Thursdays, p = 0.033), where chance expects 1.25. And the 25 are not
independent: the five indices have a mean correlation of 0.81, so when one comes out “almost significant” on a day, the
others tend to point in the same direction. The effective number of tests is smaller than it seems, and that of false
positives, more variable.

```python
g = ri.groupby(ri.index.to_period("M"))
cambio = (g.cumcount() < 3) | (g.cumcount(ascending=False) == 0)
estres = pd.Series(False, index=ri.index)
for a, b in [("2000-09-01", "2002-10-09"), ("2008-09-02", "2009-03-09"),
             ("2010-04-26", "2010-07-05"), ("2011-07-25", "2011-10-03"),
             ("2015-08-17", "2016-02-11"), ("2018-10-01", "2018-12-25"),
             ("2020-02-20", "2020-03-23"), ("2022-01-04", "2022-10-12"),
             ("2025-02-20", "2025-04-08")]:
    estres[a:b] = True

def tom(r, sel):
    a, b = r[cambio & sel], r[~cambio & sel]
    p = stats.ttest_ind(a, b, equal_var=False).pvalue
    return 1e4 * a.mean(), 1e4 * b.mean(), p

filas = {}
for m in indices:
    for nombre, sel in [("all", estres | ~estres),
                        ("calm", ~estres), ("stress", estres)]:
        filas[(m, nombre)] = tom(ri[m], sel)
pd.DataFrame(filas, index=["cambio_pb", "resto_pb", "p"]).T.round(3)
```
```output
            cambio_pb  resto_pb      p
IDA all        -3.918     4.029  0.061
    calm        3.925     8.931  0.192
    stress    -37.550   -18.249  0.202
IDB all        -3.288     4.062  0.146
    calm        6.372     7.828  0.757
    stress    -44.710   -13.054  0.069
IDC all        -1.426     3.165  0.337
    calm        8.367     8.104  0.954
    stress    -43.421   -19.280  0.122
IDD all        -4.112     4.007  0.079
    calm        3.014     9.480  0.126
    stress    -34.670   -20.870  0.400
IDE all        -2.185     3.622  0.208
    calm        7.813     7.804  0.998
    stress    -45.058   -15.388  0.064
```

The turn of the month (the last day and the first three of each month) returns less than the rest in all five indices,
although in none with p < 0.05 (the lowest, `IDA`, with 0.061). Split by period, the difference almost disappears in
calm conditions (no p falls below 0.12) and concentrates in the stress periods, where turn-of-the-month sessions fall
on average between 35 and 45 basis points, 1.7 to 3.4 times what the other sessions fall. They are a few crisis
sessions with large falls, and several of the script's stress periods start on the first day of a month. Five
differences with the same sign look like a confirmation, but with indices correlated at 0.81 they are almost the same
test repeated five times. A data artifact that looks like a calendar effect: exactly what you need to know how to
recognize.

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

A calendar effect discovered in the data that had not been formulated beforehand is a hypothesis, not a result. If you
want to trade it, you have to write down the hypothesis with its mechanism (payroll flows, fund rebalancing,
publication schedules), register it (lesson 2.2.4) and test it on data that was not used to find it. Lesson 2.1.8 gives
the corrections for when many are tested at once.

</aside>

## What carry demands in real trading

- **Execution and costs.** The signal is known at the close and the position enters in the next session. In
  currencies, the spread of the high-yield ones widens precisely in the crashes; in futures, low-priced contracts pay a
  lot per tick, and each roll is a fixed cost the carry has to pay (lesson 2.4.7).
- **Real financing.** The differential collected is that of the rates at which the broker or the bank pays and charges,
  not the official one. In a CFD or a retail forex account, the rollover deducts a margin on both rates (lesson 1.2.4)
  that can eat a good part of the carry.
- **Tail risk.** The average result hides a distribution with negative skewness. A carry portfolio has to be sized by
  its worst month, not by its volatility, and it is worth combining it with strategies that win in crises (the trend
  following of lesson 2.3.1 does so in this data).
- **Concentration by sector and by notional.** Futures carry ranked without further adjustment is partly a bet on
  sectors, and with inverse-volatility weights bonds take up the notional: in 2022, the lesson's portfolio had on
  average 135% of its capital in bonds. Neutralizing by sector (exercise 1) and capping the notional per sector change
  the portfolio a lot.

## Exercises

1. Repeat the futures carry portfolio ranking carry **within each sector** (subtract the sector mean at each date
   before ranking). Do the Sharpe and the skewness change?

**Solution.**

```python
sector = spec["sector"].reindex(c_anual.columns)
c_rel = c_anual.loc[fin_f] - c_anual.loc[fin_f].T.groupby(
    sector).transform("mean").T
puesto_s = c_rel.rank(axis=1)
lado_s = ((puesto_s > n_f.to_numpy()[:, None] - 8).astype(float)
          - (puesto_s <= 8).astype(float))
pesos_s = (lado_s * 0.025 / vol.loc[fin_f]).fillna(0)
pesos_s = pesos_s.reindex(px.index, method="ffill").shift(1)
coste_s = (pesos_s.diff().abs() * coste_pb
           + 2 * pesos_s.abs() * coste_pb * roll) / 1e4
neto_s = ((pesos_s * r_fut).sum(axis=1)
          - coste_s.sum(axis=1))["2001":]
pd.DataFrame({"global": resumen(neto_f, 252),
              "por_sector": resumen(neto_s, 252)}).round(2)
```
```output
                   global  por_sector
rent_anual_pct       5.71        2.33
vol_anual_pct       11.64        8.99
sharpe               0.49        0.26
asimetria_mensual    0.01        0.21
peor_mes_pct       -12.78       -7.17
max_dd_pct         -33.71      -34.07
```

Sector-neutral, the Sharpe drops from 0.49 to 0.26 and the return, from 5.71% to 2.33% a year. Somewhat more than half
of the premium in this data is **between** sectors: the mean exogenous carries are different (gas in deep contango,
bonds with positive carry) and the global portfolio takes advantage of them. The rest is **within** each sector, where
each market's carry moves with its price. Volatility falls (from 11.64% to 8.99%) and so does the worst month (from
−12.78% to −7.17%), but not the maximum drawdown (34.07%), and the monthly skewness turns positive (0.21). In real
markets, Koijen and coauthors (2018) find the carry premium within each asset class as well.

2. In the currency carry portfolio, raise the cost to 10 basis points per side and change the rebalance to weekly (last
   day of each week). How much Sharpe is lost in each case?

**Solution.**

```python
def cartera_fx(fechas, coste):
    pt = dif.loc[fechas].rank(axis=1)
    pm = ((pt >= 8).astype(float) - (pt <= 3).astype(float)) / 3
    w = pm.reindex(spot.index, method="ffill").shift(1)
    giro = w.diff().abs().sum(axis=1)
    r = (w * r_fx).sum(axis=1) - coste / 1e4 * giro
    return r[w.abs().sum(axis=1) > 0]

fin_sem = (spot.index.to_series()
           .groupby(spot.index.to_period("W")).max())
casos = {"monthly, 3 bp": cartera_fx(fin_mes, 3),
         "monthly, 10 bp": cartera_fx(fin_mes, 10),
         "weekly, 3 bp": cartera_fx(fin_sem, 3),
         "weekly, 10 bp": cartera_fx(fin_sem, 10)}
pd.Series({k: resumen(v)["sharpe"] for k, v in casos.items()}).round(2)
```
```output
monthly, 3 bp     0.38
monthly, 10 bp    0.37
weekly, 3 bp      0.35
weekly, 10 bp     0.33
dtype: float64
```

The Sharpe barely moves: from 0.38 to 0.37 with 10 basis points, and to 0.35 or 0.33 with weekly rebalancing. The
portfolio changes currencies so rarely that costs do not decide it; weekly rebalancing adds turnover without new
information, because rates only change once a month. In a carry strategy, the natural frequency is that of the signal:
rebalancing faster than it only pays costs.

3. A colleague finds that `IDA` rises 3% on average in November (t = 2.5) and proposes buying the index only in that
   month. Explain what is missing before taking it seriously.

**Solution.**

It is one of twelve months tested on one index, and the same test on the other four indices, which have a correlation
of 0.8 with it, is not an independent test: the effective number of tests is large and one with t = 2.5 is what you
would expect by chance (with twelve tests at 5%, the probability of at least one significant is 46%). A monthly effect
is measured with one data point per year: 26 Novembers, of which one or two years of rebound after a crisis are enough
to move the mean. What is missing is a prior hypothesis with a mechanism, the multiple-testing correction (lesson 2.1.8)
and an out-of-sample test. In this data, moreover, the answer is known: the generator plants no calendar effect at all.

[^1]: Eduardo S. Schwartz, “The Stochastic Behavior of Commodity Prices: Implications for Valuation and Hedging”,
    The Journal of Finance 52(3), 1997, <https://doi.org/10.1111/j.1540-6261.1997.tb02721.x>.

## Self-assessment

1. In the Fama regression on the data, the change in the exchange rate on the interest rate differential gives a slope of −0.53 with a standard error of 0.36. What do you conclude?

   - A. That uncovered interest rate parity holds (slope −1)
   - B. That the exchange rate is a random walk (slope 0)
   - C. That the regression is too imprecise to separate −1, −0.3 and 0 with 21 years of data; the portfolio return is a more direct measure
   - D. That carry does not make money

   *Answer C: That the regression is too imprecise to separate −1, −0.3 and 0 with 21 years of data; the portfolio return is a more direct measure.* With a standard error of 0.36, the 95% interval goes from −1.24 to +0.18: it includes parity (−1), the known answer (−0.3) and the random walk (0). The crashes, moreover, push the slope toward more negative values. The Fama regression is the academic benchmark, but with ten currencies and twenty years it decides nothing on its own.

2. The currency carry portfolio has a monthly skewness of −1.8. What does it mean for whoever trades it?

   - A. That it earns little and often and loses a lot all at once: the worst months coincide with flights to quality
   - B. That it loses money almost every month
   - C. That the Sharpe is overestimated because of a calculation error
   - D. That its returns are normal

   *Answer A: That it earns little and often and loses a lot all at once: the worst months coincide with flights to quality.* A large negative skewness is the signature of carry: the differential is collected day by day and the high-yield currencies all fall at once when the market seeks safety. In the data, the last quarter of 2008 and February 2009 take away several years of gains.

3. A future trades at 100 with expiry in 2 months and the next one at 101 with expiry in 5 months. What is its annual carry with the lesson's definition?

   - A. +4% (backwardation)
   - B. +1%
   - C. 0%
   - D. −4% (contango: the long loses if the curve does not change)

   *Answer D: −4% (contango: the long loses if the curve does not change).* c = ln(100/101) / (5/12 − 2/12) = −0.00995 / 0.25 ≈ −4% a year. The curve rises with maturity (contango): if it does not move, the contract held comes to be priced at a shorter maturity, which is cheaper, and the long loses that 4% a year.

4. The day-of-the-week effect is tested on five stock indices: 25 tests and one comes out with p = 0.04. What do you do?

   - A. Trade that day
   - B. Nothing: with 25 tests at 5% you expect 1.25 significant by chance, and the five indices also have a correlation of 0.8, so they are not 25 independent tests
   - C. Repeat the test with more decimal places
   - D. Remove the bad years and test again

   *Answer B: Nothing: with 25 tests at 5% you expect 1.25 significant by chance, and the five indices also have a correlation of 0.8, so they are not 25 independent tests.* A single p = 0.04 among 25 tests is exactly what chance gives. In this data no calendar effect is planted: it is the control. The multiple-testing correction (lesson 2.1.8) leaves it very far from any threshold.

5. Which part of the return of a 10-year bond is roll-down?

   - A. The coupon
   - B. The change in central bank rates
   - C. The price gain obtained, with an unchanged curve, because the bond ages and comes to be priced at the lower yield of a shorter maturity
   - D. The difference between the clean price and the dirty price

   *Answer C: The price gain obtained, with an unchanged curve, because the bond ages and comes to be priced at the lower yield of a shorter maturity.* With an upward-sloping curve, a 10-year bond is a 9-year bond one year later. If the curve does not move, its yield falls from the 10-year to the 9-year one and its price rises: that is the roll-down. The coupon minus the financing is carry in the strict sense.

## Key points

- Carry is the return of a position if prices do not change: interest rate differential in currencies, curve slope in futures, coupon minus financing plus roll-down in bonds.
- In the data, currency carry earns 70% of the differential (the known answer); the three-against-three portfolio returns a Sharpe of 0.38 net of costs, with a monthly skewness of −1.85 and a maximum drawdown of 50%.
- The slope of futures returns on their carry comes out at 0.27 (standard error 0.13), less than the 0.5 of the exogenous part: a good part of the carry is mean reversion that the curve already discounts; with trend in the regression, it drops to 0.17.
- Carry loses all at once in flights to quality: the risk is in the tails, not in the volatility, and the Sharpe hides it.
- The signal is computed at the close and the position starts in the next session. The futures carry portfolio gives a net Sharpe of 0.49, but negative in 2020-2025: long bonds with inverse-volatility weights, it loses in 2022 when bonds fall.
- The calendar effects in the data are the control: the few significant ones that show up are what chance gives among dozens of correlated tests.

## Resources

### Research

- [Impact of Seasonal Factor on US Bonds](https://pythonparatrading.com/en/seasonality-usa-yields/) · Research-0017 · 2 min
- [Central Banks and Monetary Policy in Python](https://pythonparatrading.com/en/monetary-policy-in-python/) · Research-0010 · 14 min

### Documentation

- [Koijen, Moskowitz, Pedersen and Vrugt (2018): Carry, Journal of Financial Economics 127(2)](https://doi.org/10.1016/j.jfineco.2017.11.002) · doi.org
- [Fama (1984): Forward and spot exchange rates, Journal of Monetary Economics 14(3)](https://doi.org/10.1016/0304-3932(84)90046-1) · doi.org
- [Brunnermeier, Nagel and Pedersen (2008): Carry Trades and Currency Crashes, NBER Macroeconomics Annual 23](https://doi.org/10.1086/593088) · doi.org
- [statsmodels: OLS with clustered errors (cov_type="cluster")](https://www.statsmodels.org/stable/generated/statsmodels.regression.linear_model.RegressionResults.get_robustcov_results.html) · statsmodels.org

### Data

- [Level 2 data: currencies with interest rates (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/fx2_carry.parquet) · Download · fx2_carry.parquet

Previous lesson: [2.3.3 · Pairs trading and statistical arbitrage: distance, cointegration and Kalman](https://pythonparatrading.com/en/python-trading-course/level-2/pairs-trading-statistical-arbitrage/)

Next lesson: [2.3.5 · Factors (value, quality, low volatility, size) and event strategies](https://pythonparatrading.com/en/python-trading-course/level-2/factor-investing-and-event-strategies/)
