SOX Breadth Extremes
Hey everyone,
This is a proprietary breadth indicator that I’ve been working on, and I’m sharing it freely with all my subscribers.
As you can see from the chart above, this indicator works pretty well at detecting bottoms. According to this indicator, we may very well see a rebound in semiconductors very soon. If I combine this insight with real-world fundamentals, I believe that rebound is shaping up after the Nvidia earnings this week.
Below is a user manual and explanation of what this indicator measures, what each line means, and what each symbol means. You will have to paste the script yourself into TradingView for this to work, but it’s extremely simple to do and I’ll provide instructions below.
What this indicator actually measures
It ignores the SOX index price entirely and instead counts how many of the 30 individual semiconductor stocks are in good shape. If SOX rises 2% but only 5 of 30 names participated, the index number lied to you and this indicator will say so.
Breadth measures the distribution of participation in a rally or sell-off, and the core principle is simple: in a healthy bull market most stocks should sit above their moving averages. Readings above 50% mean more than half the index is in an uptrend; below 50% means the majority is broken.
The three main lines
Each line answers the same question — “what percentage of the 30 SOX stocks are above this moving average?” — over a different time horizon.
🔵Cyan: 20-day - Very short-term mood. Whips between 0 and 100 constantly. Mostly noise on its own.
🟠Orange (thick): 50-day - The main line. Every buy and sell signal in this script is calculated from it.
🟣Magenta: 200-day - Long-term structural health. Slow, and the most important one for judging whether a decline is a dip or a real bear market.
⚪Thin gray — A 10-day average of the orange line. It's a smoothed version of orange, used purely to detect when orange is turning.
Think of orange as the price and gray as its moving average. When orange crosses above gray, breadth is turning up. When orange crosses below gray, breadth is turning down.
The background zones
- 🟩 Green shaded band (0-20) — oversold. Fewer than 6 of 30 stocks hold their 50DMA. This is capitulation territory, often a buying opportunity.
- 🟥 Red shaded band (80-100) — overbought. More than 24 of 30 are above. Strong, but potentially stretched.
- Dotted line at 50 — the neutral divide. Above is a bullish bias, below is bearish.
The five signals, explained plainly
🟩 Green vertical stripe — “everything is broken”
All three lines (cyan, orange, magenta) are simultaneously below 20% at the same time. This is rare and severe: the short-term, medium-term, and long-term structure have all collapsed together. Historically the deepest washouts print this.
🟥 Dark red vertical stripe — “everything is stretched”
All three lines above 80% at once. Broad, powerful participation. Important: this is not a sell signal on its own. Reaching overbought should not be read as bearish — it confirms genuine buying strength.
🔺 Green triangle at the bottom — the actual buy trigger
This is the one worth acting on. Two things must be true simultaneously:
- The 🟠 orange line crosses above the ⚪ gray line — meaning breadth has stopped falling and started recovering.
- At that same moment, orange’s percentile rank is below 10 — meaning today’s breadth level is in the lowest 10% of everything seen in the past two years.
In plain language: stocks were as broken as they’ve been in two years, and they have just begun to repair. The percentile filter is what stops the signal firing on every minor wobble. Merely being oversold guarantees nothing — markets can stay oversold for weeks — so the recovery from oversold is the real signal, not the oversold reading itself.
🔻 Red triangle at the top — a warning, not a sell
The mirror image: orange crosses below gray while sitting in the top 10% of its two-year range. Breadth was extremely strong and has just started to weaken. Treat this as “reduce enthusiasm,” not “exit.”
❌ Orange X — divergence
SOX has made a new 40-day high, but breadth is at least 8 percentage points weaker than it was at the previous high. The index went up, but fewer stocks came along. The classic description is that the generals are advancing while the soldiers retreat — a negative condition most of the time.
The hidden lines
Two plots are switched off by default: % with rising 50DMA and % with rising 200DMA. Enable them in Settings → Style. They answer a subtler question — not “is the stock above its average” but “is that average itself turning up?” — which separates a mechanical bounce from a genuine trend repair.
Why bottoms work and tops don’t
This is the single most important thing to understand about the tool, and it is not a defect in the script — it is how breadth behaves.
Breadth has a hard floor but a soft ceiling. When semis get liquidated, all 30 names lose their 50DMA within a few days. Orange collapses toward zero, and it physically cannot go lower. That produces a sharp, unambiguous, single point in time — an event.
On the upside there is no such limit. Overbought stays overbought in a real uptrend. Breadth can pin at 85-95% for months while the index keeps grinding higher, because strong markets remain overbought for extended periods. That’s a regime, and you cannot time a regime with a threshold. In fact, when stocks recover from a deep correction it is entirely normal for breadth to hit overbought quickly and stay there while sector rotation works off the extremes.
The practical consequences:
- The 🔺 green triangle is a timing tool — trade it.
- The 🔻 red triangle and ❌ X are risk-management tools — reduce size, tighten stops, stop adding. Don’t short on them alone.
- Failures to reach overbought, or exits from overbought, are the genuinely bearish setups.
- A more robust sell construction is two-phase: breadth must first become overbought, then fall back through 50%, which confirms weakening before you act.
So how do you incorporate this into your own tools?
Below is the script, and all you have to do is paste it into the TradingView Pine Script section. You can do this by finding the pine tree symbol on the right-hand side at the bottom.
Paste the script below and you’re done:
//@version=6
indicator(”SOX Breadth Extremes”, “SOXBrd”, overlay=false, precision=1, max_bars_back=700)
// ══════════════ INPUTS ══════════════
lb = input.int(504, “Percentile lookback (bars)”, minval=60)
smooth = input.int(10, “Breadth signal MA”, minval=2)
obLvl = input.int(80, “Raw overbought %”)
osLvl = input.int(20, “Raw oversold %”)
pHi = input.int(90, “Percentile high”)
pLo = input.int(10, “Percentile low”)
divLkbk = input.int(40, “Divergence high lookback”)
divDrop = input.float(8., “Min breadth drop for divergence”)
showTbl = input.bool(true,”Show readings table”)
// ══════════════ UNIVERSE (SOX, 30 names) ══════════════
var array syms = array.from(
“NASDAQ:NVDA”,”NASDAQ:AVGO”,”NASDAQ:AMD”,”NASDAQ:TXN”,”NASDAQ:QCOM”,
“NASDAQ:MU”,”NASDAQ:ADI”,”NASDAQ:INTC”,”NASDAQ:AMAT”,”NASDAQ:LRCX”,
“NASDAQ:KLAC”,”NASDAQ:NXPI”,”NASDAQ:MRVL”,”NASDAQ:MCHP”,”NASDAQ:ON”,
“NASDAQ:MPWR”,”NASDAQ:SWKS”,”NASDAQ:TER”,”NASDAQ:ENTG”,”NASDAQ:QRVO”,
“NASDAQ:ASML”,”NYSE:TSM”,”NASDAQ:GFS”,”NASDAQ:AMKR”,”NYSE:COHR”,
“NASDAQ:LSCC”,”NASDAQ:RMBS”,”NASDAQ:SNPS”,”NASDAQ:CDNS”,”NYSE:WOLF”)
n = array.size(syms)
// ══════════════ PER-SYMBOL METRICS (one request each) ══════════════
f_metrics() =>
m20 = ta.sma(close, 20)
m50 = ta.sma(close, 50)
m200 = ta.sma(close, 200)
[close > m20 ? 1 : 0,
close > m50 ? 1 : 0,
close > m200 ? 1 : 0,
m50 > m50[5] ? 1 : 0,
m200 > m200[5] ? 1 : 0]
float a20 = 0.0, float a50 = 0.0, float a200 = 0.0
float r50 = 0.0, float r200 = 0.0
for i = 0 to n - 1
s = array.get(syms, i)
[x20, x50, x200, y50, y200] = request.security(s, “D”, f_metrics(),
gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_off)
a20 += nz(x20)
a50 += nz(x50)
a200 += nz(x200)
r50 += nz(y50)
r200 += nz(y200)
p20 = a20 / n * 100
p50 = a50 / n * 100
p200 = a200 / n * 100
pr50 = r50 / n * 100
pr200 = r200 / n * 100
// ══════════════ PERCENTILE NORMALIZATION ══════════════
rank50 = ta.percentrank(p50, lb)
rank200 = ta.percentrank(p200, lb)
// ══════════════ EXTREME CONDITIONS ══════════════
// A. Triple-timeframe agreement
tripleOS = p20 < osLvl and p50 < osLvl and p200 < osLvl
tripleOB = p20 > obLvl and p50 > obLvl and p200 > obLvl
// B. Percentile extreme + turn (the reversal trigger)
brdMA = ta.sma(p50, smooth)
turnUp = ta.crossover(p50, brdMA) and rank50 < pLo
turnDown = ta.crossunder(p50, brdMA) and rank50 > pHi
// C. Class-A divergence: index new high, breadth does not confirm
var float brdAtHigh = na
isNewHigh = close >= ta.highest(close, divLkbk)
bearDiv = false
if isNewHigh
bearDiv := not na(brdAtHigh) and p50 < brdAtHigh - divDrop
brdAtHigh := p50
score = (tripleOS ? 1 : 0) + (turnUp ? 1 : 0) - (tripleOB ? 1 : 0) - (turnDown ? 1 : 0) - (bearDiv ? 1 : 0)
// ══════════════ PLOTS ══════════════
plot(p20, “% > 20DMA”, color.new(color.aqua, 30), 1)
plot(p50, “% > 50DMA”, color.new(color.orange, 0), 2)
plot(p200, “% > 200DMA”, color.new(color.fuchsia,30), 1)
plot(brdMA, “50D signal”, color.new(color.gray, 40), 1)
plot(pr50, “% 50DMA rising”, color.new(color.lime, 60), 1, display=display.none)
plot(pr200, “% 200DMA rising”, color.new(color.red, 60), 1, display=display.none)
hOB = hline(obLvl, “OB”, color.new(color.red, 70), hline.style_dashed)
hOS = hline(osLvl, “OS”, color.new(color.green, 70), hline.style_dashed)
hline(50, “Mid”, color.new(color.gray, 80))
fill(hOB, hline(100, display=display.none), color.new(color.red, 93))
fill(hOS, hline(0, display=display.none), color.new(color.green, 93))
bgcolor(tripleOS ? color.new(color.green, 80) : tripleOB ? color.new(color.red, 80) : na)
plotshape(turnUp, “Washout turn”, shape.triangleup, location.bottom, color.lime, size=size.small)
plotshape(turnDown, “Distribution”, shape.triangledown, location.top, color.red, size=size.small)
plotshape(bearDiv, “Divergence”, shape.xcross, location.top, color.orange, size=size.tiny)
// ══════════════ TABLE ══════════════
var table t = table.new(position.top_right, 2, 8, border_width=1)
if showTbl and barstate.islast
table.cell(t, 0, 0, “SOX BREADTH”, text_color=color.white, text_size=size.small)
table.cell(t, 1, 0, “”, text_size=size.small)
table.cell(t, 0, 1, “% > 20DMA”, text_color=color.gray, text_size=size.small, text_halign=text.align_left)
table.cell(t, 1, 1, str.tostring(p20, “#.#”), text_color=p20 > 50 ? color.lime : color.red, text_size=size.small)
table.cell(t, 0, 2, “% > 50DMA”, text_color=color.gray, text_size=size.small, text_halign=text.align_left)
table.cell(t, 1, 2, str.tostring(p50, “#.#”), text_color=p50 > 50 ? color.lime : color.red, text_size=size.small)
table.cell(t, 0, 3, “% > 200DMA”, text_color=color.gray, text_size=size.small, text_halign=text.align_left)
table.cell(t, 1, 3, str.tostring(p200, “#.#”), text_color=p200 > 50 ? color.lime : color.red, text_size=size.small)
table.cell(t, 0, 4, “50D rising”, text_color=color.gray, text_size=size.small, text_halign=text.align_left)
table.cell(t, 1, 4, str.tostring(pr50, “#.#”), text_color=pr50 > 50 ? color.lime : color.red, text_size=size.small)
table.cell(t, 0, 5, “Pctile 50D”, text_color=color.gray, text_size=size.small, text_halign=text.align_left)
table.cell(t, 1, 5, str.tostring(rank50, “#”), text_color=rank50 > pHi ? color.red : rank50 < pLo ? color.lime : color.gray, text_size=size.small)
table.cell(t, 0, 6, “Pctile 200D”, text_color=color.gray, text_size=size.small, text_halign=text.align_left)
table.cell(t, 1, 6, str.tostring(rank200, “#”), text_color=rank200 > pHi ? color.red : rank200 < pLo ? color.lime : color.gray, text_size=size.small)
table.cell(t, 0, 7, “Score”, text_color=color.gray, text_size=size.small, text_halign=text.align_left)
table.cell(t, 1, 7, str.tostring(score), text_color=score > 0 ? color.lime : score < 0 ? color.red : color.gray, text_size=size.small)
// ══════════════ ALERTS ══════════════
alertcondition(turnUp, “Breadth washout turn”, “SOX breadth turning up from bottom decile”)
alertcondition(turnDown, “Breadth distribution”, “SOX breadth rolling over from top decile”)
alertcondition(bearDiv, “Breadth divergence”, “SOX new high on deteriorating breadth”)
alertcondition(tripleOS, “Triple oversold”, “SOX breadth below 20% on 20/50/200DMA”)
alertcondition(tripleOB, “Triple overbought”, “SOX breadth above 80% on 20/50/200DMA”)