How Does the Stochastic Indicator Work? A Beginner's Guide to Overbought & Oversold Signals in Pine Script v6

The Stochastic Oscillator is one of the most widely used momentum indicators in technical analysis, designed to measure where the current closing price sits relative to the high-low range over a specified lookback period. By normalizing price into a 0–100 scale, it provides a mathematically consistent framework for identifying when an asset may be statistically extended — either to the upside (overbought) or downside (oversold). This guide walks through the mathematics, the Pine Script v6 implementation, and the key parameters that govern its behavior.

1. The Mathematics Behind the Stochastic Oscillator

The Stochastic Oscillator was developed by George Lane and is built on a straightforward normalization formula. The core value, called %K, answers the question: "Where does today's close fall within the recent price range?"

The formula for %K is:

$$\%K = \frac{\text{Close} - \text{Lowest Low}(n)}{\text{Highest High}(n) - \text{Lowest Low}(n)} \times 100$$

Where n is the lookback period (commonly 14 bars). The result is then smoothed using a Simple Moving Average to produce %D:

$$\%D = \text{SMA}(\%K, m)$$

Where m is the smoothing period (commonly 3 bars). The %D line acts as a signal line, and crossovers between %K and %D are often used as entry/exit cues.

2. Key Parameters Explained

Understanding each parameter is essential before writing any code. The table below summarizes the three core inputs:

Parameter Common Default Role
%K Length (n) 14 Lookback window for Highest High and Lowest Low
%K Smoothing (d) 3 SMA applied to raw %K to produce the smooth %K line
%D Smoothing (m) 3 SMA applied to smooth %K to produce the signal (%D) line

Note that TradingView's built-in Stochastic applies a first smoothing pass to raw %K (producing "Smooth %K"), and then a second smoothing pass to produce %D. This is the "Slow Stochastic" variant. The "Fast Stochastic" skips the first smoothing pass.

3. Logic Flow Diagram

The diagram below illustrates the data flow from raw price to the final overbought/oversold signal:

graph TD A[Raw Price Data: High, Low, Close] --> B[ta.highest high kLength] A --> C[ta.lowest low kLength] B --> D[Highest High HH] C --> E[Lowest Low LL] D --> F{range = HH - LL} E --> F F -->|range != 0| G[rawK = Close - LL / range x 100] F -->|range == 0| H[rawK = na] G --> I[ta.sma rawK kSmooth] I --> J[Smooth K line] J --> K[ta.sma smoothK dSmooth] K --> L[Signal D line] J --> M{smoothK >= 80?} J --> N{smoothK <= 20?} M -->|Yes| O[Overbought Zone: Red Background] N -->|Yes| P[Oversold Zone: Green Background] J --> Q{K crosses D?} L --> Q Q -->|Bull Cross in Oversold| R[Green Triangle Signal] Q -->|Bear Cross in Overbought| S[Red Triangle Signal]

4. Pine Script v6 Implementation

The following script implements the Stochastic Oscillator from scratch in Pine Script v6, using ta.highest(), ta.lowest(), and ta.sma() — all of which natively accept series int lengths, making them compatible with user-defined input lengths. Reference lines at 80 and 20 are drawn using hline(), which plots constant horizontal lines in the indicator's own pane.

🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator(
     title          = "Stochastic Oscillator — Beginner Guide",
     shorttitle     = "Stoch Guide",
     overlay        = false,   // Renders in a separate pane below the chart
     format         = format.price,
     precision      = 2
     )

// ─────────────────────────────────────────────
// INPUTS
// ─────────────────────────────────────────────
int   kLength    = input.int(14, title = "%K Length",      minval = 1)
int   kSmooth    = input.int(3,  title = "%K Smoothing",   minval = 1)
int   dSmooth    = input.int(3,  title = "%D Smoothing",   minval = 1)

float obLevel   = input.float(80.0, title = "Overbought Level")
float osLevel   = input.float(20.0, title = "Oversold Level")

// ─────────────────────────────────────────────
// CORE CALCULATION
// ─────────────────────────────────────────────
// Step 1: Find the highest high and lowest low over kLength bars.
// ta.highest() and ta.lowest() accept series int lengths — fully legal.
float highestHigh = ta.highest(high, kLength)
float lowestLow   = ta.lowest(low,  kLength)

// Step 2: Calculate raw %K — normalize close within the range.
// Guard against division by zero when highestHigh == lowestLow (flat market).
float range_hl    = highestHigh - lowestLow
float rawK        = range_hl != 0.0 ? (close - lowestLow) / range_hl * 100.0 : na

// Step 3: Smooth raw %K with an SMA to produce the "Slow %K" line.
// ta.sma() accepts series int — kSmooth is input int (≤ series), fully legal.
float smoothK     = ta.sma(rawK, kSmooth)

// Step 4: Smooth the Slow %K again to produce the %D signal line.
float signalD     = ta.sma(smoothK, dSmooth)

// ─────────────────────────────────────────────
// OVERBOUGHT / OVERSOLD STATE
// ─────────────────────────────────────────────
bool isOverbought = smoothK >= obLevel
bool isOversold   = smoothK <= osLevel

// ─────────────────────────────────────────────
// PLOTS
// ─────────────────────────────────────────────
// Plot the Slow %K line
plot(
     smoothK,
     title     = "%K",
     color     = color.new(color.blue, 0),
     linewidth = 2
     )

// Plot the %D signal line
plot(
     signalD,
     title     = "%D",
     color     = color.new(color.orange, 0),
     linewidth = 1
     )

// ─────────────────────────────────────────────
// REFERENCE LINES via hline()
// hline() draws constant horizontal lines in this indicator's own pane.
// Since overlay = false, these lines appear in the separate oscillator pane
// and do NOT affect the main chart's price scale.
// ─────────────────────────────────────────────
hline(obLevel, title = "Overbought", color = color.red,   linestyle = hline.style_dashed, linewidth = 1)
hline(osLevel, title = "Oversold",   color = color.green, linestyle = hline.style_dashed, linewidth = 1)
hline(50,      title = "Midline",    color = color.gray,  linestyle = hline.style_dotted, linewidth = 1)

// ─────────────────────────────────────────────
// BACKGROUND SHADING for overbought/oversold zones
// ─────────────────────────────────────────────
bgcolor(
     isOverbought ? color.new(color.red,   88) :
     isOversold   ? color.new(color.green, 88) :
     na,
     title = "Zone Background"
     )

// ─────────────────────────────────────────────
// CROSSOVER SIGNALS
// ─────────────────────────────────────────────
// Bullish crossover: %K crosses above %D
bool bullCross = ta.crossover(smoothK,  signalD)
// Bearish crossover: %K crosses under %D
bool bearCross = ta.crossunder(smoothK, signalD)

// Plot shapes only when crossover occurs inside the relevant zone
plotshape(
     bullCross and isOversold  ? smoothK : na,
     title    = "Bull Cross",
     style    = shape.triangleup,
     location = location.absolute,
     color    = color.green,
     size     = size.small
     )

plotshape(
     bearCross and isOverbought ? smoothK : na,
     title    = "Bear Cross",
     style    = shape.triangledown,
     location = location.absolute,
     color    = color.red,
     size     = size.small
     )

5. Understanding the Division-by-Zero Guard

A critical edge case occurs when the market is perfectly flat over the lookback window — meaning highestHigh == lowestLow. In this scenario, the denominator of the %K formula equals zero. The script handles this explicitly:

float range_hl = highestHigh - lowestLow
float rawK     = range_hl != 0.0 ? (close - lowestLow) / range_hl * 100.0 : na

When range_hl is zero, rawK is assigned na. This causes ta.sma(rawK, kSmooth) to also return na for that bar, which in turn causes plot() to break the line — visually indicating missing data rather than drawing a misleading value. This is the correct behavior per Pine Script's na propagation rules.

6. Why ta.sma() Works Here (Type Qualifier Analysis)

A common source of confusion in Pine Script v6 is the type qualifier system. The table below clarifies why ta.sma() is the correct choice for this script, while ta.ema() would require a different approach:

Function Length Qualifier Required input.int() Compatible? Dynamic (series int) Compatible?
ta.sma() series int ✅ Yes ✅ Yes
ta.highest() series int ✅ Yes ✅ Yes
ta.lowest() series int ✅ Yes ✅ Yes
ta.ema() simple int ✅ Yes ❌ No — compile error
ta.rsi() simple int ✅ Yes ❌ No — compile error

In this script, kLength, kSmooth, and dSmooth are declared via input.int(), which returns an input int qualifier. Since input int is narrower than (and therefore satisfies) series int, all three ta.sma(), ta.highest(), and ta.lowest() calls are fully legal with no compile errors.

7. Interpreting the Signals

The Stochastic Oscillator produces two primary categories of signals, both grounded in the mathematical normalization of price:

  • Overbought Zone (above 80): The closing price is near the top of its recent range. The background turns red in the script. This does not guarantee a reversal — it indicates statistical extension.
  • Oversold Zone (below 20): The closing price is near the bottom of its recent range. The background turns green. Again, this is a statistical observation, not a directional prediction.
  • %K / %D Crossovers: When %K crosses above %D inside the oversold zone, a bullish triangle is plotted. When %K crosses below %D inside the overbought zone, a bearish triangle is plotted. These crossovers are the most commonly referenced signal events.

8. Conclusion

  • The Stochastic Oscillator normalizes the closing price within the recent high-low range into a 0–100 scale using the formula $\%K = \frac{\text{Close} - \text{LL}(n)}{\text{HH}(n) - \text{LL}(n)} \times 100$, making it directly comparable across different assets and timeframes.
  • In Pine Script v6, ta.sma(), ta.highest(), and ta.lowest() all accept series int lengths, making them fully compatible with input.int() user inputs. A division-by-zero guard using na is essential for flat-market edge cases.
  • The hline() function draws constant horizontal reference lines in the indicator's own separate pane (since overlay = false), and does not interfere with the main chart's price scale.

Ideas for Further Development

  • Multi-Timeframe Stochastic: Use request.security() with barmerge.lookahead_on and close[1] to fetch a higher-timeframe Stochastic value and overlay it as a confirmation filter on the current chart's oscillator pane.
  • Adaptive Length via ATR: Replace the fixed kLength with a dynamically computed length based on ta.atr() volatility, using ta.sma() (which accepts series int) to adapt the lookback window to current market conditions.

Related Posts

Comments

Popular posts from this blog

How to Build a Volume Indicator in Pine Script v6: Buying & Selling Pressure Analysis

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles

SuperTrend Indicator in Pine Script v6: Canonical Formula, Flip Logic, and Non-Repainting Signals