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:
- Risk Management Masterclass - Position sizing is more important than this strategy
- Trading Psychology - You'll need discipline to follow the system
What is Dual Momentum?
Dual momentum combines two proven momentum strategies:
- Relative Momentum (Cross-sectional): Buy the asset that has performed best over the last X months
- 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:
- 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?)
- 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.
- 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):
- Calculate 12-month return for SPY and VEU (total return including dividends)
- Relative momentum: Identify which has higher 12-month return (SPY or VEU)
- 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
- 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
- Set calendar reminder: Last trading day of each month (or 1st trading day of next month)
- 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
- Compare SPY vs VEU: Which has higher return?
- Check if positive: Is the winner above 0% for past 12 months?
- 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
- Momentum works: 100+ years of evidence across 40+ countries and 8 asset classes
- Dual momentum beats buy-and-hold: Higher returns, lower drawdowns, better Sharpe ratio (historically)
- Not a free lunch: Underperforms in steady bull markets, requires discipline
- Transaction costs matter: Keep costs low (0.05% per trade or less)
- Tax-advantaged accounts only: Short-term gains kill returns in taxable accounts
- Discipline is critical: Follow the signal every month, no exceptions, no "gut feelings"
- Expect underperformance periods: ~40% of years will lag SPY. This is normal.
- 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!).