Stochastic Oscillator in Python — Coding It from Scratch (pandas)

Fast and slow %K/%D, plus the divide-by-zero guard

Stochastic computed in Python
Stochastic computed in Python

The Stochastic Oscillator is just “where’s the close within the recent range,” as the explainer showed, so in pandas it’s a handful of rolling operations. The one thing to get right is the divide-by-zero when the range is flat.

What you’ll need

  • Python 3.9+, pandas (pip install pandas)
  • high, low, close series.

Fast and slow stochastic

import numpy as np
import pandas as pd


def stochastic(high, low, close, k=14, d=3, smooth=3, slow=True):
    lowest = low.rolling(k).min()
    highest = high.rolling(k).max()

    rng = (highest - lowest).replace(0, np.nan)     # guard flat-range div-by-zero
    fast_k = 100 * (close - lowest) / rng

    if slow:
        percent_k = fast_k.rolling(smooth).mean()    # slow %K
    else:
        percent_k = fast_k                           # fast %K

    percent_d = percent_k.rolling(d).mean()          # %D signal
    return pd.DataFrame({"k": percent_k, "d": percent_d})

Set slow=True (the default) for the popular 14, 3, 3 slow stochastic — the blue and orange lines in the chart above; slow=False gives the raw fast version.

The %K/%D cross signal

s = stochastic(df["high"], df["low"], df["close"])
below20 = s["k"] < 20
bull = (s["k"] > s["d"]) & (s["k"].shift(1) <= s["d"].shift(1)) & below20.shift(1)

bull flags %K crossing up through %D while coming out of oversold — a classic setup to backtest in AlgoGen rather than trust blind.

Gotchas

  • Divide-by-zero. When the last N bars are perfectly flat, highest == lowest and the formula divides by zero. replace(0, np.nan) turns those bars into NaN instead of inf — the correct, honest result.
  • Fast vs slow. The difference is one extra smoothing of %K. Quoting “14,3,3” almost always means the slow version.
  • Range uses highs and lows, not closes. A common bug is computing the range from closes; use the actual high and low.

Fast, slow, and “full”

The three names you’ll see are the same code with different smoothing:

  • Fast — raw %K, %D = SMA(%K, 3). Call stochastic(..., slow=False).
  • Slow — smooth %K once, then %D = SMA(slow %K, 3). The 14,3,3 default.
  • Full — the general case where the %K lookback, the %K smoothing, and the %D length are all independent parameters. That’s exactly the (k, smooth, d) signature above, so our one function covers all three by choice of arguments.

Knowing they’re one formula with different knobs saves you from installing three “different” indicators that are really the same arithmetic.

The lazy one-liner

import pandas_ta as ta
df.ta.stoch(k=14, d=3, smooth_k=3, append=True)   # adds STOCHk/STOCHd columns

Same indicator elsewhere: MQL5, Pine Script, EasyLanguage, NinjaScript.


This post is educational, not financial advice. Indicators describe the past; they don’t predict the future. Backtest anything before you risk real money on it.

Sources and further reading

  1. The Origins of the Stochastic Oscillator (CMT Association)
  2. Windowing operations (pandas documentation)

Historical research from the Algogen archive. Not investment advice.

Blog

Have a strategy idea?

Describe it in plain English, preview where your rules fire, then decide whether the evidence is worth a backtest.

Keep me postedHow it works