Cross-Sectional Momentum — a worked example¶
This is a template post. Replace the narrative and code with your own strategy, keep the
structure: a clear story, runnable code, charts, and an out-of-sample section. Define your strategy
as a class named MyStrategy so it can also be scored on the permanent leaderboard.
The idea¶
Stocks that outperformed over the past ~12 months tend to keep outperforming over the next month. We rank a cross-section by trailing return, go long the top quintile and short the bottom, and check whether the edge survives out of sample.
import numpy as np
import pandas as pd
%matplotlib inline
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
# Synthetic monthly returns for 30 names over 240 months, with a small persistent component.
n_assets, n_months = 30, 240
persistent = rng.normal(0, 0.01, (1, n_assets))
rets = rng.normal(0, 0.05, (n_months, n_assets)) + persistent
rets = pd.DataFrame(rets, columns=[f"A{i:02d}" for i in range(n_assets)])
rets.index = pd.period_range("2005-01", periods=n_months, freq="M")
rets.head()
The strategy¶
class MyStrategy:
"""12-1 cross-sectional momentum: long top quintile, short bottom, monthly."""
def signal(self, past_returns: pd.DataFrame) -> pd.Series:
mom = (1 + past_returns).rolling(12).apply(np.prod, raw=True) - 1
return mom.shift(1) # use information available before the month traded
def weights(self, signal_row: pd.Series) -> pd.Series:
r = signal_row.rank(pct=True)
w = (r > 0.8).astype(float) - (r <= 0.2).astype(float)
s = w.abs().sum()
return w / s if s else w
strat = MyStrategy()
sig = strat.signal(rets)
w = sig.apply(strat.weights, axis=1)
port = (w.shift(0) * rets).sum(axis=1).dropna()
print(f"months traded: {len(port)}")
In-sample vs out-of-sample¶
split = port.index[len(port)//2]
ins, oos = port[port.index <= split], port[port.index > split]
def sharpe(x): return np.sqrt(12) * x.mean() / x.std() if x.std() else float("nan")
print(f"in-sample Sharpe : {sharpe(ins):.2f}")
print(f"out-of-sample : {sharpe(oos):.2f}")
eq = (1 + port).cumprod()
fig, ax = plt.subplots(figsize=(8, 3))
ax.plot(eq.index.to_timestamp(), eq.values)
ax.set_title("Strategy equity curve"); ax.set_ylabel("growth of $1")
plt.tight_layout(); plt.show()
What I'd try next¶
- A real universe (swap the synthetic returns for
yfinanceor Ken-French portfolios). - Skip the most recent month (the 12-1 convention) and test transaction costs.
- Compare against the library's
jegadeesh_titman_momentumreplication.
Fork this post to build on it.

