2026-05-22 16:18:04 +02:00
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
| Field | Value |
|
|
|
|
|
|
|-------|-------|
|
2026-05-22 21:21:56 +02:00
|
|
|
|
| Family | Momentum Oscillators |
|
2026-05-22 16:18:04 +02:00
|
|
|
|
| Input type | `Candle` |
|
|
|
|
|
|
| Output type | `StochasticOutput { k, d }` |
|
|
|
|
|
|
| Output range | `k, d ∈ [0, 100]` |
|
|
|
|
|
|
| Default parameters | `k_period = 14`, `d_period = 3` (`Stochastic::classic()`) |
|
|
|
|
|
|
| Warmup period | `k_period + d_period − 1` (16 for the classic configuration) |
|
|
|
|
|
|
| Interpretation | overbought 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
|
|
|
|
|
|
|
|
|
|
|
|
| Name | Type | Default (Python) | Valid range | Description |
|
|
|
|
|
|
|------|------|------------------|-------------|-------------|
|
|
|
|
|
|
| `k_period` | `usize` | `14` | `>= 1` | Lookback window for the `%K` extrema. |
|
|
|
|
|
|
| `d_period` | `usize` | `3` | `>= 1` | SMA period for `%D` over the `%K` stream. |
|
|
|
|
|
|
|
|
|
|
|
|
Either period being zero returns `Error::PeriodZero`.
|
|
|
|
|
|
|
|
|
|
|
|
## Inputs / Outputs
|
|
|
|
|
|
|
|
|
|
|
|
From `impl Indicator for Stochastic`:
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
type Input = Candle;
|
|
|
|
|
|
type Output = StochasticOutput;
|
|
|
|
|
|
fn update(&mut self, candle: Candle) -> Option<StochasticOutput>;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`StochasticOutput`:
|
|
|
|
|
|
|
|
|
|
|
|
| Field | Description |
|
|
|
|
|
|
|-------|-------------|
|
|
|
|
|
|
| `k` | Raw `%K` (where `close` sits inside the window's H–L range). |
|
|
|
|
|
|
| `d` | `SMA(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 `50`s 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
|
|
|
|
|
|
|
2026-05-22 21:21:56 +02:00
|
|
|
|
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — sister bounded oscillator, slower
|
2026-05-22 16:18:04 +02:00
|
|
|
|
and smoother than `%K`.
|
2026-05-22 21:21:56 +02:00
|
|
|
|
- [Indicator: WilliamsR](../momentum-oscillators/Indicator-WilliamsR.md) — the negated mirror of
|
2026-05-22 16:18:04 +02:00
|
|
|
|
fast `%K`, plotted on `[−100, 0]`.
|
|
|
|
|
|
- [Warmup Periods](../../Warmup-Periods.md) — `k_period + d_period − 1` rule
|
|
|
|
|
|
in context.
|