Files
Miha Kralj 33d20f2a18 feat(dynamics): add PlusDI, MinusDI, PlusDM, MinusDM indicators
Complete thin Dx-composition wrapper indicators with full test coverage:

- PlusDi/MinusDi: Directional Indicator wrappers (DiPlus/DiMinus from Dx)
- PlusDm/MinusDm: Directional Movement wrappers (DmPlus/DmMinus from Dx)
- Individual validation tests per indicator directory (TALib, Skender, bounds)
- Combined unit tests (DiDm.Tests.cs) and validation tests (DiDm.Validation.Tests.cs)
- Quantower wrappers + tests for all 4 indicators
- PineScript v6 implementations with compensated RMA
- Normalized .md documentation for all indicators and categories
- 182 tests passing, 0 failures
2026-03-11 20:21:52 -07:00

200 lines
10 KiB
Markdown

# STOCH: Stochastic Oscillator
> *The Stochastic Oscillator doesn't follow price. It follows the speed, or momentum, of price. Momentum changes direction before price.*
| Property | Value |
|----------|-------|
| **Category** | Oscillator |
| **Inputs** | Bar series (High, Low, Close) |
| **Parameters** | `kLength` (default 14), `dPeriod` (default 3) |
| **Outputs** | Dual series (%K line, %D signal line) |
| **Output range** | $0$ to $100$ |
| **Warmup** | `kLength` bars |
| **PineScript** | [stoch.pine](stoch.pine) |
### Key takeaways
- Measures where the close sits within the highest-high to lowest-low range, scaled to $[0, 100]$.
- Produces two lines: raw %K (position in range) and %D (SMA of %K, the signal line).
- Uses `MonotonicDeque` pairs for O(1) amortized min/max tracking in streaming mode.
- Zero range (all bars identical) returns $0$ for %K, not $50$ or NaN.
- This is the Fast Stochastic variant. %K is unsmoothed; %D is $\text{SMA}(\%K, d)$.
## Historical Context
George C. Lane developed the Stochastic Oscillator in the late 1950s while working at Investment Educators in Chicago. His core observation was deceptively simple: in uptrends, closing prices tend to cluster near the high of the trading range; in downtrends, they cluster near the low. Quantifying that tendency produces a bounded oscillator that measures momentum rather than price.
Lane was careful to distinguish between Fast and Slow variants. The Fast Stochastic uses the raw %K and its SMA as %D. The Slow Stochastic applies additional smoothing: Slow %K equals Fast %D, and Slow %D is an SMA of Slow %K. This implementation produces the Fast variant. Traders who want Slow Stochastic should wrap the output with an additional SMA pass.
The Stochastic Oscillator and Williams %R share identical mathematics. The only difference is scale: $\text{WillR} = \text{Stoch \%K} - 100$. Lane's version scales $[0, 100]$ with overbought at the top; Williams inverts to $[-100, 0]$. Same information, different packaging.
## What It Measures and Why It Matters
The Stochastic Oscillator measures the closing price's position within the recent high-low range as a percentage. A reading of $100$ means the close equals the highest high over the lookback period. A reading of $0$ means the close equals the lowest low.
The %D signal line smooths %K via a simple moving average, providing crossover signals. When %K crosses above %D, momentum is shifting upward. When %K crosses below %D, momentum is shifting downward. These crossovers are most significant when they occur in overbought ($> 80$) or oversold ($< 20$) territory.
The indicator's real utility is divergence detection. When price makes a new high but %K fails to confirm, buying momentum is weakening. When price makes a new low but %K refuses to follow, selling pressure is exhausting. These divergences often precede reversals by several bars.
## Mathematical Foundation
### Core Formula
$$
HH_n = \max(H_i) \quad \text{for } i \in [t - n + 1, \, t]
$$
$$
LL_n = \min(L_i) \quad \text{for } i \in [t - n + 1, \, t]
$$
$$
\%K_t = 100 \times \frac{C_t - LL_n}{HH_n - LL_n}
$$
$$
\%D_t = \text{SMA}(\%K, d)
$$
where $n$ is `kLength` and $d$ is `dPeriod`.
### Parameter Mapping
| Parameter | Code | Default | Constraints |
|-----------|------|---------|-------------|
| K Length | `kLength` | 14 | `> 0` |
| D Period | `dPeriod` | 3 | `> 0` |
### Warmup Period
$$
W = n
$$
The indicator requires $n$ bars to fill the sliding window for highest-high and lowest-low computation. The %D SMA uses the PineScript convention of pre-filling its buffer with the first %K value, so it produces output from bar 0.
## Architecture & Physics
### 1. MonotonicDeque Streaming
Two `MonotonicDeque` instances provide O(1) amortized min/max tracking:
- **Max deque**: decreasing order of highs; front is always the window maximum.
- **Min deque**: increasing order of lows; front is always the window minimum.
- **Circular buffers** (`_hBuf`, `_lBuf`): store raw H/L values for deque rebuild on bar correction.
### 2. %D Signal Line
A separate circular buffer (`_dBuf`) with running sum computes the SMA of %K in O(1):
- First bar pre-fills the entire buffer with the initial %K value.
- Subsequent bars replace the oldest entry and update the running sum.
### 3. Batch Path
`Batch(ReadOnlySpan, ..., Span, Span, int, int)` delegates to `Highest.Batch()` and `Lowest.Batch()` for vectorized sliding min/max. Intermediate buffers use `stackalloc` for $\leq 256$ elements and `ArrayPool<double>` for larger inputs. The %D SMA uses a local circular buffer.
### 4. Edge Cases
| Condition | Behavior |
|-----------|----------|
| `kLength <= 0` or `dPeriod <= 0` | `ArgumentException` with `nameof()` |
| `NaN` / `Infinity` input | Substitutes last valid value per channel (H/L/C) |
| All NaN (no valid data yet) | Returns `NaN` for both %K and %D |
| Zero range ($HH = LL$) | %K returns $0$ |
| `isNew = false` | Restores `_ps`, rebuilds both deques from circular buffer |
## Interpretation and Signals
### Signal Zones
| Zone | Condition | Interpretation |
|------|-----------|----------------|
| Overbought | `%K > 80` | Close near period high; potential reversal down |
| Neutral | `20 ≤ %K ≤ 80` | Normal trading range |
| Oversold | `%K < 20` | Close near period low; potential reversal up |
### Signal Patterns
- **%K/%D crossover**: Bullish when %K crosses above %D; bearish when %K crosses below %D. Most reliable in overbought/oversold zones.
- **Divergence**: Price makes new highs while %K does not (bearish) or price makes new lows while %K does not (bullish).
- **Failure swing**: %K reaches an extreme, pulls back, fails to re-reach the extreme, then reverses.
- **Hook**: Short-term reversal when %K or %D hooks at an extreme without completing a full crossover.
### Practical Notes
- In strong trends, %K stays overbought or oversold for extended periods. Fading the trend on %K readings alone produces consistent losses.
- The %D crossover is a lagging signal by design (it's an SMA). Use it for confirmation, not anticipation.
- Fast Stochastic is noisier than Slow Stochastic. If whipsaws are a problem, either increase `kLength` or apply additional smoothing.
## Related Indicators
- [**Willr**](../willr/Willr.md): Identical math with inverted $[-100, 0]$ scale; $\text{WillR} = \text{\%K} - 100$.
- [**Stochf**](../stochf/Stochf.md): Fast Stochastic variant (may differ in %D handling).
- [**KDJ**](../kdj/Kdj.md): Extended stochastic with J-line divergence amplification.
- [**SMI**](../smi/Smi.md): Stochastic Momentum Index, measures distance from range midpoint rather than boundary.
## Validation
| Library | Status | Notes |
|---------|--------|-------|
| Skender | ✅ | `GetStoch(kLength, dPeriod, smoothPeriods=1)` matches within `1e-6` after warmup |
| TA-Lib | -- | Not directly validated (separate Stochf tests) |
| Tulip | -- | Not directly validated |
| Ooples | -- | Not validated |
## Performance Profile
### Key Optimizations
- **O(1) amortized streaming**: `MonotonicDeque` avoids full-window scans for min/max on each bar.
- **O(1) %D SMA**: Circular buffer with running sum eliminates iteration over the %D window.
- **Zero allocation**: `Update` uses pre-allocated circular buffers and `record struct State`.
- **Stackalloc/ArrayPool batch**: Intermediate buffers use `stackalloc` for $\leq 256$ elements, `ArrayPool` beyond.
### Operation Count (Streaming Mode)
| Operation | Count per bar |
|-----------|---------------|
| Comparisons | 2-3 (deque push amortized) |
| Divisions | 2 (range normalization + %D SMA) |
| Multiplications | 1 (`100 *`) |
| Additions/Subtractions | 2 (%D running sum update) |
| NaN checks | 3 (high, low, close) |
| **Total** | **~10 ops** |
### SIMD Analysis (Batch Mode)
| Property | Value |
|----------|-------|
| Vectorizable | Partially (via `Highest.Batch` / `Lowest.Batch`) |
| %K final loop | Scalar: `100 * (close[i] - LL[i]) / (HH[i] - LL[i])` |
| %D computation | Scalar circular buffer with running sum |
## Common Pitfalls
1. **Fast vs Slow confusion**: This implementation outputs Fast Stochastic. Many platforms default to Slow Stochastic, which smooths %K before computing %D. Direct comparison will not match without setting `smoothPeriods=1`.
2. **Zero range returns 0**: When all bars in the window share the same high and low, %K returns $0$. Williams %R returns $-50$ for the same condition. The choice is arbitrary but not interchangeable.
3. **Overbought does not equal sell**: In trending markets, %K stays overbought/oversold for extended periods. Counter-trend trades based solely on Stochastic readings produce drawdowns.
4. **Short lookback noise**: `kLength < 5` creates excessive whipsaws. The default 14 balances responsiveness and noise rejection.
5. **%D warmup convention**: The first %D value pre-fills the SMA buffer with the initial %K, matching PineScript behavior. Other implementations may use NaN until `dPeriod` bars of %K are available.
6. **Bar correction cost**: Correcting a bar (`isNew=false`) triggers O(kLength) deque rebuild. Infrequent in normal streaming but visible when batch-correcting thousands of bars.
## FAQ
**Q: What is the difference between Fast and Slow Stochastic?**
A: Fast Stochastic uses raw %K and SMA(%K) as %D. Slow Stochastic sets Slow %K = Fast %D, then Slow %D = SMA(Slow %K). This implementation is Fast Stochastic. Apply an additional SMA to the output for Slow.
**Q: Why does zero range return 0 instead of 50?**
A: Convention. When the range is zero, the close equals both the high and the low, so the "position in range" is undefined. Returning $0$ matches the PineScript and Skender conventions. Williams %R returns $-50$ for the same condition.
**Q: How does Stoch relate to Williams %R?**
A: They are the same formula with different scales. $\text{WillR} = \text{\%K} - 100$. Stoch scales $[0, 100]$; WillR scales $[-100, 0]$.
## References
- Lane, G. C. "Lane's Stochastics." *Technical Analysis of Stocks & Commodities*, 1984.
- Murphy, J. J. *Technical Analysis of the Financial Markets*. New York Institute of Finance, 1999.
- Appel, G.; Hitschler, F. *Stock Market Trading Systems*. Dow Jones-Irwin, 1980.
- Achelis, S. B. *Technical Analysis from A to Z*. McGraw-Hill, 2000.