Trend Following and Momentum Strategies › Combining Time-Series and Cross-Sectional Momentum
Michael BrenndoerferDecember 28, 20253 min read
Quantitative FinanceAll chapters Part 11 of 15Quantitative Trading Strategies
Trend Following and Momentum Strategies › Combining Time-Series and Cross-Sectional MomentumOne section · Sections in this chapter6 / 9 ↓
Sections in this chapter9
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Sophisticated momentum strategies often combine both time-series and cross-sectional signals. Time-series momentum provides absolute direction (should we be long or short the asset class?), while cross-sectional momentum provides relative selection (which assets within the class should we overweight?).
In[28]:
Code# Combined momentum strategy
def combined_momentum_strategy(prices, ts_lookback=252, xs_lookback=126):
"""
Combine time-series and cross-sectional momentum.
Time-series: Go long only when asset has positive momentum
Cross-sectional: Overweight relative winners, underweight losers
"""
returns = prices.pct_change()
n_assets = prices.shape[1]
# Time-series signals (1 for positive momentum, 0 otherwise)
ts_signals = pd.DataFrame(index=prices.index, columns=prices.columns)
for col in prices.columns:
ts_momentum = prices[col] / prices[col].shift(ts_lookback) - 1
ts_signals[col] = (ts_momentum > 0).astype(float)
# Cross-sectional signals (rank-based weights)
xs_signals = pd.DataFrame(
index=prices.index, columns=prices.columns, dtype=float
)
xs_momentum = prices.pct_change(xs_lookback)
for date in prices.index[xs_lookback:]:
scores = xs_momentum.loc[date].dropna()
if len(scores) > 0:
ranks = scores.rank()
# Normalize ranks to weights summing to 1
xs_signals.loc[date] = (ranks - ranks.mean()) / ranks.sum()
# Combined signal: time-series filter × cross-sectional weights
combined_weights = ts_signals * xs_signals
# Normalize to fully invested when possible
combined_weights = combined_weights.div(
combined_weights.abs().sum(axis=1), axis=0
)
combined_weights = combined_weights.fillna(0)
# Portfolio returns
portfolio_ret = (combined_weights.shift(1) * returns).sum(axis=1)
return portfolio_ret, combined_weights
combined_returns, combined_weights = combined_momentum_strategy(stock_prices)In[29]:
Code# Calculate pure strategies for comparison
# Re-calculate TS momentum on the stock universe for fair comparison
ts_lookback = 252
ts_mom_stocks = stock_prices / stock_prices.shift(ts_lookback) - 1
ts_sig_stocks = (ts_mom_stocks > 0).astype(float)
pure_ts_returns = (ts_sig_stocks.shift(1) * stock_returns).mean(axis=1)
# Calculate cumulative returns for plotting
cum_combined = (1 + combined_returns).cumprod()
cum_ts = (1 + pure_ts_returns).cumprod()Out[30]:
Visualization
Out[31]:
Visualization