ConvexPi

Cross-Sectional Momentum — a worked example

smc77strategymomentumequitiestemplate

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.

In [1]:
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()
Out[1]:
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
A00 A01 A02 A03 A04 A05 A06 A07 A08 A09 ... A20 A21 A22 A23 A24 A25 A26 A27 A28 A29
2005-01 -0.049224 -0.011780 -0.001557 0.028091 0.005376 0.021385 -0.019651 0.002990 0.032161 0.062017 ... 0.016584 -0.046751 -0.006875 0.036339 -0.055383 0.020696 0.014058 0.025585 -0.063783 -0.030883
2005-02 -0.020564 -0.059811 0.093373 -0.023747 0.011092 -0.009313 0.092214 0.075489 0.024630 -0.122830 ... 0.008141 -0.017995 -0.025530 -0.051042 -0.054849 0.032461 0.021623 0.055511 -0.042308 0.086657
2005-03 -0.013112 0.077399 -0.015235 -0.035725 0.007133 0.055189 0.021090 -0.019806 -0.074098 -0.082730 ... -0.030367 0.019129 -0.010437 0.013621 0.043743 -0.036978 0.063614 0.027087 0.037609 0.060445
2005-04 0.040637 0.040883 0.010184 -0.070290 -0.012109 -0.034860 -0.058097 0.022393 -0.035465 -0.064144 ... 0.015696 0.034853 0.011909 0.022653 0.025005 -0.017006 -0.102517 -0.014663 -0.044764 0.056210
2005-05 -0.013181 0.002853 -0.036076 -0.024482 -0.005933 -0.070653 0.028074 0.004167 -0.066323 -0.132566 ... 0.052061 0.016048 0.039181 0.022062 0.039694 -0.006670 -0.081129 0.042225 -0.101325 -0.009795

5 rows × 30 columns

The strategy

In [2]:
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)}")
months traded: 240

In-sample vs out-of-sample

In [3]:
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()
in-sample Sharpe : 1.25
out-of-sample    : 1.07
No description has been provided for this image

What I'd try next

  • A real universe (swap the synthetic returns for yfinance or Ken-French portfolios).
  • Skip the most recent month (the 12-1 convention) and test transaction costs.
  • Compare against the library's jegadeesh_titman_momentum replication.

Fork this post to build on it.

Make it yours: open in Colab, then File → Save a copy in Drive (or in GitHub) to get your own editable copy.

Discussion (0)

Keep feedback constructive: what worked, what you'd try next, or a specific question.

Sign in to join the discussion.

No comments yet — be the first.