Back To Top

August 14, 2025

Measuring Market Breadth and Momentum

Detect S&P 500 breadth divergences, advances/declines, momentum returns percentiles in Python.

Market breadth and momentum expose changes that index price alone misses.

Divergences, advance-decline patterns, and momentum shifts in individual stocks signal turning points earlier than a benchmark index would.

These signals let you separate healthy rallies from fragile ones, and spot cracks as they widen.

Therefore, breadth and momentum statistics make the hidden risks and opportunities more visible.

This article shows how to measure S&P 500 breadth, advances and declines, and momentum percentiles using Python and free data.

The complete Python notebook for the analysis is provided below.

Market Breadth and Momentum Google Colab Demo AVIF

This is what we’ll cover:

  • Pull and clean daily price data for all S&P 500 constituents
  • Track price, trend, and participation signals
  • Visualize daily return extremes across all stocks
  • Compare performance for every stock
  • Rank stocks by momentum percentiles

1. Market Breadth and Momentum

Market breadth measures how many stocks participate in a market move.

Strong breadth means gains spread across most stocks. Weak breadth means a few heavyweights drive the index while the rest lag.

Momentum measures the tendency of stocks that outperform to keep outperforming, at least for a period.

Both signals provide context that raw index price cannot.

Why breadth matters:

Index rallies with strong breadth tend to last longer. When only a handful of stocks lead while most others stall or fall, risk rises.

Breadth reveals the difference between a healthy rally and a fragile one.

writes Ben Carlson.

Academic research supports this. Zaremba et al. (2021) find that

“breadth indicators such as the percentage of stocks above moving averages and the advance-decline line have significant predictive power for future index returns.”

Their study concludes that “Periods of declining breadth are often followed by lower risk-adjusted returns for the broader market.”

See below an example of breadth divergence ahead of a downturn.

Figure 1. The NYSE Advance Decline Line diverges from the S&P 500 during 2007–2008. While the index reaches new highs, underlying participation weakens. Source: Potomac.

Figure 1. The NYSE Advance Decline Line diverges from the S&P 500 during 2007–2008. While the index reaches new highs, underlying participation weakens. Source: Potomac.

This pattern also played out in the S&P 500 during 2023. Despite strong index gains, participation fell sharply.

By year end, fewer than 40% of S&P 500 stocks outperformed the index, with almost all gains concentrated in the “Magnificent 7”.

The Persistence of Momentum

Momentum is among the most persistent factors in equity returns. Winners tend to keep winning over the short and medium term.

For example, stocks ranked in the top 20 percent by trailing 6-month returns have, on average, outperformed laggards over the next several months (Jegadeesh and Titman, 1993).

Breadth signals help spot market regime shifts before price confirms. Momentum measures trend persistence and expose sector rotation.

Combined, these measures catch early cracks in rallies, false breakouts, and exhaustion in crowded trades.

2. Download S&P 500 Price Data

2.1 Get a list of all the tickers in the S&P500

We use the Wikipedia’s S&P 500 companies page to pull the list of tickers included in the S&P 500 using pandas.

				
					import pandas as pd

# Pull the table of S&P 500 constituents
url = 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies'
sp500_table = pd.read_html(url)[0]

# Extract ticker symbols and company names
tickers = sp500_table['Symbol'].tolist()
companies = sp500_table['Security'].tolist()

print("Tickers:", tickers)
# Optionally, combine into a DataFrame
df = pd.DataFrame({'Ticker': tickers, 'Company': companies})
print(df)
				
			
Figure 2. List of All the tickers included in the S&P 500.

Figure 2. List of All the tickers included in the S&P 500.

2.2 Download Data with yfinance

Historical price data is available using yfinance.

We implement a batch downloading approach to reduce errors and avoid API rate limits.

				
					import yfinance as yf
import pandas as pd
import time

def chunks(lst, n):
    for i in range(0, len(lst), n):
        yield lst[i:i + n]

start = "2015-01-01"
end = (pd.Timestamp.today() + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
batch_size = 50
pause = 2

def fetch_batch(batch):
    backoff = 5
    while True:
        try:
            print(f"Downloading: {batch[:3]}…")
            data = yf.download(
                batch,
                start=start,
                end=end,
                group_by="ticker",
                auto_adjust=True,
                threads=False,
                progress=False
            )
            return data
        except Exception as e:
            if "429" in str(e):
                print(f"Rate limit. Wait {backoff}s…")
                time.sleep(backoff)
                backoff *= 2
            else:
                raise

# 1. Batch download
all_closes = []
missing = []

for batch in chunks(tickers, batch_size):
    data = fetch_batch(batch)
    closes = data.xs("Close", axis=1, level=1)
    valid = closes.dropna(axis=1, how="all")
    print(f"  Got {len(valid.columns)} valid")
    all_closes.append(valid)

    batch_missing = [t for t in batch if t not in valid.columns]
    if batch_missing:
        print(f"  Missing in batch: {batch_missing}")
        missing.extend(batch_missing)

    time.sleep(pause)

close = pd.concat(all_closes, axis=1)
				
			

2.3 Adjust for missing tickers

Tickers which are not found, will be added to a missing list. You should update this list to retrieve the remaining set of tickers.

For example, tickers with periods (like BRK.B) or other special cases need manual correction.

Prev Post

Why Your Stop Gets Hit Before the Reversal

Next Post

Extracting Market Crash Probabilities

post-bars
Mail Icon

Newsletter

Get Every Weekly Update & Insights

[mc4wp_form id=]

Leave a Comment