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

7.9 KiB
Raw Permalink Blame History

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 (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, TRENDFLEX | 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:

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.