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
Chapter 61Trend Following and Momentum Strategies › Combining Time-Series and Cross-Sectional Momentum
Reading section 6 of 9 · Combining Time-Series and Cross-Sectional Momentum · 3 minSection 6 of 9 · 3 min

Book contents

Section 6 of 9 · 3 min read

Trend Following and Momentum Strategies › Combining Time-Series and Cross-Sectional MomentumOne section · Sections in this chapter6 / 9 ↓
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
Line chart in which pure time-series momentum rises from one to about 1.5 while the combined momentum strategy fluctuates near one and ends only slightly higher.
Cumulative wealth of the combined time-series and cross-sectional momentum rule versus pure time-series momentum. In this synthetic sample, the combined rule remains near its starting value while the simpler benchmark rises, showing that combining signals does not guarantee improvement.
Out[31]:
Visualization
Stacked area chart showing portfolio weight allocations for the top eight stocks over the final year of the sample, with each color band representing one stock's long-side weight.
Portfolio weight allocation over time for the combined momentum strategy. The dynamic weights reveal how the absolute momentum filter removes assets during market downturns, concentrating exposure in a rotating set of leading performers.