From 8d8e60098eb1d1df396d591cccfa15077bf94819 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Sat, 29 Nov 2025 16:08:45 -0800 Subject: [PATCH] Refactor and expand unit tests for TBarSeries, TSeries, and TValue classes - Enhanced TBarSeriesTests with additional constructors, methods, and assertions for better coverage. - Improved TSeriesTests to include new constructors, methods, and edge cases. - Expanded TValueTests to cover constructors, implicit conversions, equality checks, and hash codes. - Updated project file to target .NET 10.0 and include internal visibility for tests. - Added Codacy configuration for code quality checks. --- .codacy/codacy.yaml | 15 + .gitignore | 4 + lib/averages/ema/Ema.Validation.Tests.cs | 2 +- lib/averages/ema/Ema.cs | 1 - lib/averages/ema/EmaVector.Tests.cs | 662 ++++++++------- lib/core/simd/SimdExtensions.Tests.cs | 979 +++++++++++++++++------ lib/core/simd/SimdExtensions.cs | 123 +-- lib/core/tbar/TBar.Tests.cs | 429 ++++++++-- lib/core/tbarseries/TBarSeries.Tests.cs | 476 +++++++++-- lib/core/tseries/TSeries.Tests.cs | 394 +++++++-- lib/core/tvalue/TValue.Tests.cs | 236 ++++-- lib/quantalib.csproj | 6 +- 12 files changed, 2514 insertions(+), 813 deletions(-) create mode 100644 .codacy/codacy.yaml diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml new file mode 100644 index 00000000..15365c77 --- /dev/null +++ b/.codacy/codacy.yaml @@ -0,0 +1,15 @@ +runtimes: + - dart@3.7.2 + - go@1.22.3 + - java@17.0.10 + - node@22.2.0 + - python@3.11.11 +tools: + - dartanalyzer@3.7.2 + - eslint@8.57.0 + - lizard@1.17.31 + - pmd@7.11.0 + - pylint@3.3.6 + - revive@1.7.0 + - semgrep@1.78.0 + - trivy@0.66.0 diff --git a/.gitignore b/.gitignore index 43ac56a3..8940e1d3 100644 --- a/.gitignore +++ b/.gitignore @@ -405,3 +405,7 @@ memory-bank/ .DS_Store .AppleDouble .LSOverride + + +#Ignore insiders AI rules +.github/instructions/codacy.instructions.md diff --git a/lib/averages/ema/Ema.Validation.Tests.cs b/lib/averages/ema/Ema.Validation.Tests.cs index 57efd191..c8e59e63 100644 --- a/lib/averages/ema/Ema.Validation.Tests.cs +++ b/lib/averages/ema/Ema.Validation.Tests.cs @@ -86,7 +86,7 @@ public class EmaValidationTests : IDisposable var qResult = ema.Update(_data); // Calculate TA-Lib EMA - var retCode = TALib.Functions.Ema(tData, 0..^0, output, out var outRange, period); + var retCode = TALib.Functions.Ema(tData, 0..^0, output, out var outRange, period); // Check success Assert.Equal(Core.RetCode.Success, retCode); diff --git a/lib/averages/ema/Ema.cs b/lib/averages/ema/Ema.cs index 25ba6906..b8953167 100644 --- a/lib/averages/ema/Ema.cs +++ b/lib/averages/ema/Ema.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/lib/averages/ema/EmaVector.Tests.cs b/lib/averages/ema/EmaVector.Tests.cs index d0aec46f..7de9c1ed 100644 --- a/lib/averages/ema/EmaVector.Tests.cs +++ b/lib/averages/ema/EmaVector.Tests.cs @@ -1,280 +1,382 @@ -using System; -using System.Linq; -using Xunit; -using QuanTAlib; - -namespace QuanTAlib.Tests; - -public class EmaVectorTests -{ - [Fact] - public void Initialization_WithPeriods_SetsCorrectAlphas() - { - int[] periods = { 10, 20 }; - var emaVector = new EmaVector(periods); - - // We can't check private fields directly, but we can check results after 1 step - // Alpha = 2 / (P + 1) - // P=10 -> A=2/11 - // P=20 -> A=2/21 - - var res = emaVector.Update(new TValue(DateTime.Now, 100.0)); - - // First value should be 100.0 due to compensation - Assert.Equal(100.0, res[0].Value, 1e-9); - Assert.Equal(100.0, res[1].Value, 1e-9); - } - - [Fact] - public void Calc_Streaming_MatchesSingleEma() - { - int[] periods = { 5, 10, 20 }; - var emaVector = new EmaVector(periods); - var emaSingles = periods.Select(p => new Ema(p)).ToArray(); - - var values = new double[] { 10, 20, 30, 40, 50, 40, 30, 20, 10 }; - var time = DateTime.Now; - - foreach (var val in values) - { - var tVal = new TValue(time, val); - var multiRes = emaVector.Update(tVal); - - for (int i = 0; i < periods.Length; i++) - { - var singleRes = emaSingles[i].Update(tVal); - Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9); - Assert.Equal(singleRes.Time, multiRes[i].Time); - } - - time = time.AddMinutes(1); - } - } - - [Fact] - public void Calc_Series_MatchesSingleEma() - { - int[] periods = { 5, 10, 20 }; - var emaVector = new EmaVector(periods); - var emaSingles = periods.Select(p => new Ema(p)).ToArray(); - - int len = 100; - var t = new System.Collections.Generic.List(len); - var v = new System.Collections.Generic.List(len); - var now = DateTime.Now; - - for (int i = 0; i < len; i++) - { - t.Add(now.AddMinutes(i).Ticks); - v.Add(Math.Sin(i * 0.1) * 100); - } - - var series = new TSeries(t, v); - - var multiRes = emaVector.Calculate(series); - - for (int i = 0; i < periods.Length; i++) - { - var singleRes = emaSingles[i].Update(series); - - Assert.Equal(singleRes.Count, multiRes[i].Count); - for (int j = 0; j < len; j++) - { - Assert.Equal(singleRes.Values[j], multiRes[i].Values[j], 1e-8); - } - } - } - - [Fact] - public void Calc_Series_MatchesStreaming() - { - int[] periods = { 5, 10, 20 }; - var emaVectorBatch = new EmaVector(periods); - var emaVectorStream = new EmaVector(periods); - - int len = 100; - var t = new System.Collections.Generic.List(len); - var v = new System.Collections.Generic.List(len); - var now = DateTime.Now; - - for (int i = 0; i < len; i++) - { - t.Add(now.AddMinutes(i).Ticks); - v.Add(Math.Sin(i * 0.1) * 100); - } - - var series = new TSeries(t, v); - - // Batch calculation - var batchRes = emaVectorBatch.Calculate(series); - - // Streaming calculation - for (int i = 0; i < len; i++) - { - var tVal = new TValue(new DateTime(t[i]), v[i]); - var streamRes = emaVectorStream.Update(tVal); - - for (int j = 0; j < periods.Length; j++) - { - Assert.Equal(batchRes[j].Values[i], streamRes[j].Value, 1e-9); - } - } - } - - [Fact] - public void Reset_ClearsState() - { - int[] periods = { 10 }; - var emaVector = new EmaVector(periods); - - emaVector.Update(new TValue(DateTime.Now, 100.0)); - emaVector.Reset(); - - // After reset, next calculation should treat it as first value (warmup) - var res = emaVector.Update(new TValue(DateTime.Now, 200.0)); - - Assert.Equal(200.0, res[0].Value, 1e-9); - } - - [Fact] - public void Update_NaN_Input_UsesLastValidValue() - { - int[] periods = { 10, 20 }; - var emaVector = new EmaVector(periods); - - // Feed some valid values - emaVector.Update(new TValue(DateTime.Now, 100.0)); - emaVector.Update(new TValue(DateTime.Now, 110.0)); - - // Feed NaN - should use last valid value (110) - var resultAfterNaN = emaVector.Update(new TValue(DateTime.Now, double.NaN)); - - // All results should be finite (not NaN) - foreach (var result in resultAfterNaN) - { - Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}"); - } - } - - [Fact] - public void Update_Infinity_Input_UsesLastValidValue() - { - int[] periods = { 10, 20 }; - var emaVector = new EmaVector(periods); - - // Feed some valid values - emaVector.Update(new TValue(DateTime.Now, 100.0)); - emaVector.Update(new TValue(DateTime.Now, 110.0)); - - // Feed positive infinity - should use last valid value - var resultAfterPosInf = emaVector.Update(new TValue(DateTime.Now, double.PositiveInfinity)); - foreach (var result in resultAfterPosInf) - { - Assert.True(double.IsFinite(result.Value)); - } - - // Feed negative infinity - should use last valid value - var resultAfterNegInf = emaVector.Update(new TValue(DateTime.Now, double.NegativeInfinity)); - foreach (var result in resultAfterNegInf) - { - Assert.True(double.IsFinite(result.Value)); - } - } - - [Fact] - public void Update_MultipleNaN_ContinuesWithLastValid() - { - int[] periods = { 5, 10 }; - var emaVector = new EmaVector(periods); - - // Feed valid values - emaVector.Update(new TValue(DateTime.Now, 100.0)); - emaVector.Update(new TValue(DateTime.Now, 110.0)); - emaVector.Update(new TValue(DateTime.Now, 120.0)); - - // Feed multiple NaN values - var r1 = emaVector.Update(new TValue(DateTime.Now, double.NaN)); - var r2 = emaVector.Update(new TValue(DateTime.Now, double.NaN)); - var r3 = emaVector.Update(new TValue(DateTime.Now, double.NaN)); - - // All results should be finite - foreach (var result in r1) Assert.True(double.IsFinite(result.Value)); - foreach (var result in r2) Assert.True(double.IsFinite(result.Value)); - foreach (var result in r3) Assert.True(double.IsFinite(result.Value)); - } - - [Fact] - public void Calculate_Series_HandlesNaN() - { - int[] periods = { 5, 10 }; - var emaVector = new EmaVector(periods); - - // Create series with NaN values interspersed - var t = new System.Collections.Generic.List(); - var v = new System.Collections.Generic.List(); - var now = DateTime.Now; - - t.Add(now.Ticks); v.Add(100.0); - t.Add(now.AddMinutes(1).Ticks); v.Add(110.0); - t.Add(now.AddMinutes(2).Ticks); v.Add(double.NaN); - t.Add(now.AddMinutes(3).Ticks); v.Add(120.0); - t.Add(now.AddMinutes(4).Ticks); v.Add(double.PositiveInfinity); - t.Add(now.AddMinutes(5).Ticks); v.Add(130.0); - - var series = new TSeries(t, v); - var results = emaVector.Calculate(series); - - // All results should be finite for all periods - foreach (var periodResults in results) - { - foreach (var val in periodResults.Values) - { - Assert.True(double.IsFinite(val), $"Expected finite value but got {val}"); - } - } - } - - [Fact] - public void Reset_ClearsLastValidValue() - { - int[] periods = { 10 }; - var emaVector = new EmaVector(periods); - - // Feed values including NaN - emaVector.Update(new TValue(DateTime.Now, 100.0)); - emaVector.Update(new TValue(DateTime.Now, double.NaN)); - - // Reset - emaVector.Reset(); - - // After reset, first valid value should establish new baseline - var result = emaVector.Update(new TValue(DateTime.Now, 50.0)); - Assert.Equal(50.0, result[0].Value, 1e-9); - } - - [Fact] - public void NaN_Handling_MatchesSingleEma() - { - int[] periods = { 5, 10, 20 }; - var emaVector = new EmaVector(periods); - var emaSingles = periods.Select(p => new Ema(p)).ToArray(); - - // Data with NaN values - var values = new double[] { 10, 20, double.NaN, 40, double.PositiveInfinity, 60, 70 }; - var time = DateTime.Now; - - foreach (var val in values) - { - var tVal = new TValue(time, val); - var multiRes = emaVector.Update(tVal); - - for (int i = 0; i < periods.Length; i++) - { - var singleRes = emaSingles[i].Update(tVal); - Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9); - } - - time = time.AddMinutes(1); - } - } -} +using System; +using System.Linq; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests; + +public class EmaVectorTests +{ + [Fact] + public void Initialization_WithPeriods_SetsCorrectAlphas() + { + int[] periods = { 10, 20 }; + var emaVector = new EmaVector(periods); + + var res = emaVector.Update(new TValue(DateTime.Now, 100.0)); + + Assert.Equal(100.0, res[0].Value, 1e-9); + Assert.Equal(100.0, res[1].Value, 1e-9); + } + + [Fact] + public void Initialization_WithAlphas_Works() + { + double[] alphas = { 0.1, 0.2, 0.5 }; + var emaVector = new EmaVector(alphas); + + var res = emaVector.Update(new TValue(DateTime.Now, 100.0)); + + Assert.Equal(3, res.Length); + Assert.Equal(100.0, res[0].Value, 1e-9); + Assert.Equal(100.0, res[1].Value, 1e-9); + Assert.Equal(100.0, res[2].Value, 1e-9); + } + + [Fact] + public void Initialization_WithZeroPeriod_ThrowsArgumentException() + { + int[] periods = { 10, 0, 20 }; + + Assert.Throws(() => new EmaVector(periods)); + } + + [Fact] + public void Initialization_WithNegativePeriod_ThrowsArgumentException() + { + int[] periods = { 10, -5, 20 }; + + Assert.Throws(() => new EmaVector(periods)); + } + + [Fact] + public void Initialization_WithZeroAlpha_ThrowsArgumentException() + { + double[] alphas = { 0.1, 0.0, 0.5 }; + + Assert.Throws(() => new EmaVector(alphas)); + } + + [Fact] + public void Initialization_WithNegativeAlpha_ThrowsArgumentException() + { + double[] alphas = { 0.1, -0.1, 0.5 }; + + Assert.Throws(() => new EmaVector(alphas)); + } + + [Fact] + public void Initialization_WithAlphaGreaterThanOne_ThrowsArgumentException() + { + double[] alphas = { 0.1, 1.5, 0.5 }; + + Assert.Throws(() => new EmaVector(alphas)); + } + + [Fact] + public void Initialization_WithAlphaEqualToOne_Works() + { + double[] alphas = { 0.1, 1.0, 0.5 }; + var emaVector = new EmaVector(alphas); + + var res = emaVector.Update(new TValue(DateTime.Now, 100.0)); + + Assert.Equal(3, res.Length); + } + + [Fact] + public void Calc_Streaming_MatchesSingleEma() + { + int[] periods = { 5, 10, 20 }; + var emaVector = new EmaVector(periods); + var emaSingles = periods.Select(p => new Ema(p)).ToArray(); + + var values = new double[] { 10, 20, 30, 40, 50, 40, 30, 20, 10 }; + var time = DateTime.Now; + + foreach (var val in values) + { + var tVal = new TValue(time, val); + var multiRes = emaVector.Update(tVal); + + for (int i = 0; i < periods.Length; i++) + { + var singleRes = emaSingles[i].Update(tVal); + Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9); + Assert.Equal(singleRes.Time, multiRes[i].Time); + } + + time = time.AddMinutes(1); + } + } + + [Fact] + public void Calc_Series_MatchesSingleEma() + { + int[] periods = { 5, 10, 20 }; + var emaVector = new EmaVector(periods); + var emaSingles = periods.Select(p => new Ema(p)).ToArray(); + + int len = 100; + var t = new System.Collections.Generic.List(len); + var v = new System.Collections.Generic.List(len); + var now = DateTime.Now; + + for (int i = 0; i < len; i++) + { + t.Add(now.AddMinutes(i).Ticks); + v.Add(Math.Sin(i * 0.1) * 100); + } + + var series = new TSeries(t, v); + + var multiRes = emaVector.Calculate(series); + + for (int i = 0; i < periods.Length; i++) + { + var singleRes = emaSingles[i].Update(series); + + Assert.Equal(singleRes.Count, multiRes[i].Count); + for (int j = 0; j < len; j++) + { + Assert.Equal(singleRes.Values[j], multiRes[i].Values[j], 1e-8); + } + } + } + + [Fact] + public void Calc_Series_MatchesStreaming() + { + int[] periods = { 5, 10, 20 }; + var emaVectorBatch = new EmaVector(periods); + var emaVectorStream = new EmaVector(periods); + + int len = 100; + var t = new System.Collections.Generic.List(len); + var v = new System.Collections.Generic.List(len); + var now = DateTime.Now; + + for (int i = 0; i < len; i++) + { + t.Add(now.AddMinutes(i).Ticks); + v.Add(Math.Sin(i * 0.1) * 100); + } + + var series = new TSeries(t, v); + + var batchRes = emaVectorBatch.Calculate(series); + + for (int i = 0; i < len; i++) + { + var tVal = new TValue(new DateTime(t[i]), v[i]); + var streamRes = emaVectorStream.Update(tVal); + + for (int j = 0; j < periods.Length; j++) + { + Assert.Equal(batchRes[j].Values[i], streamRes[j].Value, 1e-9); + } + } + } + + [Fact] + public void Calculate_Static_MatchesInstanceMethod() + { + int[] periods = { 5, 10, 20 }; + + int len = 50; + var t = new System.Collections.Generic.List(len); + var v = new System.Collections.Generic.List(len); + var now = DateTime.Now; + + for (int i = 0; i < len; i++) + { + t.Add(now.AddMinutes(i).Ticks); + v.Add(Math.Sin(i * 0.1) * 100); + } + + var series = new TSeries(t, v); + + var instanceEma = new EmaVector(periods); + var instanceRes = instanceEma.Calculate(series); + + var staticRes = EmaVector.Calculate(series, periods); + + for (int i = 0; i < periods.Length; i++) + { + Assert.Equal(instanceRes[i].Count, staticRes[i].Count); + for (int j = 0; j < len; j++) + { + Assert.Equal(instanceRes[i].Values[j], staticRes[i].Values[j], 1e-9); + } + } + } + + [Fact] + public void Reset_ClearsState() + { + int[] periods = { 10 }; + var emaVector = new EmaVector(periods); + + emaVector.Update(new TValue(DateTime.Now, 100.0)); + emaVector.Reset(); + + var res = emaVector.Update(new TValue(DateTime.Now, 200.0)); + + Assert.Equal(200.0, res[0].Value, 1e-9); + } + + [Fact] + public void Update_NaN_Input_UsesLastValidValue() + { + int[] periods = { 10, 20 }; + var emaVector = new EmaVector(periods); + + emaVector.Update(new TValue(DateTime.Now, 100.0)); + emaVector.Update(new TValue(DateTime.Now, 110.0)); + + var resultAfterNaN = emaVector.Update(new TValue(DateTime.Now, double.NaN)); + + foreach (var result in resultAfterNaN) + { + Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}"); + } + } + + [Fact] + public void Update_Infinity_Input_UsesLastValidValue() + { + int[] periods = { 10, 20 }; + var emaVector = new EmaVector(periods); + + emaVector.Update(new TValue(DateTime.Now, 100.0)); + emaVector.Update(new TValue(DateTime.Now, 110.0)); + + var resultAfterPosInf = emaVector.Update(new TValue(DateTime.Now, double.PositiveInfinity)); + foreach (var result in resultAfterPosInf) + { + Assert.True(double.IsFinite(result.Value)); + } + + var resultAfterNegInf = emaVector.Update(new TValue(DateTime.Now, double.NegativeInfinity)); + foreach (var result in resultAfterNegInf) + { + Assert.True(double.IsFinite(result.Value)); + } + } + + [Fact] + public void Update_MultipleNaN_ContinuesWithLastValid() + { + int[] periods = { 5, 10 }; + var emaVector = new EmaVector(periods); + + emaVector.Update(new TValue(DateTime.Now, 100.0)); + emaVector.Update(new TValue(DateTime.Now, 110.0)); + emaVector.Update(new TValue(DateTime.Now, 120.0)); + + var r1 = emaVector.Update(new TValue(DateTime.Now, double.NaN)); + var r2 = emaVector.Update(new TValue(DateTime.Now, double.NaN)); + var r3 = emaVector.Update(new TValue(DateTime.Now, double.NaN)); + + foreach (var result in r1) Assert.True(double.IsFinite(result.Value)); + foreach (var result in r2) Assert.True(double.IsFinite(result.Value)); + foreach (var result in r3) Assert.True(double.IsFinite(result.Value)); + } + + [Fact] + public void Calculate_Series_HandlesNaN() + { + int[] periods = { 5, 10 }; + var emaVector = new EmaVector(periods); + + var t = new System.Collections.Generic.List(); + var v = new System.Collections.Generic.List(); + var now = DateTime.Now; + + t.Add(now.Ticks); v.Add(100.0); + t.Add(now.AddMinutes(1).Ticks); v.Add(110.0); + t.Add(now.AddMinutes(2).Ticks); v.Add(double.NaN); + t.Add(now.AddMinutes(3).Ticks); v.Add(120.0); + t.Add(now.AddMinutes(4).Ticks); v.Add(double.PositiveInfinity); + t.Add(now.AddMinutes(5).Ticks); v.Add(130.0); + + var series = new TSeries(t, v); + var results = emaVector.Calculate(series); + + foreach (var periodResults in results) + { + foreach (var val in periodResults.Values) + { + Assert.True(double.IsFinite(val), $"Expected finite value but got {val}"); + } + } + } + + [Fact] + public void Reset_ClearsLastValidValue() + { + int[] periods = { 10 }; + var emaVector = new EmaVector(periods); + + emaVector.Update(new TValue(DateTime.Now, 100.0)); + emaVector.Update(new TValue(DateTime.Now, double.NaN)); + + emaVector.Reset(); + + var result = emaVector.Update(new TValue(DateTime.Now, 50.0)); + Assert.Equal(50.0, result[0].Value, 1e-9); + } + + [Fact] + public void NaN_Handling_MatchesSingleEma() + { + int[] periods = { 5, 10, 20 }; + var emaVector = new EmaVector(periods); + var emaSingles = periods.Select(p => new Ema(p)).ToArray(); + + var values = new double[] { 10, 20, double.NaN, 40, double.PositiveInfinity, 60, 70 }; + var time = DateTime.Now; + + foreach (var val in values) + { + var tVal = new TValue(time, val); + var multiRes = emaVector.Update(tVal); + + for (int i = 0; i < periods.Length; i++) + { + var singleRes = emaSingles[i].Update(tVal); + Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9); + } + + time = time.AddMinutes(1); + } + } + + [Fact] + public void Values_Property_UpdatesAfterUpdate() + { + int[] periods = { 5, 10 }; + var emaVector = new EmaVector(periods); + + var result = emaVector.Update(new TValue(DateTime.Now, 100.0)); + + Assert.Equal(result[0].Value, emaVector.Values[0].Value); + Assert.Equal(result[1].Value, emaVector.Values[1].Value); + } + + [Fact] + public void Values_Property_UpdatesAfterCalculate() + { + int[] periods = { 5, 10 }; + var emaVector = new EmaVector(periods); + + var t = new System.Collections.Generic.List { 100, 200, 300 }; + var v = new System.Collections.Generic.List { 10.0, 20.0, 30.0 }; + var series = new TSeries(t, v); + + var results = emaVector.Calculate(series); + + Assert.Equal(results[0].Last.Value, emaVector.Values[0].Value, 1e-9); + Assert.Equal(results[1].Last.Value, emaVector.Values[1].Value, 1e-9); + } +} diff --git a/lib/core/simd/SimdExtensions.Tests.cs b/lib/core/simd/SimdExtensions.Tests.cs index 999d4f24..c9118924 100644 --- a/lib/core/simd/SimdExtensions.Tests.cs +++ b/lib/core/simd/SimdExtensions.Tests.cs @@ -1,249 +1,730 @@ -using System; -using Xunit; -using QuanTAlib; - -namespace QuanTAlib.Tests; - -public class SimdExtensionsTests -{ - [Fact] - public void SumSIMD_EmptySpan_ReturnsZero() - { - var span = ReadOnlySpan.Empty; - Assert.Equal(0.0, span.SumSIMD()); - } - - [Fact] - public void SumSIMD_SingleElement_ReturnsElement() - { - double[] data = [42.5]; - var span = new ReadOnlySpan(data); - Assert.Equal(42.5, span.SumSIMD()); - } - - [Fact] - public void SumSIMD_MultipleElements_ReturnsCorrectSum() - { - double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; - var span = new ReadOnlySpan(data); - Assert.Equal(55.0, span.SumSIMD(), precision: 10); - } - - [Fact] - public void SumSIMD_LargeArray_ReturnsCorrectSum() - { - double[] data = new double[1000]; - for (int i = 0; i < data.Length; i++) - data[i] = i + 1.0; - - var span = new ReadOnlySpan(data); - double expected = 1000.0 * 1001.0 / 2.0; // Sum of 1..1000 - Assert.Equal(expected, span.SumSIMD(), precision: 8); - } - - [Fact] - public void MinSIMD_EmptySpan_ReturnsNaN() - { - var span = ReadOnlySpan.Empty; - Assert.True(double.IsNaN(span.MinSIMD())); - } - - [Fact] - public void MinSIMD_SingleElement_ReturnsElement() - { - double[] data = [42.5]; - var span = new ReadOnlySpan(data); - Assert.Equal(42.5, span.MinSIMD()); - } - - [Fact] - public void MinSIMD_MultipleElements_ReturnsMinimum() - { - double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0]; - var span = new ReadOnlySpan(data); - Assert.Equal(1.0, span.MinSIMD()); - } - - [Fact] - public void MaxSIMD_EmptySpan_ReturnsNaN() - { - var span = ReadOnlySpan.Empty; - Assert.True(double.IsNaN(span.MaxSIMD())); - } - - [Fact] - public void MaxSIMD_SingleElement_ReturnsElement() - { - double[] data = [42.5]; - var span = new ReadOnlySpan(data); - Assert.Equal(42.5, span.MaxSIMD()); - } - - [Fact] - public void MaxSIMD_MultipleElements_ReturnsMaximum() - { - double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0]; - var span = new ReadOnlySpan(data); - Assert.Equal(9.0, span.MaxSIMD()); - } - - [Fact] - public void AverageSIMD_EmptySpan_ReturnsNaN() - { - var span = ReadOnlySpan.Empty; - Assert.True(double.IsNaN(span.AverageSIMD())); - } - - [Fact] - public void AverageSIMD_MultipleElements_ReturnsCorrectAverage() - { - double[] data = [1.0, 2.0, 3.0, 4.0, 5.0]; - var span = new ReadOnlySpan(data); - Assert.Equal(3.0, span.AverageSIMD(), precision: 10); - } - - [Fact] - public void VarianceSIMD_LessThanTwoElements_ReturnsNaN() - { - double[] data = [42.5]; - var span = new ReadOnlySpan(data); - Assert.True(double.IsNaN(span.VarianceSIMD())); - } - - [Fact] - public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance() - { - double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; - var span = new ReadOnlySpan(data); - - // Expected variance: 4.571428... (sample variance) - double variance = span.VarianceSIMD(); - Assert.True(Math.Abs(variance - 4.571428) < 0.0001); - } - - [Fact] - public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev() - { - double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; - var span = new ReadOnlySpan(data); - - // Expected std dev: sqrt(4.571428) ≈ 2.138 - double stdDev = span.StdDevSIMD(); - Assert.True(Math.Abs(stdDev - 2.138) < 0.01); - } - - [Fact] - public void MinMaxSIMD_EmptySpan_ReturnsBothNaN() - { - var span = ReadOnlySpan.Empty; - var (min, max) = span.MinMaxSIMD(); - Assert.True(double.IsNaN(min)); - Assert.True(double.IsNaN(max)); - } - - [Fact] - public void MinMaxSIMD_SingleElement_ReturnsSameValue() - { - double[] data = [42.5]; - var span = new ReadOnlySpan(data); - var (min, max) = span.MinMaxSIMD(); - Assert.Equal(42.5, min); - Assert.Equal(42.5, max); - } - - [Fact] - public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax() - { - double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0]; - var span = new ReadOnlySpan(data); - var (min, max) = span.MinMaxSIMD(); - Assert.Equal(1.0, min); - Assert.Equal(9.0, max); - } - - [Fact] - public void SIMD_WorksWithTSeriesValues() - { - var series = new TSeries(100); - - for (int i = 0; i < 100; i++) - { - series.Add(DateTime.UtcNow.Ticks + i, i + 1.0); - } - - var values = series.Values; - - double sum = values.SumSIMD(); - double avg = values.AverageSIMD(); - double min = values.MinSIMD(); - double max = values.MaxSIMD(); - var (minAlt, maxAlt) = values.MinMaxSIMD(); - - Assert.Equal(5050.0, sum, precision: 8); // Sum of 1..100 - Assert.Equal(50.5, avg, precision: 8); - Assert.Equal(1.0, min); - Assert.Equal(100.0, max); - Assert.Equal(min, minAlt); - Assert.Equal(max, maxAlt); - } - - [Fact] - public void SIMD_WorksWithTBarSeriesClose() - { - var gbm = new GBM(startPrice: 100.0); - long startTime = DateTime.UtcNow.Ticks; - var interval = TimeSpan.FromMinutes(1); - var bars = gbm.Fetch(1000, startTime, interval); - - var closeValues = bars.Close.Values; - - double sum = closeValues.SumSIMD(); - double avg = closeValues.AverageSIMD(); - double min = closeValues.MinSIMD(); - double max = closeValues.MaxSIMD(); - - Assert.True(sum > 0); - Assert.True(avg > 0); - Assert.True(min > 0); - Assert.True(max > min); - } - - [Fact] - public void SIMD_PerformanceTest_LargeDataset() - { - // Generate large dataset - var gbm = new GBM(startPrice: 100.0); - long startTime = DateTime.UtcNow.Ticks; - var interval = TimeSpan.FromMinutes(1); - var bars = gbm.Fetch(10000, startTime, interval); - var closeValues = bars.Close.Values; - - // Warm up - _ = closeValues.SumSIMD(); - - // Test SIMD operations - var sw = System.Diagnostics.Stopwatch.StartNew(); - - double sum = closeValues.SumSIMD(); - double avg = closeValues.AverageSIMD(); - double min = closeValues.MinSIMD(); - double max = closeValues.MaxSIMD(); - var (minAlt, maxAlt) = closeValues.MinMaxSIMD(); - double variance = closeValues.VarianceSIMD(); - double stdDev = closeValues.StdDevSIMD(); - - sw.Stop(); - - // Verify results are valid - Assert.True(sum > 0); - Assert.True(avg > 0); - Assert.True(min > 0); - Assert.True(max > min); - Assert.True(variance > 0); - Assert.True(stdDev > 0); - - // Performance should be sub-millisecond for 10k elements - Assert.True(sw.ElapsedMilliseconds < 10, - $"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms"); - } -} +using System; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests; + +public class SimdExtensionsTests +{ + // ContainsNonFinite tests + [Fact] + public void ContainsNonFinite_EmptySpan_ReturnsFalse() + { + var span = ReadOnlySpan.Empty; + Assert.False(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_AllFinite_ReturnsFalse() + { + double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; + var span = new ReadOnlySpan(data); + Assert.False(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_ContainsNaN_ReturnsTrue() + { + double[] data = [1.0, 2.0, double.NaN, 4.0, 5.0, 6.0, 7.0, 8.0]; + var span = new ReadOnlySpan(data); + Assert.True(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_ContainsPositiveInfinity_ReturnsTrue() + { + double[] data = [1.0, 2.0, 3.0, double.PositiveInfinity, 5.0, 6.0, 7.0, 8.0]; + var span = new ReadOnlySpan(data); + Assert.True(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_ContainsNegativeInfinity_ReturnsTrue() + { + double[] data = [1.0, 2.0, 3.0, 4.0, double.NegativeInfinity, 6.0, 7.0, 8.0]; + var span = new ReadOnlySpan(data); + Assert.True(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_NonFiniteInRemainder_ReturnsTrue() + { + double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, double.NaN]; + var span = new ReadOnlySpan(data); + Assert.True(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_SingleNaN_ReturnsTrue() + { + double[] data = [double.NaN]; + var span = new ReadOnlySpan(data); + Assert.True(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_TwoElements_AllFinite_ReturnsFalse() + { + double[] data = [1.0, 2.0]; + var span = new ReadOnlySpan(data); + Assert.False(span.ContainsNonFinite()); + } + + [Fact] + public void ContainsNonFinite_TwoElements_OneNaN_ReturnsTrue() + { + double[] data = [1.0, double.NaN]; + var span = new ReadOnlySpan(data); + Assert.True(span.ContainsNonFinite()); + } + + // SumSIMD tests + [Fact] + public void SumSIMD_EmptySpan_ReturnsZero() + { + var span = ReadOnlySpan.Empty; + Assert.Equal(0.0, span.SumSIMD()); + } + + [Fact] + public void SumSIMD_SingleElement_ReturnsElement() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + Assert.Equal(42.5, span.SumSIMD()); + } + + [Fact] + public void SumSIMD_TwoElements_ReturnsSum() + { + double[] data = [1.5, 2.5]; + var span = new ReadOnlySpan(data); + Assert.Equal(4.0, span.SumSIMD()); + } + + [Fact] + public void SumSIMD_MultipleElements_ReturnsCorrectSum() + { + double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(55.0, span.SumSIMD(), precision: 10); + } + + [Fact] + public void SumSIMD_LargeArray_ReturnsCorrectSum() + { + double[] data = new double[1000]; + for (int i = 0; i < data.Length; i++) + data[i] = i + 1.0; + + var span = new ReadOnlySpan(data); + double expected = 1000.0 * 1001.0 / 2.0; + Assert.Equal(expected, span.SumSIMD(), precision: 8); + } + + [Fact] + public void SumSIMD_ContainsNaN_ReturnsNaN() + { + double[] data = [1.0, 2.0, double.NaN, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.SumSIMD())); + } + + [Fact] + public void SumSIMD_ContainsInfinity_ReturnsNaN() + { + double[] data = [1.0, 2.0, double.PositiveInfinity, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.SumSIMD())); + } + + [Fact] + public void SumSIMD_NegativeValues_ReturnsCorrectSum() + { + double[] data = [-1.0, -2.0, -3.0, -4.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(-10.0, span.SumSIMD()); + } + + // MinSIMD tests + [Fact] + public void MinSIMD_EmptySpan_ReturnsNaN() + { + var span = ReadOnlySpan.Empty; + Assert.True(double.IsNaN(span.MinSIMD())); + } + + [Fact] + public void MinSIMD_SingleElement_ReturnsElement() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + Assert.Equal(42.5, span.MinSIMD()); + } + + [Fact] + public void MinSIMD_TwoElements_ReturnsMinimum() + { + double[] data = [5.0, 2.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(2.0, span.MinSIMD()); + } + + [Fact] + public void MinSIMD_MultipleElements_ReturnsMinimum() + { + double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(1.0, span.MinSIMD()); + } + + [Fact] + public void MinSIMD_ContainsNaN_ReturnsNaN() + { + double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.MinSIMD())); + } + + [Fact] + public void MinSIMD_MinInRemainder_ReturnsCorrectMin() + { + double[] data = [5.0, 2.0, 8.0, 6.0, 9.0, 3.0, 7.0, 4.0, 0.5]; + var span = new ReadOnlySpan(data); + Assert.Equal(0.5, span.MinSIMD()); + } + + [Fact] + public void MinSIMD_NegativeValues_ReturnsMinimum() + { + double[] data = [-5.0, -2.0, -8.0, -1.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(-8.0, span.MinSIMD()); + } + + // MaxSIMD tests + [Fact] + public void MaxSIMD_EmptySpan_ReturnsNaN() + { + var span = ReadOnlySpan.Empty; + Assert.True(double.IsNaN(span.MaxSIMD())); + } + + [Fact] + public void MaxSIMD_SingleElement_ReturnsElement() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + Assert.Equal(42.5, span.MaxSIMD()); + } + + [Fact] + public void MaxSIMD_TwoElements_ReturnsMaximum() + { + double[] data = [5.0, 9.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(9.0, span.MaxSIMD()); + } + + [Fact] + public void MaxSIMD_MultipleElements_ReturnsMaximum() + { + double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(9.0, span.MaxSIMD()); + } + + [Fact] + public void MaxSIMD_ContainsNaN_ReturnsNaN() + { + double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.MaxSIMD())); + } + + [Fact] + public void MaxSIMD_MaxInRemainder_ReturnsCorrectMax() + { + double[] data = [5.0, 2.0, 8.0, 6.0, 4.0, 3.0, 7.0, 1.0, 99.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(99.0, span.MaxSIMD()); + } + + [Fact] + public void MaxSIMD_NegativeValues_ReturnsMaximum() + { + double[] data = [-5.0, -2.0, -8.0, -1.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(-1.0, span.MaxSIMD()); + } + + // AverageSIMD tests + [Fact] + public void AverageSIMD_EmptySpan_ReturnsNaN() + { + var span = ReadOnlySpan.Empty; + Assert.True(double.IsNaN(span.AverageSIMD())); + } + + [Fact] + public void AverageSIMD_TwoElements_ReturnsAverage() + { + double[] data = [2.0, 4.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(3.0, span.AverageSIMD()); + } + + [Fact] + public void AverageSIMD_MultipleElements_ReturnsCorrectAverage() + { + double[] data = [1.0, 2.0, 3.0, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(3.0, span.AverageSIMD(), precision: 10); + } + + [Fact] + public void AverageSIMD_ContainsNaN_ReturnsNaN() + { + double[] data = [1.0, double.NaN, 3.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.AverageSIMD())); + } + + // VarianceSIMD tests + [Fact] + public void VarianceSIMD_LessThanTwoElements_ReturnsNaN() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.VarianceSIMD())); + } + + [Fact] + public void VarianceSIMD_EmptySpan_ReturnsNaN() + { + var span = ReadOnlySpan.Empty; + Assert.True(double.IsNaN(span.VarianceSIMD())); + } + + [Fact] + public void VarianceSIMD_TwoElements_ReturnsCorrect() + { + double[] data = [1.0, 3.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(2.0, span.VarianceSIMD(), precision: 10); + } + + [Fact] + public void VarianceSIMD_ThreeElements_ReturnsCorrect() + { + double[] data = [1.0, 2.0, 3.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(1.0, span.VarianceSIMD(), precision: 10); + } + + [Fact] + public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance() + { + double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; + var span = new ReadOnlySpan(data); + + double variance = span.VarianceSIMD(); + Assert.True(Math.Abs(variance - 4.571428) < 0.0001); + } + + [Fact] + public void VarianceSIMD_WithProvidedMean_UsesProvidedMean() + { + double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; + var span = new ReadOnlySpan(data); + + double mean = 5.0; + double variance = span.VarianceSIMD(mean); + + Assert.True(variance > 0); + } + + [Fact] + public void VarianceSIMD_ContainsNaN_ReturnsNaN() + { + double[] data = [2.0, double.NaN, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.VarianceSIMD())); + } + + [Fact] + public void VarianceSIMD_WithNaNMean_ReturnsNaN() + { + double[] data = [2.0, 4.0, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.VarianceSIMD(double.NaN))); + } + + [Fact] + public void VarianceSIMD_WithInfinityMean_ReturnsNaN() + { + double[] data = [2.0, 4.0, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.VarianceSIMD(double.PositiveInfinity))); + } + + // StdDevSIMD tests + [Fact] + public void StdDevSIMD_TwoElements_ReturnsCorrect() + { + double[] data = [1.0, 3.0]; + var span = new ReadOnlySpan(data); + Assert.True(Math.Abs(span.StdDevSIMD() - 1.414) < 0.01); + } + + [Fact] + public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev() + { + double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; + var span = new ReadOnlySpan(data); + + double stdDev = span.StdDevSIMD(); + Assert.True(Math.Abs(stdDev - 2.138) < 0.01); + } + + [Fact] + public void StdDevSIMD_WithProvidedMean_Works() + { + double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; + var span = new ReadOnlySpan(data); + + double stdDev = span.StdDevSIMD(5.0); + Assert.True(stdDev > 0); + } + + [Fact] + public void StdDevSIMD_ContainsNaN_ReturnsNaN() + { + double[] data = [2.0, double.NaN, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.True(double.IsNaN(span.StdDevSIMD())); + } + + // MinMaxSIMD tests + [Fact] + public void MinMaxSIMD_EmptySpan_ReturnsBothNaN() + { + var span = ReadOnlySpan.Empty; + var (min, max) = span.MinMaxSIMD(); + Assert.True(double.IsNaN(min)); + Assert.True(double.IsNaN(max)); + } + + [Fact] + public void MinMaxSIMD_SingleElement_ReturnsSameValue() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + var (min, max) = span.MinMaxSIMD(); + Assert.Equal(42.5, min); + Assert.Equal(42.5, max); + } + + [Fact] + public void MinMaxSIMD_TwoElements_ReturnsCorrect() + { + double[] data = [5.0, 2.0]; + var span = new ReadOnlySpan(data); + var (min, max) = span.MinMaxSIMD(); + Assert.Equal(2.0, min); + Assert.Equal(5.0, max); + } + + [Fact] + public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax() + { + double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0]; + var span = new ReadOnlySpan(data); + var (min, max) = span.MinMaxSIMD(); + Assert.Equal(1.0, min); + Assert.Equal(9.0, max); + } + + [Fact] + public void MinMaxSIMD_ContainsNaN_ReturnsBothNaN() + { + double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0]; + var span = new ReadOnlySpan(data); + var (min, max) = span.MinMaxSIMD(); + Assert.True(double.IsNaN(min)); + Assert.True(double.IsNaN(max)); + } + + [Fact] + public void MinMaxSIMD_MinMaxInRemainder_ReturnsCorrect() + { + double[] data = [5.0, 2.0, 8.0, 6.0, 4.0, 3.0, 7.0, 5.0, 0.1, 99.0]; + var span = new ReadOnlySpan(data); + var (min, max) = span.MinMaxSIMD(); + Assert.Equal(0.1, min); + Assert.Equal(99.0, max); + } + + [Fact] + public void MinMaxSIMD_NegativeValues_ReturnsCorrect() + { + double[] data = [-5.0, -2.0, -8.0, -1.0]; + var span = new ReadOnlySpan(data); + var (min, max) = span.MinMaxSIMD(); + Assert.Equal(-8.0, min); + Assert.Equal(-1.0, max); + } + + // Integration tests + [Fact] + public void SIMD_WorksWithTSeriesValues() + { + var series = new TSeries(100); + + for (int i = 0; i < 100; i++) + { + series.Add(DateTime.UtcNow.Ticks + i, i + 1.0); + } + + var values = series.Values; + + double sum = values.SumSIMD(); + double avg = values.AverageSIMD(); + double min = values.MinSIMD(); + double max = values.MaxSIMD(); + var (minAlt, maxAlt) = values.MinMaxSIMD(); + + Assert.Equal(5050.0, sum, precision: 8); + Assert.Equal(50.5, avg, precision: 8); + Assert.Equal(1.0, min); + Assert.Equal(100.0, max); + Assert.Equal(min, minAlt); + Assert.Equal(max, maxAlt); + } + + [Fact] + public void SIMD_WorksWithTBarSeriesClose() + { + var gbm = new GBM(startPrice: 100.0); + long startTime = DateTime.UtcNow.Ticks; + var interval = TimeSpan.FromMinutes(1); + var bars = gbm.Fetch(1000, startTime, interval); + + var closeValues = bars.Close.Values; + + double sum = closeValues.SumSIMD(); + double avg = closeValues.AverageSIMD(); + double min = closeValues.MinSIMD(); + double max = closeValues.MaxSIMD(); + + Assert.True(sum > 0); + Assert.True(avg > 0); + Assert.True(min > 0); + Assert.True(max > min); + } + + [Fact] + public void SIMD_PerformanceTest_LargeDataset() + { + var gbm = new GBM(startPrice: 100.0); + long startTime = DateTime.UtcNow.Ticks; + var interval = TimeSpan.FromMinutes(1); + var bars = gbm.Fetch(10000, startTime, interval); + var closeValues = bars.Close.Values; + + _ = closeValues.SumSIMD(); + + var sw = System.Diagnostics.Stopwatch.StartNew(); + + double sum = closeValues.SumSIMD(); + double avg = closeValues.AverageSIMD(); + double min = closeValues.MinSIMD(); + double max = closeValues.MaxSIMD(); + var (minAlt, maxAlt) = closeValues.MinMaxSIMD(); + double variance = closeValues.VarianceSIMD(); + double stdDev = closeValues.StdDevSIMD(); + + sw.Stop(); + + Assert.True(sum > 0); + Assert.True(avg > 0); + Assert.True(min > 0); + Assert.True(max > min); + Assert.True(variance > 0); + Assert.True(stdDev > 0); + + Assert.True(sw.ElapsedMilliseconds < 10, + $"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms"); + } + + [Fact] + public void SIMD_ScalarFallback_SmallArray() + { + double[] data = [1.0, 2.0, 3.0]; + var span = new ReadOnlySpan(data); + + Assert.Equal(6.0, span.SumSIMD()); + Assert.Equal(1.0, span.MinSIMD()); + Assert.Equal(3.0, span.MaxSIMD()); + Assert.Equal(2.0, span.AverageSIMD()); + + var (min, max) = span.MinMaxSIMD(); + Assert.Equal(1.0, min); + Assert.Equal(3.0, max); + } +} + +// Tests for internal scalar implementations +public class SimdScalarFallbackTests +{ + [Fact] + public void ContainsNonFiniteScalar_AllFinite_ReturnsFalse() + { + double[] data = [1.0, 2.0, 3.0, 4.0]; + var span = new ReadOnlySpan(data); + Assert.False(SimdExtensions.ContainsNonFiniteScalar(span)); + } + + [Fact] + public void ContainsNonFiniteScalar_ContainsNaN_ReturnsTrue() + { + double[] data = [1.0, double.NaN, 3.0]; + var span = new ReadOnlySpan(data); + Assert.True(SimdExtensions.ContainsNonFiniteScalar(span)); + } + + [Fact] + public void ContainsNonFiniteScalar_ContainsInfinity_ReturnsTrue() + { + double[] data = [1.0, double.PositiveInfinity, 3.0]; + var span = new ReadOnlySpan(data); + Assert.True(SimdExtensions.ContainsNonFiniteScalar(span)); + } + + [Fact] + public void ContainsNonFiniteScalar_Empty_ReturnsFalse() + { + var span = ReadOnlySpan.Empty; + Assert.False(SimdExtensions.ContainsNonFiniteScalar(span)); + } + + [Fact] + public void SumScalar_MultipleElements_ReturnsCorrectSum() + { + double[] data = [1.0, 2.0, 3.0, 4.0, 5.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(15.0, SimdExtensions.SumScalar(span)); + } + + [Fact] + public void SumScalar_NegativeValues_ReturnsCorrectSum() + { + double[] data = [-1.0, -2.0, 3.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(0.0, SimdExtensions.SumScalar(span)); + } + + [Fact] + public void SumScalar_Empty_ReturnsZero() + { + var span = ReadOnlySpan.Empty; + Assert.Equal(0.0, SimdExtensions.SumScalar(span)); + } + + [Fact] + public void MinScalar_MultipleElements_ReturnsMinimum() + { + double[] data = [5.0, 2.0, 8.0, 1.0, 9.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(1.0, SimdExtensions.MinScalar(span)); + } + + [Fact] + public void MinScalar_NegativeValues_ReturnsMinimum() + { + double[] data = [-5.0, -2.0, -8.0, -1.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(-8.0, SimdExtensions.MinScalar(span)); + } + + [Fact] + public void MinScalar_SingleElement_ReturnsElement() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + Assert.Equal(42.5, SimdExtensions.MinScalar(span)); + } + + [Fact] + public void MaxScalar_MultipleElements_ReturnsMaximum() + { + double[] data = [5.0, 2.0, 8.0, 1.0, 9.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(9.0, SimdExtensions.MaxScalar(span)); + } + + [Fact] + public void MaxScalar_NegativeValues_ReturnsMaximum() + { + double[] data = [-5.0, -2.0, -8.0, -1.0]; + var span = new ReadOnlySpan(data); + Assert.Equal(-1.0, SimdExtensions.MaxScalar(span)); + } + + [Fact] + public void MaxScalar_SingleElement_ReturnsElement() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + Assert.Equal(42.5, SimdExtensions.MaxScalar(span)); + } + + [Fact] + public void VarianceScalar_MultipleElements_ReturnsCorrectVariance() + { + double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]; + var span = new ReadOnlySpan(data); + double mean = 5.0; + double variance = SimdExtensions.VarianceScalar(span, mean); + Assert.True(Math.Abs(variance - 4.571428) < 0.0001); + } + + [Fact] + public void VarianceScalar_TwoElements_ReturnsCorrectVariance() + { + double[] data = [1.0, 3.0]; + var span = new ReadOnlySpan(data); + double mean = 2.0; + Assert.Equal(2.0, SimdExtensions.VarianceScalar(span, mean), precision: 10); + } + + [Fact] + public void MinMaxScalar_MultipleElements_ReturnsCorrectMinMax() + { + double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0]; + var span = new ReadOnlySpan(data); + var (min, max) = SimdExtensions.MinMaxScalar(span); + Assert.Equal(1.0, min); + Assert.Equal(9.0, max); + } + + [Fact] + public void MinMaxScalar_NegativeValues_ReturnsCorrectMinMax() + { + double[] data = [-5.0, -2.0, -8.0, -1.0]; + var span = new ReadOnlySpan(data); + var (min, max) = SimdExtensions.MinMaxScalar(span); + Assert.Equal(-8.0, min); + Assert.Equal(-1.0, max); + } + + [Fact] + public void MinMaxScalar_SingleElement_ReturnsSameValue() + { + double[] data = [42.5]; + var span = new ReadOnlySpan(data); + var (min, max) = SimdExtensions.MinMaxScalar(span); + Assert.Equal(42.5, min); + Assert.Equal(42.5, max); + } +} diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs index 66139fbd..b8853441 100644 --- a/lib/core/simd/SimdExtensions.cs +++ b/lib/core/simd/SimdExtensions.cs @@ -9,6 +9,76 @@ namespace QuanTAlib; /// public static class SimdExtensions { + // Internal scalar implementations for testability + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool ContainsNonFiniteScalar(ReadOnlySpan span) + { + for (int i = 0; i < span.Length; i++) + { + if (!double.IsFinite(span[i])) + return true; + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double SumScalar(ReadOnlySpan span) + { + double scalar = 0.0; + for (int i = 0; i < span.Length; i++) + scalar += span[i]; + return scalar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double MinScalar(ReadOnlySpan span) + { + double min = span[0]; + for (int i = 1; i < span.Length; i++) + { + if (span[i] < min) + min = span[i]; + } + return min; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double MaxScalar(ReadOnlySpan span) + { + double max = span[0]; + for (int i = 1; i < span.Length; i++) + { + if (span[i] > max) + max = span[i]; + } + return max; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double VarianceScalar(ReadOnlySpan span, double mean) + { + double sumSquares = 0.0; + for (int i = 0; i < span.Length; i++) + { + double diff = span[i] - mean; + sumSquares += diff * diff; + } + return sumSquares / (span.Length - 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan span) + { + double scalarMin = span[0]; + double scalarMax = span[0]; + for (int i = 1; i < span.Length; i++) + { + if (span[i] < scalarMin) scalarMin = span[i]; + if (span[i] > scalarMax) scalarMax = span[i]; + } + return (scalarMin, scalarMax); + } + /// /// Checks if span contains any non-finite values (NaN or Infinity). /// Returns true if any non-finite value is found. @@ -26,8 +96,6 @@ public static class SimdExtensions for (; i <= span.Length - vectorSize; i += vectorSize) { var vector = new Vector(span.Slice(i, vectorSize)); - // Check for NaN: NaN != NaN is true - // Check for Infinity: IsInfinity for (int j = 0; j < vectorSize; j++) { if (!double.IsFinite(vector[j])) @@ -45,13 +113,7 @@ public static class SimdExtensions return false; } - // Scalar fallback - for (int i = 0; i < span.Length; i++) - { - if (!double.IsFinite(span[i])) - return true; - } - return false; + return ContainsNonFiniteScalar(span); } /// @@ -92,11 +154,7 @@ public static class SimdExtensions return result; } - // Scalar fallback - double scalar = 0.0; - for (int i = 0; i < span.Length; i++) - scalar += span[i]; - return scalar; + return SumScalar(span); } /// @@ -144,14 +202,7 @@ public static class SimdExtensions return result; } - // Scalar fallback - double min = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] < min) - min = span[i]; - } - return min; + return MinScalar(span); } /// @@ -199,14 +250,7 @@ public static class SimdExtensions return result; } - // Scalar fallback - double max = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] > max) - max = span[i]; - } - return max; + return MaxScalar(span); } /// @@ -270,14 +314,7 @@ public static class SimdExtensions return result / (span.Length - 1); } - // Scalar fallback - double sumSquares = 0.0; - for (int i = 0; i < span.Length; i++) - { - double diff = span[i] - m; - sumSquares += diff * diff; - } - return sumSquares / (span.Length - 1); + return VarianceScalar(span, m); } /// @@ -339,14 +376,6 @@ public static class SimdExtensions return (min, max); } - // Scalar fallback - double scalarMin = span[0]; - double scalarMax = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] < scalarMin) scalarMin = span[i]; - if (span[i] > scalarMax) scalarMax = span[i]; - } - return (scalarMin, scalarMax); + return MinMaxScalar(span); } } diff --git a/lib/core/tbar/TBar.Tests.cs b/lib/core/tbar/TBar.Tests.cs index 1d6a4f86..6d22c8c6 100644 --- a/lib/core/tbar/TBar.Tests.cs +++ b/lib/core/tbar/TBar.Tests.cs @@ -1,76 +1,353 @@ -using System; -using Xunit; -using QuanTAlib; - -namespace QuanTAlib.Tests -{ - public class TBarTests - { - [Fact] - public void Constructor_SetsPropertiesCorrectly() - { - long time = DateTime.UtcNow.Ticks; - double open = 100; - double high = 110; - double low = 90; - double close = 105; - double volume = 1000; - - var bar = new TBar(time, open, high, low, close, volume); - - Assert.Equal(time, bar.Time); - Assert.Equal(open, bar.Open); - Assert.Equal(high, bar.High); - Assert.Equal(low, bar.Low); - Assert.Equal(close, bar.Close); - Assert.Equal(volume, bar.Volume); - } - - [Fact] - public void HL2_CalculatesCorrectly() - { - var bar = new TBar(0, 100, 110, 90, 105, 1000); - Assert.Equal(100.0, bar.HL2); // (110 + 90) / 2 - } - - [Fact] - public void OHL3_CalculatesCorrectly() - { - var bar = new TBar(0, 100, 110, 90, 105, 1000); - Assert.Equal(100.0, bar.OHL3); // (100 + 110 + 90) / 3 - } - - [Fact] - public void HLC3_CalculatesCorrectly() - { - var bar = new TBar(0, 100, 110, 90, 100, 1000); - Assert.Equal(100.0, bar.HLC3); // (110 + 90 + 100) / 3 - } - - [Fact] - public void OHLC4_CalculatesCorrectly() - { - var bar = new TBar(0, 100, 110, 90, 100, 1000); - Assert.Equal(100.0, bar.OHLC4); // (100 + 110 + 90 + 100) / 4 - } - - [Fact] - public void HLCC4_CalculatesCorrectly() - { - var bar = new TBar(0, 100, 110, 90, 100, 1000); - Assert.Equal(100.0, bar.HLCC4); // (110 + 90 + 100 + 100) / 4 - } - - [Fact] - public void ImplicitConversion_ToTValue_ReturnsClosePriceWithTime() - { - long time = DateTime.UtcNow.Ticks; - var bar = new TBar(time, 100, 110, 90, 105, 1000); - - TValue tv = bar; - - Assert.Equal(time, tv.Time); - Assert.Equal(105.0, tv.Value); - } - } -} +using System; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests +{ + public class TBarTests + { + [Fact] + public void Constructor_SetsPropertiesCorrectly() + { + long time = DateTime.UtcNow.Ticks; + double open = 100; + double high = 110; + double low = 90; + double close = 105; + double volume = 1000; + + var bar = new TBar(time, open, high, low, close, volume); + + Assert.Equal(time, bar.Time); + Assert.Equal(open, bar.Open); + Assert.Equal(high, bar.High); + Assert.Equal(low, bar.Low); + Assert.Equal(close, bar.Close); + Assert.Equal(volume, bar.Volume); + } + + [Fact] + public void Constructor_WithDateTime_SetsPropertiesCorrectly() + { + var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + double open = 100; + double high = 110; + double low = 90; + double close = 105; + double volume = 1000; + + var bar = new TBar(dateTime, open, high, low, close, volume); + + Assert.Equal(dateTime.Ticks, bar.Time); + Assert.Equal(open, bar.Open); + Assert.Equal(high, bar.High); + Assert.Equal(low, bar.Low); + Assert.Equal(close, bar.Close); + Assert.Equal(volume, bar.Volume); + } + + [Fact] + public void AsDateTime_ReturnsCorrectDateTime() + { + var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + var bar = new TBar(dateTime, 100, 110, 90, 105, 1000); + + Assert.Equal(dateTime, bar.AsDateTime); + Assert.Equal(DateTimeKind.Utc, bar.AsDateTime.Kind); + } + + [Fact] + public void O_Property_ReturnsTValueWithOpenPrice() + { + long time = DateTime.UtcNow.Ticks; + var bar = new TBar(time, 100, 110, 90, 105, 1000); + + TValue o = bar.O; + + Assert.Equal(time, o.Time); + Assert.Equal(100.0, o.Value); + } + + [Fact] + public void H_Property_ReturnsTValueWithHighPrice() + { + long time = DateTime.UtcNow.Ticks; + var bar = new TBar(time, 100, 110, 90, 105, 1000); + + TValue h = bar.H; + + Assert.Equal(time, h.Time); + Assert.Equal(110.0, h.Value); + } + + [Fact] + public void L_Property_ReturnsTValueWithLowPrice() + { + long time = DateTime.UtcNow.Ticks; + var bar = new TBar(time, 100, 110, 90, 105, 1000); + + TValue l = bar.L; + + Assert.Equal(time, l.Time); + Assert.Equal(90.0, l.Value); + } + + [Fact] + public void C_Property_ReturnsTValueWithClosePrice() + { + long time = DateTime.UtcNow.Ticks; + var bar = new TBar(time, 100, 110, 90, 105, 1000); + + TValue c = bar.C; + + Assert.Equal(time, c.Time); + Assert.Equal(105.0, c.Value); + } + + [Fact] + public void V_Property_ReturnsTValueWithVolume() + { + long time = DateTime.UtcNow.Ticks; + var bar = new TBar(time, 100, 110, 90, 105, 1000); + + TValue v = bar.V; + + Assert.Equal(time, v.Time); + Assert.Equal(1000.0, v.Value); + } + + [Fact] + public void HL2_CalculatesCorrectly() + { + var bar = new TBar(0, 100, 110, 90, 105, 1000); + Assert.Equal(100.0, bar.HL2); + } + + [Fact] + public void OC2_CalculatesCorrectly() + { + var bar = new TBar(0, 100, 110, 90, 104, 1000); + Assert.Equal(102.0, bar.OC2); + } + + [Fact] + public void OHL3_CalculatesCorrectly() + { + var bar = new TBar(0, 100, 110, 90, 105, 1000); + Assert.Equal(100.0, bar.OHL3); + } + + [Fact] + public void HLC3_CalculatesCorrectly() + { + var bar = new TBar(0, 100, 110, 90, 100, 1000); + Assert.Equal(100.0, bar.HLC3); + } + + [Fact] + public void OHLC4_CalculatesCorrectly() + { + var bar = new TBar(0, 100, 110, 90, 100, 1000); + Assert.Equal(100.0, bar.OHLC4); + } + + [Fact] + public void HLCC4_CalculatesCorrectly() + { + var bar = new TBar(0, 100, 110, 90, 100, 1000); + Assert.Equal(100.0, bar.HLCC4); + } + + [Fact] + public void ImplicitConversion_ToDouble_ReturnsClosePrice() + { + var bar = new TBar(0, 100, 110, 90, 105, 1000); + double closePrice = bar; + Assert.Equal(105.0, closePrice); + } + + [Fact] + public void ImplicitConversion_ToTValue_ReturnsClosePriceWithTime() + { + long time = DateTime.UtcNow.Ticks; + var bar = new TBar(time, 100, 110, 90, 105, 1000); + + TValue tv = bar; + + Assert.Equal(time, tv.Time); + Assert.Equal(105.0, tv.Value); + } + + [Fact] + public void ImplicitConversion_ToDateTime_ReturnsCorrectDateTime() + { + var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + var bar = new TBar(dateTime, 100, 110, 90, 105, 1000); + + DateTime result = bar; + + Assert.Equal(dateTime, result); + Assert.Equal(DateTimeKind.Utc, result.Kind); + } + + [Fact] + public void ToString_ReturnsFormattedString() + { + var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + var bar = new TBar(dateTime, 100.5, 110.25, 90.75, 105.0, 1000.0); + + string result = bar.ToString(); + + Assert.Contains("2024-06-15", result); + Assert.Contains("10:30:00", result); + Assert.Contains("O=100.50", result); + Assert.Contains("H=110.25", result); + Assert.Contains("L=90.75", result); + Assert.Contains("C=105.00", result); + Assert.Contains("V=1000.00", result); + } + + [Fact] + public void Equals_TBar_SameBars_ReturnsTrue() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 110, 90, 105, 1000); + + Assert.True(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_TBar_DifferentTime_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12346, 100, 110, 90, 105, 1000); + + Assert.False(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_TBar_DifferentOpen_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 101, 110, 90, 105, 1000); + + Assert.False(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_TBar_DifferentHigh_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 111, 90, 105, 1000); + + Assert.False(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_TBar_DifferentLow_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 110, 91, 105, 1000); + + Assert.False(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_TBar_DifferentClose_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 110, 90, 106, 1000); + + Assert.False(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_TBar_DifferentVolume_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 110, 90, 105, 1001); + + Assert.False(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_Object_SameTBar_ReturnsTrue() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + object bar2 = new TBar(12345, 100, 110, 90, 105, 1000); + + Assert.True(bar1.Equals(bar2)); + } + + [Fact] + public void Equals_Object_DifferentType_ReturnsFalse() + { + var bar = new TBar(12345, 100, 110, 90, 105, 1000); + object other = "not a TBar"; + + Assert.False(bar.Equals(other)); + } + + [Fact] + public void Equals_Object_Null_ReturnsFalse() + { + var bar = new TBar(12345, 100, 110, 90, 105, 1000); + + Assert.False(bar.Equals(null)); + } + + [Fact] + public void GetHashCode_SameBars_ReturnsSameHashCode() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 110, 90, 105, 1000); + + Assert.Equal(bar1.GetHashCode(), bar2.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentBars_ReturnsDifferentHashCode() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12346, 100, 110, 90, 105, 1000); + + Assert.NotEqual(bar1.GetHashCode(), bar2.GetHashCode()); + } + + [Fact] + public void EqualityOperator_SameBars_ReturnsTrue() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 110, 90, 105, 1000); + + Assert.True(bar1 == bar2); + } + + [Fact] + public void EqualityOperator_DifferentBars_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12346, 100, 110, 90, 105, 1000); + + Assert.False(bar1 == bar2); + } + + [Fact] + public void InequalityOperator_SameBars_ReturnsFalse() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12345, 100, 110, 90, 105, 1000); + + Assert.False(bar1 != bar2); + } + + [Fact] + public void InequalityOperator_DifferentBars_ReturnsTrue() + { + var bar1 = new TBar(12345, 100, 110, 90, 105, 1000); + var bar2 = new TBar(12346, 100, 110, 90, 105, 1000); + + Assert.True(bar1 != bar2); + } + } +} diff --git a/lib/core/tbarseries/TBarSeries.Tests.cs b/lib/core/tbarseries/TBarSeries.Tests.cs index d538ac9c..e505ce3a 100644 --- a/lib/core/tbarseries/TBarSeries.Tests.cs +++ b/lib/core/tbarseries/TBarSeries.Tests.cs @@ -1,58 +1,418 @@ -using System; -using Xunit; -using QuanTAlib; - -namespace QuanTAlib.Tests -{ - public class TBarSeriesTests - { - [Fact] - public void Add_NewBar_IncreasesCount() - { - var series = new TBarSeries(); - var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000); - - series.Add(bar, isNew: true); - - Assert.Single(series); - Assert.Equal(105.0, series.Last.Close); - } - - [Fact] - public void Add_UpdateBar_DoesNotIncreaseCount() - { - var series = new TBarSeries(); - long time = DateTime.UtcNow.Ticks; - var bar1 = new TBar(time, 100, 110, 90, 105, 1000); - var bar2 = new TBar(time, 100, 112, 90, 108, 1200); - - series.Add(bar1, isNew: true); - series.Add(bar2, isNew: false); - - Assert.Single(series); - Assert.Equal(108.0, series.Last.Close); - Assert.Equal(112.0, series.Last.High); - } - - [Fact] - public void SubSeries_AreUpdated() - { - var series = new TBarSeries(); - var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000); - - series.Add(bar, isNew: true); - - Assert.Single(series.Open); - Assert.Single(series.High); - Assert.Single(series.Low); - Assert.Single(series.Close); - Assert.Single(series.Volume); - - Assert.Equal(100.0, series.Open.Last.Value); - Assert.Equal(110.0, series.High.Last.Value); - Assert.Equal(90.0, series.Low.Last.Value); - Assert.Equal(105.0, series.Close.Last.Value); - Assert.Equal(1000.0, series.Volume.Last.Value); - } - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests +{ + public class TBarSeriesTests + { + [Fact] + public void Constructor_Default_CreatesEmptySeries() + { + var series = new TBarSeries(); + + Assert.Empty(series); + } + + [Fact] + public void Constructor_WithCapacity_CreatesEmptySeries() + { + var series = new TBarSeries(100); + + Assert.Empty(series); + } + + [Fact] + public void Name_DefaultValue_IsBar() + { + var series = new TBarSeries(); + + Assert.Equal("Bar", series.Name); + } + + [Fact] + public void Name_CanBeSet() + { + var series = new TBarSeries { Name = "TestBars" }; + + Assert.Equal("TestBars", series.Name); + } + + [Fact] + public void Add_NewBar_IncreasesCount() + { + var series = new TBarSeries(); + var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000); + + series.Add(bar, isNew: true); + + Assert.Single(series); + Assert.Equal(105.0, series.Last.Close); + } + + [Fact] + public void Add_UpdateBar_DoesNotIncreaseCount() + { + var series = new TBarSeries(); + long time = DateTime.UtcNow.Ticks; + var bar1 = new TBar(time, 100, 110, 90, 105, 1000); + var bar2 = new TBar(time, 100, 112, 90, 108, 1200); + + series.Add(bar1, isNew: true); + series.Add(bar2, isNew: false); + + Assert.Single(series); + Assert.Equal(108.0, series.Last.Close); + Assert.Equal(112.0, series.Last.High); + } + + [Fact] + public void Add_UpdateOnEmptySeries_AddsNewBar() + { + var series = new TBarSeries(); + var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000); + + series.Add(bar, isNew: false); + + Assert.Single(series); + } + + [Fact] + public void Add_WithLongTime_AddsBar() + { + var series = new TBarSeries(); + long time = DateTime.UtcNow.Ticks; + + series.Add(time, 100, 110, 90, 105, 1000, isNew: true); + + Assert.Single(series); + Assert.Equal(time, series.Last.Time); + } + + [Fact] + public void Add_WithDateTime_AddsBar() + { + var series = new TBarSeries(); + var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + + series.Add(dt, 100, 110, 90, 105, 1000, isNew: true); + + Assert.Single(series); + Assert.Equal(dt.Ticks, series.Last.Time); + } + + [Fact] + public void Add_WithEnumerables_AddsMultipleBars() + { + var series = new TBarSeries(); + var times = new long[] { 100, 200, 300 }; + var opens = new double[] { 10, 20, 30 }; + var highs = new double[] { 15, 25, 35 }; + var lows = new double[] { 5, 15, 25 }; + var closes = new double[] { 12, 22, 32 }; + var volumes = new double[] { 100, 200, 300 }; + + series.Add(times, opens, highs, lows, closes, volumes); + + Assert.Equal(3, series.Count); + Assert.Equal(10, series[0].Open); + Assert.Equal(32, series[2].Close); + } + + [Fact] + public void SubSeries_AreUpdated() + { + var series = new TBarSeries(); + var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000); + + series.Add(bar, isNew: true); + + Assert.Single(series.Open); + Assert.Single(series.High); + Assert.Single(series.Low); + Assert.Single(series.Close); + Assert.Single(series.Volume); + + Assert.Equal(100.0, series.Open.Last.Value); + Assert.Equal(110.0, series.High.Last.Value); + Assert.Equal(90.0, series.Low.Last.Value); + Assert.Equal(105.0, series.Close.Last.Value); + Assert.Equal(1000.0, series.Volume.Last.Value); + } + + [Fact] + public void SubSeries_Aliases_Work() + { + var series = new TBarSeries(); + var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000); + series.Add(bar, isNew: true); + + Assert.Same(series.Open, series.O); + Assert.Same(series.High, series.H); + Assert.Same(series.Low, series.L); + Assert.Same(series.Close, series.C); + Assert.Same(series.Volume, series.V); + } + + [Fact] + public void SubSeries_HaveCorrectNames() + { + var series = new TBarSeries(); + + Assert.Equal("Open", series.Open.Name); + Assert.Equal("High", series.High.Name); + Assert.Equal("Low", series.Low.Name); + Assert.Equal("Close", series.Close.Name); + Assert.Equal("Volume", series.Volume.Name); + } + + [Fact] + public void Last_EmptySeries_ReturnsDefault() + { + var series = new TBarSeries(); + + var last = series.Last; + + Assert.Equal(0, last.Time); + Assert.Equal(0.0, last.Open); + Assert.Equal(0.0, last.Close); + } + + [Fact] + public void Last_NonEmptySeries_ReturnsLastBar() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + var last = series.Last; + + Assert.Equal(200, last.Time); + Assert.Equal(22.0, last.Close); + } + + [Fact] + public void LastTime_EmptySeries_ReturnsZero() + { + var series = new TBarSeries(); + Assert.Equal(0, series.LastTime); + } + + [Fact] + public void LastTime_NonEmptySeries_ReturnsLastTime() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + Assert.Equal(200, series.LastTime); + } + + [Fact] + public void LastOpen_EmptySeries_ReturnsNaN() + { + var series = new TBarSeries(); + Assert.True(double.IsNaN(series.LastOpen)); + } + + [Fact] + public void LastOpen_NonEmptySeries_ReturnsLastOpen() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + Assert.Equal(20.0, series.LastOpen); + } + + [Fact] + public void LastHigh_EmptySeries_ReturnsNaN() + { + var series = new TBarSeries(); + Assert.True(double.IsNaN(series.LastHigh)); + } + + [Fact] + public void LastHigh_NonEmptySeries_ReturnsLastHigh() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + Assert.Equal(25.0, series.LastHigh); + } + + [Fact] + public void LastLow_EmptySeries_ReturnsNaN() + { + var series = new TBarSeries(); + Assert.True(double.IsNaN(series.LastLow)); + } + + [Fact] + public void LastLow_NonEmptySeries_ReturnsLastLow() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + Assert.Equal(15.0, series.LastLow); + } + + [Fact] + public void LastClose_EmptySeries_ReturnsNaN() + { + var series = new TBarSeries(); + Assert.True(double.IsNaN(series.LastClose)); + } + + [Fact] + public void LastClose_NonEmptySeries_ReturnsLastClose() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + Assert.Equal(22.0, series.LastClose); + } + + [Fact] + public void LastVolume_EmptySeries_ReturnsNaN() + { + var series = new TBarSeries(); + Assert.True(double.IsNaN(series.LastVolume)); + } + + [Fact] + public void LastVolume_NonEmptySeries_ReturnsLastVolume() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + Assert.Equal(200.0, series.LastVolume); + } + + [Fact] + public void Indexer_ReturnsCorrectBar() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + series.Add(300, 30, 35, 25, 32, 300); + + Assert.Equal(100, series[0].Time); + Assert.Equal(10.0, series[0].Open); + Assert.Equal(200, series[1].Time); + Assert.Equal(22.0, series[1].Close); + Assert.Equal(300, series[2].Time); + Assert.Equal(32.0, series[2].Close); + } + + [Fact] + public void Count_ReturnsCorrectValue() + { + var series = new TBarSeries(); + + Assert.Empty(series); + + series.Add(100, 10, 15, 5, 12, 100); + Assert.Single(series); + + series.Add(200, 20, 25, 15, 22, 200); + Assert.Equal(2, series.Count); + } + + [Fact] + public void GetEnumerator_IteratesAllBars() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + series.Add(300, 30, 35, 25, 32, 300); + + var list = series.ToList(); + + Assert.Equal(3, list.Count); + Assert.Equal(10.0, list[0].Open); + Assert.Equal(22.0, list[1].Close); + Assert.Equal(32.0, list[2].Close); + } + + [Fact] + public void GetEnumerator_NonGeneric_Works() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + IEnumerable enumerable = series; + var list = new List(); + foreach (var item in enumerable) + { + list.Add(item); + } + + Assert.Equal(2, list.Count); + } + + [Fact] + public void Pub_Event_IsRaisedOnAdd() + { + var series = new TBarSeries(); + TBar? received = null; + series.Pub += bar => received = bar; + + var barToAdd = new TBar(100, 10, 15, 5, 12, 100); + series.Add(barToAdd, isNew: true); + + Assert.NotNull(received); + Assert.Equal(100, received.Value.Time); + Assert.Equal(12.0, received.Value.Close); + } + + [Fact] + public void Pub_Event_IsRaisedOnUpdate() + { + var series = new TBarSeries(); + TBar? received = null; + series.Add(100, 10, 15, 5, 12, 100); + series.Pub += bar => received = bar; + + series.Add(100, 10, 18, 5, 15, 150, isNew: false); + + Assert.NotNull(received); + Assert.Equal(15.0, received.Value.Close); + Assert.Equal(18.0, received.Value.High); + } + + [Fact] + public void SubSeries_ShareSameTimeArray() + { + var series = new TBarSeries(); + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + + Assert.Equal(series.Open.Times[0], series.Close.Times[0]); + Assert.Equal(series.High.Times[1], series.Volume.Times[1]); + } + + [Fact] + public void Add_MultipleBars_MaintainsOrder() + { + var series = new TBarSeries(); + + series.Add(100, 10, 15, 5, 12, 100); + series.Add(200, 20, 25, 15, 22, 200); + series.Add(300, 30, 35, 25, 32, 300); + + Assert.Equal(3, series.Count); + Assert.Equal(100, series[0].Time); + Assert.Equal(200, series[1].Time); + Assert.Equal(300, series[2].Time); + } + } +} diff --git a/lib/core/tseries/TSeries.Tests.cs b/lib/core/tseries/TSeries.Tests.cs index 4482d9ce..b29fe2b4 100644 --- a/lib/core/tseries/TSeries.Tests.cs +++ b/lib/core/tseries/TSeries.Tests.cs @@ -1,49 +1,345 @@ -using System; -using Xunit; -using QuanTAlib; - -namespace QuanTAlib.Tests -{ - public class TSeriesTests - { - [Fact] - public void Add_NewValue_IncreasesCount() - { - var series = new TSeries(); - long time = DateTime.UtcNow.Ticks; - - series.Add(time, 10.0, isNew: true); - - Assert.Single(series); - Assert.Equal(10.0, series.Last.Value); - } - - [Fact] - public void Add_UpdateValue_DoesNotIncreaseCount() - { - var series = new TSeries(); - long time = DateTime.UtcNow.Ticks; - - series.Add(time, 10.0, isNew: true); - series.Add(time, 11.0, isNew: false); - - Assert.Single(series); - Assert.Equal(11.0, series.Last.Value); - } - - [Fact] - public void Add_MultipleValues_MaintainsOrder() - { - var series = new TSeries(); - long t0 = DateTime.UtcNow.Ticks; - long t1 = t0 + TimeSpan.TicksPerMinute; - - series.Add(t0, 10.0, isNew: true); - series.Add(t1, 20.0, isNew: true); - - Assert.Equal(2, series.Count); - Assert.Equal(10.0, series[0].Value); - Assert.Equal(20.0, series[1].Value); - } - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests +{ + public class TSeriesTests + { + [Fact] + public void Constructor_Default_CreatesEmptySeries() + { + var series = new TSeries(); + + Assert.Empty(series); + } + + [Fact] + public void Constructor_WithCapacity_CreatesEmptySeries() + { + var series = new TSeries(100); + + Assert.Empty(series); + } + + [Fact] + public void Constructor_WithLists_WrapsExistingData() + { + var times = new List { 100, 200, 300 }; + var values = new List { 1.0, 2.0, 3.0 }; + + var series = new TSeries(times, values); + + Assert.Equal(3, series.Count); + Assert.Equal(1.0, series[0].Value); + Assert.Equal(2.0, series[1].Value); + Assert.Equal(3.0, series[2].Value); + } + + [Fact] + public void Name_DefaultValue_IsData() + { + var series = new TSeries(); + + Assert.Equal("Data", series.Name); + } + + [Fact] + public void Name_CanBeSet() + { + var series = new TSeries { Name = "TestSeries" }; + + Assert.Equal("TestSeries", series.Name); + } + + [Fact] + public void Add_NewValue_IncreasesCount() + { + var series = new TSeries(); + long time = DateTime.UtcNow.Ticks; + + series.Add(time, 10.0, isNew: true); + + Assert.Single(series); + Assert.Equal(10.0, series.Last.Value); + } + + [Fact] + public void Add_UpdateValue_DoesNotIncreaseCount() + { + var series = new TSeries(); + long time = DateTime.UtcNow.Ticks; + + series.Add(time, 10.0, isNew: true); + series.Add(time, 11.0, isNew: false); + + Assert.Single(series); + Assert.Equal(11.0, series.Last.Value); + } + + [Fact] + public void Add_MultipleValues_MaintainsOrder() + { + var series = new TSeries(); + long t0 = DateTime.UtcNow.Ticks; + long t1 = t0 + TimeSpan.TicksPerMinute; + + series.Add(t0, 10.0, isNew: true); + series.Add(t1, 20.0, isNew: true); + + Assert.Equal(2, series.Count); + Assert.Equal(10.0, series[0].Value); + Assert.Equal(20.0, series[1].Value); + } + + [Fact] + public void Add_TValue_AddsNewItem() + { + var series = new TSeries(); + var tv = new TValue(DateTime.UtcNow.Ticks, 42.0); + + series.Add(tv); + + Assert.Single(series); + Assert.Equal(42.0, series.Last.Value); + } + + [Fact] + public void Add_TValueWithIsNew_AddsOrUpdates() + { + var series = new TSeries(); + var tv1 = new TValue(DateTime.UtcNow.Ticks, 42.0); + var tv2 = new TValue(DateTime.UtcNow.Ticks, 43.0); + + series.Add(tv1, isNew: true); + series.Add(tv2, isNew: false); + + Assert.Single(series); + Assert.Equal(43.0, series.Last.Value); + } + + [Fact] + public void Add_WithDateTime_AddsValue() + { + var series = new TSeries(); + var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + + series.Add(dt, 100.0, isNew: true); + + Assert.Single(series); + Assert.Equal(dt.Ticks, series.Last.Time); + Assert.Equal(100.0, series.Last.Value); + } + + [Fact] + public void Add_EnumerableDoubles_AddsAllValues() + { + var series = new TSeries(); + var values = new[] { 1.0, 2.0, 3.0, 4.0, 5.0 }; + + series.Add(values); + + Assert.Equal(5, series.Count); + Assert.Equal(1.0, series[0].Value); + Assert.Equal(5.0, series[4].Value); + } + + [Fact] + public void Add_UpdateOnEmptySeries_AddsNewItem() + { + var series = new TSeries(); + var tv = new TValue(DateTime.UtcNow.Ticks, 42.0); + + series.Add(tv, isNew: false); + + Assert.Single(series); + Assert.Equal(42.0, series.Last.Value); + } + + [Fact] + public void Last_EmptySeries_ReturnsDefault() + { + var series = new TSeries(); + + var last = series.Last; + + Assert.Equal(0, last.Time); + Assert.Equal(0.0, last.Value); + } + + [Fact] + public void Last_NonEmptySeries_ReturnsLastValue() + { + var series = new TSeries(); + series.Add(100, 10.0); + series.Add(200, 20.0); + + var last = series.Last; + + Assert.Equal(200, last.Time); + Assert.Equal(20.0, last.Value); + } + + [Fact] + public void LastValue_EmptySeries_ReturnsNaN() + { + var series = new TSeries(); + + Assert.True(double.IsNaN(series.LastValue)); + } + + [Fact] + public void LastValue_NonEmptySeries_ReturnsLastValue() + { + var series = new TSeries(); + series.Add(100, 10.0); + series.Add(200, 20.0); + + Assert.Equal(20.0, series.LastValue); + } + + [Fact] + public void LastTime_EmptySeries_ReturnsZero() + { + var series = new TSeries(); + + Assert.Equal(0, series.LastTime); + } + + [Fact] + public void LastTime_NonEmptySeries_ReturnsLastTime() + { + var series = new TSeries(); + series.Add(100, 10.0); + series.Add(200, 20.0); + + Assert.Equal(200, series.LastTime); + } + + [Fact] + public void Values_ReturnsReadOnlySpanOfValues() + { + var series = new TSeries(); + series.Add(100, 1.0); + series.Add(200, 2.0); + series.Add(300, 3.0); + + ReadOnlySpan values = series.Values; + + Assert.Equal(3, values.Length); + Assert.Equal(1.0, values[0]); + Assert.Equal(2.0, values[1]); + Assert.Equal(3.0, values[2]); + } + + [Fact] + public void Times_ReturnsReadOnlySpanOfTimes() + { + var series = new TSeries(); + series.Add(100, 1.0); + series.Add(200, 2.0); + series.Add(300, 3.0); + + ReadOnlySpan times = series.Times; + + Assert.Equal(3, times.Length); + Assert.Equal(100, times[0]); + Assert.Equal(200, times[1]); + Assert.Equal(300, times[2]); + } + + [Fact] + public void Indexer_ReturnsCorrectTValue() + { + var series = new TSeries(); + series.Add(100, 10.0); + series.Add(200, 20.0); + series.Add(300, 30.0); + + Assert.Equal(100, series[0].Time); + Assert.Equal(10.0, series[0].Value); + Assert.Equal(200, series[1].Time); + Assert.Equal(20.0, series[1].Value); + Assert.Equal(300, series[2].Time); + Assert.Equal(30.0, series[2].Value); + } + + [Fact] + public void GetEnumerator_IteratesAllValues() + { + var series = new TSeries(); + series.Add(100, 1.0); + series.Add(200, 2.0); + series.Add(300, 3.0); + + var list = series.ToList(); + + Assert.Equal(3, list.Count); + Assert.Equal(1.0, list[0].Value); + Assert.Equal(2.0, list[1].Value); + Assert.Equal(3.0, list[2].Value); + } + + [Fact] + public void GetEnumerator_NonGeneric_Works() + { + var series = new TSeries(); + series.Add(100, 1.0); + series.Add(200, 2.0); + + IEnumerable enumerable = series; + var list = new List(); + foreach (var item in enumerable) + { + list.Add(item); + } + + Assert.Equal(2, list.Count); + } + + [Fact] + public void Pub_Event_IsRaisedOnAdd() + { + var series = new TSeries(); + TValue? received = null; + series.Pub += tv => received = tv; + + series.Add(100, 42.0); + + Assert.NotNull(received); + Assert.Equal(100, received.Value.Time); + Assert.Equal(42.0, received.Value.Value); + } + + [Fact] + public void Pub_Event_IsRaisedOnUpdate() + { + var series = new TSeries(); + TValue? received = null; + series.Add(100, 42.0); + series.Pub += tv => received = tv; + + series.Add(100, 43.0, isNew: false); + + Assert.NotNull(received); + Assert.Equal(43.0, received.Value.Value); + } + + [Fact] + public void Count_ReturnsCorrectValue() + { + var series = new TSeries(); + + Assert.Empty(series); + + series.Add(100, 1.0); + Assert.Single(series); + + series.Add(200, 2.0); + Assert.Equal(2, series.Count); + } + } +} diff --git a/lib/core/tvalue/TValue.Tests.cs b/lib/core/tvalue/TValue.Tests.cs index b51e9f0b..2eab88ab 100644 --- a/lib/core/tvalue/TValue.Tests.cs +++ b/lib/core/tvalue/TValue.Tests.cs @@ -1,51 +1,185 @@ -using System; -using Xunit; -using QuanTAlib; - -namespace QuanTAlib.Tests -{ - public class TValueTests - { - [Fact] - public void Constructor_SetsPropertiesCorrectly() - { - long time = DateTime.UtcNow.Ticks; - double value = 123.45; - - var tValue = new TValue(time, value); - - Assert.Equal(time, tValue.Time); - Assert.Equal(value, tValue.Value); - } - - [Fact] - public void AsDateTime_ReturnsCorrectDateTime() - { - DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc); - long ticks = dt.Ticks; - var tValue = new TValue(ticks, 100.0); - - Assert.Equal(dt, tValue.AsDateTime); - } - - [Fact] - public void ToString_FormatsCorrectly() - { - DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc); - var tValue = new TValue(dt.Ticks, 123.456); - - string result = tValue.ToString(); - - Assert.Contains(dt.ToString("yyyy-MM-dd HH:mm:ss"), result); - Assert.Contains("123.46", result); // Default formatting usually 2 decimals or similar - } - - [Fact] - public void ImplicitConversion_ToDouble() - { - var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0); - double val = tValue; - Assert.Equal(42.0, val); - } - } -} +using System; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests +{ + public class TValueTests + { + [Fact] + public void Constructor_WithLongTime_SetsPropertiesCorrectly() + { + long time = DateTime.UtcNow.Ticks; + double value = 123.45; + + var tValue = new TValue(time, value); + + Assert.Equal(time, tValue.Time); + Assert.Equal(value, tValue.Value); + } + + [Fact] + public void Constructor_WithDateTime_SetsPropertiesCorrectly() + { + var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + double value = 123.45; + + var tValue = new TValue(dateTime, value); + + Assert.Equal(dateTime.Ticks, tValue.Time); + Assert.Equal(value, tValue.Value); + } + + [Fact] + public void AsDateTime_ReturnsCorrectDateTime() + { + var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc); + var tValue = new TValue(dt.Ticks, 100.0); + + Assert.Equal(dt, tValue.AsDateTime); + Assert.Equal(DateTimeKind.Utc, tValue.AsDateTime.Kind); + } + + [Fact] + public void ToString_FormatsCorrectly() + { + var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc); + var tValue = new TValue(dt.Ticks, 123.456); + + string result = tValue.ToString(); + + Assert.Contains("2023-01-01", result); + Assert.Contains("12:00:00", result); + Assert.Contains("123.46", result); + } + + [Fact] + public void ImplicitConversion_ToDouble_ReturnsValue() + { + var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0); + + double val = tValue; + + Assert.Equal(42.0, val); + } + + [Fact] + public void ImplicitConversion_ToDateTime_ReturnsCorrectDateTime() + { + var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc); + var tValue = new TValue(dateTime.Ticks, 100.0); + + DateTime result = tValue; + + Assert.Equal(dateTime, result); + Assert.Equal(DateTimeKind.Utc, result.Kind); + } + + [Fact] + public void Equals_TValue_SameValues_ReturnsTrue() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12345, 100.0); + + Assert.True(tv1.Equals(tv2)); + } + + [Fact] + public void Equals_TValue_DifferentTime_ReturnsFalse() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12346, 100.0); + + Assert.False(tv1.Equals(tv2)); + } + + [Fact] + public void Equals_TValue_DifferentValue_ReturnsFalse() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12345, 101.0); + + Assert.False(tv1.Equals(tv2)); + } + + [Fact] + public void Equals_Object_SameTValue_ReturnsTrue() + { + var tv1 = new TValue(12345, 100.0); + object tv2 = new TValue(12345, 100.0); + + Assert.True(tv1.Equals(tv2)); + } + + [Fact] + public void Equals_Object_DifferentType_ReturnsFalse() + { + var tv = new TValue(12345, 100.0); + object other = "not a TValue"; + + Assert.False(tv.Equals(other)); + } + + [Fact] + public void Equals_Object_Null_ReturnsFalse() + { + var tv = new TValue(12345, 100.0); + + Assert.False(tv.Equals(null)); + } + + [Fact] + public void GetHashCode_SameValues_ReturnsSameHashCode() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12345, 100.0); + + Assert.Equal(tv1.GetHashCode(), tv2.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentValues_ReturnsDifferentHashCode() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12346, 100.0); + + Assert.NotEqual(tv1.GetHashCode(), tv2.GetHashCode()); + } + + [Fact] + public void EqualityOperator_SameValues_ReturnsTrue() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12345, 100.0); + + Assert.True(tv1 == tv2); + } + + [Fact] + public void EqualityOperator_DifferentValues_ReturnsFalse() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12346, 100.0); + + Assert.False(tv1 == tv2); + } + + [Fact] + public void InequalityOperator_SameValues_ReturnsFalse() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12345, 100.0); + + Assert.False(tv1 != tv2); + } + + [Fact] + public void InequalityOperator_DifferentValues_ReturnsTrue() + { + var tv1 = new TValue(12345, 100.0); + var tv2 = new TValue(12346, 100.0); + + Assert.True(tv1 != tv2); + } + } +} diff --git a/lib/quantalib.csproj b/lib/quantalib.csproj index e179cc73..257a4657 100644 --- a/lib/quantalib.csproj +++ b/lib/quantalib.csproj @@ -1,6 +1,6 @@  - net8.0;net9.0;net10.0 + net10.0 QuanTAlib Library of TA Calculations, Charts and Strategies for Quantower Quantitative Technical Analysis Library in C# for Quantower @@ -32,6 +32,10 @@ 6afc11a7-4355-4f5e-9fdf-22431e5b03cb + + + +