Files
Miha Kralj 6f0a339c9b fix: resolve build and test errors
- Sar.Quantower.Tests.cs: add missing opening quote on string literal (line 48)
- Exports.cs: rename Correlation.Batch → Correl.Batch (CS0103)
- Ad.Validation.Tests.cs: fix Ooples OutputValues key "Ad" → "Adl"
2026-03-16 12:45:13 -07:00

13 KiB
Raw Permalink Blame History

DYMI: Dynamic Momentum Index

The market is not a fixed-frequency oscillator. Why would you analyze it with one?

Property Value
Category Oscillator
Inputs Source (close)
Parameters basePeriod (default 14), shortPeriod (default 5), longPeriod (default 10), minPeriod (default 3), maxPeriod (default 30)
Outputs Single series (Dymi)
Output range Varies (see docs)
Warmup 1 bar
PineScript dymi.pine
  • DYMI is a volatility-adaptive RSI: when recent price swings are large relative to longer-term swings, the RSI period shortens and the indicator be...
  • Similar: RSI, Stoch | Complementary: ATR | Trading note: Dynamic Momentum Index; RSI with variable lookback based on volatility. Faster in calm, slower in volatile markets.
  • Validated against TA-Lib, Skender, and Tulip reference implementations where available.

DYMI is a volatility-adaptive RSI: when recent price swings are large relative to longer-term swings, the RSI period shortens and the indicator becomes more responsive; when price action tightens, the period extends and the output smooths. The result is an oscillator that self-adjusts its sensitivity to the market's current state, avoiding both the lag of long fixed-period RSIs in trending regimes and the noise of short-period RSIs in ranging ones.

Historical Context

Tushar Chande and Stanley Kroll introduced DYMI in The New Technical Trader (1994) as a practical answer to a genuine problem: the standard RSI's fixed period is a blunt instrument. A 14-bar RSI responds identically whether the market has been oscillating ±5% per day or ±0.2%. Chande and Kroll observed that a shorter period in high-volatility environments catches reversals earlier; a longer period in quiet conditions eliminates whipsaws.

The mechanism they chose was straightforward: compute the ratio of short-term to long-term price standard deviation. When this ratio exceeds 1, the market is more volatile than its recent baseline — shorten the period. When the ratio is below 1, lengthen it. The result gets clamped to a configurable [minPeriod, maxPeriod] range, and a standard Wilder RSI runs on the resulting dynamic period.

The indicator has no widely adopted C# open-source implementation, which is why cross-library validation is self-consistency only. The original book uses population standard deviation over rolling windows — this implementation matches that specification.

Indicator Adaptation Mechanism Output Range Warmup
RSI (Wilder) None — fixed period 0100 period+1
CRSI (Connors) Three-component composite, no period adaptation 0100 rankPeriod+rsiPeriod
DYMI (Chande/Kroll) Dual StdDev ratio drives period selection 0100 longPeriod+maxPeriod
LRSI (Ehlers Laguerre) Cycle-adaptive Laguerre filter stages 01 4

Architecture & Physics

3.1 Stage 1: Dual Circular-Buffer Standard Deviation

Two O(1) StdDev estimators maintain running sums for windows of shortPeriod and longPeriod bars respectively. Each bar, the oldest value is evicted and the new value is ingested:

\bar{x} = \frac{\sum x_i}{n}, \quad \sigma = \sqrt{\frac{\sum x_i^2}{n} - \bar{x}^2}

This form avoids rescanning the window on every bar. Floating-point drift is inherent but bounded — the window size keeps the accumulated error small in practice (typical window sizes 530 bars).

3.2 Stage 2: Volatility Ratio → Dynamic Period

V = \frac{\sigma_{\text{short}}}{\sigma_{\text{long}}} n_{\text{dyn}} = \operatorname{clamp}\!\left(\operatorname{round}\!\left(\frac{n_{\text{base}}}{V}\right),\; n_{\text{min}},\; n_{\text{max}}\right)

When V = 0 (both windows have identical prices, e.g., a flat series), n_{\text{dyn}} defaults to n_{\text{max}} as the safest fallback. When V \leq 10^{-10} (effectively zero), the same clamp applies.

The clamp ensures the RSI period cannot collapse to 1 (which is numerically unstable and meaningless) or expand to absurd lengths. Default bounds [3, 30] match Chande and Kroll's original recommendation.

3.3 Stage 3: Wilder RMA RSI with Adaptive Alpha

Per-bar, a new alpha is derived from the current n_{\text{dyn}}:

\alpha = \frac{1}{n_{\text{dyn}}}, \quad \beta = 1 - \alpha

The Wilder smoothing (RMA) of gains and losses then updates:

\overline{G}_t = \beta \cdot \overline{G}_{t-1} + \alpha \cdot \max(\Delta p, 0) \overline{L}_t = \beta \cdot \overline{L}_{t-1} + \alpha \cdot \max(-\Delta p, 0) \text{RSI} = 100 \cdot \frac{\overline{G}}{\overline{G} + \overline{L}}

FMA is used in the hot path to reduce rounding error:

s.AvgGain = Math.FusedMultiplyAdd(s.AvgGain, beta, alpha * gain);
s.AvgLoss = Math.FusedMultiplyAdd(s.AvgLoss, beta, alpha * loss);

3.4 Warmup Compensation

A warmup compensator tracks the accumulated decay e_t = \beta^t and scales the raw RMA values to produce valid output from bar 1:

\hat{G}_t = \frac{\overline{G}_t}{1 - e_t}, \quad \hat{L}_t = \frac{\overline{L}_t}{1 - e_t}

Once e_t \leq 10^{-10}, the compensator deactivates and standard Wilder smoothing proceeds. This is the same design used throughout QuanTAlib's RSI-based oscillators (CRSI, QQE, DOSC).

3.5 Bar Correction (isNew Rollback)

The streaming Update(TValue, bool isNew) contract requires:

  • isNew = true: snapshot state and both circular buffers, then advance.
  • isNew = false: restore state and buffers from snapshot, recompute with new value.

Since RingBuffer instances are heap objects that cannot be rolled back via struct copy alone, explicit Array.Copy snapshots (_shortBufSnap, _longBufSnap) are maintained alongside the State record struct.

Mathematical Foundation

Full Derivation

Given close prices c_1, c_2, \ldots, c_t, let windows be W_s of size n_s and W_l of size n_l, with n_s < n_l:

Population variance (O(1) form):

\sigma^2 = \frac{\sum_{i \in W} c_i^2}{|W|} - \left(\frac{\sum_{i \in W} c_i}{|W|}\right)^2

Volatility ratio:

V_t = \begin{cases} \sigma_s / \sigma_l & \text{if } \sigma_l > 10^{-10} \\ 1 & \text{otherwise} \end{cases}

Dynamic period:

n_t = \operatorname{clamp}\!\left(\left\lfloor \frac{n_{\text{base}}}{V_t} + 0.5 \right\rfloor,\; n_{\min},\; n_{\max}\right)

Wilder RSI at bar t with adaptive alpha \alpha_t = 1 / n_t:

\overline{G}_t = \alpha_t \cdot G_t + (1 - \alpha_t) \cdot \overline{G}_{t-1} \text{DYMI}_t = 100 \cdot \frac{\overline{G}_t}{\overline{G}_t + \overline{L}_t}

Degenerate Cases

Condition V n_{\text{dyn}} Effect
\sigma_l = 0 (constant prices) n_{\max} Maximally smooth; RSI→50
\sigma_s \gg \sigma_l (V \gg 1) large n_{\min} Fastest possible RSI
\sigma_s \ll \sigma_l (V \ll 1) small n_{\max} Slowest possible RSI
n_{\min} = n_{\max} = n_{\text{base}} any n_{\text{base}} Identical to RSI(n_{\text{base}})

Performance Profile

Operation Count (Streaming Mode)

DYMI computes a dynamic momentum oscillator using an EMA-smoothed velocity + acceleration blend.

Operation Count Cost (cycles) Subtotal
FMA × 2 (fast/slow EMA updates) 2 4 8
SUB (velocity = fast slow EMA) 1 1 1
FMA (acceleration = EMA of velocity) 1 4 4
FMA (blend velocity + acceleration) 1 4 4
Total 5 ~17 cycles

Three EMA instances. ~17 cycles per bar at steady state.

Batch Mode (SIMD Analysis)

Operation Vectorizable? Notes
All EMA passes × 3 No Recursive IIR — sequential
Subtraction + blend Yes VSUBPD + VFMADD after EMA arrays known

Operations per bar (streaming Update):

Operation Count
Short StdDev O(1) update (evict + insert + recompute mean/var) 6
Long StdDev O(1) update 6
Division (vol ratio) 1
Round + clamp 3
FMA ×2 (gain/loss Wilder) 2
RSI formula 3
Array.Copy (isNew snapshots, amortized) ~2n/bar
Total arithmetic ~23 + 2n copy

SIMD is not applicable to the streaming Update path because the period changes per bar, breaking vectorization. The static Batch(Span) path processes the entire series in a single loop with O(1) arithmetic per bar; AVX2 vectorization of the StdDev summation is structurally possible but not implemented, as the gains are marginal for typical window sizes (530).

Complexity: O(1) per bar for Update; O(n) total for Batch.

Memory: O(shortPeriod + longPeriod) for buffers; O(1) state beyond that.

Quality metrics (110):

Attribute Score Note
Adaptiveness 9 Period covers minPeriodmaxPeriod range continuously
Smoothness 7 Wilder smoothing inherits lag characteristics
Responsiveness 8 Shortens on volatility spikes
Noise rejection 7 Clamp prevents degenerate periods
Interpretability 8 [0,100] RSI scale is familiar

Validation

No external C# library (Skender, TA-Lib, Tulip, Ooples) implements DYMI. Validation is self-consistency only.

Test Method Tolerance Result
Streaming == Batch (TSeries) GBM 300 bars 1e-10 Pass
Streaming == Batch (Span) GBM 300 bars 1e-10 Pass
Streaming == Eventing GBM 200 bars 1e-10 Pass
Output ∈ [0,100] GBM 500 bars, σ=0.5 Pass
Constant price → RSI=50 100 bars @ 100.0 1e-6 Pass
Fixed period identity minPeriod=maxPeriod=basePeriod 1e-9 Pass
Determinism Two identical GBM seeds 1e-10 Pass

Mathematical identity test: When minPeriod == maxPeriod == basePeriod, the dynamic period is always fixed at basePeriod regardless of the volatility ratio. Under this constraint, DYMI produces output numerically identical to Rsi(basePeriod) (verified at tolerance 1e-9).

Common Pitfalls

  1. longPeriod <= shortPeriod: The constructor throws ArgumentException if this constraint is violated. The volatility ratio is undefined when both windows cover the same bars.

  2. Zero-variance series (flat price): When σ_long = 0, the ratio is undefined; the implementation defaults to V = 1n_dyn = n_base. This is correct — a flat series should produce neutral RSI(=50) at the base period rate, not a degenerate output.

  3. Warmup period misinterpretation: WarmupPeriod = longPeriod + maxPeriod. The dominant warmup is the Wilder RMA, which takes maxPeriod bars to settle after the long StdDev window fills. Using DYMI output before IsHot = true will produce compensated but less accurate values.

  4. Period clamp masking pathology: If minPeriod and maxPeriod are very close (e.g., both 14), the adaptive behavior is effectively disabled and DYMI degenerates to standard RSI. This is a valid use case but should be intentional.

  5. Floating-point drift in running sums: The O(1) variance formula E[x^2] - E[x]^2 is numerically unstable for large values or large windows — specifically, catastrophic cancellation can occur. For price data in the range [0.01, 100000] and periods ≤ 100, drift is negligible in practice. For exotic inputs, a periodic full-recalculation reset (every N steps) would be appropriate; the current implementation does not perform this.

  6. Assumption of IID returns: The period-selection formula n_{\text{dyn}} = n_{\text{base}} / V implicitly assumes that the volatility ratio directly translates to an appropriate lookback scaling. This holds approximately for Gaussian returns but can under- or over-shoot in heavy-tailed regimes where short spikes inflate V transiently.

  7. Array.Copy cost on rollback: Each isNew = false call copies two arrays of size shortPeriod and longPeriod. For default periods (5+10=15 doubles = 120 bytes), this is negligible. For periods > 256, the copy still occurs on heap memory and remains fast relative to any downstream computation.

References

  • Chande, T. & Kroll, S. (1994). The New Technical Trader. John Wiley & Sons. Ch. 3: Dynamic Momentum Index.
  • Wilder, J.W. (1978). New Concepts in Technical Trading Systems. Trend Research. (RSI original source)
  • Connors, L. & Alvarez, C. (2012). An Introduction to ConnorsRSI. TradingMarkets. (CRSI comparison reference)