Skip to content

Stochastic

The fast Stochastic Oscillator — %K measures where the current close sits inside the high/low range of the last k_period bars, and %D is a short SMA on top of %K.

Wickra ships a single fast variant (%K is the raw oscillator value, %D is its SMA). The "slow stochastic" wraps an additional SMA on %K; that variant is not built in — if you need it, smooth %K yourself via a Chain with Sma::new(slow_period).

Quick reference

FieldValue
FamilyMomentum Oscillators
Input typeCandle
Output typeStochasticOutput { k, d }
Output rangek, d ∈ [0, 100]
Default parametersk_period = 14, d_period = 3 (Stochastic::classic())
Warmup periodk_period + d_period − 1 (16 for the classic configuration)
Interpretationoverbought above 80, oversold below 20; %K / %D crossovers

Formula

For each new candle at time t, let HH and LL be the highest high and lowest low over the last k_period candles:

HH_t = max(high_{t-k_period+1}, …, high_t)
LL_t = min(low_{t-k_period+1},  …, low_t)

%K_t =  100 · (close_t − LL_t) / (HH_t − LL_t)        when HH ≠ LL
%K_t =  50                                            when HH == LL (flat range)

%D_t =  SMA_{d_period}(%K)_t

The implementation maintains HH and LL with two monotonic deques so each update is amortized O(1).

Parameters

NameTypeDefault (Python)Valid rangeDescription
k_periodusize14>= 1Lookback window for the %K extrema.
d_periodusize3>= 1SMA period for %D over the %K stream.

Either period being zero returns Error::PeriodZero.

Inputs / Outputs

From impl Indicator for Stochastic:

rust
use wickra::{Indicator, Stochastic, Candle, StochasticOutput};
// Stochastic: Input = Candle, Output = StochasticOutput
const _: fn(&mut Stochastic, Candle) -> Option<StochasticOutput> = <Stochastic as Indicator>::update;

StochasticOutput:

FieldDescription
kRaw %K (where close sits inside the window's H–L range).
dSMA(d_period) of the %K series — the slower "signal" line.

Python's Stochastic.batch(high, low, close) returns a (n, 2) array with columns [k, d]; warmup rows are [NaN, NaN].

Node's Stochastic.batch(high, low, close) returns a flat number[] of length n * 2, interleaved as [k_0, d_0, k_1, d_1, …]. There is no streaming update() on the Node binding — only batch is exposed.

Warmup

warmup_period() returns k_period + d_period − 1. The %K series itself becomes available at input k_period; the %D SMA then needs d_period of those %K values to seed, producing its first output at input k_period + d_period − 1. For the classic (14, 3) configuration this is 16 — verified above.

Edge cases

  • Flat range (HH == LL). The implementation returns %K = 50 by convention (mirroring RSI's flat-input behaviour). The unit test flat_range_yields_k_50 pins this; with a constant input both %K and %D collapse to 50.
  • Close at the window high. %K = 100 exactly; close at the window low gives %K = 0 exactly (tests close_at_high_yields_k_100 and close_at_low_yields_k_0).
  • Reset. reset() clears the candle buffer, both monotonic deques, the SMA, and last_k — the indicator returns to a freshly-constructed state.

Examples

Rust

rust
use wickra::{BatchExt, Candle, Indicator, Stochastic};

let candles: Vec<Candle> = (0..20)
    .map(|i| {
        let m = 10.0 + (i as f64 * 0.5).sin() * 2.0;
        Candle::new(m, m + 1.0, m - 1.0, m, 1.0, 0).unwrap()
    })
    .collect();
let mut s = Stochastic::new(14, 3)?;
let out = s.batch(&candles);
let v = out[15].unwrap();
println!("row 15  k={} d={}", v.k, v.d);
let v = out[19].unwrap();
println!("row 19  k={} d={}", v.k, v.d);
# Ok::<(), wickra::Error>(())

Verified output:

row 15  k=81.19360374383255 d=69.94559370965067
row 19  k=47.26766986190959 d=62.55762656278284

Python

python
import numpy as np
import wickra as ta

n = 20
i = np.arange(n, dtype=float)
m = 10.0 + np.sin(i * 0.5) * 2.0
high = m + 1.0
low  = m - 1.0
close = m
stoch = ta.Stochastic(14, 3)
out = stoch.batch(high, low, close)
print('shape :', out.shape)
print('warmup:', stoch.warmup_period())
print('row 15:', out[15])
print('row 19:', out[19])

Verified output:

shape : (20, 2)
warmup: 16
row 15: [81.19360374 69.94559371]
row 19: [47.26766986 62.55762656]

Node

javascript
const wickra = require('wickra');

const n = 20;
const high = [], low = [], close = [];
for (let i = 0; i < n; i++) {
  const m = 10.0 + Math.sin(i * 0.5) * 2.0;
  high.push(m + 1.0);
  low.push(m - 1.0);
  close.push(m);
}
const s = new wickra.Stochastic(14, 3);
const out = s.batch(high, low, close);
console.log('len    :', out.length);
console.log('row 15 :', { k: out[15 * 2], d: out[15 * 2 + 1] });
console.log('row 19 :', { k: out[19 * 2], d: out[19 * 2 + 1] });

Verified output:

len    : 40
row 15 : { k: 81.19360374383255, d: 69.94559370965067 }
row 19 : { k: 47.26766986190959, d: 62.55762656278284 }

Interpretation

  • Overbought / oversold zones. The canonical Lane thresholds are 80 and 20. Crossings back from outside these bands are typically used as reversal-confirmation signals, not entries on their own.
  • %K / %D crossover. %K crossing above %D from below is a short-horizon bullish signal; the mirror cross is bearish.
  • Divergence. A price making a new high but %K failing to confirm is a classic bearish divergence — same logic as RSI divergence but on a faster, range-based oscillator.

Common pitfalls

  • %K on a flat candle window is 50, not undefined. During a quiet drift where HH == LL, the convention used here is 50.0 and %D therefore also converges to 50.0. Do not interpret a sequence of 50s as a real oversold/overbought cycle — it is the silent-market fallback path.
  • Wickra exposes only the fast variant. "Slow stochastic" is %K = SMA(raw_%K, slow_k) with %D = SMA(%K, d_period) on top. The built-in Stochastic skips the first SMA; to reproduce the slow variant, drive the raw %K (taken from stoch.update(candle).k) through your own Sma.

References

  • George C. Lane, Investment Educators seminars and articles (late 1950s, popularised through the 1980s) — the original formulation of %K and %D as a fast oscillator.

See also