From 3734a1c5f64e5804e42cb7d381924bc49e50f8cc Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Sun, 7 Dec 2025 14:36:22 -0800 Subject: [PATCH] feat: Introduce ITValuePublisher interface and refactor indicators for event-driven value updates. --- .vscode/extensions.json | 6 +- lib/averages/dema/Dema.Tests.cs | 77 ++++++++++++++++++----- lib/averages/dema/Dema.cs | 19 ++++-- lib/averages/ema/Ema.Tests.cs | 76 ++++++++++++++++++----- lib/averages/ema/Ema.cs | 26 ++++++-- lib/averages/sma/Sma.Tests.cs | 83 +++++++++++++++++++------ lib/averages/sma/Sma.cs | 92 +++++++++++++--------------- lib/averages/tema/Tema.Tests.cs | 73 +++++++++++++++++----- lib/averages/tema/Tema.cs | 19 ++++-- lib/averages/trima/Trima.Tests.cs | 50 +++++++++++++-- lib/averages/trima/Trima.cs | 21 ++++--- lib/averages/wma/Wma.Tests.cs | 92 ++++++++++++++++++++-------- lib/averages/wma/Wma.cs | 19 ++++-- lib/core/tseries/ITValuePublisher.cs | 14 +++++ lib/core/tseries/tseries.cs | 4 +- perf/Benchmark.cs | 78 +++++++++++++++++++++++ 16 files changed, 572 insertions(+), 177 deletions(-) create mode 100644 lib/core/tseries/ITValuePublisher.cs diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f5b09f39..b6ac99d7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,8 @@ "ms-dotnettools.csdevkit", "ms-dotnettools.csharp", "ms-dotnettools.dotnet-interactive-vscode", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "visualstudioexptteam.vscodeintellicode", + "SonarSource.sonarlint-vscode" ] -} +} \ No newline at end of file diff --git a/lib/averages/dema/Dema.Tests.cs b/lib/averages/dema/Dema.Tests.cs index 24661044..00b519c4 100644 --- a/lib/averages/dema/Dema.Tests.cs +++ b/lib/averages/dema/Dema.Tests.cs @@ -13,13 +13,13 @@ public class DemaTests var dema = new Dema(period); var ema1 = new Ema(period); var ema2 = new Ema(period); - var r = new Random(123); // nosemgrep + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); // Act & Assert for (int i = 0; i < 100; i++) { - double val = r.NextDouble() * 100; - var tVal = new TValue(DateTime.Now.AddMinutes(i), val); + var bar = gbm.Next(isNew: true); + var tVal = new TValue(bar.Time, bar.Close); var dVal = dema.Update(tVal); @@ -37,10 +37,12 @@ public class DemaTests // Arrange int period = 10; var source = new TSeries(); - var r = new Random(123); // nosemgrep + + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); for (int i = 0; i < 100; i++) { - source.Add(new TValue(DateTime.Now.AddMinutes(i), r.NextDouble() * 100)); + var bar = gbm.Next(isNew: true); + source.Add(new TValue(bar.Time, bar.Close)); } // Act @@ -63,10 +65,11 @@ public class DemaTests int count = 100; var source = new double[count]; var output = new double[count]; - var r = new Random(123); // nosemgrep + + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); for (int i = 0; i < count; i++) { - source[i] = r.NextDouble() * 100; + source[i] = gbm.Next().Close; } // Act @@ -89,13 +92,14 @@ public class DemaTests double alpha = 2.0 / (period + 1); var demaPeriod = new Dema(period); var demaAlpha = new Dema(alpha); - var r = new Random(123); // nosemgrep + + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); // Act & Assert for (int i = 0; i < 100; i++) { - double val = r.NextDouble() * 100; - var tVal = new TValue(DateTime.Now.AddMinutes(i), val); + var bar = gbm.Next(isNew: true); + var tVal = new TValue(bar.Time, bar.Close); var pVal = demaPeriod.Update(tVal); var aVal = demaAlpha.Update(tVal); @@ -110,10 +114,12 @@ public class DemaTests // Arrange double alpha = 0.15; var source = new TSeries(); - var r = new Random(123); // nosemgrep + + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); for (int i = 0; i < 100; i++) { - source.Add(new TValue(DateTime.Now.AddMinutes(i), r.NextDouble() * 100)); + var bar = gbm.Next(isNew: true); + source.Add(new TValue(bar.Time, bar.Close)); } // Act @@ -136,10 +142,11 @@ public class DemaTests int count = 100; var source = new double[count]; var output = new double[count]; - var r = new Random(123); // nosemgrep + + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); for (int i = 0; i < count; i++) { - source[i] = r.NextDouble() * 100; + source[i] = gbm.Next().Close; } // Act @@ -153,4 +160,46 @@ public class DemaTests Assert.Equal(val.Value, output[i], 1e-9); } } + [Fact] + public void Dema_AllModes_ProduceSameResult() + { + // Arrange + int period = 10; + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); + var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + var series = bars.Close; + + // 1. Batch Mode + var batchSeries = Dema.Calculate(series, period); + double expected = batchSeries.Last.Value; + + // 2. Span Mode + var tValues = series.Values.ToArray(); + var spanInput = new ReadOnlySpan(tValues); + var spanOutput = new double[tValues.Length]; + Dema.Calculate(spanInput, spanOutput, period); + double spanResult = spanOutput[^1]; + + // 3. Streaming Mode + var streamingInd = new Dema(period); + for (int i = 0; i < series.Count; i++) + { + streamingInd.Update(series[i]); + } + double streamingResult = streamingInd.Last.Value; + + // 4. Eventing Mode + var pubSource = new TSeries(); + var eventingInd = new Dema(pubSource, period); + for (int i = 0; i < series.Count; i++) + { + pubSource.Add(series[i]); + } + double eventingResult = eventingInd.Last.Value; + + // Assert + Assert.Equal(expected, spanResult, precision: 9); + Assert.Equal(expected, streamingResult, precision: 9); + Assert.Equal(expected, eventingResult, precision: 9); + } } diff --git a/lib/averages/dema/Dema.cs b/lib/averages/dema/Dema.cs index 3815368c..880c4984 100644 --- a/lib/averages/dema/Dema.cs +++ b/lib/averages/dema/Dema.cs @@ -22,7 +22,7 @@ namespace QuanTAlib; /// Becomes true when the second EMA converges (approx. 2x EMA convergence time). /// [SkipLocalsInit] -public sealed class Dema +public sealed class Dema : ITValuePublisher { private struct EmaState { @@ -45,8 +45,9 @@ public sealed class Dema private double _lastValidValue; public string Name { get; } - public TValue Value { get; private set; } + public TValue Last { get; private set; } public bool IsHot => _state2.IsHot; + public event Action? Pub; public Dema(int period) { @@ -57,6 +58,11 @@ public sealed class Dema Name = $"Dema({period})"; } + public Dema(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + public Dema(double alpha) { if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); @@ -93,8 +99,9 @@ public sealed class Dema double e2 = Compute(e1, _alpha, _decay, ref _state2); double result = 2 * e1 - e2; - Value = new TValue(input.Time, result); - return Value; + Last = new TValue(input.Time, result); + Pub?.Invoke(Last); + return Last; } public TSeries Update(TSeries source) @@ -141,7 +148,7 @@ public sealed class Dema _p_state2 = s2; _lastValidValue = lastValid; - Value = new TValue(tSpan[len - 1], vSpan[len - 1]); + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } @@ -281,6 +288,6 @@ public sealed class Dema _p_state1 = EmaState.New(); _p_state2 = EmaState.New(); _lastValidValue = 0; - Value = default; + Last = default; } } diff --git a/lib/averages/ema/Ema.Tests.cs b/lib/averages/ema/Ema.Tests.cs index af5fe9da..ffc6e40b 100644 --- a/lib/averages/ema/Ema.Tests.cs +++ b/lib/averages/ema/Ema.Tests.cs @@ -29,12 +29,12 @@ public class EmaTests { var ema = new Ema(10); - Assert.Equal(0, ema.Value.Value); + Assert.Equal(0, ema.Last.Value); TValue result = ema.Update(new TValue(DateTime.UtcNow, 100)); Assert.True(result.Value > 0); - Assert.Equal(result.Value, ema.Value.Value); + Assert.Equal(result.Value, ema.Last.Value); } [Fact] @@ -43,10 +43,10 @@ public class EmaTests var ema = new Ema(10); ema.Update(new TValue(DateTime.UtcNow, 100), isNew: true); - double value1 = ema.Value; + double value1 = ema.Last.Value; ema.Update(new TValue(DateTime.UtcNow, 105), isNew: true); - double value2 = ema.Value; + double value2 = ema.Last.Value; // Values should change with new bars Assert.NotEqual(value1, value2); @@ -59,10 +59,10 @@ public class EmaTests ema.Update(new TValue(DateTime.UtcNow, 100)); ema.Update(new TValue(DateTime.UtcNow, 110), isNew: true); - double beforeUpdate = ema.Value; + double beforeUpdate = ema.Last.Value; ema.Update(new TValue(DateTime.UtcNow, 120), isNew: false); - double afterUpdate = ema.Value; + double afterUpdate = ema.Last.Value; // Update should change the value Assert.NotEqual(beforeUpdate, afterUpdate); @@ -75,16 +75,16 @@ public class EmaTests ema.Update(new TValue(DateTime.UtcNow, 100)); ema.Update(new TValue(DateTime.UtcNow, 105)); - double valueBefore = ema.Value; + double valueBefore = ema.Last.Value; ema.Reset(); - Assert.Equal(0, ema.Value.Value); + Assert.Equal(0, ema.Last.Value); // After reset, should accept new values ema.Update(new TValue(DateTime.UtcNow, 50)); - Assert.NotEqual(0, ema.Value.Value); - Assert.NotEqual(valueBefore, ema.Value.Value); + Assert.NotEqual(0, ema.Last.Value); + Assert.NotEqual(valueBefore, ema.Last.Value); } [Fact] @@ -92,12 +92,12 @@ public class EmaTests { var ema = new Ema(10); - Assert.Equal(0, ema.Value.Value); + Assert.Equal(0, ema.Last.Value); Assert.False(ema.IsHot); ema.Update(new TValue(DateTime.UtcNow, 100)); - Assert.NotEqual(0, ema.Value.Value); + Assert.NotEqual(0, ema.Last.Value); } [Fact] @@ -195,7 +195,7 @@ public class EmaTests } // Remember EMA state after 10 values - double emaAfterTen = ema.Value; + double emaAfterTen = ema.Last.Value; // Generate 9 corrections with isNew=false (different values) for (int i = 0; i < 9; i++) @@ -254,7 +254,7 @@ public class EmaTests ema.Update(new TValue(DateTime.UtcNow, 100)); // This should compile and work because TValue has implicit conversion to double - double result = ema.Value; + double result = ema.Last.Value; Assert.Equal(100.0, result, 1e-10); } @@ -442,9 +442,10 @@ public class EmaTests { double[] source = new double[10000]; double[] output = new double[10000]; - var rng = new Random(42); // nosemgrep + + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) - source[i] = rng.NextDouble() * 100; + source[i] = gbm.Next().Close; // Warm up Ema.Calculate(source.AsSpan(), output.AsSpan(), 100); @@ -499,4 +500,47 @@ public class EmaTests Assert.True(double.IsFinite(output[^1])); Assert.True(output[^1] > 10 && output[^1] <= 50); } + [Fact] + public void Ema_AllModes_ProduceSameResult() + { + // Arrange + int period = 10; + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); + var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + var series = bars.Close; + + // 1. Batch Mode + var batchSeries = Ema.Calculate(series, period); + double expected = batchSeries.Last.Value; + + // 2. Span Mode + var tValues = series.Values.ToArray(); // Need array for Span modification safety if any + var spanInput = new ReadOnlySpan(tValues); + var spanOutput = new double[tValues.Length]; + Ema.Calculate(spanInput, spanOutput, period); + double spanResult = spanOutput[^1]; + + // 3. Streaming Mode + var streamingInd = new Ema(period); + for (int i = 0; i < series.Count; i++) + { + streamingInd.Update(series[i]); + } + double streamingResult = streamingInd.Last.Value; + + // 4. Eventing Mode + var pubSource = new TSeries(); + var eventingInd = new Ema(pubSource, period); + for (int i = 0; i < series.Count; i++) + { + pubSource.Add(series[i]); + } + double eventingResult = eventingInd.Last.Value; + + // Assert + // Precision 9 due to potential accumulation differences in loop vs batch optimizations + Assert.Equal(expected, spanResult, precision: 9); + Assert.Equal(expected, streamingResult, precision: 9); + Assert.Equal(expected, eventingResult, precision: 9); + } } diff --git a/lib/averages/ema/Ema.cs b/lib/averages/ema/Ema.cs index 818eea97..13edbe7e 100644 --- a/lib/averages/ema/Ema.cs +++ b/lib/averages/ema/Ema.cs @@ -25,7 +25,7 @@ namespace QuanTAlib; /// Becomes true when n = ln(0.05) / ln(1 - alpha) /// [SkipLocalsInit] -public sealed class Ema +public sealed class Ema : ITValuePublisher { private struct State { @@ -48,6 +48,8 @@ public sealed class Ema /// public string Name { get; } + public event Action? Pub; + /// /// Creates EMA with specified period. /// Alpha = 2 / (period + 1) @@ -63,6 +65,17 @@ public sealed class Ema Name = $"Ema({period})"; } + /// + /// Creates EMA with specified source and period. + /// Subscribes to source.Pub event. + /// + /// Source to subscribe to + /// Period for EMA calculation + public Ema(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + /// /// Creates EMA with specified alpha smoothing factor. /// @@ -80,7 +93,7 @@ public sealed class Ema /// /// Current EMA value. /// - public TValue Value { get; private set; } + public TValue Last { get; private set; } /// /// True if the EMA has warmed up and is providing valid results. @@ -115,8 +128,9 @@ public sealed class Ema double val = GetValidValue(input.Value); val = Compute(val, _alpha, _decay, ref _state); - Value = new TValue(input.Time, val); - return Value; + Last = new TValue(input.Time, val); + Pub?.Invoke(Last); + return Last; } public TSeries Update(TSeries source) @@ -145,7 +159,7 @@ public sealed class Ema sourceTimes.CopyTo(tSpan); _p_state = _state; - Value = new TValue(tSpan[len - 1], vSpan[len - 1]); + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } @@ -277,6 +291,6 @@ public sealed class Ema _state = State.New(); _p_state = _state; _lastValidValue = 0; - Value = default; + Last = default; } } diff --git a/lib/averages/sma/Sma.Tests.cs b/lib/averages/sma/Sma.Tests.cs index d65e0948..cd0bfda7 100644 --- a/lib/averages/sma/Sma.Tests.cs +++ b/lib/averages/sma/Sma.Tests.cs @@ -18,12 +18,12 @@ public class SmaTests { var sma = new Sma(10); - Assert.Equal(0, sma.Value.Value); + Assert.Equal(0, sma.Last.Value); TValue result = sma.Update(new TValue(DateTime.UtcNow, 100)); Assert.True(result.Value > 0); - Assert.Equal(result.Value, sma.Value.Value); + Assert.Equal(result.Value, sma.Last.Value); } [Fact] @@ -42,10 +42,10 @@ public class SmaTests var sma = new Sma(10); sma.Update(new TValue(DateTime.UtcNow, 100), isNew: true); - double value1 = sma.Value; + double value1 = sma.Last.Value; sma.Update(new TValue(DateTime.UtcNow, 200), isNew: true); - double value2 = sma.Value; + double value2 = sma.Last.Value; // Values should change with new bars Assert.NotEqual(value1, value2); @@ -58,10 +58,10 @@ public class SmaTests sma.Update(new TValue(DateTime.UtcNow, 100)); sma.Update(new TValue(DateTime.UtcNow, 110), isNew: true); - double beforeUpdate = sma.Value; + double beforeUpdate = sma.Last.Value; sma.Update(new TValue(DateTime.UtcNow, 120), isNew: false); - double afterUpdate = sma.Value; + double afterUpdate = sma.Last.Value; // Update should change the value Assert.NotEqual(beforeUpdate, afterUpdate); @@ -74,16 +74,16 @@ public class SmaTests sma.Update(new TValue(DateTime.UtcNow, 100)); sma.Update(new TValue(DateTime.UtcNow, 105)); - double valueBefore = sma.Value; + double valueBefore = sma.Last.Value; sma.Reset(); - Assert.Equal(0, sma.Value.Value); + Assert.Equal(0, sma.Last.Value); // After reset, should accept new values sma.Update(new TValue(DateTime.UtcNow, 50)); - Assert.NotEqual(0, sma.Value.Value); - Assert.NotEqual(valueBefore, sma.Value.Value); + Assert.NotEqual(0, sma.Last.Value); + Assert.NotEqual(valueBefore, sma.Last.Value); } [Fact] @@ -91,12 +91,12 @@ public class SmaTests { var sma = new Sma(10); - Assert.Equal(0, sma.Value.Value); + Assert.Equal(0, sma.Last.Value); Assert.False(sma.IsHot); sma.Update(new TValue(DateTime.UtcNow, 100)); - Assert.NotEqual(0, sma.Value.Value); + Assert.NotEqual(0, sma.Last.Value); } [Fact] @@ -128,7 +128,7 @@ public class SmaTests sma.Update(new TValue(DateTime.UtcNow, 50)); // SMA(5) of 10,20,30,40,50 = 150/5 = 30 - Assert.Equal(30.0, sma.Value.Value, 1e-10); + Assert.Equal(30.0, sma.Last.Value, 1e-10); } [Fact] @@ -141,17 +141,17 @@ public class SmaTests sma.Update(new TValue(DateTime.UtcNow, 30)); // SMA(3) of 10,20,30 = 60/3 = 20 - Assert.Equal(20.0, sma.Value.Value, 1e-10); + Assert.Equal(20.0, sma.Last.Value, 1e-10); sma.Update(new TValue(DateTime.UtcNow, 40)); // SMA(3) of 20,30,40 = 90/3 = 30 - Assert.Equal(30.0, sma.Value.Value, 1e-10); + Assert.Equal(30.0, sma.Last.Value, 1e-10); sma.Update(new TValue(DateTime.UtcNow, 50)); // SMA(3) of 30,40,50 = 120/3 = 40 - Assert.Equal(40.0, sma.Value.Value, 1e-10); + Assert.Equal(40.0, sma.Last.Value, 1e-10); } [Fact] @@ -170,7 +170,7 @@ public class SmaTests } // Remember SMA state after 10 values - double smaAfterTen = sma.Value; + double smaAfterTen = sma.Last.Value; // Generate 9 corrections with isNew=false (different values) for (int i = 0; i < 9; i++) @@ -229,7 +229,7 @@ public class SmaTests sma.Update(new TValue(DateTime.UtcNow, 100)); // This should compile and work because TValue has implicit conversion to double - double result = sma.Value; + double result = sma.Last.Value; Assert.Equal(100.0, result, 1e-10); } @@ -422,10 +422,11 @@ public class SmaTests public void Sma_SpanCalc_ZeroAllocation() { double[] source = new double[10000]; + double[] output = new double[10000]; - var rng = new Random(42); // nosemgrep + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) - source[i] = rng.NextDouble() * 100; // nosemgrep + source[i] = gbm.Next().Close; // Warm up Sma.Calculate(source.AsSpan(), output.AsSpan(), 100); @@ -463,4 +464,46 @@ public class SmaTests Assert.Equal(source[i], output[i], 1e-10); } } + [Fact] + public void Sma_AllModes_ProduceSameResult() + { + // Arrange + int period = 10; + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); + var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + var series = bars.Close; + + // 1. Batch Mode + var batchSeries = Sma.Calculate(series, period); + double expected = batchSeries.Last.Value; + + // 2. Span Mode + var tValues = series.Values.ToArray(); + var spanInput = new ReadOnlySpan(tValues); + var spanOutput = new double[tValues.Length]; + Sma.Calculate(spanInput, spanOutput, period); + double spanResult = spanOutput[^1]; + + // 3. Streaming Mode + var streamingInd = new Sma(period); + for (int i = 0; i < series.Count; i++) + { + streamingInd.Update(series[i]); + } + double streamingResult = streamingInd.Last.Value; + + // 4. Eventing Mode + var pubSource = new TSeries(); + var eventingInd = new Sma(pubSource, period); + for (int i = 0; i < series.Count; i++) + { + pubSource.Add(series[i]); + } + double eventingResult = eventingInd.Last.Value; + + // Assert + Assert.Equal(expected, spanResult, precision: 9); + Assert.Equal(expected, streamingResult, precision: 9); + Assert.Equal(expected, eventingResult, precision: 9); + } } diff --git a/lib/averages/sma/Sma.cs b/lib/averages/sma/Sma.cs index c6ad658a..425276c9 100644 --- a/lib/averages/sma/Sma.cs +++ b/lib/averages/sma/Sma.cs @@ -25,7 +25,7 @@ namespace QuanTAlib; /// Becomes true when the buffer is full (period samples processed). /// [SkipLocalsInit] -public sealed class Sma +public sealed class Sma : ITValuePublisher { private readonly int _period; private readonly RingBuffer _buffer; @@ -44,6 +44,8 @@ public sealed class Sma /// public string Name { get; } + public event Action? Pub; + /// /// Creates SMA with specified period. /// @@ -58,10 +60,15 @@ public sealed class Sma Name = $"Sma({period})"; } + public Sma(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + /// /// Current SMA value. /// - public TValue Value { get; private set; } + public TValue Last { get; private set; } /// /// True if the SMA has enough data to produce valid results. @@ -100,14 +107,26 @@ public sealed class Sma } } + + // Removed GetValidValue and UpdateState as they are not used in the new Update logic + [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { double val = GetValidValue(input.Value); - - UpdateState(val); + + double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0; + _sum = _sum - removedValue + val; + _buffer.Add(val); + + _tickCount++; + if (_buffer.IsFull && _tickCount >= ResyncInterval) + { + _tickCount = 0; + _sum = _buffer.Sum(); + } _p_sum = _sum; _p_lastInput = val; @@ -116,23 +135,22 @@ public sealed class Sma else { _lastValidValue = _p_lastValidValue; - double val = GetValidValue(input.Value); - + _sum = _p_sum - _p_lastInput + val; - _buffer.UpdateNewest(val); } double result = _sum / _buffer.Count; - Value = new TValue(input.Time, result); - return Value; + Last = new TValue(input.Time, result); + Pub?.Invoke(Last); + return Last; } public TSeries Update(TSeries source) { if (source.Count == 0) return new TSeries(new List(), new List()); - + int len = source.Count; var t = new List(len); var v = new List(len); @@ -144,44 +162,25 @@ public sealed class Sma var sourceValues = source.Values; var sourceTimes = source.Times; - Calculate(sourceValues, vSpan, _period); - - sourceTimes.CopyTo(tSpan); - - int windowSize = Math.Min(len, _period); - int startIndex = len - windowSize; - - if (startIndex > 0) - { - for (int i = startIndex - 1; i >= 0; i--) - { - if (double.IsFinite(sourceValues[i])) - { - _lastValidValue = sourceValues[i]; - break; - } - } - } - else - { - _lastValidValue = 0; - } - - _buffer.Clear(); - _sum = 0; - _tickCount = 0; - - for (int i = startIndex; i < len; i++) + // Reset state for batch calculation + Reset(); + + // We can optimize this later with specific batch logic, but for now use core loop + for(int i=0; i < len; i++) { double val = GetValidValue(sourceValues[i]); - UpdateState(val); + double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0; + _sum = _sum - removedValue + val; + _buffer.Add(val); + vSpan[i] = _sum / _buffer.Count; } - _p_sum = _sum; - _p_lastInput = sourceValues[len - 1]; + sourceTimes.CopyTo(tSpan); _p_lastValidValue = _lastValidValue; + _p_sum = _sum; + _p_lastInput = sourceValues[len-1]; - Value = new TValue(tSpan[len - 1], vSpan[len - 1]); + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } @@ -382,12 +381,9 @@ public sealed class Sma public void Reset() { _buffer.Clear(); - _sum = 0; - _p_sum = 0; - _p_lastInput = 0; - _lastValidValue = 0; - _p_lastValidValue = 0; + var resetSum = 0; + _sum = resetSum; + Last = default; _tickCount = 0; - Value = default; } } diff --git a/lib/averages/tema/Tema.Tests.cs b/lib/averages/tema/Tema.Tests.cs index d577de36..ee280133 100644 --- a/lib/averages/tema/Tema.Tests.cs +++ b/lib/averages/tema/Tema.Tests.cs @@ -29,12 +29,12 @@ public class TemaTests { var tema = new Tema(10); - Assert.Equal(0, tema.Value.Value); + Assert.Equal(0, tema.Last.Value); TValue result = tema.Update(new TValue(DateTime.UtcNow, 100)); Assert.True(result.Value > 0); - Assert.Equal(result.Value, tema.Value.Value); + Assert.Equal(result.Value, tema.Last.Value); } [Fact] @@ -43,10 +43,10 @@ public class TemaTests var tema = new Tema(10); tema.Update(new TValue(DateTime.UtcNow, 100), isNew: true); - double value1 = tema.Value; + double value1 = tema.Last.Value; tema.Update(new TValue(DateTime.UtcNow, 105), isNew: true); - double value2 = tema.Value; + double value2 = tema.Last.Value; // Values should change with new bars Assert.NotEqual(value1, value2); @@ -59,10 +59,10 @@ public class TemaTests tema.Update(new TValue(DateTime.UtcNow, 100)); tema.Update(new TValue(DateTime.UtcNow, 110), isNew: true); - double beforeUpdate = tema.Value; + double beforeUpdate = tema.Last.Value; tema.Update(new TValue(DateTime.UtcNow, 120), isNew: false); - double afterUpdate = tema.Value; + double afterUpdate = tema.Last.Value; // Update should change the value Assert.NotEqual(beforeUpdate, afterUpdate); @@ -75,16 +75,16 @@ public class TemaTests tema.Update(new TValue(DateTime.UtcNow, 100)); tema.Update(new TValue(DateTime.UtcNow, 105)); - double valueBefore = tema.Value; + double valueBefore = tema.Last.Value; tema.Reset(); - Assert.Equal(0, tema.Value.Value); + Assert.Equal(0, tema.Last.Value); // After reset, should accept new values tema.Update(new TValue(DateTime.UtcNow, 50)); - Assert.NotEqual(0, tema.Value.Value); - Assert.NotEqual(valueBefore, tema.Value.Value); + Assert.NotEqual(0, tema.Last.Value); + Assert.NotEqual(valueBefore, tema.Last.Value); } [Fact] @@ -92,12 +92,12 @@ public class TemaTests { var tema = new Tema(10); - Assert.Equal(0, tema.Value.Value); + Assert.Equal(0, tema.Last.Value); Assert.False(tema.IsHot); tema.Update(new TValue(DateTime.UtcNow, 100)); - Assert.NotEqual(0, tema.Value.Value); + Assert.NotEqual(0, tema.Last.Value); } [Fact] @@ -152,7 +152,7 @@ public class TemaTests } // Remember TEMA state after 10 values - double temaAfterTen = tema.Value; + double temaAfterTen = tema.Last.Value; // Generate 9 corrections with isNew=false (different values) for (int i = 0; i < 9; i++) @@ -253,10 +253,11 @@ public class TemaTests public void Tema_SpanCalc_ZeroAllocation() { double[] source = new double[10000]; + double[] output = new double[10000]; - var rng = new Random(42); // nosemgrep + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) - source[i] = rng.NextDouble() * 100; + source[i] = gbm.Next().Close; // Warm up Tema.Calculate(source.AsSpan(), output.AsSpan(), 100); @@ -264,4 +265,46 @@ public class TemaTests // This test verifies the method runs without throwing Assert.True(double.IsFinite(output[^1])); } + [Fact] + public void Tema_AllModes_ProduceSameResult() + { + // Arrange + int period = 10; + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); + var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + var series = bars.Close; + + // 1. Batch Mode + var batchSeries = Tema.Calculate(series, period); + double expected = batchSeries.Last.Value; + + // 2. Span Mode + var tValues = series.Values.ToArray(); + var spanInput = new ReadOnlySpan(tValues); + var spanOutput = new double[tValues.Length]; + Tema.Calculate(spanInput, spanOutput, period); + double spanResult = spanOutput[^1]; + + // 3. Streaming Mode + var streamingInd = new Tema(period); + for (int i = 0; i < series.Count; i++) + { + streamingInd.Update(series[i]); + } + double streamingResult = streamingInd.Last.Value; + + // 4. Eventing Mode + var pubSource = new TSeries(); + var eventingInd = new Tema(pubSource, period); + for (int i = 0; i < series.Count; i++) + { + pubSource.Add(series[i]); + } + double eventingResult = eventingInd.Last.Value; + + // Assert + Assert.Equal(expected, spanResult, precision: 9); + Assert.Equal(expected, streamingResult, precision: 9); + Assert.Equal(expected, eventingResult, precision: 9); + } } diff --git a/lib/averages/tema/Tema.cs b/lib/averages/tema/Tema.cs index 550a7ec9..d8ed2edf 100644 --- a/lib/averages/tema/Tema.cs +++ b/lib/averages/tema/Tema.cs @@ -24,7 +24,7 @@ namespace QuanTAlib; /// which is faster than the standard EMA convergence (3/alpha steps). /// [SkipLocalsInit] -public sealed class Tema +public sealed class Tema : ITValuePublisher { private struct EmaState { @@ -49,8 +49,9 @@ public sealed class Tema private double _lastValidValue; public string Name { get; } - public TValue Value { get; private set; } + public TValue Last { get; private set; } public bool IsHot => _state3.E <= 0.09; + public event Action? Pub; public Tema(int period) { @@ -61,6 +62,11 @@ public sealed class Tema Name = $"Tema({period})"; } + public Tema(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + public Tema(double alpha) { if (alpha <= 0 || alpha > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); @@ -102,8 +108,9 @@ public sealed class Tema double e3 = Compute(e2, _alpha, _decay, ref _state3); double result = 3 * e1 - 3 * e2 + e3; - Value = new TValue(input.Time, result); - return Value; + Last = new TValue(input.Time, result); + Pub?.Invoke(Last); + return Last; } public TSeries Update(TSeries source) @@ -154,7 +161,7 @@ public sealed class Tema _p_state3 = s3; _lastValidValue = lastValid; - Value = new TValue(tSpan[len - 1], vSpan[len - 1]); + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } @@ -322,6 +329,6 @@ public sealed class Tema _p_state2 = EmaState.New(); _p_state3 = EmaState.New(); _lastValidValue = 0; - Value = default; + Last = default; } } diff --git a/lib/averages/trima/Trima.Tests.cs b/lib/averages/trima/Trima.Tests.cs index 82cbcaa2..6e5abee5 100644 --- a/lib/averages/trima/Trima.Tests.cs +++ b/lib/averages/trima/Trima.Tests.cs @@ -19,12 +19,12 @@ public class TrimaTests { var trima = new Trima(10); - Assert.Equal(0, trima.Value.Value); + Assert.Equal(0, trima.Last.Value); TValue result = trima.Update(new TValue(DateTime.UtcNow, 100)); Assert.True(result.Value > 0); - Assert.Equal(result.Value, trima.Value.Value); + Assert.Equal(result.Value, trima.Last.Value); } [Fact] @@ -111,12 +111,12 @@ public class TrimaTests trima.Reset(); - Assert.Equal(0, trima.Value.Value); + Assert.Equal(0, trima.Last.Value); Assert.False(trima.IsHot); // After reset, should accept new values trima.Update(new TValue(DateTime.UtcNow, 50)); - Assert.NotEqual(0, trima.Value.Value); + Assert.NotEqual(0, trima.Last.Value); } [Fact] @@ -197,4 +197,46 @@ public class TrimaTests Assert.Equal(tseriesResult[i].Value, output[i], 1e-10); } } + [Fact] + public void Trima_AllModes_ProduceSameResult() + { + // Arrange + int period = 10; + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); + var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + var series = bars.Close; + + // 1. Batch Mode + var batchSeries = Trima.Calculate(series, period); + double expected = batchSeries.Last.Value; + + // 2. Span Mode + var tValues = series.Values.ToArray(); + var spanInput = new ReadOnlySpan(tValues); + var spanOutput = new double[tValues.Length]; + Trima.Calculate(spanInput, spanOutput, period); + double spanResult = spanOutput[^1]; + + // 3. Streaming Mode + var streamingInd = new Trima(period); + for (int i = 0; i < series.Count; i++) + { + streamingInd.Update(series[i]); + } + double streamingResult = streamingInd.Last.Value; + + // 4. Eventing Mode + var pubSource = new TSeries(); + var eventingInd = new Trima(pubSource, period); + for (int i = 0; i < series.Count; i++) + { + pubSource.Add(series[i]); + } + double eventingResult = eventingInd.Last.Value; + + // Assert + Assert.Equal(expected, spanResult, precision: 9); + Assert.Equal(expected, streamingResult, precision: 9); + Assert.Equal(expected, eventingResult, precision: 9); + } } diff --git a/lib/averages/trima/Trima.cs b/lib/averages/trima/Trima.cs index f6ccb2c2..ab6854a4 100644 --- a/lib/averages/trima/Trima.cs +++ b/lib/averages/trima/Trima.cs @@ -23,7 +23,7 @@ namespace QuanTAlib; /// Becomes true when the buffer is full (period samples processed). /// [SkipLocalsInit] -public sealed class Trima +public sealed class Trima : ITValuePublisher { private readonly int _period; private readonly int _p1; @@ -41,8 +41,9 @@ public sealed class Trima private const int ResyncInterval = 1000; public string Name { get; } - public TValue Value { get; private set; } + public TValue Last { get; private set; } public bool IsHot => _sampleCount >= _period; + public event Action? Pub; public Trima(int period) { @@ -58,6 +59,11 @@ public sealed class Trima Name = $"Trima({period})"; } + public Trima(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { @@ -110,7 +116,7 @@ public sealed class Trima _p_sum2 = _sum2; _p_lastInput2 = sma1Result; - Value = new TValue(input.Time, _sum2 / _buffer2.Count); + Last = new TValue(input.Time, _sum2 / _buffer2.Count); } else { @@ -126,10 +132,11 @@ public sealed class Trima _sum2 = _p_sum2 - _p_lastInput2 + sma1Result; _buffer2.UpdateNewest(sma1Result); - Value = new TValue(input.Time, _sum2 / _buffer2.Count); + Last = new TValue(input.Time, _sum2 / _buffer2.Count); } - return Value; + Pub?.Invoke(Last); + return Last; } public TSeries Update(TSeries source) @@ -158,7 +165,7 @@ public sealed class Trima Update(new TValue(source.Times[i], source.Values[i]), isNew: true); } - Value = new TValue(tSpan[len - 1], vSpan[len - 1]); + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } @@ -204,6 +211,6 @@ public sealed class Trima _tickCount2 = 0; _sampleCount = 0; - Value = default; + Last = default; } } diff --git a/lib/averages/wma/Wma.Tests.cs b/lib/averages/wma/Wma.Tests.cs index 4f334073..8a3e7317 100644 --- a/lib/averages/wma/Wma.Tests.cs +++ b/lib/averages/wma/Wma.Tests.cs @@ -18,12 +18,12 @@ public class WmaTests { var wma = new Wma(10); - Assert.Equal(0, wma.Value.Value); + Assert.Equal(0, wma.Last.Value); TValue result = wma.Update(new TValue(DateTime.UtcNow, 100)); Assert.True(result.Value > 0); - Assert.Equal(result.Value, wma.Value.Value); + Assert.Equal(result.Value, wma.Last.Value); } [Fact] @@ -42,10 +42,10 @@ public class WmaTests var wma = new Wma(10); wma.Update(new TValue(DateTime.UtcNow, 100), isNew: true); - double value1 = wma.Value; + double value1 = wma.Last.Value; wma.Update(new TValue(DateTime.UtcNow, 200), isNew: true); - double value2 = wma.Value; + double value2 = wma.Last.Value; // Values should change with new bars Assert.NotEqual(value1, value2); @@ -58,10 +58,10 @@ public class WmaTests wma.Update(new TValue(DateTime.UtcNow, 100)); wma.Update(new TValue(DateTime.UtcNow, 110), isNew: true); - double beforeUpdate = wma.Value; + double beforeUpdate = wma.Last.Value; wma.Update(new TValue(DateTime.UtcNow, 120), isNew: false); - double afterUpdate = wma.Value; + double afterUpdate = wma.Last.Value; // Update should change the value Assert.NotEqual(beforeUpdate, afterUpdate); @@ -74,16 +74,16 @@ public class WmaTests wma.Update(new TValue(DateTime.UtcNow, 100)); wma.Update(new TValue(DateTime.UtcNow, 105)); - double valueBefore = wma.Value; + double valueBefore = wma.Last.Value; wma.Reset(); - Assert.Equal(0, wma.Value.Value); + Assert.Equal(0, wma.Last.Value); // After reset, should accept new values wma.Update(new TValue(DateTime.UtcNow, 50)); - Assert.NotEqual(0, wma.Value.Value); - Assert.NotEqual(valueBefore, wma.Value.Value); + Assert.NotEqual(0, wma.Last.Value); + Assert.NotEqual(valueBefore, wma.Last.Value); } [Fact] @@ -91,12 +91,12 @@ public class WmaTests { var wma = new Wma(10); - Assert.Equal(0, wma.Value.Value); + Assert.Equal(0, wma.Last.Value); Assert.False(wma.IsHot); wma.Update(new TValue(DateTime.UtcNow, 100)); - Assert.NotEqual(0, wma.Value.Value); + Assert.NotEqual(0, wma.Last.Value); } [Fact] @@ -129,7 +129,7 @@ public class WmaTests // WMA(5) of 10,20,30,40,50 = (1*10 + 2*20 + 3*30 + 4*40 + 5*50) / 15 // = (10 + 40 + 90 + 160 + 250) / 15 = 550 / 15 = 36.666... - Assert.Equal(550.0 / 15.0, wma.Value.Value, 1e-10); + Assert.Equal(550.0 / 15.0, wma.Last.Value, 1e-10); } [Fact] @@ -142,17 +142,17 @@ public class WmaTests wma.Update(new TValue(DateTime.UtcNow, 30)); // WMA(3) of 10,20,30 = (1*10 + 2*20 + 3*30) / 6 = (10 + 40 + 90) / 6 = 140/6 = 23.333... - Assert.Equal(140.0 / 6.0, wma.Value.Value, 1e-10); + Assert.Equal(140.0 / 6.0, wma.Last.Value, 1e-10); wma.Update(new TValue(DateTime.UtcNow, 40)); // WMA(3) of 20,30,40 = (1*20 + 2*30 + 3*40) / 6 = (20 + 60 + 120) / 6 = 200/6 = 33.333... - Assert.Equal(200.0 / 6.0, wma.Value.Value, 1e-10); + Assert.Equal(200.0 / 6.0, wma.Last.Value, 1e-10); wma.Update(new TValue(DateTime.UtcNow, 50)); // WMA(3) of 30,40,50 = (1*30 + 2*40 + 3*50) / 6 = (30 + 80 + 150) / 6 = 260/6 = 43.333... - Assert.Equal(260.0 / 6.0, wma.Value.Value, 1e-10); + Assert.Equal(260.0 / 6.0, wma.Last.Value, 1e-10); } [Fact] @@ -171,7 +171,7 @@ public class WmaTests } // Remember WMA state after 10 values - double wmaAfterTen = wma.Value; + double wmaAfterTen = wma.Last.Value; // Generate 9 corrections with isNew=false (different values) for (int i = 0; i < 9; i++) @@ -230,7 +230,7 @@ public class WmaTests wma.Update(new TValue(DateTime.UtcNow, 100)); // This should compile and work because TValue has implicit conversion to double - double result = wma.Value; + double result = wma.Last.Value; Assert.Equal(100.0, result, 1e-10); } @@ -375,9 +375,9 @@ public class WmaTests // WMA should be higher than SMA because it weights the high recent value more // SMA = (10 + 20 + 100) / 3 = 43.333... // WMA = (1*10 + 2*20 + 3*100) / 6 = (10 + 40 + 300) / 6 = 58.333... - Assert.True(wma.Value.Value > sma.Value.Value); - Assert.Equal(350.0 / 6.0, wma.Value.Value, 1e-10); - Assert.Equal(130.0 / 3.0, sma.Value.Value, 1e-10); + Assert.True(wma.Last.Value > sma.Last.Value); + Assert.Equal(350.0 / 6.0, wma.Last.Value, 1e-10); + Assert.Equal(130.0 / 3.0, sma.Last.Value, 1e-10); } [Fact] @@ -469,9 +469,9 @@ public class WmaTests { double[] source = new double[10000]; double[] output = new double[10000]; - var rng = new Random(42); // nosemgrep + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) - source[i] = rng.NextDouble() * 100; + source[i] = gbm.Next().Close; // Warm up Wma.Calculate(source.AsSpan(), output.AsSpan(), 100); @@ -514,9 +514,9 @@ public class WmaTests { double[] source = new double[1000]; double[] output = new double[1000]; - var rng = new Random(42); // nosemgrep + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42); for (int i = 0; i < source.Length; i++) - source[i] = rng.NextDouble() * 100; + source[i] = gbm.Next().Close; // Period <= 512 uses stackalloc Wma.Calculate(source.AsSpan(), output.AsSpan(), 100); @@ -527,4 +527,46 @@ public class WmaTests Wma.Calculate(source.AsSpan(), output2.AsSpan(), 600); Assert.True(double.IsFinite(output2[^1])); } + [Fact] + public void Wma_AllModes_ProduceSameResult() + { + // Arrange + int period = 10; + var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123); + var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + var series = bars.Close; + + // 1. Batch Mode + var batchSeries = Wma.Calculate(series, period); + double expected = batchSeries.Last.Value; + + // 2. Span Mode + var tValues = series.Values.ToArray(); + var spanInput = new ReadOnlySpan(tValues); + var spanOutput = new double[tValues.Length]; + Wma.Calculate(spanInput, spanOutput, period); + double spanResult = spanOutput[^1]; + + // 3. Streaming Mode + var streamingInd = new Wma(period); + for (int i = 0; i < series.Count; i++) + { + streamingInd.Update(series[i]); + } + double streamingResult = streamingInd.Last.Value; + + // 4. Eventing Mode + var pubSource = new TSeries(); + var eventingInd = new Wma(pubSource, period); + for (int i = 0; i < series.Count; i++) + { + pubSource.Add(series[i]); + } + double eventingResult = eventingInd.Last.Value; + + // Assert + Assert.Equal(expected, spanResult, precision: 9); + Assert.Equal(expected, streamingResult, precision: 9); + Assert.Equal(expected, eventingResult, precision: 9); + } } diff --git a/lib/averages/wma/Wma.cs b/lib/averages/wma/Wma.cs index 720edbae..26c4812b 100644 --- a/lib/averages/wma/Wma.cs +++ b/lib/averages/wma/Wma.cs @@ -25,7 +25,7 @@ namespace QuanTAlib; /// Becomes true when the buffer is full (period samples processed). /// [SkipLocalsInit] -public sealed class Wma +public sealed class Wma : ITValuePublisher { private readonly int _period; private readonly double _divisor; @@ -38,8 +38,9 @@ public sealed class Wma private const int ResyncInterval = 1000; public string Name { get; } - public TValue Value { get; private set; } + public TValue Last { get; private set; } public bool IsHot => _buffer.IsFull; + public event Action? Pub; public Wma(int period) { @@ -51,6 +52,11 @@ public sealed class Wma Name = $"Wma({period})"; } + public Wma(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { @@ -128,8 +134,9 @@ public sealed class Wma } double currentDivisor = _buffer.IsFull ? _divisor : _buffer.Count * (_buffer.Count + 1) * 0.5; - Value = new TValue(input.Time, _wsum / currentDivisor); - return Value; + Last = new TValue(input.Time, _wsum / currentDivisor); + Pub?.Invoke(Last); + return Last; } public TSeries Update(TSeries source) @@ -184,7 +191,7 @@ public sealed class Wma _p_lastInput = source.Values[len - 1]; _p_lastValidValue = _lastValidValue; - Value = new TValue(tSpan[len - 1], vSpan[len - 1]); + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } @@ -485,6 +492,6 @@ public sealed class Wma { _buffer.Clear(); _sum = _wsum = _p_sum = _p_wsum = _p_lastInput = _lastValidValue = _p_lastValidValue = 0; - Value = default; + Last = default; } } diff --git a/lib/core/tseries/ITValuePublisher.cs b/lib/core/tseries/ITValuePublisher.cs new file mode 100644 index 00000000..318990b8 --- /dev/null +++ b/lib/core/tseries/ITValuePublisher.cs @@ -0,0 +1,14 @@ +using System; + +namespace QuanTAlib; + +/// +/// Interface for objects that publish TValue updates. +/// +public interface ITValuePublisher +{ + /// + /// Event triggered when a new TValue is available. + /// + event Action Pub; +} diff --git a/lib/core/tseries/tseries.cs b/lib/core/tseries/tseries.cs index 2a9ceac4..582e8ec1 100644 --- a/lib/core/tseries/tseries.cs +++ b/lib/core/tseries/tseries.cs @@ -11,8 +11,8 @@ namespace QuanTAlib; /// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency. /// Supports "New Bar" vs "Update Last" streaming semantics. /// -public class TSeries : IReadOnlyList -{ + public class TSeries : IReadOnlyList, ITValuePublisher + { protected readonly List _t; protected readonly List _v; diff --git a/perf/Benchmark.cs b/perf/Benchmark.cs index 43a2804e..0a1a00cc 100644 --- a/perf/Benchmark.cs +++ b/perf/Benchmark.cs @@ -153,6 +153,19 @@ public class IndicatorBenchmarks } } + [BenchmarkCategory("SMA")] + [Benchmark(Description = "QuanTAlib SMA (Eventing)")] + public void QuanTAlib_Sma_Eventing() + { + var source = new TSeries(); + var sma = new Sma(source, Period); + for (int i = 0; i < _closeValues.Length; i++) + { + source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); + _quantalibOutput[i] = sma.Last.Value; + } + } + [BenchmarkCategory("SMA")] [Benchmark(Description = "Tulip SMA")] public void Tulip_Sma() => Tulip.Indicators.sma.Run(_tulipSmaInputs, _tulipSmaOptions, _tulipSmaOutputs); @@ -193,6 +206,19 @@ public class IndicatorBenchmarks } } + [BenchmarkCategory("EMA")] + [Benchmark(Description = "QuanTAlib EMA (Eventing)")] + public void QuanTAlib_Ema_Eventing() + { + var source = new TSeries(); + var ema = new Ema(source, Period); + for (int i = 0; i < _closeValues.Length; i++) + { + source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); + _quantalibOutput[i] = ema.Last.Value; + } + } + [BenchmarkCategory("EMA")] [Benchmark(Description = "Tulip EMA")] public void Tulip_Ema() => Tulip.Indicators.ema.Run(_tulipEmaInputs, _tulipEmaOptions, _tulipEmaOutputs); @@ -233,6 +259,19 @@ public class IndicatorBenchmarks } } + [BenchmarkCategory("WMA")] + [Benchmark(Description = "QuanTAlib WMA (Eventing)")] + public void QuanTAlib_Wma_Eventing() + { + var source = new TSeries(); + var wma = new Wma(source, Period); + for (int i = 0; i < _closeValues.Length; i++) + { + source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); + _quantalibOutput[i] = wma.Last.Value; + } + } + [BenchmarkCategory("WMA")] [Benchmark(Description = "Tulip WMA")] public void Tulip_Wma() => Tulip.Indicators.wma.Run(_tulipWmaInputs, _tulipWmaOptions, _tulipWmaOutputs); @@ -273,6 +312,19 @@ public class IndicatorBenchmarks } } + [BenchmarkCategory("TRIMA")] + [Benchmark(Description = "QuanTAlib TRIMA (Eventing)")] + public void QuanTAlib_Trima_Eventing() + { + var source = new TSeries(); + var trima = new Trima(source, Period); + for (int i = 0; i < _closeValues.Length; i++) + { + source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); + _quantalibOutput[i] = trima.Last.Value; + } + } + [BenchmarkCategory("TRIMA")] [Benchmark(Description = "Tulip TRIMA")] public void Tulip_Trima() => Tulip.Indicators.trima.Run(_tulipTrimaInputs, _tulipTrimaOptions, _tulipTrimaOutputs); @@ -301,6 +353,19 @@ public class IndicatorBenchmarks } } + [BenchmarkCategory("DEMA")] + [Benchmark(Description = "QuanTAlib DEMA (Eventing)")] + public void QuanTAlib_Dema_Eventing() + { + var source = new TSeries(); + var dema = new Dema(source, Period); + for (int i = 0; i < _closeValues.Length; i++) + { + source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); + _quantalibOutput[i] = dema.Last.Value; + } + } + [BenchmarkCategory("DEMA")] [Benchmark(Description = "Tulip DEMA")] public void Tulip_Dema() => Tulip.Indicators.dema.Run(_tulipDemaInputs, _tulipDemaOptions, _tulipDemaOutputs); @@ -341,6 +406,19 @@ public class IndicatorBenchmarks } } + [BenchmarkCategory("TEMA")] + [Benchmark(Description = "QuanTAlib TEMA (Eventing)")] + public void QuanTAlib_Tema_Eventing() + { + var source = new TSeries(); + var tema = new Tema(source, Period); + for (int i = 0; i < _closeValues.Length; i++) + { + source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); + _quantalibOutput[i] = tema.Last.Value; + } + } + [BenchmarkCategory("TEMA")] [Benchmark(Description = "Tulip TEMA")] public void Tulip_Tema() => Tulip.Indicators.tema.Run(_tulipTemaInputs, _tulipTemaOptions, _tulipTemaOutputs);