Files
wickra/docs/wiki/indicators/momentum/Indicator-Stochastic.md
T
kingchenc 278b6afaa4 E4: commit the documentation sources
The 33 Markdown files under docs/wiki/ were never tracked. Commit them
into the repository so the documentation is versioned alongside the
code: 8 top-level pages plus 25 per-indicator deep dives under
indicators/{momentum,trend,volatility,volume}/.

The pages are kept in-repo (not pushed to a flat GitHub Wiki), so the
relative indicators/<family>/... links in Home.md resolve correctly
when rendered on GitHub.
2026-05-22 16:18:04 +02:00

221 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
|-------|-------|
| Family | Momentum |
| Sub-category | bounded oscillator |
| 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 HL 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
- [Indicator: Rsi](Indicator-Rsi.md) — sister bounded oscillator, slower
and smoother than `%K`.
- [Indicator: WilliamsR](Indicator-WilliamsR.md) — the negated mirror of
fast `%K`, plotted on `[100, 0]`.
- [Warmup Periods](../../Warmup-Periods.md) — `k_period + d_period 1` rule
in context.