namespace QuanTAlib.Tests; public class BlmaTests { private readonly GBM _gbm; public BlmaTests() { _gbm = new GBM(); } [Fact] public void Constructor_ValidatesInput() { Assert.Throws(() => new Blma(0)); Assert.Throws(() => new Blma(-1)); } [Fact] public void Constructor_ValidatesSource() { Assert.Throws(() => new Blma(null!, 10)); } [Fact] public void BasicCalculation_MatchesManual() { var blma = new Blma(3); var input = new[] { 10.0, 20.0, 30.0 }; // Bar 1: Count=1. Weights for n=1: [1]. Result = 10. var r1 = blma.Update(new TValue(DateTime.UtcNow, input[0])); Assert.Equal(10.0, r1.Value); // Bar 2: Count=2. Weights for n=2 sum to 0. Fallback to average: (10+20)/2 = 15. var r2 = blma.Update(new TValue(DateTime.UtcNow, input[1])); Assert.Equal(15.0, r2.Value); // Bar 3: Count=3. Weights [0, 1, 0]. Sum=1. Result=20. var r3 = blma.Update(new TValue(DateTime.UtcNow, input[2])); Assert.Equal(20.0, r3.Value, 1e-6); } [Fact] public void AllModes_ProduceSameResult() { const int period = 10; var bars = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var series = bars.Close; // 1. Batch Mode var batchSeries = new Blma(period).Update(series); double expected = batchSeries.Last.Value; // 2. Span Mode var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; Blma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode var streamingInd = new Blma(period); for (int i = 0; i < series.Count; i++) { streamingInd.Update(series[i]); } double streamingResult = streamingInd.Last.Value; // Assert Assert.Equal(expected, spanResult, 1e-9); Assert.Equal(expected, streamingResult, 1e-9); } [Fact] public void NaN_Handling() { var blma = new Blma(5); blma.Update(new TValue(DateTime.UtcNow, 10)); blma.Update(new TValue(DateTime.UtcNow, 20)); // For N=2, weights sum to 0. Fallback to average: (10+20)/2 = 15. var result = blma.Update(new TValue(DateTime.UtcNow, double.NaN)); Assert.Equal(15.0, result.Value); // Should return last valid value Assert.Equal(15.0, blma.Last.Value); // Should retain last valid value } [Fact] public void IsNew_Behavior() { var blma = new Blma(3); // Bar 1 blma.Update(new TValue(DateTime.UtcNow, 10), isNew: true); // Bar 2 blma.Update(new TValue(DateTime.UtcNow, 20), isNew: true); // Bar 3 (Update) blma.Update(new TValue(DateTime.UtcNow, 30), isNew: true); var val1 = blma.Last.Value; // Bar 3 (Correction) blma.Update(new TValue(DateTime.UtcNow, 40), isNew: false); var val2 = blma.Last.Value; // For Blackman window, the newest value (index N-1) has weight 0. // So changing the newest value does NOT change the current result. Assert.Equal(val1, val2); // However, the internal buffer MUST be updated. // Case A: Bar 3 = 40 (current state) blma.Update(new TValue(DateTime.UtcNow, 100), isNew: true); var valWith40 = blma.Last.Value; // Case B: Reconstruct scenario with Bar 3 = 30 var blma2 = new Blma(3); blma2.Update(new TValue(DateTime.UtcNow, 10), isNew: true); blma2.Update(new TValue(DateTime.UtcNow, 20), isNew: true); blma2.Update(new TValue(DateTime.UtcNow, 30), isNew: true); blma2.Update(new TValue(DateTime.UtcNow, 100), isNew: true); var valWith30 = blma2.Last.Value; Assert.NotEqual(valWith30, valWith40); } [Fact] public void Prime_PreservesTimestamps() { var blma = new Blma(5); double[] input = [1, 2, 3, 4, 5]; var timestamps = new List(); blma.Pub += (object? sender, in TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime); blma.Prime(input); Assert.Equal(input.Length, timestamps.Count); // Verify timestamps are unique and increasing for (int i = 1; i < timestamps.Count; i++) { Assert.True(timestamps[i] > timestamps[i - 1], $"Timestamp at {i} ({timestamps[i].Ticks}) should be greater than {i - 1} ({timestamps[i - 1].Ticks})"); } } [Fact] public void Prime_Overload_UsesProvidedTimestamps() { var blma = new Blma(5); var now = DateTime.UtcNow; TValue[] input = [ new(now, 1), new(now.AddMinutes(1), 2), new(now.AddMinutes(2), 3) ]; var timestamps = new List(); blma.Pub += (object? sender, in TValueEventArgs args) => timestamps.Add(args.Value.AsDateTime); blma.Prime(input); Assert.Equal(input.Length, timestamps.Count); Assert.Equal(input[0].AsDateTime, timestamps[0]); Assert.Equal(input[1].AsDateTime, timestamps[1]); Assert.Equal(input[2].AsDateTime, timestamps[2]); } }