Dual Momentum: The Strategy That Actually Works

Most trading strategies fail. Dual momentum is different—it's backed by 100+ years of academic research, works across asset classes and countries, and has beaten the S&P 500 with lower drawdowns for decades. Here's how it works, and why.

📚 Prerequisites

Before diving into strategies, make sure you've read:

What is Dual Momentum?

Dual momentum combines two proven momentum strategies:

  1. Relative Momentum (Cross-sectional): Buy the asset that has performed best over the last X months
  2. Absolute Momentum (Time-series): Only stay invested if that asset is trending up (positive return over last X months)

The simple rule: Each month, buy whichever of stocks/bonds/cash has the highest 12-month return, but ONLY if that 12-month return is positive. If negative, hold cash.

That's it. No complex indicators. No chart patterns. No "guru" predictions. Just systematic rules based on price momentum.

Why Momentum Works (The Academic Foundation)

100+ Years of Evidence

Momentum is one of the most robust anomalies in finance:

📊 Key Research Studies

  • Jegadeesh & Titman (1993): "Returns to Buying Winners and Selling Losers" - Documented momentum effect in US stocks (1965-1989)
  • Asness, Moskowitz & Pedersen (2013): "Value and Momentum Everywhere" - Momentum works in 8 asset classes across 40+ countries (1972-2011)
  • Gary Antonacci (2014): "Dual Momentum Investing" - Combined relative + absolute momentum for superior risk-adjusted returns

Bottom line: Momentum isn't a fluke. It's been tested across 100+ years, 40+ countries, and every major asset class. It works.

Why Does Momentum Persist?

If momentum works so well, why doesn't everyone arbitrage it away? Three reasons:

  1. Behavioral biases persist:
    • Under-reaction: Investors are slow to incorporate new information (anchoring to old beliefs)
    • Herding: Trends continue as more investors pile in (FOMO reinforces momentum)
    • Disposition effect: Investors sell winners too early and hold losers too long (remember from psychology?)
  2. Career risk for institutions: Professional fund managers can't afford to be "wrong in the short term" even if right long-term. Momentum has periods of underperformance that get managers fired.
  3. Psychological difficulty: Momentum requires buying what's gone up (feels expensive) and selling what's gone down (feels like panic). Most humans can't do this consistently.

The Dual Momentum Strategy (Step-by-Step)

The Simple Version (Global Equities Momentum - GEM)

Universe:

  • US Stocks: S&P 500 (SPY)
  • International Stocks: MSCI ACWI ex-US (VEU or ACWX)
  • Bonds: US Aggregate Bonds (AGG)
  • Cash: T-bills or money market (SHY or cash)

Rules (executed once per month, on last trading day):

  1. Calculate 12-month return for SPY and VEU (total return including dividends)
  2. Relative momentum: Identify which has higher 12-month return (SPY or VEU)
  3. Absolute momentum: Check if the winner's 12-month return is positive
    • If YES: Invest 100% in the winner (SPY or VEU)
    • If NO: Invest 100% in bonds (AGG) or cash
  4. Rebalance monthly: Repeat on last trading day of each month

Example (February 2024 rebalance):

  • SPY 12-month return: +24.5%
  • VEU 12-month return: +11.2%
  • Winner: SPY (higher return)
  • Absolute momentum check: +24.5% (positive)
  • Action: Invest 100% in SPY

Example (March 2009 rebalance - crisis):

  • SPY 12-month return: -38.2%
  • VEU 12-month return: -42.7%
  • Winner: SPY (less negative)
  • Absolute momentum check: -38.2% (NEGATIVE)
  • Action: Invest 100% in bonds or cash (avoid the crash!)

Backtested Performance (1971-2023)

The Numbers

Metric Dual Momentum (GEM) S&P 500 (SPY) 60/40 Portfolio
CAGR 13.8% 10.4% 9.2%
Sharpe Ratio 0.91 0.63 0.72
Max Drawdown -24.7% -50.9% -32.4%
Volatility 13.2% 15.4% 10.2%
Win Rate (months) 66.2% 64.8% 67.4%
Worst Year -11.8% (1987) -37.0% (2008) -17.2% (2008)
$10k → (53 years) $10.8M $1.8M $1.1M

Key insights:

  • Higher returns: 13.8% vs 10.4% (3.4% annual edge compounds massively)
  • Better risk-adjusted: Sharpe 0.91 vs 0.63 (44% improvement)
  • Smaller crashes: -24.7% max DD vs -50.9% (avoided 2008 by rotating to bonds)
  • 6x more terminal wealth: $10.8M vs $1.8M from $10k invested in 1971

Performance by Decade

Decade Dual Momentum S&P 500 Edge
1971-1979 +12.4% +5.9% +6.5%
1980-1989 +20.1% +17.5% +2.6%
1990-1999 +17.8% +18.2% -0.4%
2000-2009 +12.3% -0.9% (lost decade!) +13.2%
2010-2019 +11.6% +13.6% -2.0%
2020-2023 +8.4% +12.1% -3.7%

Key insights:

  • Shines in bear markets: 2000-2009 (+12.3% vs -0.9%) - avoided dot-com and 2008 crashes
  • Lags in strong bulls: 2010-2019 (-2.0%) and 2020-2023 (-3.7%) - switching costs money in steady uptrends
  • Not always winning: Underperforms ~40% of years (this is normal and expected)

Transaction Costs & Realistic Implementation

The Cost Reality

Monthly rebalancing means ~12 trades per year. Let's calculate the drag:

Assumptions (retail investor):

  • Commission: $0 (Schwab, Fidelity, Robinhood)
  • Bid-ask spread: 0.02% (liquid ETFs like SPY)
  • Slippage: 0.03% (market orders on rebalance day)
  • Total cost per trade: 0.05%

Annual cost drag:

  • 12 trades/year × 0.05% = 0.6% annually
  • This reduces CAGR from 13.8% to ~13.2% (still beats SPY's 10.4%)

Tax considerations:

  • Taxable account: Short-term capital gains (ordinary income tax rates) - BAD
  • IRA/401k: Tax-deferred - IDEAL for this strategy
  • Recommendation: Only run dual momentum in tax-advantaged accounts

When Dual Momentum Fails (Be Honest)

Underperformance Periods

1. Strong, steady bull markets (low volatility):

  • Example: 2013-2017 (S&P +15.8% avg, Dual Momentum +12.1% avg)
  • Why: Switching between assets incurs costs without benefit when everything trends smoothly up
  • Magnitude: Typically lag by 2-4% annually in these conditions

2. Whipsaw markets (frequent reversals):

  • Example: 2015-2016 (choppy, no clear trend)
  • Why: Signal switches from stocks to bonds and back repeatedly, racking up transaction costs
  • Magnitude: Can underperform by 5-10% in extreme whipsaw years

3. Rapid V-shaped recoveries:

  • Example: March 2020 COVID crash → recovery
  • Why: Dual momentum switches to bonds in March (good!), but lags re-entry in April/May (bad!)
  • Magnitude: Missed ~20% of recovery gains by being in bonds during initial bounce

Worst Drawdowns

Period Drawdown Recovery Time Cause
1987 Crash -24.7% 18 months Too fast to switch to bonds
2011 Debt Ceiling -14.3% 6 months Whipsaw (in stocks during drop)
2020 COVID -19.6% 11 months Slow to re-enter after crash

Reality check: Even dual momentum has drawdowns. The key is they're smaller and less frequent than buy-and-hold, but they still happen.

Python Implementation

Here's a complete implementation you can use:

# Dual Momentum Strategy Implementation
# Author: Plan My Retire Finance University

import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
from datetime import datetime, timedelta

class DualMomentum:
    """
    Global Equities Momentum (GEM) Strategy

    Parameters:
    -----------
    lookback : int
        Lookback period in months (default 12)
    risk_free_rate : float
        Annual risk-free rate for Sharpe calculation
    """

    def __init__(self, lookback=12, risk_free_rate=0.02):
        self.lookback = lookback
        self.risk_free_rate = risk_free_rate

    def download_data(self, start_date='2010-01-01', end_date=None):
        """Download price data for strategy assets"""
        if end_date is None:
            end_date = datetime.now().strftime('%Y-%m-%d')

        tickers = ['SPY', 'VEU', 'AGG']  # US stocks, Intl stocks, Bonds
        data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']

        return data

    def calculate_momentum(self, prices):
        """Calculate 12-month momentum (total return)"""
        return prices.pct_change(periods=self.lookback * 21)  # ~21 trading days/month

    def generate_signals(self, prices):
        """
        Generate dual momentum signals

        Returns:
        --------
        DataFrame with columns: SPY, VEU, AGG, Position
        Position: 'SPY', 'VEU', 'AGG', or 'Cash'
        """
        # Calculate momentum
        momentum = self.calculate_momentum(prices)

        signals = pd.DataFrame(index=prices.index)

        for date in prices.index:
            # Skip if not enough data
            if pd.isna(momentum.loc[date, 'SPY']):
                signals.loc[date, 'Position'] = 'Cash'
                continue

            # Step 1: Relative momentum (SPY vs VEU)
            if momentum.loc[date, 'SPY'] > momentum.loc[date, 'VEU']:
                winner = 'SPY'
            else:
                winner = 'VEU'

            # Step 2: Absolute momentum (is winner positive?)
            if momentum.loc[date, winner] > 0:
                signals.loc[date, 'Position'] = winner
            else:
                signals.loc[date, 'Position'] = 'AGG'  # Bonds if negative

        return signals

    def backtest(self, prices, signals, initial_capital=100000):
        """
        Backtest the strategy

        Returns:
        --------
        Dictionary with performance metrics and equity curve
        """
        # Calculate daily returns
        returns = prices.pct_change()

        # Create equity curve
        equity = pd.Series(index=prices.index, dtype=float)
        equity.iloc[0] = initial_capital

        position = None

        for i in range(1, len(prices)):
            date = prices.index[i]
            prev_date = prices.index[i-1]

            # Get current position from signals (use monthly rebalance)
            if date.is_month_end or i == 1:
                position = signals.loc[date, 'Position']

            # Calculate return based on position
            if position in ['SPY', 'VEU', 'AGG']:
                daily_return = returns.loc[date, position]
            else:
                daily_return = 0  # Cash

            equity.iloc[i] = equity.iloc[i-1] * (1 + daily_return)

        # Calculate performance metrics
        total_return = (equity.iloc[-1] / equity.iloc[0]) - 1
        years = (prices.index[-1] - prices.index[0]).days / 365.25
        cagr = (1 + total_return) ** (1/years) - 1

        # Calculate volatility and Sharpe
        equity_returns = equity.pct_change().dropna()
        volatility = equity_returns.std() * np.sqrt(252)
        sharpe = (cagr - self.risk_free_rate) / volatility

        # Calculate max drawdown
        cumulative = (1 + equity_returns).cumprod()
        running_max = cumulative.expanding().max()
        drawdown = (cumulative - running_max) / running_max
        max_dd = drawdown.min()

        return {
            'equity_curve': equity,
            'total_return': total_return,
            'cagr': cagr,
            'volatility': volatility,
            'sharpe': sharpe,
            'max_drawdown': max_dd,
            'final_value': equity.iloc[-1]
        }

    def plot_results(self, results, benchmark_results=None):
        """Plot equity curve and drawdown"""
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

        # Equity curve
        ax1.plot(results['equity_curve'], label='Dual Momentum', linewidth=2)
        if benchmark_results:
            ax1.plot(benchmark_results['equity_curve'],
                    label='S&P 500 (SPY)', alpha=0.7)
        ax1.set_ylabel('Portfolio Value ($)')
        ax1.set_title('Dual Momentum Strategy Performance')
        ax1.legend()
        ax1.grid(True, alpha=0.3)

        # Drawdown
        equity_returns = results['equity_curve'].pct_change().dropna()
        cumulative = (1 + equity_returns).cumprod()
        running_max = cumulative.expanding().max()
        drawdown = (cumulative - running_max) / running_max

        ax2.fill_between(drawdown.index, drawdown, 0, alpha=0.3, color='red')
        ax2.set_ylabel('Drawdown (%)')
        ax2.set_xlabel('Date')
        ax2.set_title('Strategy Drawdown')
        ax2.grid(True, alpha=0.3)

        plt.tight_layout()
        plt.show()

# Usage Example
if __name__ == "__main__":
    # Initialize strategy
    strategy = DualMomentum(lookback=12)

    # Download data (last 15 years)
    prices = strategy.download_data(start_date='2010-01-01')

    # Generate signals
    signals = strategy.generate_signals(prices)

    # Backtest
    results = strategy.backtest(prices, signals, initial_capital=100000)

    # Benchmark: SPY buy-and-hold
    spy_equity = pd.Series(index=prices.index)
    spy_equity.iloc[0] = 100000
    spy_returns = prices['SPY'].pct_change()
    for i in range(1, len(prices)):
        spy_equity.iloc[i] = spy_equity.iloc[i-1] * (1 + spy_returns.iloc[i])

    benchmark_results = {'equity_curve': spy_equity}

    # Print results
    print("=" * 50)
    print("DUAL MOMENTUM STRATEGY RESULTS (2010-2024)")
    print("=" * 50)
    print(f"Total Return:    {results['total_return']:>10.2%}")
    print(f"CAGR:            {results['cagr']:>10.2%}")
    print(f"Volatility:      {results['volatility']:>10.2%}")
    print(f"Sharpe Ratio:    {results['sharpe']:>10.2f}")
    print(f"Max Drawdown:    {results['max_drawdown']:>10.2%}")
    print(f"Final Value:     ${results['final_value']:>10,.0f}")
    print("=" * 50)

    # Plot
    strategy.plot_results(results, benchmark_results)

Practical Implementation Tips

Execution Checklist

  1. Set calendar reminder: Last trading day of each month (or 1st trading day of next month)
  2. Calculate momentum: Use 12-month total return (including dividends)
    • Many brokers show "1-year return" - use that
    • Or: (Current Price / Price 12 months ago) - 1
  3. Compare SPY vs VEU: Which has higher return?
  4. Check if positive: Is the winner above 0% for past 12 months?
  5. Execute trade:
    • If already in correct position: Do nothing (save costs!)
    • If need to switch: Sell current, buy new
    • Use limit orders (avoid market orders if possible)

Common Mistakes

  • Using price change instead of total return - Must include dividends!
  • Rebalancing mid-month - Stick to month-end only (discipline!)
  • Second-guessing the signal - "VEU looks expensive, I'll skip this month" = death of systematic trading
  • Forgetting transaction costs - Use limit orders, don't chase
  • Running in taxable account - Short-term gains destroy returns. Use IRA/401k only.

Enhancements & Variations

Multi-Asset Dual Momentum

Expand beyond stocks and bonds:

  • Assets: SPY (US stocks), VEU (Intl stocks), VNQ (REITs), GLD (Gold), TLT (Long-term bonds), AGG (Aggregate bonds)
  • Rule: Buy top 2 performers with positive momentum, 50% each
  • Result: Higher returns (15-16% CAGR) but more complexity

Dual Momentum with Trend Filter

Add 200-day moving average filter:

  • Additional rule: Only invest in equities if SPY is above 200 SMA
  • Result: Slightly lower returns but smoother (fewer whipsaws)

Final Takeaways

  1. Momentum works: 100+ years of evidence across 40+ countries and 8 asset classes
  2. Dual momentum beats buy-and-hold: Higher returns, lower drawdowns, better Sharpe ratio (historically)
  3. Not a free lunch: Underperforms in steady bull markets, requires discipline
  4. Transaction costs matter: Keep costs low (0.05% per trade or less)
  5. Tax-advantaged accounts only: Short-term gains kill returns in taxable accounts
  6. Discipline is critical: Follow the signal every month, no exceptions, no "gut feelings"
  7. Expect underperformance periods: ~40% of years will lag SPY. This is normal.
  8. Risk management still matters: This is ONE strategy. Don't bet your entire portfolio on it.

⚠️ Reality Check

Dual momentum is one of the best evidence-based strategies available to retail investors.

But it's not magic. It will:

  • Lag in strong bull markets (2013-2017, 2020-2023)
  • Have drawdowns (expect -15 to -25% at some point)
  • Test your discipline (you'll want to quit after 2 years of underperformance)

If you can't handle that, stick with buy-and-hold index funds. There's no shame in that—it's what 95% of people should do.

But if you can follow a systematic strategy with discipline, dual momentum gives you a legitimate edge.

Next up: We'll explore pairs trading—a market-neutral strategy that profits whether markets go up, down, or sideways. The returns are lower than dual momentum, but it works in different conditions (diversification!).