Factor Decay: Why Yesterday’s Edge Might Not Work Tomorrow

Aug 2025

All firms love factors because they turn messy markets into simple rules. Buy cheap, buy winners, buy quality, and so on. The catch is that many edges fade. This is called factor decay. It shows up as shrinking returns, worse risk-adjusted performance, or long “cold” stretches where a factor stops working.

This note explains what decay is, why it happens, and shows a tiny Python experiment that tracks a momentum factor across sector ETFs over time.

What “decay” looks like
  • Sharpe ratios drift down as more capital crowds into the same trades
  • Turnover and costs rise and eat the edge
  • Structural shifts change payoffs, so the old pattern weakens or reverses

You do not need a huge dataset to see this. A rolling Sharpe or rolling hit rate often tells the story.

Why factors decay
  1. Crowding and capacity

If everyone piles into the same signal, spreads compress and reversals get sharper.

  1. Publication effect

Once a factor becomes popular in papers and blogs, it gets arbitraged away or needs refinements.

  1. Regime change

Macro, microstructure, index composition, regulation, and technology all shift the environment the factor lived in.

  1. Hidden costs

Slippage, borrow fees, and rebalancing costs rise when liquidity dries up or competition tightens.

A tiny momentum demo

We use eleven U.S. sector ETFs as a toy universe. Each month we form a 12 minus 1 momentum signal, go long the top three, short the bottom three, and track a rolling Sharpe. The point is not to build a tradable strategy, only to visualize how performance breathes over time.

import numpy as np
import pandas as pd
import yfinance as yf


# a fair amount of sector specific etfs from SPDR
tickers = ["XLY","XLP","XLE","XLF","XLV","XLI","XLK","XLB","XLRE","XLU","XLC"]



px = yf.download(tickers, start="2000-01-01", progress=False)["Adj Close"]
mx = px.resample("M").last().dropna(how="all")
ret = mx.pct_change().dropna()


roll = (1 + ret).rolling(12).apply(np.prod, raw=True) - 1
signal = roll.shift(1)  


def long_short_month(row, k=3):
    s = row.dropna()
    if s.empty:
        return 0.0
    top = s.nlargest(k).index
    bot = s.nsmallest(k).index
    lw = 1.0 / len(top) if len(top) else 0.0
    sw = -1.0 / len(bot) if len(bot) else 0.0
    weights = pd.Series(0.0, index=s.index)
    weights.loc[top] = lw
    weights.loc[bot] = sw
    return float((ret.loc[row.name, weights.index] * weights).sum())

# strat returns
ls_ret = signal.apply(long_short_month, axis=1)
cum = (1 + ls_ret).cumprod()


def rolling_sharpe(x):
    if len(x) < 12 or x.std() == 0:
        return np.nan
    return (x.mean() / x.std()) * np.sqrt(12)

sh36 = ls_ret.rolling(36).apply(rolling_sharpe, raw=False)


print("Start:", ls_ret.index[0].date(), "End:", ls_ret.index[-1].date())
print("CAGR:", (cum.iloc[-1]) ** (12/len(ls_ret)) - 1)
print("Full-period Sharpe:", (ls_ret.mean()/ls_ret.std()) * np.sqrt(12))
print("Worst 3y rolling Sharpe:", np.nanmin(sh36.values))
print("Best 3y rolling Sharpe:", np.nanmax(sh36.values))
How to Read This
  • The full period Sharpe might look decent, yet the worst 3 year Sharpe can be near zero or negative
  • That spread between best and worst periods is what decay feels like in practice
  • If you push the start date, the universe, or the formation rule, the story changes again, which is another clue that edges are fragile
What to Check When You Suspect Decay

Rolling stats
Plot rolling Sharpe, rolling alpha, and rolling hit rate. Look for a downward drift or longer cold streaks.

Breadth
Does the factor still work across regions, sectors, or size buckets, or is it surviving in only a few niches?

Capacity and costs
Restimate slippage and borrow fees. A twoway long-short with rising costs can silently cross from positive to negative alpha.

Crowding proxies
Check correlations with known factor indices or other managers. High correlation spikes often precede weak performance.

Ways to Slow Decay

Refresh the signal
Use slight variations that capture the same intuition but reduce predictability, like different lookback windows or blended definitions.

Combine signals
Mix independent predictors so that no single trade becomes too obvious or crowded.

Adaptive weighting
Tilt toward factors with stronger recent out of sample performance, but cap turnover to avoid chasing noise.

Capacity discipline
Size positions with realistic impact models and be willing to scale down when liquidity tightens.

Closing Thought

Factor decay is not a mystery so much as a reminder that markets learn. An edge that is obvious and scalable will not stay generous for long. The habit to build is simple: measure performance through time, stress it across universes and costs, and assume that success attracts competition. If the edge survives those checks, you probably have something worth keeping alive.