// 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);
}
}
}