Implementation Scope

This keeps the dual-momentum intuition visible, but it is not a canonical Antonacci switcher across offensive and defensive asset sleeves.

The Intuition

Dual Momentum, popularised by Gary Antonacci (2014), combines two distinct momentum filters to create a more robust trend-following system. Absolute momentum asks: is this asset beating the risk-free rate (T-bills)? Relative momentum asks: is this asset beating a relevant benchmark (SPY)? Only when both filters are positive do you hold the asset long; if absolute momentum fails, you exit to safety.

The two-filter design solves a major problem with pure relative momentum (cross-sectional): even the "best" asset can be in a bear market. In 2008, U.S. equities dominated international equities on relative terms — but both were down 40–60%. Absolute momentum would have exited both before the crash, because even the "best" asset was below its T-bill return. This is why Dual Momentum avoided the 2008 crisis much more effectively than cross-sectional momentum alone.

Antonacci's backtests across equities, bonds, and real estate from 1974–2013 showed annualised returns of ~17% vs. ~10% for the S&P 500, with a maximum drawdown of ~18% vs. ~51% for buy-and-hold. This remarkable risk-adjusted performance made Dual Momentum one of the most cited systematic trading frameworks among retail systematic investors in the 2010s.

Key assumptions: (1) The 6-12 month lookback captures intermediate-term momentum that persists (Jegadeesh & Titman 1993). (2) T-bills serve as a meaningful risk-free rate proxy — in near-zero rate environments (2010–2021) this filter was frequently crossed, generating excessive trades. (3) SPY is an appropriate relative benchmark for the asset being traded. The strategy may need to adjust the benchmark to the asset class.

Known failure modes: momentum crashes (sudden sharp reversals in momentum leaders) and low-yield environments where the absolute momentum filter fires too frequently. Post-2008, the extremely low T-bill yield made the absolute momentum filter relatively easy to beat, reducing its defensive power. The strategy is most powerful when rates are meaningfully positive — its original design was calibrated to 1970s–1990s rate regimes.

The Math

Read this as a compact model summary: what the signal sees, what it ignores, and where fragility can creep in.

r_ticker(t)    = Close(t) / Close(t - h) - 1
r_tbill(t)     = T-bill yield proxy return over h days
r_spy(t)       = SPY return over h days

abs_mom = r_ticker > r_tbill   [beats risk-free rate?]
rel_mom = r_ticker > r_spy     [beats market?]

Signal(t) = +1  if abs_mom AND rel_mom
          = -1  if NOT abs_mom
          =  0  otherwise

Parameters

ParameterTypeDefaultDescription
lookback int 126 Momentum lookback period in trading days (~6 months)

Source Code

def run(ticker: str, start: str, end: str, **params) -> dict:
    lookback = int(params.get("lookback", 126))
    df = fetch_ohlcv(ticker, start, end)

    # Absolute momentum: compare to T-bill proxy (^IRX)
    try:
        df_irx = fetch_ohlcv("^IRX", start, end)
        irx_common = df_irx.index.intersection(df.index)
        irx_return = df_irx.loc[irx_common, "Close"].pct_change(lookback)
    except Exception:
        irx_return = pd.Series(0.0, index=df.index)

    # Relative momentum: compare to SPY
    try:
        df_spy = fetch_ohlcv("SPY", start, end)
        spy_common = df_spy.index.intersection(df.index)
        spy_return = df_spy.loc[spy_common, "Close"].pct_change(lookback)
    except Exception:
        spy_return = pd.Series(float("nan"), index=df.index)

    ticker_return = df["Close"].pct_change(lookback)

    pos = pd.Series(0.0, index=df.index)
    for i, date in enumerate(df.index):
        if i < lookback:
            continue
        tr = ticker_return.iloc[i]
        # Absolute: beat T-bills
        try:
            irx_val = irx_return.get(date, 0.0)
        except Exception:
            irx_val = 0.0
        abs_mom = tr > (irx_val if not pd.isna(irx_val) else 0.0)

        # Relative: beat SPY
        try:
            spy_val = spy_return.get(date, float("nan"))
        except Exception:
            spy_val = float("nan")
        rel_mom = (not pd.isna(spy_val)) and (tr > spy_val)

        if abs_mom and rel_mom:
            pos.iloc[i] = 1.0
        elif not abs_mom:
            pos.iloc[i] = -1.0

    return run_backtest(df, pos, ticker=ticker, start=start, end=end,
                        strategy=METADATA["slug"], params={"lookback": lookback})

Further Reading

  • Antonacci, G. (2014). Dual Momentum Investing. McGraw-Hill.
  • Antonacci, G. (2012). Risk Premia Harvesting Through Dual Momentum. SSRN Working Paper.
  • Asness, C., Moskowitz, T. & Pedersen, L. (2013). Value and Momentum Everywhere. Journal of Finance, 68(3), 929–985.

When It Works / When It Fails

Works
  • Strong bull markets with clear leader/laggard dynamics
  • Long-only diversified portfolios needing a regime filter
  • When SPY clearly beats T-bills on absolute momentum
Fails
  • Sideways or choppy markets — absolute momentum near zero
  • Rapid reversals in relative ranking between assets
  • Bear markets: goes flat (no short), caps drawdown but earns nothing

Regime Fit

Bull / Calm or Normal vol
Best conditions at 1.0 long. Both absolute and relative momentum filters are clearly positive.
Bull / Stressed vol
Scaled back to 0.65 long. Signal remains valid but vol-adjusted sizing applies.
Transition (any vol)
0.0–0.25. Absolute momentum is ambiguous; strategy moves toward cash.
Bear (all vol)
0.0 across all — long-only; no short exposure taken. Strategy sits in cash or bonds.

Compared to Alternatives

vs TSMOM
TSMOM uses only absolute return sign; Dual Momentum adds a relative momentum filter vs the market. Dual Momentum is more selective with fewer active signals.
vs MA Crossover
Both are trend-following; MA Crossover uses daily SMA level comparison; Dual Momentum uses monthly lookback returns plus a cross-asset relative screen. Different timescales.
vs ROC
ROC measures raw return against a single threshold; Dual Momentum requires both absolute AND relative momentum positive — stricter entry conditions and fewer signals.
Run This Strategy →