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

Aug 2025

Analysts use 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.
  2. Publication effect: Once a factor becomes popular in papers and blogs, it gets arbitraged away or needs refinements.
  3. Regime change: Macro, microstructure, index composition, regulation, and technology all shift the environment the factor lived in.
  4. 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






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())







ls_ret = signal.apply(long_short_month, axis=1)
cumulative = (1 + ls_ret).cumulativeprod()


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:", (cumulative.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
  1. Rolling stats:
    Plot rolling Sharpe, rolling alpha, and rolling hit rate. Look for a downward drift or longer cold streaks.
  2. Breadth:
    Does the factor still work across regions, sectors, or size buckets, or is it surviving in only a few niches?
  3. Capacity and costs:
    Restimate slippage and borrow fees. A twoway long-short with rising costs can silently cross from positive to negative alpha.
  4. Crowding proxies:
    Check correlations with known factor indices or other managers. High correlation spikes often precede weak performance.
Ways to Slow Decay
  1. Refresh the signal:
    Use slight variations that capture the same intuition but reduce predictability, like different lookback windows or blended definitions.
  2. Combine signals:
    Mix independent predictors so that no single trade becomes too obvious or crowded.
  3. Adaptive weighting:
    Tilt toward factors with stronger recent out of sample performance, but cap turnover to avoid chasing noise.
  4. 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.