using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// NYQMA: Nyquist Moving Average /// /// /// Dr. Manfred G. Dürschner's lag-compensated filter applying the Nyquist-Shannon /// sampling theorem to cascaded LWMAs. A single-smoothed LWMA and a double-smoothed /// LWMA are combined via extrapolation to cancel lag while preventing aliasing. /// /// Calculation: NYQMA = (1 + α) × MA1 − α × MA2 where α = N2 / (N1 − N2). /// MA1 = WMA(src, N1), MA2 = WMA(MA1, N2). /// Nyquist constraint: N2 ≤ floor(N1/2). /// O(1) per bar via composed Wma instances. /// /// Detailed documentation /// Reference Pine Script implementation [SkipLocalsInit] public sealed class Nyqma : AbstractBase { private readonly int _period; private readonly int _nyquistPeriod; private readonly double _alpha; private readonly Wma _wma1; private readonly Wma _wma2; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _handler; private bool _disposed; private int _sampleCount; public override bool IsHot => _sampleCount >= WarmupPeriod; /// /// Creates NYQMA with specified periods. /// /// Primary LWMA period N1 (must be ≥ 3, default: 89) /// Secondary LWMA period N2 (clamped to ≤ floor(N1/2), default: 21) public Nyqma(int period = 89, int nyquistPeriod = 21) { if (period < 3) { throw new ArgumentException("Period must be at least 3", nameof(period)); } _period = period; _nyquistPeriod = Math.Clamp(nyquistPeriod, 1, period / 2); _alpha = (double)_nyquistPeriod / (_period - _nyquistPeriod); _wma1 = new Wma(_period); _wma2 = new Wma(_nyquistPeriod); Name = $"Nyqma({_period},{_nyquistPeriod})"; WarmupPeriod = _period + _nyquistPeriod - 1; } /// /// Creates NYQMA subscribed to a source publisher. /// public Nyqma(ITValuePublisher source, int period = 89, int nyquistPeriod = 21) : this(period, nyquistPeriod) { _source = source; _handler = Handle; source.Pub += _handler; } protected override void Dispose(bool disposing) { if (!_disposed) { if (disposing && _source != null && _handler != null) { _source.Pub -= _handler; } _disposed = true; } base.Dispose(disposing); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _sampleCount++; } TValue wma1Result = _wma1.Update(input, isNew); TValue wma2Result = _wma2.Update(wma1Result, isNew); double w1 = wma1Result.Value; double w2 = wma2Result.Value; // NYQMA = (1 + α) × MA1 − α × MA2 double nyqma = Math.FusedMultiplyAdd(1.0 + _alpha, w1, -_alpha * w2); Last = new TValue(input.Time, nyqma); PubEvent(Last, isNew); return Last; } public override TSeries Update(TSeries source) { if (source.Count == 0) { return []; } int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); source.Times.CopyTo(tSpan); Batch(source.Values, vSpan, _period, _nyquistPeriod); Reset(); int lookback = WarmupPeriod + 10; int startIndex = Math.Max(0, len - lookback); for (int i = startIndex; i < len; i++) { Update(new TValue(source.Times[i], source.Values[i])); } _sampleCount = len; Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } private void Handle(object? sender, in TValueEventArgs args) { Update(args.Value, args.IsNew); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { Reset(); foreach (var value in source) { Update(new TValue(DateTime.MinValue, value)); } } public static TSeries Batch(TSeries source, int period, int nyquistPeriod) { var nyqma = new Nyqma(period, nyquistPeriod); return nyqma.Update(source); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int period, int nyquistPeriod) { if (period < 3) { throw new ArgumentException("Period must be at least 3", nameof(period)); } if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } int len = source.Length; if (len == 0) { return; } int n2 = Math.Clamp(nyquistPeriod, 1, period / 2); double alpha = (double)n2 / (period - n2); double[]? wma1Array = len > 1024 ? ArrayPool.Shared.Rent(len) : null; Span wma1 = len <= 1024 ? stackalloc double[len] : wma1Array!.AsSpan(0, len); double[]? wma2Array = len > 1024 ? ArrayPool.Shared.Rent(len) : null; Span wma2 = len <= 1024 ? stackalloc double[len] : wma2Array!.AsSpan(0, len); try { Wma.Batch(source, wma1, period); Wma.Batch(wma1, wma2, n2); // NYQMA = (1 + α) × MA1 − α × MA2 double onePlusAlpha = 1.0 + alpha; for (int i = 0; i < len; i++) { output[i] = Math.FusedMultiplyAdd(onePlusAlpha, wma1[i], -alpha * wma2[i]); } } finally { if (wma1Array != null) { ArrayPool.Shared.Return(wma1Array); } if (wma2Array != null) { ArrayPool.Shared.Return(wma2Array); } } } public static (TSeries Results, Nyqma Indicator) Calculate(TSeries source, int period, int nyquistPeriod) { var indicator = new Nyqma(period, nyquistPeriod); TSeries results = indicator.Update(source); return (results, indicator); } public override void Reset() { _wma1.Reset(); _wma2.Reset(); _sampleCount = 0; Last = default; } }