---
title: "2.3.1 Trend following and momentum"
description: "Time series momentum with volatility scaling, crossovers, Donchian and Turtle breakouts, 12-1 cross-sectional momentum and sector rotation."
url: "https://pythonparatrading.com/en/python-trading-course/level-2/trend-following-and-momentum/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-2/trend-following-and-momentum.png"
---

# 2.3.1 · Trend following and momentum

> Two families with the same name: buying what rises against its own history (trend) or against the others (cross-sectional momentum). This lesson builds both with costs and next-session execution, on 24 futures and 120 synthetic stocks with the premium planted, and compares them with controls that have none.

- Course: [Level 2: Systematic strategies and rigorous backtesting](https://pythonparatrading.com/en/python-trading-course/level-2/)
- Module: 2.3 · Strategy families
- Duration: 75 min
- Prerequisites: [1.4.6 · Continuous futures series: roll rules, difference adjustment and ratio adjustment](https://pythonparatrading.com/en/python-trading-course/level-1/continuous-futures-series-and-roll-adjustment/), [1.4.4 · Universes without survivorship bias: delistings, mergers and symbol changes](https://pythonparatrading.com/en/python-trading-course/level-1/survivorship-bias-and-point-in-time-data/), [1.6.1 · Returns, volatility, rolling correlations and drawdown](https://pythonparatrading.com/en/python-trading-course/level-1/returns-volatility-correlation-and-drawdown/)

## By the end of this lesson

- Code time series momentum with volatility scaling, a moving-average crossover and a Turtle-style Donchian breakout, with per-side costs, roll costs and next-session execution.
- Check with permutation controls how much of the result is trend and how much is the mean drift of each market.
- Check whether trend following has a “smile” against equities and in which crises it wins or loses.
- Evaluate 12-1 cross-sectional momentum with rank ICs, decile portfolios, delisting returns and a placebo, and explain why the linear IC and the decile spread exaggerate.
- Recognize momentum crashes after market bottoms and the statistical limit of a rotation across ten sectors.

## Two ideas with the same name

“Momentum” is used for two different strategies that share an intuition: what has gone up tends to keep going up for
a while. The difference lies in what it is compared against.

- **Time series momentum** (TSMOM, or simply **trend**): each asset is compared with its own past. If the crude oil
  future has risen over the last twelve months, you buy; if it has fallen, you sell. Each market is decided
  separately and the portfolio can be long or short everything at once. It is what CTAs and managed futures funds do.
- **Cross-sectional momentum**: assets are compared with each other. Each month the stocks are ranked by their past
  return, the best are bought and the worst are sold. The portfolio is neutral by construction: it wins if the winners
  beat the losers, even if the whole market falls.

```mermaid
flowchart TB
  P[Past prices] --> TS[Against own past]
  P --> XS[Against the others]
  TS --> S1[Sign per asset]
  XS --> S2[Cross-sectional rank]
  S1 --> R1[Net long or short]
  S2 --> R2[Neutral long-short]
```

Both have broad academic evidence in real data. Jegadeesh and Titman documented in 1993 that buying winners and
selling losers of the previous 3 to 12 months gave significant positive returns in US stocks, and that part of it was
undone over the following two years[^1]. Moskowitz, Ooi and Pedersen found in 2012 time series momentum in each of the
58 liquid futures they studied (indices, currencies, commodities and bonds): persistence of 1 to 12 months that partly
reverses at longer horizons, and a diversified portfolio that performs best in extreme markets[^2]. This lesson does
not repeat those figures: it builds the strategies with the method of those papers and tests them on data where the
answer is known.

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

The data is **synthetic and the effect is planted**. In the futures, the generator adds to the expected annual return
`0.15·σ·sign(return over the previous 252 sessions)`: a trend Sharpe of ≈ 0.15 per market. In the stocks, a monthly
premium of `0.0025·z` on the rank score of 12-1 momentum, and crashes in the three months after the 2009 and 2020
bottoms. A rule winning here shows that the code recovers it, not that it works in a real market.

</aside>

## Futures: returns, rolls and costs

The 24 markets in `futuros2_continuos.parquet` cover indices, bonds, energy, metals, agriculturals and currencies from
2000 to 2025. The `cierre_ajustado` column is the continuous series with backward proportional adjustment at each roll:
its percentage change is the return of holding the future (lesson 1.4.6). The `contrato` column says which expiry is
held each day, and with it you detect the rolls, which cost money.

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

fut = pd.read_parquet("futuros2_continuos.parquet")
spec = pd.read_csv("futuros2_especificaciones.csv")
spec = spec.set_index("mercado")
ancho = lambda col: fut.pivot(index="fecha", columns="mercado",
                              values=col)
px, cierre, contrato = (ancho("cierre_ajustado"), ancho("cierre"),
                        ancho("contrato"))
r = px.pct_change()
roll = (contrato != contrato.shift()).astype(float)
roll.iloc[0] = 0.0
print(px.shape[1], "markets,", len(px), "sessions,",
      px.index[0].date(), "to", px.index[-1].date())
rolls_ano = (roll.sum() / 26).round().astype(int)
print("rolls per year:", rolls_ano.value_counts().sort_index().to_dict())
```
```output
24 markets, 6539 sessions, 2000-01-03 to 2025-12-31
rolls per year: {4: 14, 5: 5, 6: 1, 7: 1, 12: 3}
```

Four rolls a year in the financials (quarterly expiries), twelve in energy (monthly) and between four and seven in
metals and agriculturals. Each roll is two trades: closing the expiring contract and opening the next one.

The cost of a trade comes from the specifications: a typical slippage in ticks per side plus the commission per
contract. To express it as a fraction of the notional you have to divide by the contract value,
`close × multiplier`:

$$
c_{\text{side}} = \frac{n_{\text{ticks}} \cdot \text{tick}}{F} + \frac{\text{commission}}{F \cdot \text{multiplier}}
$$

```python
c_lado = (spec["coste_tipico_ticks"] * spec["tick"] / cierre
          + spec["comision_usd"] / (cierre * spec["multiplicador"]))
pb = (1e4 * c_lado.median()).sort_values()
print(pb.round(1).iloc[[0, 1, 2, 12, 21, 22, 23]].to_string())
print(f"median of the 24 markets: {pb.median():.1f} bp")
```
```output
mercado
IDB     0.7
DIA     0.8
BOA     0.9
BOE     3.1
AGA    10.9
AGB    11.3
MEB    13.2
median of the 24 markets: 2.7 bp
```

The output shows the three cheapest, the market in the middle and the three most expensive. The median of the 24 is
2.7 basis points per side, but the extremes are almost twenty times apart for a reason worth remembering with real
data: the tick is fixed in price. In the IDB index, a 0.25-point tick on a median price of about 5,000 weighs half a
basis point; in silver (MEB), a 0.005 USD tick on about 7.9 USD weighs more than 6, and the specifications assume two
ticks of slippage per side: 13.2 bp. The relevant cost is not “one tick”, but one tick divided by the price, and it
changes over time: wheat (AGB) moves in this data between 1.86 and 11.58 USD, so the same tick costs it six times more
in percentage terms at its low than at its peak.

## Time series momentum with volatility scaling

The Moskowitz, Ooi and Pedersen rule has two pieces. The signal is the sign of the return over the last twelve months.
The sizing gives each market the same ex ante risk: the position is a volatility target (40% a year in the paper)
divided by the volatility estimated with an exponential average of squared daily returns, with a center of mass of 60
sessions and data up to the previous day[^2]:

$$
w_{i,t} = \operatorname{sign}\left(\frac{P_{i,t}}{P_{i,t-252}} - 1\right) \cdot \frac{40\%}{\hat\sigma_{i,t}}
$$

Without that scaling, natural gas (50% annual volatility in this data) would weigh twenty-five times more than the
2-year bond (2%). With it, the average of the 24 markets is a portfolio in which each one contributes a similar risk.

Execution is the same as in the whole level: the signal is computed with the close of session `t` and traded at the
close of `t+1`; the position gains or loses from there. Costs are charged on each change of position and on the whole
position at each roll (two sides).

```mermaid
flowchart LR
  C[Close t] --> S[Signal and size]
  S --> E[Order close t+1]
  E --> G[Earns from t+1]
```

```python
def backtest(senal, r, objetivo=0.40):
    """Signal in {-1, 0, 1} decided at the close of t; traded at t+1."""
    vol = r.ewm(com=60, min_periods=60).std() * np.sqrt(252)
    w = (senal * objetivo / vol).clip(-4, 4)
    pos = w.shift(1).fillna(0.0)          # held from the close of t+1
    bruto = (pos.shift(1) * r).fillna(0.0)
    giro = (pos - pos.shift()).abs()
    coste = (giro * c_lado + pos.abs() * roll * 2 * c_lado).fillna(0.0)
    return bruto.mean(axis=1), coste.mean(axis=1), pos

def sharpe(x):
    return x.mean() / x.std() * np.sqrt(252)

def resumen(bruto, coste):
    neto = bruto - coste
    return pd.Series({
        "sharpe_bruto": sharpe(bruto), "sharpe_neto": sharpe(neto),
        "vol_anual": neto.std() * np.sqrt(252),
        "coste_anual": coste.mean() * 252,
        "max_dd": ((1 + neto).cumprod()
                   / (1 + neto).cumprod().cummax() - 1).min()})

tsmom = np.sign(px / px.shift(252) - 1)
b_ts, c_ts, pos_ts = backtest(tsmom, r)
resumen(b_ts, c_ts).round(3)
```
```output
sharpe_bruto    0.946
sharpe_neto     0.772
vol_anual       0.105
coste_anual     0.018
max_dd         -0.212
dtype: float64
```

The 24-market portfolio has a gross Sharpe of 0.95 and a net one of 0.77, with a volatility of 10.5% a year (close to
the 12% the paper gets with the same 40% target per market) and a maximum drawdown of 21%. Costs, 1.8 points a year,
take almost a fifth of the Sharpe: volatility scaling changes the position a little every day, and each roll pays two
sides on the whole position. The data README gives, with another weighting (10% over the 63-session volatility) and
without costs, a Sharpe of 0.98 for the same signal: the engine recovers what the generator put in. The `clip(-4, 4)`
caps a market's leverage at four times its notional when its estimated volatility collapses. The figure at the top
shows how 1 USD grows with this rule and with the other two in the lesson, against the control in the next section.

```python
vol = r.ewm(com=60, min_periods=60).std() * np.sqrt(252)
pos_m = (tsmom * 0.40 / vol).clip(-4, 4).shift(1).fillna(0.0)
bruto_m = (pos_m.shift(1) * r).fillna(0.0)
coste_m = ((pos_m - pos_m.shift()).abs() * c_lado
           + pos_m.abs() * roll * 2 * c_lado).fillna(0.0)
por_mercado = pd.DataFrame({
    "bruto": bruto_m.apply(sharpe),
    "neto": (bruto_m - coste_m).apply(sharpe)})
print(por_mercado.describe().loc[["mean", "50%", "min", "max"]]
      .round(2).T.to_string())
print("markets with net Sharpe > 0:",
      (por_mercado["neto"] > 0).sum(), "of 24")
rho = (bruto_m.corr().to_numpy()[np.triu_indices(24, 1)]).mean()
print(f"mean correlation between markets: {rho:.2f}")
```
```output
       mean   50%   min   max
bruto  0.29  0.26 -0.13  0.79
neto   0.24  0.24 -0.25  0.71
markets with net Sharpe > 0: 21 of 24
mean correlation between markets: 0.05
```

Market by market, the mean gross Sharpe is 0.29 and the median 0.26: above the planted 0.15, because the rule also
captures the trends in the script (the long crises in the indices, the fall in bonds in 2022) and the drift of the
indices. The range goes from −0.13 to 0.79, and 21 of the 24 markets win net: with 26 years of data, the standard
error of an annual Sharpe is ≈ 1/√26 ≈ 0.20, so the dispersion between markets is almost all noise. What turns a
small, noisy edge into a portfolio with a Sharpe of 0.95 is diversification: with a mean correlation of 0.05 between
the results of the markets,

$$
SR_{\text{portfolio}} \approx SR_{\text{market}} \sqrt{\frac{N}{1 + (N-1)\rho}} = 0.29 \sqrt{\frac{24}{1 + 23 \cdot 0.05}} \approx 0.97
$$

It is the raison d'être of CTAs: no individual trend is reliable, but many weakly correlated ones do add up.

## A control you have to know how to read

How much of that Sharpe is real trend? The natural control is to repeat the strategy on **permuted** returns: the same
daily returns of each market in a random order, without any memory. If the rule only exploited trend, it would give
zero. There are two ways to permute, and in general they do not say the same thing:

```python
rng = np.random.default_rng(2028)

def permuta(r, quitar_media):
    out = r.copy()
    for m in r:
        v = r[m].to_numpy().copy()
        ok = ~np.isnan(v)
        x = v[ok] - (v[ok].mean() if quitar_media else 0.0)
        v[ok] = rng.permutation(x)
        out[m] = v
    return out

for quitar_media in (False, True):
    res = []
    for _ in range(10):
        rp = permuta(r, quitar_media)
        pp = (1 + rp.fillna(0)).cumprod()
        b, c, _ = backtest(np.sign(pp / pp.shift(252) - 1), rp)
        res.append([sharpe(b), sharpe(b - c)])
    m, s = np.mean(res, axis=0), np.std(res, axis=0)
    print(f"demeaned={quitar_media!s:5}  gross {m[0]:+.2f} "
          f"± {s[0]:.2f}   net {m[1]:+.2f} ± {s[1]:.2f}")
```
```output
demeaned=False  gross +0.06 ± 0.17   net -0.21 ± 0.18
demeaned=True   gross -0.12 ± 0.22   net -0.39 ± 0.22
```

Both permutations give a gross Sharpe compatible with zero: +0.06 ± 0.17 without subtracting the mean and
−0.12 ± 0.22 subtracting it, over ten permutations each. Net, −0.21 and −0.39: what it costs to trade without an edge.
Against those controls, the real rule's 0.95 is more than four standard deviations away: it is trend, not noise.

The difference between the two versions is the one you have to know how to read. Permuting preserves each market's
**mean**. In a market that rises on average, the twelve-month return is positive most of the time and the rule is
almost always long: it collects the drift, not a trend. That is why the control without subtracting the mean comes out
above the other (here, by about 0.2 of Sharpe, which with ten permutations does not separate from noise either). In
this data that part is small because there is almost no drift to collect: bonds have no term premium, commodities and
currencies revert to an anchor through the carry, and only the five indices carry the equity premium. In real markets,
with decades of rising bonds and equity premium, the drift can be a large part of what a trend rule that is long on
average earns. Both sources are legitimate, but they are two different bets and it is worth knowing how much each one
weighs.

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

Moskowitz, Ooi and Pedersen make a similar decomposition: they separate what the strategy earns from the covariance
between the signal and the future return (momentum proper) from what it earns from the means of the returns. It is
the right question for any long-short rule on assets with drift: does it beat being permanently long or short
according to the sign of the mean?

</aside>

## Moving-average crossovers and Donchian breakouts: the same idea, another horizon

The classic CTA rules are variations of the same idea with other filters:

- **Moving-average crossover**: long if the 50-session average is above the 200-session one; short if it is below
  (lesson 1.7.1 for the averages).
- **Donchian breakout**: long when the price exceeds the high of the previous `n` sessions; short when it breaks the
  low. The original Turtle rules, published by one of the traders in the group, used two systems based on Richard
  Donchian's channel: System 1, with a 20-session breakout and an exit on breaking the opposite 10-session extreme,
  and System 2, with 55 and 20[^3]. Sizing depended on `N`, the 20-session exponential average of the true range: each
  unit was sized so that a move of `N` cost 1% of the account. It is another way of scaling by volatility.

The breakout is a state machine: flat, long or short, with transitions that depend on the channel.

```mermaid
stateDiagram-v2
  [*] --> FLAT
  FLAT --> LONG: breaks 55 high
  FLAT --> SHORT: breaks 55 low
  LONG --> FLAT: loses 20 low
  SHORT --> FLAT: exceeds 20 high
  LONG --> SHORT: breaks 55 low
  SHORT --> LONG: breaks 55 high
```

With close data there are no session highs and lows, so the channel is computed with closes and the sizing uses the
same EWMA volatility as the 12-month rule: that way the three rules differ only in the signal.

```python
cruce = np.sign(px.rolling(50).mean() - px.rolling(200).mean())

def donchian(p, entrada=55, salida=20):
    alto = p.rolling(entrada).max().shift(1).to_numpy()
    bajo = p.rolling(entrada).min().shift(1).to_numpy()
    s_alto = p.rolling(salida).max().shift(1).to_numpy()
    s_bajo = p.rolling(salida).min().shift(1).to_numpy()
    a = p.to_numpy()
    out = np.zeros(a.shape)
    for j in range(a.shape[1]):
        est = 0
        for i in range(a.shape[0]):
            if np.isnan(alto[i, j]):
                continue
            x = a[i, j]
            if x > alto[i, j]:
                est = 1
            elif x < bajo[i, j]:
                est = -1
            elif est == 1 and x < s_bajo[i, j]:
                est = 0
            elif est == -1 and x > s_alto[i, j]:
                est = 0
            out[i, j] = est
    return pd.DataFrame(out, index=p.index, columns=p.columns)

ruptura = donchian(px)
b_cr, c_cr, _ = backtest(cruce, r)
b_dn, c_dn, _ = backtest(ruptura, r)
tabla = pd.DataFrame({"tsmom_12m": resumen(b_ts, c_ts),
                      "cruce_50_200": resumen(b_cr, c_cr),
                      "donchian_55_20": resumen(b_dn, c_dn)})
print(tabla.round(3).to_string())
print("time in the market of the breakout:",
      f"{(ruptura != 0).to_numpy().mean():.0%}")
```
```output
              tsmom_12m  cruce_50_200  donchian_55_20
sharpe_bruto      0.946         0.819           0.647
sharpe_neto       0.772         0.701           0.491
vol_anual         0.105         0.106           0.091
coste_anual       0.018         0.012           0.014
max_dd           -0.212        -0.214          -0.311
time in the market of the breakout: 63%
```

All three rules win with the same planted premium, and the order follows the horizon. The 12-month rule, which looks
at exactly what the generator rewards (the sign of the previous 252 sessions), has the best net Sharpe (0.77); the
50/200 crossover, which looks at trends of several months, comes close (0.70, with the same 21% maximum drawdown); the
Donchian breakout is the worst (0.49 net, 31% drawdown). The breakout is only in the market 63% of the time, because
the 20-session exit leaves it flat after any pullback, and it reacts to moves of two or three months that here are
mostly noise. With a single dataset and three rules, the difference between 0.70 and 0.77 is within the error of a
26-year Sharpe (≈ 0.2); the one from 0.49 to 0.77 starts not to be, and it has a reason: the planted effect is one
year long.

```python
netos = pd.DataFrame({"tsmom": b_ts - c_ts, "cruce": b_cr - c_cr,
                      "donchian": b_dn - c_dn})
netos.loc["2001":].corr().round(2)
```
```output
          tsmom  cruce  donchian
tsmom      1.00   0.71      0.33
cruce      0.71   1.00      0.30
donchian   0.33   0.30      1.00
```

The 12-month rule and the 50/200 crossover have a correlation of 0.71: both look at trends of months. The 55-session
breakout is faster and only correlates 0.33 and 0.30 with the other two. In practice, CTAs combine several horizons for
exactly that reason: they diversify across trend speeds as well as across markets.

## Is there a trend smile?

One property that makes trend valuable in a portfolio is how it behaves when equities move a lot. Moskowitz, Ooi and
Pedersen plot the quarterly returns of their diversified portfolio against those of the S&P 500 and find a smile
shape: it does better in extreme quarters, up and down[^2]. It is not a law of the rule, but a consequence of what the
crises in their sample were like: long, and preceded by declines that had already put the rule short. Here you can
check whether it shows up in data where the planted effect does not depend on the state of equities.

```python
trim = lambda x: (1 + x).resample("QE").prod() - 1
q = pd.DataFrame({"indice": trim(r["IDA"].fillna(0)),
                  "tendencia": trim(b_ts - c_ts)}).loc["2001":]
a2, a1, a0 = np.polyfit(q["indice"], q["tendencia"], 2)
print(f"fit: {a0:+.3f} {a1:+.3f}·x {a2:+.3f}·x²")
extremos = q["indice"].abs() > q["indice"].abs().quantile(0.8)
print(q.groupby(extremos)["tendencia"].mean().rename(
    {False: "central 80%", True: "extreme 20%"}).round(4))
print(q.nsmallest(4, "indice").round(3))
```
```output
fit: +0.021 +0.078·x -0.094·x²
indice
central 80%    0.0236
extreme 20%    0.0131
Name: tendencia, dtype: float64
            indice  tendencia
fecha
2020-03-31  -0.510     -0.078
2001-09-30  -0.275      0.178
2008-12-31  -0.268      0.062
2011-09-30  -0.209     -0.086
```

*Fig. — No smile: quarterly return of the trend portfolio on 24 futures against the IDA index, 2001-2025.* Scatter of 100 quarters: horizontally, the return of the IDA index future, from −51.0% to +32.5%; vertically, that of the 12-month trend rule, from −14% to +18%. The fitted parabola (highlighted) is almost flat and slightly concave: there is no smile. Labelled, Q1 2020 (index −51.0%, trend −7.8%), Q3 2001 (−27.5% and +17.8%) and Q4 2008 (−26.8% and +6.2%).

It does not show up. The x² term of the fit is −0.094, slightly negative (an almost flat and somewhat concave parabola,
not a smile), and the slope, +0.078: the trend portfolio rises a little with equities, because it is long the indices
most of the time. In the central 80% of the index's quarters, trend gains 2.4% on average; in the 20% most extreme
quarters, 1.3%. Of the index's four worst quarters, two were good for trend (the third quarter of 2001, −27.5% for the
index and +17.8% for trend, and the fourth of 2008, −26.8% and +6.2%) and two bad (the first of 2020, −51.0% and
−7.8%, and the third of 2011, −20.9% and −8.6%).

The difference between some crises and others is easier to see with the stress periods in the data script (those in
the README), from when the signal exists (January 2001), looking at the position in the IDA index at the start of each
period:

```python
tramos = [("2001-01-03", "2002-10-09"), ("2008-09-02", "2009-03-09"),
          ("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")]
neto = b_ts - c_ts
filas = []
for a, b in tramos:
    filas.append({"desde": a, "sesiones": len(r.loc[a:b]),
                  "IDA": (1 + r["IDA"].loc[a:b]).prod() - 1,
                  "tendencia": (1 + neto.loc[a:b]).prod() - 1,
                  "pos_IDA_ini": pos_ts["IDA"].loc[a:].iloc[0]})
print(pd.DataFrame(filas).set_index("desde").round(3).to_string())
```
```output
            sesiones    IDA  tendencia  pos_IDA_ini
desde
2001-01-03       442 -0.499      0.001        1.559
2008-09-02       130 -0.406      0.140       -2.693
2011-07-25        50 -0.213     -0.098        2.731
2015-08-17       124 -0.071     -0.138        3.689
2018-10-01        59 -0.115     -0.088        4.000
2020-02-20        23 -0.400     -0.082        2.939
2022-01-04       195 -0.038      0.072        2.307
2025-02-20        34 -0.144     -0.049        2.743
```

In six of the eight periods the rule started long the index (`pos_IDA_ini` positive), because they came after a
bullish period. The fast declines of 23 to 59 sessions (2011, 2018, 2020 and 2025) catch it long and do not give it
time to turn: it loses between 4.9% and 9.8% in each one. In 2015-2016 it loses 13.8% in a choppy period in which the
index only falls 7.1%. The 2001-2002 period, the longest (442 sessions and −49.9% for the index), it ends flat
(+0.1%): it turns short and wins the third quarter of 2001, but gives almost everything back in the rebounds. The two
periods it wins are the ones the script makes long or preceded by declines: 2008-2009 (+14.0%), in which it was already
short the index at the start, and 2022 (+7.2% with the index at −3.8%), where the gain comes from bonds, which fall for
months.

That is the useful reading of the smile. A twelve-month rule protects in crises that arrive slowly or that last, and
loses in sharp declines after a bull market; the premium planted here (`0.15·σ·sign`) does not depend on the state of
equities, so the shape of the chart depends only on what the crises in the sample are like. In the Moskowitz, Ooi and
Pedersen data, the first kind predominated. It is not insurance against the next crash.

## 12-1 cross-sectional momentum in stocks

The `universo2` universe has 120 fictitious stocks from 2006 to 2025, with IPOs, four bankruptcies and six
acquisitions or mergers. The classic signal is the **12-1** return: from the close twelve months ago to the one a month
ago, skipping the last month (which in real data has a short-term reversal effect of its own; lesson 2.3.2).

Three decisions avoid the usual biases:

1. **Point-in-time universe**: each month only the stocks that traded that month enter (lesson 1.4.4).
2. **Next-session execution**: the signal is computed with the close of the last day of the month and the portfolio
   is formed at the close of the first session of the following month. The return runs from that session to the first
   one of the following month.
3. **Delisting return**: if a stock stops trading during the month, its return includes the delisting return (−100%
   in a bankruptcy). Without it, bankruptcies disappear just before their worst day.

```python
aj = pd.read_parquet("universo2_ajustados.parquet")
P = aj.pivot(index="fecha", columns="ticker",
             values="cierre_ajustado")
ev = pd.read_csv("universo2_eventos.csv", parse_dates=["fecha"])
desl = ev.dropna(subset=["rentabilidad_deslistado"])
ses = P.index
mes = ses.to_period("M")
fin = ses[np.r_[mes[1:] != mes[:-1], True]][:-1]   # last day
ent = ses[ses.searchsorted(fin, side="right")]      # next session

Pf = P.loc[fin]
mom = Pf.shift(1) / Pf.shift(12) - 1                # 12-1
Pe = P.loc[ent].set_axis(fin)
fwd = Pe.shift(-1) / Pe - 1                         # from ent to ent

for _, d in desl.iterrows():            # month in which it stops trading
    k = ent.searchsorted(d["fecha"]) - 1
    p0 = Pe.iloc[k][d["ticker"]]
    p_ult = P[d["ticker"]].dropna().iloc[-1]
    fwd.iloc[k, fwd.columns.get_loc(d["ticker"])] = (
        p_ult / p0 * (1 + d["rentabilidad_deslistado"]) - 1)

mom, fwd = mom.loc["2007":], fwd.loc["2007":]
validas = mom.notna() & fwd.notna()
n_mes = validas.sum(axis=1).iloc[:-1]   # the last month has no future
print(len(n_mes), "portfolio months; stocks per month:",
      n_mes.min(), "to", n_mes.max())
print(fwd.loc[:, ["QXQT", "QXKV"]].min().round(3).to_dict())
```
```output
226 portfolio months; stocks per month: 106 to 110
{'QXQT': -1.0, 'QXKV': -1.0}
```

On the order of a hundred stocks per month (from 106 to 110: IPOs enter when they have twelve months of history and
delistings leave). The first two bankruptcies have a return of −100% in their last month: the data point that would be
missing if only prices were used.

The `universo2_caracteristicas.parquet` file brings those returns already computed: each stock that stops trading has
a last row without a price, with a `rentabilidad_mes` that chains the month-to-date return with the delisting return.
Here they are built by hand from the events to see the mechanism and because the portfolio runs from the first session
of one month to that of the next, not from month end to month end. When that file is read further down for the ESG
placebo and the bid-ask spread, those rows change nothing: they fall in the month in which the stock no longer has a
next return to predict.

The standard measure of a cross-sectional signal is the **information coefficient** (IC): the correlation, each month,
between the signal and the next return across the whole cross-section. The mean of the monthly ICs and its statistic
$t = \overline{IC} / (s_{IC} / \sqrt{T})$ say whether there is a relationship. There are two versions:

```python
from scipy import stats

def ic_mensual(senal):
    filas = []
    for d in senal.index[:-1]:
        v = senal.loc[d].notna() & fwd.loc[d].notna()
        if v.sum() < 50:
            continue
        x, y = senal.loc[d][v], fwd.loc[d][v]
        filas.append((d, stats.spearmanr(x, y).statistic,
                      np.corrcoef(x, y)[0, 1]))
    return pd.DataFrame(filas, columns=["fecha", "rangos", "lineal"]
                        ).set_index("fecha")

ic = ic_mensual(mom)
t = ic.mean() / ic.std() * np.sqrt(len(ic))
pd.DataFrame({"ic_medio": ic.mean(), "t": t}).round(3)
```
```output
        ic_medio      t
rangos     0.027  2.808
lineal     0.041  4.295
```

The rank IC (Spearman) is 0.027 with t = 2.8: what the generator predicts (≈ 0.03). The linear IC (Pearson) is 0.041
with t = 4.3: it looks like a signal 50% stronger and much more reliable. It is not. The linear correlation weights each
observation by its distance from the mean, and four bankruptcies, guided over 320 sessions to a last price of between
0.30 and 1.50 USD and ending at −100%, are the most extreme observations in the panel in both variables at once: very
negative momentum and very negative next return. A few rows move the correlation. The rank IC only uses the order and
sees them as no more than “the worst” of their month.

## Deciles, bankruptcies and a placebo

The usual picture of momentum is ten portfolios: each month the stocks are ranked by their signal, split into deciles
and the mean return of each one is measured. The “winners minus losers” portfolio (WML) buys decile 10 and sells
decile 1.

```python
def deciles(senal, rent):
    out = {}
    for d in senal.index[:-1]:
        v = senal.loc[d].notna() & rent.loc[d].notna()
        if v.sum() < 50:
            continue
        g = pd.qcut(senal.loc[d][v].rank(method="first"), 10,
                    labels=range(1, 11))
        out[d] = rent.loc[d][v].groupby(g, observed=True).mean()
    return pd.DataFrame(out).T

D = deciles(mom, fwd)
quiebras = ["QXQT", "QXKV", "QXZT", "QXYX"]
D_sq = deciles(mom, fwd.drop(columns=quiebras))
car = pd.read_parquet("universo2_caracteristicas.parquet")
esg = car.pivot(index="fecha", columns="ticker",
                values="puntuacion_esg").reindex(mom.index)
D_esg = deciles(esg.where(mom.notna()), fwd)
anual = pd.DataFrame({"momentum": 12 * D.mean(),
                      "sin_quiebras": 12 * D_sq.mean(),
                      "placebo_esg": 12 * D_esg.mean()})
anual.loc["10-1"] = anual.loc[10] - anual.loc[1]
print(anual.round(3).T.to_string())
wml = D[10] - D[1]
w_esg = D_esg[10] - D_esg[1]
for nombre, x in (("WML momentum", wml), ("10-1 placebo", w_esg)):
    print(f"{nombre}: t = {x.mean() / x.std() * np.sqrt(len(x)):.2f}")
ic_esg = ic_mensual(esg.where(mom.notna()))["rangos"]
print(f"rank IC of the placebo: {ic_esg.mean():.3f}, t = "
      f"{ic_esg.mean() / ic_esg.std() * np.sqrt(len(ic_esg)):.2f}")
```
```output
                  1      2      3      4      5      6      7      8      9     10   10-1
momentum     -0.046  0.049  0.097  0.101  0.096  0.089  0.110  0.094  0.075  0.114  0.160
sin_quiebras  0.013  0.045  0.103  0.101  0.098  0.096  0.106  0.094  0.076  0.112  0.098
placebo_esg   0.056  0.085  0.101  0.063  0.060  0.045  0.083  0.077  0.085  0.119  0.063
WML momentum: t = 4.25
10-1 placebo: t = 2.18
rank IC of the placebo: 0.010, t = 1.46
```

*Fig. — Mean annual return by decile of 12-1 momentum and of a placebo (ESG score) in 120 synthetic stocks, 2007-2025.* Two panels of decile bars. 12-1 momentum: decile 1 of losers (highlighted) returns −4.6% a year with the four bankruptcies and +1.3% without them; deciles 2 to 10 return between 4.9% and 11.4% with no clear staircase. ESG placebo, with no effect at all: from 4.5% to 11.9%, with decile 10 at 11.9% and decile 1 at 5.6%: a 6.3-point spread by chance.

Three readings of the table and the figure:

- **The bankruptcies make half the spread.** With them, winners minus losers gives 16.0% a year (t = 4.25); without
  the four stocks that went bankrupt, 9.8%. Decile 1 goes from −4.6% to +1.3% a year. What was planted (`0.0025·z` a
  month with z of ±1.75 in the extreme deciles) gives ≈ 10% a year: the spread without bankruptcies is the one that
  comes close to the truth.
- **The middle is not ordered.** Deciles 2 to 9 do not rise like a staircase (decile 9 returns less than decile 3):
  with a dozen stocks per decile and idiosyncratic volatilities of 20-30%, the 19-year mean of each decile has an
  error of several points.
- **The placebo also has a spread.** The ESG score, which has no effect at all, gives 6.3 points a year between its
  top and bottom deciles, with a t of 2.18 that would pass a test at 5%. Its rank IC, with the whole cross-section, is
  0.010 with t = 1.46: not significant. The spread of two extreme portfolios is a picture, not a test (lesson 2.1.8
  for the problem of testing many variants).

## Costs and crashes

A decile portfolio is rebuilt every month. The cost of each change is, at the very least, half the bid-ask spread:
the `horquilla_pb` column of the characteristics gives the average quoted spread of each stock and month.

```python
hs = car.pivot(index="fecha", columns="ticker",
               values="horquilla_pb").reindex(mom.index) / 2 / 1e4

def pesos(senal):
    W = {}
    for d in senal.index[:-1]:
        v = senal.loc[d].dropna()
        g = pd.qcut(v.rank(method="first"), 10, labels=False)
        w = pd.Series(0.0, index=senal.columns)
        w[g.index[g == 9]] = 1 / (g == 9).sum()
        w[g.index[g == 0]] = -1 / (g == 0).sum()
        W[d] = w
    return pd.DataFrame(W).T

W = pesos(mom)
bruto = (W * fwd.reindex(W.index)).sum(axis=1)
giro = (W - W.shift().fillna(0.0)).abs()
coste = (giro * hs.reindex(W.index)).sum(axis=1)
print(f"monthly turnover: {giro.sum(axis=1).mean():.0%} of capital")
print(f"gross {12 * bruto.mean():.2%}  cost {12 * coste.mean():.2%}"
      f"  net {12 * (bruto - coste).mean():.2%} a year")
```
```output
monthly turnover: 113% of capital
gross 16.14%  cost 0.49%  net 15.65% a year
```

The portfolio changes more than half of its positions every month (a turnover of 113% on a gross capital of 200%, 100%
long and 100% short), but the stocks in the universe are large and the half spread, of a few basis points, costs 0.5% a
year. Monthly momentum in large stocks is not killed by spread costs; it is made more expensive by impact in large
portfolios and by borrowing on the short side, which for stocks about to go bankrupt is expensive or impossible
(lessons 2.4.7 and 2.3.5).

The risk of cross-sectional momentum is not in the costs, but in the **crashes**. Daniel and Moskowitz show that
momentum suffers infrequent and persistent losing streaks that are partly predictable: they arrive in “panic” states,
after market declines and with high volatility, and tend to coincide with rebounds[^4]. The mechanism is simple: after
a sharp decline, the losers are the stocks with the highest beta that have been hit hardest; when the market rebounds,
they are the ones that rise most, and the portfolio is short them. The generator of this data plants it in the three
months following the 2009 and 2020 bottoms.

```python
meses_mes = wml.copy()
meses_mes.index = (wml.index.to_period("M") + 1).strftime("%Y-%m")
crash = meses_mes.loc[["2009-04", "2009-05", "2009-06",
                       "2020-04", "2020-05", "2020-06"]]
print(crash.round(3).to_string())
print("worst months of the whole sample:")
print(meses_mes.nsmallest(5).round(3).to_string())
```
```output
2009-04   -0.073
2009-05    0.035
2009-06   -0.161
2020-04   -0.046
2020-05   -0.032
2020-06   -0.032
worst months of the whole sample:
2009-06   -0.161
2022-02   -0.123
2014-09   -0.090
2008-06   -0.087
2023-03   -0.083
```

The quarter after the 2009 bottom adds up to −20% (−7.3%, +3.5% and −16.1%), and that of 2020, −11%. What was planted
(a coefficient of −0.012 a month in those six months, with z of ±1.75) gives ≈ −4.2% a month, −12.6% per quarter; noise
makes 2009 come out worse and 2020 somewhat better. June 2009 is the worst month of the whole sample. The other four
worst (February 2022, September 2014, June 2008 and March 2023) are not planted: they are the noise of a 24-stock
portfolio. For a real system, the consequence is about size: a strategy with a 10% annual mean that can lose 20% in a
quarter needs a volatility target and capital that can withstand it. Daniel and Moskowitz propose scaling momentum by
its forecast volatility, the same idea as in the futures[^4].

## Sector rotation: ten assets are few

Sector rotation is cross-sectional momentum across sectors: each month, buy the sectors with the best 12-1 return and
sell the worst. Moskowitz and Grinblatt found in 1999 that industry momentum explained a good part of individual stock
momentum in the US[^5]. In this universe there is no planted sector premium: sectors inherit the premium of their
stocks and have some stretches of drift (financials in 2008, energy in 2014-2016 and 2020, technology in 2022).

```python
sector = pd.read_csv("universo2_sectores.csv").set_index("ticker")
R = P.pct_change(fill_method=None)
R_sec = R.T.groupby(sector["sector"]).mean().T     # equal-weighted
S = (1 + R_sec.fillna(0.0)).cumprod()
Sf = S.loc[fin]
s_mom = (Sf.shift(1) / Sf.shift(12) - 1).loc["2007":]
Se = S.loc[ent].set_axis(fin)
s_fwd = (Se.shift(-1) / Se - 1).loc[s_mom.index]
rk = s_mom.rank(axis=1)
ls = (s_fwd[rk >= 8].mean(axis=1)
      - s_fwd[rk <= 3].mean(axis=1)).dropna()
ic_s = pd.Series([stats.spearmanr(s_mom.loc[d], s_fwd.loc[d]).statistic
                  for d in ls.index])
print(f"long-short: {12 * ls.mean():.2%} a year, t = "
      f"{ls.mean() / ls.std() * np.sqrt(len(ls)):.2f}")
print(f"rank IC: {ic_s.mean():.3f}, t = "
      f"{ic_s.mean() / ic_s.std() * np.sqrt(len(ic_s)):.2f}")
```
```output
long-short: 3.98% a year, t = 1.34
rank IC: 0.024, t = 1.02
```

The rotation earns 4.0% a year with t = 1.34, and its rank IC is 0.024 with t = 1.02. Both figures are compatible with
a small premium and also with zero. The problem is the amount of information: an IC across ten sectors has an enormous
monthly standard error (with N = 10, a rank correlation by chance has a standard deviation of ≈ 1/√9 ≈ 0.33), and 226
months are not enough to separate 0.02 from zero. For a strategy with so few assets, the evidence has to come from
somewhere else: from decades of data, from several markets or from a strong economic reason.

## What this changes in your backtests

- **Ask for the right control.** A long-short rule on assets with drift earns something just by being on the side of
  the mean. The demeaned control separates that part from the trend.
- **The cost is per tick, not per contract.** A market whose price has fallen a lot can be expensive to trade even
  though its tick has not changed.
- **Ranks before levels.** In the cross-section, the rank IC and its statistic are more robust than the linear IC or
  the spread of two deciles, which a few extreme observations move.
- **Delistings included.** Without the delisting return the loser decile looks better than it was; with it, you have to
  check how much of the result depends on a handful of bankruptcies.
- **The risk of momentum is in the tail.** A positive mean coexists with months of −10% or worse just when the market
  rebounds.

## Exercises

1. Repeat the time series rule with horizons of 1, 3 and 12 months (21, 63 and 252 sessions). Which one comes closest
   to the planted effect, which depends on the previous 252 sessions, and what happens to costs when the horizon is
   shortened?

**Solution.**

```python
filas = {}
for n in (21, 63, 252):
    b, c, _ = backtest(np.sign(px / px.shift(n) - 1), r)
    filas[f"{n} sessions"] = resumen(b, c)
pd.DataFrame(filas).loc[["sharpe_bruto", "sharpe_neto",
                         "coste_anual"]].round(3)
```
```output
              21 sessions  63 sessions  252 sessions
sharpe_bruto        0.414        0.607         0.946
sharpe_neto        -0.029        0.312         0.772
coste_anual         0.049        0.032         0.018
```

All three horizons win gross because a one-year trend is also visible, with more noise, in stretches of one and three
months, but the 252-session one is the one that captures the planted premium (0.95 gross, against 0.61 and 0.41). As
the horizon shortens the signal changes much more often: the annual cost goes from 1.8% to 4.9% and the one-month rule
loses everything net (−0.03). A short horizon needs a much larger edge to pay for its turnover.

2. The Turtle rules also had a faster “System 1”: entry at 20 sessions and exit at 10. Test it with the `donchian`
   function and compare it with the 55/20 one in net Sharpe, cost and correlation.

**Solution.**

```python
b_d1, c_d1, _ = backtest(donchian(px, 20, 10), r)
print(pd.DataFrame({"20_10": resumen(b_d1, c_d1),
                    "55_20": resumen(b_dn, c_dn)}).round(3).to_string())
print("correlation of the net returns:",
      round(np.corrcoef(b_d1 - c_d1, b_dn - c_dn)[0, 1], 2))
```
```output
              20_10  55_20
sharpe_bruto  0.484  0.647
sharpe_neto   0.212  0.491
vol_anual     0.096  0.091
coste_anual   0.026  0.014
max_dd       -0.483 -0.311
correlation of the net returns: 0.76
```

System 1 is faster and trades more: it costs almost twice as much (2.6% against 1.4% a year), its net Sharpe drops to
0.21 and its maximum drawdown rises to 48%. The two versions are very similar (correlation of 0.76), so combining them
diversifies little. In this data the trend premium is at the one-year horizon, and a 20-session breakout reacts to
moves that are mostly noise.

3. A colleague shows you a cross-sectional momentum backtest on real stocks with a decile spread of 15% a year,
   without delisting returns and with the universe of the index's current members. List what you would ask for before
   believing it.

**Solution.**

First, the point-in-time universe: with the current members, the companies that went up survive, and survivorship
bias (lesson 1.4.4) changes both extremes. Next, the delisting returns: without them the loser decile loses its worst
months and the spread changes, in either direction depending on how the last quote is treated. Third, the rank IC with
its statistic, not just two deciles: in this lesson, a placebo gives a 6-point spread with a t of 2.2, even though its
rank IC is not significant (t = 1.5). Fourth, the result without the five or ten most extreme observations. Fifth,
execution: signal at the month-end close and portfolio formed at the following close, with half the bid-ask spread
and, on the short side, the borrowing cost (lesson 2.4.7). And finally, the crash months after market bottoms: how
much it loses and whether the position size would survive them.

[^1]: N. Jegadeesh and S. Titman, “Returns to Buying Winners and Selling Losers: Implications for Stock Market
    Efficiency”, *Journal of Finance* 48(1), 1993, 65-91. DOI 10.1111/j.1540-6261.1993.tb04702.x.
[^2]: T. J. Moskowitz, Y. H. Ooi and L. H. Pedersen, “Time series momentum”, *Journal of Financial Economics* 104(2),
    2012, 228-250. DOI 10.1016/j.jfineco.2011.11.003. Scaling at 40% and EWMA estimator with a center of mass of 60
    days: sections 2.4 and 4.1; the smile, figure 4.
[^3]: “The Original Turtle Trading Rules”, published at tradingblox.com/originalturtles: N and units on pages 13-14,
    entry systems on page 18 and exits on page 26. The Turtles traded the breakout as soon as it happened during the
    session; here it is traded at the next close, which is more conservative.
[^4]: K. Daniel and T. J. Moskowitz, “Momentum crashes”, *Journal of Financial Economics* 122(2), 2016, 221-247. DOI
    10.1016/j.jfineco.2015.12.002.
[^5]: T. J. Moskowitz and M. Grinblatt, “Do Industries Explain Momentum?”, *Journal of Finance* 54(4), 1999,
    1249-1290. DOI 10.1111/0022-1082.00146.

## Self-assessment

1. To control a `sign(12-month return)` rule, why is it worth subtracting each market's mean before permuting its returns?

   - A. Because without subtracting it the permutation is done wrong
   - B. Because costs are negative
   - C. Because volatility scaling creates return
   - D. Because the permutation preserves each market's mean drift: if a market rises on average, its 12-month return is usually positive and the rule is almost always long, collecting that drift without any trend

   *Answer D: Because the permutation preserves each market's mean drift: if a market rises on average, its 12-month return is usually positive and the rule is almost always long, collecting that drift without any trend.* Permuting destroys the time order but not the mean. In this data the drift is small (bonds without a term premium, commodities and currencies that revert to the anchor), and both versions give a gross Sharpe compatible with zero: +0.06 ± 0.17 without subtracting it and −0.12 ± 0.22 subtracting it. In markets with a lot of drift, what the rule earns by being on the side of the mean can be a large part, and that is not “momentum”.

2. What does the volatility scaling of Moskowitz, Ooi and Pedersen do?

   - A. It trades only when volatility is low
   - B. It scales the signal by the strength of the trend
   - C. It sets the position in each market as target/σ estimated with data up to the previous day, so that each market contributes a similar risk
   - D. It reduces commissions

   *Answer C: It sets the position in each market as target/σ estimated with data up to the previous day, so that each market contributes a similar risk.* Without scaling, natural gas (50% volatility) would dominate a 2-year bond (2%). With a position of 40%/σ each market has the same ex ante risk, and the average portfolio of many weakly correlated markets ends up at around 10.5% a year.

3. In the stock universe, the linear IC of 12-1 momentum is 0.041 and the rank IC 0.027. Which better describes the planted premium, and why?

   - A. The linear one, because it uses more information
   - B. The rank one: the linear one is inflated by four bankruptcies with a −100% return that were in the loser decile; a few extreme observations move a Pearson correlation
   - C. Neither: the IC measures nothing
   - D. The linear one, because ranks lose the sign

   *Answer B: The rank one: the linear one is inflated by four bankruptcies with a −100% return that were in the loser decile; a few extreme observations move a Pearson correlation.* The generator's premium gives an expected rank IC of ≈ 0.03, which is what the rank IC measures. The four guided bankruptcies fall in the loser decile and their −100% weighs a great deal in a linear correlation and in the decile spread (16.0% a year with them, 9.8% without them).

4. A decile portfolio on the ESG score (a placebo with no effect at all) gives a spread of 6.3 points a year between the top and bottom deciles. Which conclusion is correct?

   - A. That a spread between two portfolios of 12 stocks is very noisy: you have to look at the IC of the whole cross-section and its t (0.010 with t = 1.46 here); the decile spread reaches t = 2.18 by chance
   - B. That ESG has a 6% annual premium
   - C. That there is an error in the data
   - D. That the placebo works like momentum

   *Answer A: That a spread between two portfolios of 12 stocks is very noisy: you have to look at the IC of the whole cross-section and its t (0.010 with t = 1.46 here); the decile spread reaches t = 2.18 by chance.* The extreme decile has a dozen stocks with idiosyncratic volatilities of 20-30%: its 19-year mean has an error of several points. The decile spread is a good picture, but the test is the IC and its statistic.

5. When does cross-sectional momentum lose the most, according to the evidence and to the generator of this data?

   - A. In sharp market declines
   - B. In rebounds after a market bottom: the losers, with higher beta, rise more than the winners
   - C. In sideways markets
   - D. It never loses in a month

   *Answer B: In rebounds after a market bottom: the losers, with higher beta, rise more than the winners.* Daniel and Moskowitz document crashes in panic states, after declines and with high volatility, which coincide with market rebounds. Here it is planted in the three months after the 2009 and 2020 bottoms: the winners-minus-losers portfolio loses 20% in the 2009 quarter.

## Key points

- Trend (time series) compares an asset with its own past and can be short everything; cross-sectional momentum compares assets with each other and is neutral by construction.
- The 12-month rule with volatility scaling recovers the planted premium in the 24 futures (gross Sharpe 0.95, net 0.77), more than four standard deviations above a permutation control; the demeaned control also separates out what would amount to collecting each market's drift.
- Moving-average crossover, Donchian and 12-month momentum are the same idea at different horizons: correlations of 0.3 to 0.7 between them.
- The “smile” against equities depends on the crises in the sample: it does not show up here; the 12-month rule wins in long declines or those preceded by declines (2008-2009) and loses in sharp ones after a bull market (2020).
- In stocks, rank IC (0.027, t = 2.8) instead of linear IC or decile spread, which are inflated by a few bankruptcies and the noise of small portfolios; a placebo gives a 6-point spread (t = 2.2) by chance.
- Cross-sectional momentum suffers crashes in the rebounds after bottoms, and a rotation across ten sectors does not have enough data to separate a premium from zero.

## Resources

### Research

- [Volatility Targeting in Python](https://pythonparatrading.com/en/volatility-targeting-2/) · Research-0004 · 7 min
- [Your first system in Python: moving-average crossover on SPY](https://pythonparatrading.com/en/first-system-moving-average-crossover/) · Research-0018 · 7 min

### Documentation

- [Moskowitz, Ooi and Pedersen (2012): “Time series momentum”, Journal of Financial Economics 104(2), 228-250](https://doi.org/10.1016/j.jfineco.2011.11.003) · doi.org
- [Jegadeesh and Titman (1993): “Returns to Buying Winners and Selling Losers”, Journal of Finance 48(1), 65-91](https://doi.org/10.1111/j.1540-6261.1993.tb04702.x) · doi.org
- [Daniel and Moskowitz: “Momentum Crashes” (NBER w20439; Journal of Financial Economics 122(2), 2016)](https://www.nber.org/papers/w20439) · nber.org
- [Moskowitz and Grinblatt (1999): “Do Industries Explain Momentum?”, Journal of Finance 54(4), 1249-1290](https://doi.org/10.1111/0022-1082.00146) · doi.org
- [The Original Turtle Trading Rules (PDF; systems 1 and 2, N and units)](https://www.tradingblox.com/originalturtles/originalturtlerules.pdf) · tradingblox.com

### Data

- [Level 2 data: 24 continuous futures, 2000-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/futuros2_continuos.parquet) · Download · futuros2_continuos.parquet

Previous lesson: [2.2.4 · Research journal and experiment tracking against data snooping](https://pythonparatrading.com/en/python-trading-course/level-2/research-journal-experiment-tracking/)

Next lesson: [2.3.2 · Mean reversion: RSI and Bollinger, intraday reversal and gaps](https://pythonparatrading.com/en/python-trading-course/level-2/mean-reversion-and-gaps/)
