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.
- 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.
- Crowding and capacity
If everyone piles into the same signal, spreads compress and reversals get sharper.
- Publication effect
Once a factor becomes popular in papers and blogs, it gets arbitraged away or needs refinements.
- Regime change
Macro, microstructure, index composition, regulation, and technology all shift the environment the factor lived in.
- Hidden costs
Slippage, borrow fees, and rebalancing costs rise when liquidity dries up or competition tightens.
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))
- 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
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.
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.
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.