Files
Miha Kralj 7db48e2418 feat(oscillators): add DSO - Ehlers Deviation-Scaled Oscillator
Implement DSO (TASC Oct 2018) with SSF 2-pole filter, RMS normalization,
and Fisher Transform (±0.99 clamp). Sealed class, O(1) streaming RMS via
RingBuffer, precomputed SSF coefficients.

New files: Dso.cs, Dso.Quantower.cs, Dso.md, dso.pine,
  Dso.Tests.cs (27), Dso.Validation.Tests.cs (7), Dso.Quantower.Tests.cs (11)

Updated: Exports.cs, _bridge.py, oscillators.py, SPEC.md,
  _sidebar.md, lib/_index.md, oscillators/_index.md,
  docs/indicators.md, docs/pinescript.md

All 19,565 tests pass, 0 warnings.
2026-03-17 11:59:04 -07:00

159 lines
7.9 KiB
Markdown
Raw Permalink 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.
# DSO: Ehlers Deviation-Scaled Oscillator
> *When price deviates from its smoothed norm, DSO amplifies the signal through Fisher transformation—producing sharp, decisive oscillator readings that compress during noise and expand during trends.*
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Oscillator |
| **Inputs** | Source (close) |
| **Parameters** | `period` (default 40) |
| **Outputs** | Single series (Dso) |
| **Output range** | Unbounded (typically ±3) |
| **Warmup** | `period` bars |
| **PineScript** | [dso.pine](dso.pine) |
- DSO (Deviation-Scaled Oscillator) is a Fisher-transformed, RMS-normalized Super Smoother oscillator that measures price deviation from its filtered trend, amplified through a nonlinear Fisher Transform.
- **Similar:** [REFLEX](../reflex/Reflex.md), [TRENDFLEX](../trendflex/Trendflex.md) | **Complementary:** ADX for trend confirmation | **Trading note:** Values beyond ±2 indicate extreme deviation; zero crossings signal direction changes.
- No external validation libraries implement DSO. Validated through self-consistency and behavioral testing.
DSO applies three stages of signal processing: (1) input whitening to remove DC bias and Nyquist aliasing, (2) a 2-pole Super Smoother filter for trend extraction, and (3) RMS normalization followed by a Fisher Transform that amplifies readings near the center and compresses extremes, producing sharp turning-point signals.
## Historical Context
The Deviation-Scaled Oscillator was published by John F. Ehlers in the October 2018 issue of *Technical Analysis of Stocks & Commodities* magazine. Ehlers described it as a "Fisherized" version of his deviation-scaled approach, combining the Super Smoother filter (his signature contribution to technical analysis) with RMS normalization and the Fisher Transform (inverse hyperbolic tangent) to produce an oscillator with Gaussian-distributed output—ideal for statistical threshold-based trading.
## Architecture & Physics
DSO operates in four stages:
### Stage 1: Input Whitening
The raw price is whitened by computing a 2-bar difference:
$$ \text{Zeros}_t = \text{Close}_t - \text{Close}_{t-2} $$
This removes the DC (constant) component and rejects Nyquist frequency aliasing, ensuring only meaningful mid-frequency cycles pass through to the filter.
### Stage 2: Super Smoother Filter (2-pole Butterworth)
The whitened input is smoothed using Ehlers' 2-pole Super Smoother at half-period cutoff:
$$ \text{Filt}_t = \frac{c_1}{2}(\text{Zeros}_t + \text{Zeros}_{t-1}) + c_2 \cdot \text{Filt}_{t-1} + c_3 \cdot \text{Filt}_{t-2} $$
Coefficients are precomputed from the period:
$$ a_1 = e^{-\sqrt{2} \cdot \pi / (\text{period}/2)} $$
$$ c_2 = 2 a_1 \cos\left(\sqrt{2} \cdot \pi / (\text{period}/2)\right), \quad c_3 = -a_1^2, \quad c_1 = 1 - c_2 - c_3 $$
### Stage 3: RMS Normalization
Root Mean Square over the period window normalizes the filtered signal by its recent volatility:
$$ \text{RMS}_t = \sqrt{\frac{1}{N} \sum_{i=0}^{N-1} \text{Filt}_{t-i}^2} $$
$$ \text{ScaledFilt}_t = \frac{\text{Filt}_t}{\text{RMS}_t} $$
Implemented using a `RingBuffer` for O(1) running sum updates. RMS is floored at `1e-10` to prevent division by zero.
### Stage 4: Fisher Transform
The scaled filter output is clamped to ±0.99 and passed through the Fisher (inverse hyperbolic tangent) Transform:
$$ \text{DSO}_t = \frac{1}{2} \ln\left(\frac{1 + \text{clamp}(\text{ScaledFilt}_t)}{1 - \text{clamp}(\text{ScaledFilt}_t)}\right) $$
The Fisher Transform converts the bounded [-1, 1] input into an unbounded Gaussian-like output, amplifying readings near zero (where reversals often originate) and compressing extreme values.
Implemented with FMA for the SSF filter:
```csharp
filt = Math.FusedMultiplyAdd(_c1Half, zeros + _s.Zeros1,
Math.FusedMultiplyAdd(_c2, _s.Filt, _c3 * _s.Filt1));
```
## Performance Profile
DSO combines a 2-pole IIR filter, O(1) RMS via ring buffer, and the Fisher Transform logarithm.
### Operation Count (Streaming Mode, Scalar)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| **Stage 1: Input Whitening** | | | |
| SUB (Close - Close[2]) | 1 | 1 | 1 |
| **Stage 2: Super Smoother (2-pole Butterworth)** | | | |
| ADD (zeros + zeros1) | 1 | 1 | 1 |
| FMA (c1Half × sum + c2×filt + c3×filt1) | 2 | 4 | 8 |
| MUL (c3 × filt1) | 1 | 3 | 3 |
| **Stage 3: RMS Buffer Update** | | | |
| MUL (filt × filt) | 1 | 3 | 3 |
| ADD/SUB (sumSquared update) | 2 | 1 | 2 |
| FMA (running sum) | 1 | 4 | 4 |
| MUL (sumSquared × periodRecip) | 1 | 3 | 3 |
| SQRT | 1 | 15 | 15 |
| **Stage 4: Fisher Transform** | | | |
| DIV (filt / rms) | 1 | 15 | 15 |
| CLAMP (max/min) | 2 | 1 | 2 |
| ADD/SUB (1±clamped) | 2 | 1 | 2 |
| DIV (ratio) | 1 | 15 | 15 |
| LOG | 1 | 20 | 20 |
| MUL (0.5 × log) | 1 | 3 | 3 |
| **Total** | | | **~97 cycles** |
**Dominant costs:**
- LOG (20 cycles, 21%) — Fisher Transform
- SQRT (15 cycles, 15%) — RMS calculation
- DIV (2×15 cycles, 31%) — RMS normalization + Fisher ratio
### Batch Mode (SIMD Analysis)
DSO is **not SIMD-parallelizable** across bars due to:
1. Super Smoother is a 2-pole IIR filter with recursive state
2. RMS depends on running sum of squared values
3. Fisher Transform LOG is inherently scalar
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 8/10 | Fisher Transform amplifies clean signals near zero |
| **Timeliness** | 8/10 | Input whitening + SSF = low lag for oscillator class |
| **Overshoot** | 7/10 | Clamping at ±0.99 prevents infinity, but Fisher amplifies |
| **Smoothness** | 7/10 | Super Smoother provides good noise rejection |
## Validation
DSO is not implemented in mainstream libraries. Validation relies on behavioral testing.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **TA-Lib** | N/A | Not implemented |
| **Skender** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented |
| **Ooples** | N/A | Not implemented |
| **Behavioral** | ✅ | Validated: constant→zero, symmetry, mode consistency |
### Behavioral Test Summary
- **Constant Input → Zero**: Constant close → zeros=0 → filt=0 → scaledFilt=0 → Fisher(0)=0
- **Fisher Symmetry**: DSO(-x) = -DSO(x) — output is antisymmetric
- **Trending Input**: Strong trend produces non-zero DSO values
- **Mode Consistency**: Streaming, batch, span, and event-driven modes produce identical results
- **Bar Correction**: Snapshot/Restore via RingBuffer produces exact rollback
## Common Pitfalls
1. **Warmup Period**: DSO requires `Period` bars to fill the RMS buffer. Before warmup, output will be unstable. Use `IsHot` to detect readiness.
2. **Close[2] Dependency**: The whitening step `Close - Close[2]` requires tracking two-bar-ago close. State stores both `Src1` and `Src2` for this purpose. On the first two bars, the filter output is zero.
3. **Fisher Transform Singularity**: The Fisher Transform has a singularity at ±1 (ln(0)). Clamping at ±0.99 prevents this. The maximum possible DSO value is ±2.646 (`0.5 * ln(199) ≈ 2.646`).
4. **RMS Floor**: During perfectly flat markets (zero volatility), RMS approaches zero. The `MinRms = 1e-10` floor prevents division by zero but may produce large scaled values. The ±0.99 Fisher clamp provides a second safety net.
5. **Not a Bounded Oscillator**: Unlike RSI or Stochastics, DSO is unbounded. Values beyond ±2 indicate extreme deviation—roughly equivalent to a 2-sigma event in the Fisher-transformed space.
6. **Period Selection**: Ehlers recommends period=40 (approximately one market month of bars on daily charts). Shorter periods increase sensitivity but also noise; longer periods add lag.
7. **Bar Correction**: Like all QuanTAlib indicators, DSO supports bar correction via the `isNew` parameter. The RingBuffer `Snapshot()`/`Restore()` mechanism handles this atomically.