Trend Following and Momentum Strategies › Time-Series Momentum and Trend Following

Michael BrenndoerferDecember 28, 202518 min read
Quantitative FinanceAll chapters
Part 11 of 15Quantitative Trading Strategies
Chapter 61Trend Following and Momentum Strategies › Time-Series Momentum and Trend Following
Reading section 2 of 9 · Time-Series Momentum and Trend Following · 18 minSection 2 of 9 · 18 min

Book contents

Section 2 of 9 · 18 min read

Trend Following and Momentum Strategies › Time-Series Momentum and Trend FollowingOne section · Sections in this chapter2 / 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

Time-series momentum, also called absolute momentum or trend following, generates signals based on an asset's own historical performance rather than comparing it to other assets. The core premise is straightforward: if an asset has been rising, go long; if it has been falling, go short or stay out. This approach has been implemented by trend-following hedge funds and CTAs for decades, with documented profitability extending back over a century.

The Mathematical Foundation

The foundation of time-series momentum rests on a deceptively simple observation: recent price direction tends to persist. Before we formalize this intuition, consider why such persistence might exist. When new information enters the market, perhaps an unexpected earnings surprise or a shift in monetary policy, prices do not instantly jump to their new equilibrium value. Instead, the adjustment unfolds gradually as different market participants process and trade on the news. This gradual adjustment creates a window during which prices exhibit directional persistence, and momentum strategies seek to profit from this window.

The simplest time-series momentum signal compares an asset's current price to its price kk periods ago. This comparison answers a fundamental question: has the asset been trending upward or downward over the recent past? If we denote the price at time tt as PtP_t, the momentum signal is:

TSMOMt=sign(Pt−Pt−k)=sign(rt−k,t)\text{TSMOM}_t = \text{sign}(P_t - P_{t-k}) = \text{sign}(r_{t-k,t})

where:

  • PtP_t: price of the asset at time tt
  • Pt−kP_{t-k}: price of the asset kk periods ago
  • rt−k,tr_{t-k,t}: cumulative return over the lookback period
  • sign(⋅)\text{sign}(\cdot): function returning +1+1 if positive, −1-1 if negative, and 00 if zero

The sign function turns the continuous return into a discrete trading signal. A positive signal indicates a long position, while a negative signal indicates a short position. This binary rule reflects the core philosophy of trend following: we care primarily about the direction of the trend rather than its magnitude. A stock that rose 5% over the past year receives the same long signal as one that rose 50%, because both have demonstrated positive momentum.

Out[3]:
Visualization
Scatter plot mapping cumulative returns on the x-axis to discrete trading signals on the y-axis, showing short at -1 for negative returns and long at +1 for positive returns.
The sign function turns continuous returns into discrete trading signals. Positive returns generate long signals (+1), negative returns generate short signals (-1), creating a binary decision rule for trend following.

In practice, however, raw price comparisons can generate noisy signals that whipsaw between long and short positions during sideways markets. You often use moving averages to smooth noise and generate more stable signals. The smoothing process filters out short-term fluctuations while preserving the underlying trend direction. The dual moving average crossover represents one of the most popular implementations, comparing a fast moving average with a short lookback period to a slow moving average with a longer lookback period:

Signalt=sign(MAtfast−MAtslow)\text{Signal}_t = \text{sign}(\text{MA}_t^{\text{fast}} - \text{MA}_t^{\text{slow}})

where:

  • MAtfast\text{MA}_t^{\text{fast}}: moving average value with a short lookback period
  • MAtslow\text{MA}_t^{\text{slow}}: moving average value with a long lookback period
  • sign(⋅)\text{sign}(\cdot): function returning +1+1 if positive, −1-1 if negative, and 00 otherwise

The intuition behind this signal is elegant: the fast moving average represents recent price momentum, while the slow moving average represents the longer-term price equilibrium. When the fast average crosses above the slow average, it suggests that short-term momentum has turned positive relative to the longer-term baseline. This crossover often signals a shift in market sentiment or a fundamental change in the asset's supply and demand dynamics. Conversely, when the fast average crosses below the slow average, it indicates deteriorating short-term momentum and suggests the trend may be reversing.

The simple moving average (SMA) calculates the arithmetic mean of prices over a lookback window nn, treating each observation within the window equally:

MAtn=1n∑i=0n−1Pt−i\text{MA}_t^n = \frac{1}{n}\sum_{i=0}^{n-1} P_{t-i}

where:

  • MAtn\text{MA}_t^n: simple moving average at time tt with lookback nn
  • Pt−iP_{t-i}: asset price at lag ii
  • nn: number of periods in the moving average window

By weighting all prices in the window equally with coefficient 1/n1/n, this formula filters out high-frequency noise but introduces a deliberate lag. The average reacts more slowly to new price changes compared to the raw price series because each new observation only contributes 1/n1/n of the total average value. This lag represents a tradeoff inherent in all smoothing techniques: greater noise reduction comes at the cost of slower signal response. For a 50-day moving average, today's price contributes only 2% of the average value, meaning the average responds gradually to even substantial price movements.

While simple moving averages provide effective smoothing, you might prefer alternatives that give more weight to recent observations. Exponential moving averages (EMAs) provide such an alternative, assigning exponentially decaying weights to historical prices:

EMAt=αPt+(1−α)EMAt−1\text{EMA}_t = \alpha P_t + (1-\alpha)\text{EMA}_{t-1}

where:

  • α\alpha: smoothing factor, typically calculated as 2/(n+1)2/(n+1)
  • nn: effective span or lookback period
  • PtP_t: current asset price
  • EMAt−1\text{EMA}_{t-1}: value of the exponential moving average in the previous period

This recursive formulation reveals the elegant structure of the EMA. The formula updates the average by combining two components: the new information αPt\alpha P_t representing today's price contribution, and the accumulated history (1−α)EMAt−1(1-\alpha)\text{EMA}_{t-1} carrying forward the weighted sum of all previous prices. The smoothing factor α\alpha determines the relative importance of new versus old information. A larger α\alpha value creates a more responsive average that tracks price changes closely, while a smaller α\alpha produces a smoother average that emphasizes historical prices. The exponential weighting scheme assigns geometrically declining weights to older observations, so the average maintains a fading memory of the entire price series while remaining more responsive to recent trend changes than a simple moving average of equivalent span.

Out[4]:
Visualization
Line chart showing a synthetic price series with overlaid 20-day SMA and 20-day EMA, with vertical dashed lines marking two trend reversal points.
Comparison of Simple Moving Average (SMA) and Exponential Moving Average (EMA) with equivalent 20-day spans. The EMA responds more quickly to price changes due to its exponentially decaying weights, while the SMA provides smoother but lagged signals.

Implementing Moving Average Crossovers

Let's implement a dual moving average crossover strategy on equity index data. We'll use a 50-day fast moving average and a 200-day slow moving average, a popular combination known as the golden cross (when fast crosses above slow) and death cross (when fast crosses below slow).

In[5]:
Code
# import yfinance as yf
from datetime import datetime

import numpy as np
import pandas as pd

# Download S&P 500 ETF data
# end_date = datetime.now()
# start_date = end_date - timedelta(days=365*10)
# spy = yf.download('SPY', start=start_date, end=end_date, progress=False)
# prices = spy['Close'].squeeze()

# Generate synthetic data for demonstration
np.random.seed(42)
dates = pd.date_range(end=datetime.now(), periods=252 * 10, freq="B")
returns = np.random.normal(0.0004, 0.012, len(dates))  # Drift + Noise
prices = pd.Series(100 * (1 + returns).cumprod(), index=dates)

# Calculate moving averages
ma_fast = prices.rolling(window=50).mean()
ma_slow = prices.rolling(window=200).mean()

# Generate signals: 1 for long, -1 for short/flat
signal = np.where(ma_fast > ma_slow, 1, -1)
signal = pd.Series(signal, index=prices.index)
Out[6]:
Visualization
Line chart showing SPY price with two moving average lines and shaded regions for long and short signals.
S&P 500 ETF price tracked by 50-day and 200-day moving averages. The crossovers successfully identify primary trends, with the strategy remaining long during sustained rallies while signaling defensive shifts during major corrections.

The shaded regions reveal how the strategy positions itself during different market regimes. During sustained uptrends, the fast moving average stays above the slow, keeping the strategy long. During corrections, the crossover generates a short or flat signal.

Now let's calculate the strategy's returns and compare them to buy-and-hold:

In[7]:
Code
# Calculate daily returns
daily_returns = prices.pct_change()

# Strategy returns: signal is lagged by 1 day to avoid look-ahead bias
strategy_returns = signal.shift(1) * daily_returns

# Calculate cumulative returns
cumulative_market = (1 + daily_returns).cumprod()
cumulative_strategy = (1 + strategy_returns).cumprod()


# Performance metrics
def calculate_metrics(returns, periods_per_year=252):
    """Calculate key performance metrics for a return series."""
    returns = returns.dropna()
    total_return = (1 + returns).prod() - 1
    years = len(returns) / periods_per_year
    ann_return = (1 + total_return) ** (1 / years) - 1
    ann_vol = returns.std() * np.sqrt(periods_per_year)
    sharpe = ann_return / ann_vol if ann_vol > 0 else 0

    # Maximum drawdown
    cumulative = (1 + returns).cumprod()
    rolling_max = cumulative.expanding().max()
    drawdowns = cumulative / rolling_max - 1
    max_dd = drawdowns.min()

    return {
        "Total Return": total_return,
        "Annualized Return": ann_return,
        "Annualized Volatility": ann_vol,
        "Sharpe Ratio": sharpe,
        "Max Drawdown": max_dd,
    }


market_metrics = calculate_metrics(daily_returns)
strategy_metrics = calculate_metrics(strategy_returns)
Out[8]:
Console
Performance Comparison: Moving Average Crossover vs Buy-and-Hold
=================================================================
Metric                          Buy-and-Hold       MA Crossover
-----------------------------------------------------------------
Total Return                         523.7%            128.5%
Annualized Return                     20.1%              8.6%
Annualized Volatility                   0.19               0.19
Sharpe Ratio                            1.07               0.46
Max Drawdown                         -24.6%            -34.5%

The metrics comparison highlights the trade-offs of the strategy. While the Buy-and-Hold approach tracks the market's total return, the Moving Average Crossover strategy aims to improve the Sharpe Ratio by sidestepping major drawdowns, albeit often at the cost of lower total returns during strong bull markets.

Out[9]:
Visualization
Line chart in which synthetic buy-and-hold rises to more than six times its initial value while the moving-average crossover ends near 2.3.
Cumulative returns of the synthetic moving-average crossover strategy and buy-and-hold. In this sample, repeated signal reversals leave the crossover strategy well behind the sustained equity rally, illustrating the cost of whipsaws and missed upside.

Key Parameters

The key parameters for the Moving Average Crossover strategy are:

  • Fast Window: The lookback period for the short-term moving average (e.g., 50 days). Shorter windows make the strategy more responsive but prone to false signals.
  • Slow Window: The lookback period for the long-term baseline (e.g., 200 days). Longer windows filter more noise but increase lag.
  • Regime: The market state (trending vs. mean-reverting) heavily influences performance. Crossovers perform best in sustained trends.

Breakout Strategies

Breakout strategies represent another powerful implementation of time-series momentum, grounded in a different but related intuition. Rather than using moving average crossovers, breakout systems enter positions when prices exceed recent highs or lows. The underlying premise is that price extremes carry informational content: when an asset reaches a new high, it signals that buyers have overwhelmed sellers at all previously established price levels, suggesting strong demand that may persist. Conversely, a new low indicates that selling pressure has broken through all previous support levels.

The classic Donchian channel strategy, popularized by the famous "Turtle Traders" in the 1980s, operationalizes this intuition by buying when price breaks above the highest high of the past nn days and selling when price breaks below the lowest low. The Turtle Traders demonstrated that this systematic approach, when combined with proper position sizing and risk management, could generate substantial profits across diverse futures markets.

The Donchian channel defines dynamic support and resistance levels based on recent price extremes:

Upper_Bandt=max⁡(Pt−1,Pt−2,…,Pt−n)Lower_Bandt=min⁡(Pt−1,Pt−2,…,Pt−n)\begin{aligned} \text{Upper\_Band}_t &= \max(P_{t-1}, P_{t-2}, \ldots, P_{t-n}) \\ \text{Lower\_Band}_t &= \min(P_{t-1}, P_{t-2}, \ldots, P_{t-n}) \end{aligned}

where:

  • Upper_Bandt\text{Upper\_Band}_t: highest price over the past nn periods
  • Lower_Bandt\text{Lower\_Band}_t: lowest price over the past nn periods
  • Pt−iP_{t-i}: asset price ii periods ago
  • nn: lookback window size

These formulas create a price channel that contains all price action over the lookback window. The upper band represents the resistance level that buyers have been unable to breach, while the lower band represents the support level that sellers have been unable to break. When price finally moves outside this channel, it suggests a potential shift in the market's supply and demand equilibrium.

A long entry occurs when Pt>Upper_BandtP_t > \text{Upper\_Band}_t, indicating that today's price has exceeded all prices in the lookback window. This breakout above resistance suggests that new buyers have entered the market or that existing buyers have become more aggressive, potentially initiating an upward trend. A short entry occurs when Pt<Lower_BandtP_t < \text{Lower\_Band}_t, indicating that today's price has fallen below all recent support levels. This breakout logic assumes that prices reaching new extremes signal a fundamental shift in market dynamics that is likely to persist as other market participants recognize and react to the new price regime.

In[10]:
Code
# Donchian Channel breakout strategy
lookback = 20  # 20-day breakout

upper_band = prices.rolling(window=lookback).max().shift(1)
lower_band = prices.rolling(window=lookback).min().shift(1)

# Generate signals
breakout_signal = pd.Series(0.0, index=prices.index)
position = 0

for i in range(lookback, len(prices)):
    if prices.iloc[i] > upper_band.iloc[i]:
        position = 1  # Long breakout
    elif prices.iloc[i] < lower_band.iloc[i]:
        position = -1  # Short breakout
    breakout_signal.iloc[i] = position

# Calculate returns
breakout_returns = breakout_signal.shift(1) * daily_returns
cumulative_breakout = (1 + breakout_returns).cumprod()
Out[11]:
Visualization
Synthetic price chart with upper and lower Donchian bands, a shaded channel, teal upward triangles at long breakouts, and rose downward triangles at short breakouts.
Donchian Channel breakout strategy showing the lagged 20-day high and low bands. Upward and downward triangles mark new long and short entries when price crosses beyond the prior channel.

Key Parameters

The key parameters for Breakout strategies are:

  • Lookback Period (nn): The window for determining the breakout levels (e.g., 20 days).
  • Upper/Lower Bands: The highest high and lowest low over the lookback period, which serve as dynamic support and resistance levels.

CTA Strategies and Multi-Asset Trend Following

Commodity Trading Advisors (CTAs) are managed futures funds that primarily employ trend-following strategies across a diversified portfolio of futures contracts. The power of CTA strategies comes from combining trend signals across multiple uncorrelated asset classes: equity indices, government bonds, currencies, commodities, and interest rates.

The main point is that while any single trend signal may fail, combining signals across 50 to 100 futures markets with low correlation to each other produces a more stable return stream. The strategy also benefits from the ability to profit in both rising and falling markets, and gives useful diversification during equity bear markets.

A typical CTA strategy involves several components:

  • Signal generation: Each market receives a trend signal (e.g., moving average crossover, momentum score, or breakout)
  • Volatility scaling: Position sizes are inversely proportional to recent volatility, so more volatile markets receive smaller positions
  • Risk allocation: Capital is allocated across asset classes and individual markets based on target risk contribution
  • Portfolio construction: Final positions combine signals, volatility scaling, and risk budgets

Let's implement a simplified multi-asset trend following system:

In[12]:
Code
from datetime import datetime, timedelta

# Download futures proxy data for multiple asset classes
tickers = {
    "SPY": "Equities",  # S&P 500
    "TLT": "Bonds",  # Long-term Treasuries
    "GLD": "Commodities",  # Gold
    "UUP": "Currencies",  # US Dollar Index
}

end_date = datetime.now()
start_date = end_date - timedelta(days=365 * 8)

# data = {}
# for ticker, asset_class in tickers.items():
#     df = yf.download(ticker, start=start_date, end=end_date, progress=False)
#     data[ticker] = df['Close'].squeeze()

# prices_df = pd.DataFrame(data)

# Generate synthetic data for demonstration
np.random.seed(42)
dates = pd.date_range(end=datetime.now(), periods=252 * 8, freq="B")
data = {
    ticker: 100 * (1 + np.random.normal(0.0002, 0.01, len(dates))).cumprod()
    for ticker in tickers
}
prices_df = pd.DataFrame(data, index=dates)
returns_df = prices_df.pct_change()

# Calculate momentum signals for each asset
lookback_signal = 252  # 12-month momentum
lookback_vol = 63  # 3-month volatility for scaling

signals = pd.DataFrame(index=prices_df.index, columns=prices_df.columns)
vol_scaled_signals = pd.DataFrame(
    index=prices_df.index, columns=prices_df.columns
)

for ticker in tickers.keys():
    # 12-month momentum signal
    momentum = prices_df[ticker] / prices_df[ticker].shift(lookback_signal) - 1
    signals[ticker] = np.sign(momentum)

    # Volatility scaling: target 10% annual vol per position
    realized_vol = returns_df[ticker].rolling(lookback_vol).std() * np.sqrt(252)
    target_vol = 0.10
    vol_scalar = target_vol / realized_vol
    vol_scalar = vol_scalar.clip(upper=2.0)  # Cap leverage at 2x

    vol_scaled_signals[ticker] = signals[ticker] * vol_scalar

# Equal-weight portfolio of vol-scaled signals
n_assets = len(tickers)
portfolio_returns = (vol_scaled_signals.shift(1) * returns_df).sum(
    axis=1
) / n_assets
cumulative_cta = (1 + portfolio_returns).cumprod()
Out[13]:
Visualization
Line chart comparing CTA strategy cumulative returns to SPY buy-and-hold.
Cumulative returns of the synthetic multi-asset trend-following strategy compared with synthetic SPY. The strategy finishes only modestly above its initial value while equities rally strongly, emphasizing that diversification can come with substantial opportunity cost.
Time series of rolling correlation between CTA returns and equity returns.
Rolling 12-month correlation between the synthetic CTA strategy and equities. Correlation is usually positive in this sample but turns negative for a distinct interval, showing that diversification benefits vary through time.

The rolling correlation chart illustrates why institutional investors value trend-following strategies, but it also shows that diversification is not constant. Correlation is positive through most of this synthetic sample and negative only for a limited interval. In real markets, a trend strategy's ability to go short during sustained declines can produce the "crisis alpha" investors seek, but the outcome depends on the signals, markets, and speed of the reversal.

Key Parameters

The key parameters for the Multi-Asset CTA strategy are:

  • Signal Lookback: The period for measuring momentum (e.g., 12 months).
  • Volatility Lookback: The window for estimating asset volatility to scale positions (e.g., 3 months).
  • Target Volatility: The desired annualized volatility level for each position or the portfolio.
  • Leverage Cap: Maximum leverage allowed per position to prevent excessive risk.