using System; namespace QuanTAlib.Tests; public class ZlemaValidationTests { // Note: External library validation is not feasible for ZLEMA: // - Tulip: Uses SMA-seeded EMA initialization, producing a persistent offset vs QuanTAlib's // debiased warmup (diff ~0.009% at bar 200, does not converge). Algorithm variant. // - Skender.Stock.Indicators: Does not have a ZLEMA implementation. // - TALib: Does not have a ZLEMA function. // - OoplesFinance: Does not have a ZLEMA implementation. // Validated against independent reference implementation in tests below. [Fact] public void Zlema_Streaming_MatchesReference() { const int period = 20; TSeries series = BuildSeries(300, seed: 5); double[] reference = new double[series.Count]; ReferenceZlema(series.Values, reference, period); var zlema = new Zlema(period); for (int i = 0; i < series.Count; i++) { double actual = zlema.Update(series[i]).Value; Assert.Equal(reference[i], actual, precision: 10); } } [Fact] public void Zlema_Batch_MatchesReference() { const int period = 14; TSeries series = BuildSeries(250, seed: 9); double[] reference = new double[series.Count]; ReferenceZlema(series.Values, reference, period); TSeries batch = Zlema.Batch(series, period); for (int i = 0; i < series.Count; i++) { Assert.Equal(reference[i], batch[i].Value, precision: 10); } } [Fact] public void Zlema_Span_MatchesReference() { const int period = 30; TSeries series = BuildSeries(200, seed: 12); double[] values = series.Values.ToArray(); var output = new double[values.Length]; var reference = new double[values.Length]; ReferenceZlema(values, reference, period); Zlema.Batch(values, output, period); for (int i = 0; i < values.Length; i++) { Assert.Equal(reference[i], output[i], precision: 10); } } private static void ReferenceZlema(ReadOnlySpan source, Span output, int period) { double alpha = 2.0 / (period + 1); double beta = 1.0 - alpha; int lag = ComputeLag(period); int bufferSize = lag + 1; double zlemaRaw = 0.0; double e = 1.0; bool warmup = true; double lastValid = double.NaN; double[] buffer = new double[bufferSize]; int head = 0; for (int i = 0; i < source.Length; i++) { double val = source[i]; if (double.IsFinite(val)) { lastValid = val; } else { val = lastValid; } if (double.IsNaN(val)) { output[i] = double.NaN; continue; } buffer[head] = val; head++; if (head == bufferSize) { head = 0; } double lagged = buffer[head]; double signal = Math.FusedMultiplyAdd(2.0, val, -lagged); zlemaRaw = Math.FusedMultiplyAdd(zlemaRaw, beta, alpha * signal); if (warmup) { e *= beta; if (e <= 1e-10) { warmup = false; output[i] = zlemaRaw; } else { output[i] = zlemaRaw / (1.0 - e); } } else { output[i] = zlemaRaw; } } } private static int ComputeLag(double period) { double lag = (period - 1.0) * 0.5; int lagInt = (int)Math.Round(lag, MidpointRounding.AwayFromZero); return Math.Max(1, lagInt); } private static TSeries BuildSeries(int count, int seed) { var series = new TSeries(); var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: seed); for (int i = 0; i < count; i++) { var bar = gbm.Next(isNew: true); series.Add(bar.Time, bar.Close); } return series; } }