diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index c86d49cb..454f2b85 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -65,20 +65,22 @@ jobs: run: | dotnet test --no-build --configuration Debug \ --collect:"XPlat Code Coverage" \ + --results-directory:./TestResults \ -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover - name: Collect Coverage Files run: | mkdir -p coverage count=1 - while read -r file; do - dest="coverage/coverage_${count}.opencover.xml" - cp "$file" "$dest" - # Sanitize paths for Qodana (convert absolute to relative) - # This strips the current working directory from the paths in the XML - sed -i "s|$(pwd)/||g" "$dest" + find TestResults -name "coverage.opencover.xml" | while read file; do + cp "$file" "coverage/coverage_${count}.opencover.xml" count=$((count+1)) - done < <(find . -name "coverage.opencover.xml" -type f) + done + + - name: Debug Coverage Paths + run: | + echo "=== Coverage file paths ===" + grep -h "fullPath=" coverage/*.xml | head -20 || true - name: Upload Coverage Artifacts uses: actions/upload-artifact@v4 diff --git a/.vscode/settings.json b/.vscode/settings.json index 37976004..cefe716d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -168,8 +168,8 @@ "connectionId": "mihakralj-quantalib", "projectKey": "mihakralj_QuanTAlib" }, - "qodana.projectId": "KbxmN", - "coderabbit.agentType": "Cline" + "coderabbit.agentType": "Cline", + "qodana.projectId": "KbxmN" // Note: Native mode is configured in qodana.yaml with withinDocker: false // ??????????????????????????????????????????????????????????????????? diff --git a/lib/statistics/linreg/LinReg.Quantower.Tests.cs b/lib/statistics/linreg/LinReg.Quantower.Tests.cs index d30bb084..0e5a1dd1 100644 --- a/lib/statistics/linreg/LinReg.Quantower.Tests.cs +++ b/lib/statistics/linreg/LinReg.Quantower.Tests.cs @@ -67,3 +67,192 @@ public class LinRegIndicatorTests Assert.True(double.IsFinite(linreg)); } } + +public class LinRegSlopeIndicatorTests +{ + [Fact] + public void LinRegSlopeIndicator_Constructor_SetsDefaults() + { + var indicator = new LinRegSlopeIndicator(); + + Assert.Equal(14, indicator.Period); + Assert.True(indicator.ShowColdValues); + Assert.Equal("LinReg Slope", indicator.Name); + Assert.True(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + Assert.Equal(SourceType.Close, indicator.Source); + } + + [Fact] + public void LinRegSlopeIndicator_MinHistoryDepths_EqualsZero() + { + var indicator = new LinRegSlopeIndicator { Period = 20 }; + + Assert.Equal(0, LinRegSlopeIndicator.MinHistoryDepths); + IWatchlistIndicator watchlistIndicator = indicator; + Assert.Equal(0, watchlistIndicator.MinHistoryDepths); + } + + [Fact] + public void LinRegSlopeIndicator_Initialize_CreatesInternalLinReg() + { + var indicator = new LinRegSlopeIndicator { Period = 10 }; + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + Assert.Equal("Slope", indicator.LinesSeries[0].Name); + } + + [Fact] + public void LinRegSlopeIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new LinRegSlopeIndicator { Period = 5 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + // Need enough bars for Period + for (int i = 0; i < 20; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); + + // Process update for each bar to simulate history loading + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + } + + // Line series should have a value + double slope = indicator.LinesSeries[0].GetValue(0); + + Assert.True(double.IsFinite(slope)); + } +} + +public class LinRegInterceptIndicatorTests +{ + [Fact] + public void LinRegInterceptIndicator_Constructor_SetsDefaults() + { + var indicator = new LinRegInterceptIndicator(); + + Assert.Equal(14, indicator.Period); + Assert.True(indicator.ShowColdValues); + Assert.Equal("LinReg Intercept", indicator.Name); + Assert.True(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + Assert.Equal(SourceType.Close, indicator.Source); + } + + [Fact] + public void LinRegInterceptIndicator_MinHistoryDepths_EqualsZero() + { + var indicator = new LinRegInterceptIndicator { Period = 20 }; + + Assert.Equal(0, LinRegInterceptIndicator.MinHistoryDepths); + IWatchlistIndicator watchlistIndicator = indicator; + Assert.Equal(0, watchlistIndicator.MinHistoryDepths); + } + + [Fact] + public void LinRegInterceptIndicator_Initialize_CreatesInternalLinReg() + { + var indicator = new LinRegInterceptIndicator { Period = 10 }; + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + Assert.Equal("Intercept", indicator.LinesSeries[0].Name); + } + + [Fact] + public void LinRegInterceptIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new LinRegInterceptIndicator { Period = 5 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + // Need enough bars for Period + for (int i = 0; i < 20; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); + + // Process update for each bar to simulate history loading + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + } + + // Line series should have a value + double intercept = indicator.LinesSeries[0].GetValue(0); + + Assert.True(double.IsFinite(intercept)); + } +} + +public class LinRegRSquaredIndicatorTests +{ + [Fact] + public void LinRegRSquaredIndicator_Constructor_SetsDefaults() + { + var indicator = new LinRegRSquaredIndicator(); + + Assert.Equal(14, indicator.Period); + Assert.True(indicator.ShowColdValues); + Assert.Equal("LinReg R-Squared", indicator.Name); + Assert.True(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + Assert.Equal(SourceType.Close, indicator.Source); + } + + [Fact] + public void LinRegRSquaredIndicator_MinHistoryDepths_EqualsZero() + { + var indicator = new LinRegRSquaredIndicator { Period = 20 }; + + Assert.Equal(0, LinRegRSquaredIndicator.MinHistoryDepths); + IWatchlistIndicator watchlistIndicator = indicator; + Assert.Equal(0, watchlistIndicator.MinHistoryDepths); + } + + [Fact] + public void LinRegRSquaredIndicator_Initialize_CreatesInternalLinReg() + { + var indicator = new LinRegRSquaredIndicator { Period = 10 }; + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + Assert.Equal("RSquared", indicator.LinesSeries[0].Name); + } + + [Fact] + public void LinRegRSquaredIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new LinRegRSquaredIndicator { Period = 5 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + // Need enough bars for Period + for (int i = 0; i < 20; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); + + // Process update for each bar to simulate history loading + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + } + + // Line series should have a value + double r2 = indicator.LinesSeries[0].GetValue(0); + + Assert.True(double.IsFinite(r2)); + } +} diff --git a/lib/statistics/skew/Skew.Tests.cs b/lib/statistics/skew/Skew.Tests.cs index 23d45913..235f9f36 100644 --- a/lib/statistics/skew/Skew.Tests.cs +++ b/lib/statistics/skew/Skew.Tests.cs @@ -129,4 +129,84 @@ public class SkewTests Assert.Equal(streamingResults[i], batchResult.Values[i], precision: 10); } } + + [Fact] + public void Update_CalculatesCorrectly_Population() + { + // Test data: 1, 2, 3 + // Mean = 2 + // Variance (Pop) = ((1-2)^2 + (2-2)^2 + (3-2)^2) / 3 = 2/3 + // StdDev (Pop) = sqrt(2/3) + // M3 (Pop) = ((1-2)^3 + (2-2)^3 + (3-2)^3) / 3 = 0 + // Skew (Pop) = 0 + + var skew = new Skew(3, isPopulation: true); + skew.Update(new TValue(DateTime.UtcNow, 1)); + skew.Update(new TValue(DateTime.UtcNow, 2)); + var result = skew.Update(new TValue(DateTime.UtcNow, 3)); + + Assert.Equal(0, result.Value, precision: 10); + } + + [Fact] + public void Update_HandlesConstantValues_ZeroVariance() + { + var skew = new Skew(5); + for (int i = 0; i < 5; i++) + { + var result = skew.Update(new TValue(DateTime.UtcNow, 10)); + Assert.Equal(0, result.Value); // Skew is undefined or 0 for constant values + } + } + + [Fact] + public void Update_HandlesNaN() + { + var skew = new Skew(5); + skew.Update(new TValue(DateTime.UtcNow, 1)); + skew.Update(new TValue(DateTime.UtcNow, 2)); + skew.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should be treated as 0 or handled gracefully + + var result = skew.Last.Value; + Assert.True(double.IsNaN(result) || result == 0); + } + + [Fact] + public void Resync_DoesNotDrift() + { + // Run for > 1000 updates to trigger Resync + var skew = new Skew(10); + var random = new Random(123); + + for (int i = 0; i < 1100; i++) + { + skew.Update(new TValue(DateTime.UtcNow, random.NextDouble() * 100)); + } + + Assert.True(double.IsFinite(skew.Last.Value)); + } + + [Fact] + public void Batch_LargeDataset_Simd() + { + // Create large dataset to trigger SIMD path (>= 256) + int count = 1000; + var data = new double[count]; + for (int i = 0; i < count; i++) data[i] = (double)i; + + var series = new TSeries(new System.Collections.Generic.List(new long[count]), new System.Collections.Generic.List(data)); + + // Batch calculation + var batchResult = Skew.Calculate(series, 10); + + // Verify last value against streaming + var skew = new Skew(10); + double lastStreaming = 0; + foreach (var val in data) + { + lastStreaming = skew.Update(new TValue(DateTime.UtcNow, val)).Value; + } + + Assert.Equal(lastStreaming, batchResult.Last.Value, precision: 10); + } } diff --git a/lib/statistics/stddev/StdDev.Quantower.Tests.cs b/lib/statistics/stddev/StdDev.Quantower.Tests.cs new file mode 100644 index 00000000..ae3aa237 --- /dev/null +++ b/lib/statistics/stddev/StdDev.Quantower.Tests.cs @@ -0,0 +1,69 @@ +using Xunit; +using TradingPlatform.BusinessLayer; +using QuanTAlib; + +namespace QuanTAlib.Tests; + +public class StdDevIndicatorTests +{ + [Fact] + public void StdDevIndicator_Constructor_SetsDefaults() + { + var indicator = new StdDevIndicator(); + + Assert.Equal(20, indicator.Period); + Assert.False(indicator.IsPopulation); + Assert.True(indicator.ShowColdValues); + Assert.Equal("StdDev - Standard Deviation", indicator.Name); + Assert.True(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + Assert.Equal(SourceType.Close, indicator.Source); + } + + [Fact] + public void StdDevIndicator_MinHistoryDepths_EqualsZero() + { + var indicator = new StdDevIndicator { Period = 20 }; + + Assert.Equal(0, StdDevIndicator.MinHistoryDepths); + IWatchlistIndicator watchlistIndicator = indicator; + Assert.Equal(0, watchlistIndicator.MinHistoryDepths); + } + + [Fact] + public void StdDevIndicator_Initialize_CreatesInternalStdDev() + { + var indicator = new StdDevIndicator { Period = 10 }; + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + Assert.Equal("StdDev", indicator.LinesSeries[0].Name); + } + + [Fact] + public void StdDevIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new StdDevIndicator { Period = 5 }; + indicator.Initialize(); + + // Add historical data + var now = DateTime.UtcNow; + // Need enough bars for Period + for (int i = 0; i < 20; i++) + { + indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); + + // Process update for each bar to simulate history loading + var args = new UpdateArgs(UpdateReason.HistoricalBar); + indicator.ProcessUpdate(args); + } + + // Line series should have a value + double stdDev = indicator.LinesSeries[0].GetValue(0); + + Assert.True(double.IsFinite(stdDev)); + } +} diff --git a/lib/statistics/variance/Variance.Tests.cs b/lib/statistics/variance/Variance.Tests.cs index 913cd9b4..d2695a07 100644 --- a/lib/statistics/variance/Variance.Tests.cs +++ b/lib/statistics/variance/Variance.Tests.cs @@ -122,4 +122,70 @@ public class VarianceTests Assert.Equal(iterativeResults[i], batchResults[i], precision: 7); } } + + [Fact] + public void Update_HandlesConstantValues_ZeroVariance() + { + var variance = new Variance(5); + for (int i = 0; i < 5; i++) + { + var result = variance.Update(new TValue(DateTime.UtcNow, 10)); + if (i >= 1) // Variance defined for N >= 2 + { + Assert.Equal(0, result.Value); + } + } + } + + [Fact] + public void Update_HandlesNaN() + { + var variance = new Variance(5); + variance.Update(new TValue(DateTime.UtcNow, 1)); + variance.Update(new TValue(DateTime.UtcNow, 2)); + variance.Update(new TValue(DateTime.UtcNow, double.NaN)); + + var result = variance.Last.Value; + Assert.True(double.IsNaN(result)); + } + + [Fact] + public void Resync_DoesNotDrift() + { + // Run for > 1000 updates to trigger Resync + var variance = new Variance(10); + var random = new Random(123); + + for (int i = 0; i < 1100; i++) + { + variance.Update(new TValue(DateTime.UtcNow, random.NextDouble() * 100)); + } + + Assert.True(double.IsFinite(variance.Last.Value)); + Assert.True(variance.Last.Value >= 0); + } + + [Fact] + public void Batch_LargeDataset_Simd() + { + // Create large dataset to trigger SIMD path (>= 256) + int count = 1000; + var data = new double[count]; + for (int i = 0; i < count; i++) data[i] = (double)i; + + var series = new TSeries(new System.Collections.Generic.List(new long[count]), new System.Collections.Generic.List(data)); + + // Batch calculation + var batchResult = Variance.Calculate(series, 10); + + // Verify last value against streaming + var variance = new Variance(10); + double lastStreaming = 0; + foreach (var val in data) + { + lastStreaming = variance.Update(new TValue(DateTime.UtcNow, val)).Value; + } + + Assert.Equal(lastStreaming, batchResult.Last.Value, precision: 10); + } } diff --git a/lib/trends/mama/Mama.Tests.cs b/lib/trends/mama/Mama.Tests.cs index 1cbcb240..46b5ff85 100644 --- a/lib/trends/mama/Mama.Tests.cs +++ b/lib/trends/mama/Mama.Tests.cs @@ -178,4 +178,57 @@ public class MamaTests Assert.Equal(series1[i].Value, series2[i].Value, 1e-9); } } + + [Fact] + public void Calculate_Span_Matches_Update() + { + int count = 100; + var data = new double[count]; + var gbm = new GBM(startPrice: 100, seed: 42); + for (int i = 0; i < count; i++) data[i] = gbm.Next().Close; + + var output = new double[count]; + Mama.Calculate(data, output); + + var mama = new Mama(); + for (int i = 0; i < count; i++) + { + var res = mama.Update(new TValue(DateTime.UtcNow, data[i])); + Assert.Equal(res.Value, output[i], precision: 8); + } + } + + [Fact] + public void Calculate_Span_ThrowsOnSmallOutput() + { + var data = new double[10]; + var output = new double[5]; + Assert.Throws(() => Mama.Calculate(data, output)); + } + + [Fact] + public void Prime_PreloadsState() + { + var data = new double[60]; + var gbm = new GBM(startPrice: 100, seed: 42); + for (int i = 0; i < 60; i++) data[i] = gbm.Next().Close; + + // 1. Prime with all but last value + var mamaPrimed = new Mama(); + mamaPrimed.Prime(data.AsSpan().Slice(0, 59)); + + // 2. Update with last value + var resultPrimed = mamaPrimed.Update(new TValue(DateTime.UtcNow, data[59])); + + // 3. Run normal updates for comparison + var mamaNormal = new Mama(); + TValue resultNormal = default; + for (int i = 0; i < 60; i++) + { + resultNormal = mamaNormal.Update(new TValue(DateTime.UtcNow, data[i])); + } + + Assert.True(mamaPrimed.IsHot); + Assert.Equal(resultNormal.Value, resultPrimed.Value, precision: 9); + } } diff --git a/lib/trends/pwma/Pwma.Tests.cs b/lib/trends/pwma/Pwma.Tests.cs index fe190a78..4c4e4396 100644 --- a/lib/trends/pwma/Pwma.Tests.cs +++ b/lib/trends/pwma/Pwma.Tests.cs @@ -8,6 +8,7 @@ public class PwmaTests { Assert.Throws(() => new Pwma(0)); Assert.Throws(() => new Pwma(-1)); + Assert.Throws(() => new Pwma(null!, 10)); var pwma = new Pwma(10); Assert.NotNull(pwma); diff --git a/lib/trends/pwma/Pwma.cs b/lib/trends/pwma/Pwma.cs index a510f10d..61e83be6 100644 --- a/lib/trends/pwma/Pwma.cs +++ b/lib/trends/pwma/Pwma.cs @@ -56,6 +56,7 @@ public sealed class Pwma : AbstractBase public Pwma(ITValuePublisher source, int period) : this(period) { + if (source == null) throw new ArgumentNullException(nameof(source)); source.Pub += (item) => Update(item); } diff --git a/qodana.yaml b/qodana.yaml index 65bd0c46..f2360db9 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -109,3 +109,8 @@ linter: jetbrains/qodana-dotnet:2024.3 dotnet: solution: QuanTAlib.sln + +coverage: + pathMappings: + - from: "" + to: /data/project