// DEM: DeMarker Oscillator // Measures demand by comparing current bar's High/Low against the previous bar's High/Low. // Tom DeMark, "The New Science of Technical Analysis" (1994). using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// DEM: DeMarker Oscillator /// /// /// Bounded [0, 1] oscillator measuring sequential buying/selling pressure: /// /// DeMax = max(High − prevHigh, 0) /// DeMin = max(prevLow − Low, 0) /// DEM = SMA(DeMax, period) / (SMA(DeMax, period) + SMA(DeMin, period)) /// /// Two O(1) rolling sums via circular buffers — 2 additions + 2 subtractions per bar /// regardless of period length. Guard: zero denominator → 0.5 (neutral). /// /// References: /// DeMark, Tom (1994). The New Science of Technical Analysis. /// PineScript reference: dem.pine /// [SkipLocalsInit] public sealed class Dem : ITValuePublisher { private readonly int _period; // Two circular buffers for O(1) SMA rolling sums private readonly double[] _deMaxBuf; private readonly double[] _deMinBuf; // Snapshots for idempotent isNew=false rollback — full array copy required // because isNew=false must restore the exact buffer state before the last new bar private readonly double[] _deMaxSnap; private readonly double[] _deMinSnap; [StructLayout(LayoutKind.Auto)] private record struct State( double DeMaxSum, double DeMinSum, double PrevHigh, double PrevLow, double LastValid, int Count, int Idx); private State _s; private State _ps; private readonly TBarPublishedHandler _barHandler; /// Display name for the indicator. public string Name { get; } /// Bars required for the first valid output. public int WarmupPeriod { get; } /// True once the rolling window is fully populated (needs period+1 bars). public bool IsHot => _s.Count > _period; /// Current DEM value in [0, 1]. public TValue Last { get; private set; } public event TValuePublishedHandler? Pub; /// /// Creates DEM with the specified SMA period. /// /// SMA lookback period (must be >= 1, default 14) public Dem(int period = 14) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _period = period; _deMaxBuf = new double[period]; _deMinBuf = new double[period]; _deMaxSnap = new double[period]; _deMinSnap = new double[period]; _s = new State(0, 0, double.NaN, double.NaN, 0.5, 0, 0); _ps = _s; WarmupPeriod = period + 1; Name = $"Dem({period})"; _barHandler = HandleBar; } /// /// Creates DEM chained to a TBarSeries source. /// public Dem(TBarSeries source, int period = 14) : this(period) { Prime(source); source.Pub += _barHandler; } private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); /// Resets all state to initial conditions. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(0, 0, double.NaN, double.NaN, 0.5, 0, 0); _ps = _s; Last = default; Array.Clear(_deMaxBuf); Array.Clear(_deMinBuf); Array.Clear(_deMaxSnap); Array.Clear(_deMinSnap); } /// /// Updates DEM with a new OHLCV bar. /// /// OHLCV bar data /// True to advance state; false to rewrite the latest bar /// Current DEM value as TValue [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { var s = _s; if (isNew) { // Snapshot buffers before mutation — required for idempotent rollback _ps = s; Array.Copy(_deMaxBuf, _deMaxSnap, _period); Array.Copy(_deMinBuf, _deMinSnap, _period); s.Count++; } else { // Rollback: restore scalar state + both buffer snapshots s = _ps; Array.Copy(_deMaxSnap, _deMaxBuf, _period); Array.Copy(_deMinSnap, _deMinBuf, _period); } // Sanitize OHLC inputs — use last-valid on NaN/Infinity double rawHigh = input.High; double rawLow = input.Low; double high = double.IsFinite(rawHigh) ? rawHigh : s.LastValid; double low = double.IsFinite(rawLow) ? rawLow : s.LastValid; // First bar: no previous high/low — DeMax=DeMin=0 by convention double prevHigh = double.IsFinite(s.PrevHigh) ? s.PrevHigh : high; double prevLow = double.IsFinite(s.PrevLow) ? s.PrevLow : low; // Per-bar demand/supply components double deMax = Math.Max(high - prevHigh, 0.0); double deMin = Math.Max(prevLow - low, 0.0); // O(1) circular-buffer rolling sums: subtract outgoing, write new, add incoming int idx = s.Idx; s.DeMaxSum -= _deMaxBuf[idx]; s.DeMinSum -= _deMinBuf[idx]; _deMaxBuf[idx] = deMax; _deMinBuf[idx] = deMin; s.DeMaxSum += deMax; s.DeMinSum += deMin; // Advance circular index only on new bars if (isNew) { s.Idx = (idx + 1) % _period; } // Compute DEM — default to 0.5 (neutral) on zero denominator double denom = s.DeMaxSum + s.DeMinSum; double dem = denom != 0.0 ? s.DeMaxSum / denom : 0.5; // Store last valid value for NaN protection if (double.IsFinite(dem)) { s.LastValid = dem; } // Store current high/low as next bar's prev s.PrevHigh = high; s.PrevLow = low; _s = s; Last = new TValue(input.Time, IsHot ? dem : s.LastValid); PubEvent(Last, isNew); return Last; } /// /// Updates DEM from a scalar TValue (uses Val as proxy; High=Low=Val). /// Primarily for ITValuePublisher compatibility — not the natural input for DEM. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { double v = double.IsFinite(input.Value) ? input.Value : _s.LastValid; return Update(new TBar(input.Time, v, v, v, v, 0), isNew); } /// /// Batch-computes DEM over raw High/Low spans. Zero-allocation path for large datasets. /// /// Source high prices /// Source low prices /// Destination span for DEM values /// SMA period (must be > 0) public static void Batch( ReadOnlySpan high, ReadOnlySpan low, Span output, int period = 14) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } int len = high.Length; if (low.Length != len) { throw new ArgumentException("Low length must match high length", nameof(low)); } if (output.Length != len) { throw new ArgumentException("Output length must match input length", nameof(output)); } if (len == 0) { return; } const int StackallocThreshold = 256; double[]? rentedMax = null; double[]? rentedMin = null; scoped Span deMaxBuf; scoped Span deMinBuf; if (period <= StackallocThreshold) { deMaxBuf = stackalloc double[period]; deMinBuf = stackalloc double[period]; } else { rentedMax = ArrayPool.Shared.Rent(period); rentedMin = ArrayPool.Shared.Rent(period); deMaxBuf = rentedMax.AsSpan(0, period); deMinBuf = rentedMin.AsSpan(0, period); } try { deMaxBuf.Clear(); deMinBuf.Clear(); double deMaxSum = 0.0; double deMinSum = 0.0; double prevHigh = double.NaN; double prevLow = double.NaN; int idx = 0; for (int i = 0; i < len; i++) { double h = high[i]; double l = low[i]; // First bar bootstrap: DeMax=DeMin=0 double ph = double.IsFinite(prevHigh) ? prevHigh : h; double pl = double.IsFinite(prevLow) ? prevLow : l; double deMax = Math.Max(h - ph, 0.0); double deMin = Math.Max(pl - l, 0.0); deMaxSum -= deMaxBuf[idx]; deMinSum -= deMinBuf[idx]; deMaxBuf[idx] = deMax; deMinBuf[idx] = deMin; deMaxSum += deMax; deMinSum += deMin; idx = (idx + 1) % period; prevHigh = h; prevLow = l; double denom = deMaxSum + deMinSum; output[i] = denom != 0.0 ? deMaxSum / denom : 0.5; } } finally { if (rentedMax != null) { ArrayPool.Shared.Return(rentedMax); } if (rentedMin != null) { ArrayPool.Shared.Return(rentedMin); } } } /// Primes the indicator by replaying historical data without firing events. public void Prime(TBarSeries source) { foreach (var bar in source) { Update(bar, isNew: true); } } }