From 6bd0096297a004e79752696b0a463975072569a6 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Mon, 22 Dec 2025 15:31:38 -0800 Subject: [PATCH] feat: enhance validation tests for Aroon Oscillator and ADOSC --- .config/dotnet-tools.json | 7 +-- .github/prepare-qodana.sh | 10 ++++ .gitignore | 3 ++ docs/validation.md | 2 +- lib/momentum/adx/Adx.cs | 6 +-- lib/momentum/adxr/Adxr.cs | 6 +-- lib/momentum/ao/Ao.cs | 6 +-- lib/momentum/aroon/Aroon.cs | 8 +-- .../aroonosc/AroonOsc.Validation.Tests.cs | 10 ++-- lib/momentum/aroonosc/AroonOsc.cs | 8 +-- lib/momentum/aroonosc/AroonOsc.md | 4 ++ lib/trends/sma/Sma.Tolerance.Tests.cs | 45 +++++++++++++++++ lib/trends/trima/Trima.Tolerance.Tests.cs | 50 +++++++++++++++++++ lib/volume/adosc/Adosc.Validation.Tests.cs | 11 ++-- qodana.yaml | 4 +- 15 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 .github/prepare-qodana.sh create mode 100644 lib/trends/sma/Sma.Tolerance.Tests.cs create mode 100644 lib/trends/trima/Trima.Tolerance.Tests.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c5b13197..5782c9e6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "gitversion.tool": { - "version": "5.12.0", + "version": "6.5.1", "commands": [ "dotnet-gitversion" - ] + ], + "rollForward": false } } -} +} \ No newline at end of file diff --git a/.github/prepare-qodana.sh b/.github/prepare-qodana.sh new file mode 100644 index 00000000..6e7ee44a --- /dev/null +++ b/.github/prepare-qodana.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +echo "Installing .NET 10.0 SDK..." +wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh +chmod +x dotnet-install.sh +./dotnet-install.sh --channel 10.0 --quality daily --install-dir /usr/share/dotnet + +echo ".NET 10.0 SDK installed." +dotnet --list-sdks diff --git a/.gitignore b/.gitignore index 93dd9a11..507c27eb 100644 --- a/.gitignore +++ b/.gitignore @@ -413,3 +413,6 @@ ilspy/ # Ignore insiders AI rules .github/instructions/codacy.instructions.md + +# Ignore dotnet install script +dotnet-install.sh diff --git a/docs/validation.md b/docs/validation.md index a7a628e7..2df3437e 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -14,7 +14,7 @@ | **Archer On-Balance Volume** | Aobv | - | - | - | - | | **Arnaud Legoux Moving Average** | [Alma](../lib/trends/alma/alma.md) | - | - | ✔️ | ✔️ | | **Aroon** | [Aroon](../lib/momentum/aroon/aroon.md) | ✔️ | ✔️ | ✔️ | - | -| **Aroon Oscillator** | [AroonOsc](../lib/momentum/aroonosc/AroonOsc.md) | ✔️ | ✔️ | ✔️ | ✔️ | +| **Aroon Oscillator** | [AroonOsc](../lib/momentum/aroonosc/AroonOsc.md) | ✔️ | ✔️ | ✔️ | [⚠️](../lib/momentum/aroonosc/AroonOsc.md#external-library-discrepancies) | | **ATR Bands** | Atrbands | - | - | - | - | | **Autoregressive FIR MA** | Afirma | - | - | - | - | | **Average Daily Range** | Adr | - | - | - | - | diff --git a/lib/momentum/adx/Adx.cs b/lib/momentum/adx/Adx.cs index 4b27083f..87fc68ac 100644 --- a/lib/momentum/adx/Adx.cs +++ b/lib/momentum/adx/Adx.cs @@ -277,7 +277,7 @@ public sealed class Adx : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); var len = source.Count; var v = new double[len]; @@ -347,7 +347,7 @@ public sealed class Adx : ITValuePublisher int len = high.Length; if (len < period * 2) { - destination.Fill(0); + destination.Clear(); return; } @@ -417,7 +417,7 @@ public sealed class Adx : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); var len = source.Count; var v = new double[len]; Calculate(source.Open.Values, source.High.Values, source.Low.Values, source.Close.Values, period, v); diff --git a/lib/momentum/adxr/Adxr.cs b/lib/momentum/adxr/Adxr.cs index 8fce3abb..152dbb2d 100644 --- a/lib/momentum/adxr/Adxr.cs +++ b/lib/momentum/adxr/Adxr.cs @@ -142,7 +142,7 @@ public sealed class Adxr : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; @@ -175,7 +175,7 @@ public sealed class Adxr : ITValuePublisher { if (destination.Length > 0) { - destination.Fill(0); + destination.Clear(); } return; } @@ -216,7 +216,7 @@ public sealed class Adxr : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; diff --git a/lib/momentum/ao/Ao.cs b/lib/momentum/ao/Ao.cs index c4b3b432..c8f77d59 100644 --- a/lib/momentum/ao/Ao.cs +++ b/lib/momentum/ao/Ao.cs @@ -128,7 +128,7 @@ public sealed class Ao : ITValuePublisher /// The AO series public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; @@ -207,7 +207,7 @@ public sealed class Ao : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; @@ -221,6 +221,6 @@ public sealed class Ao : ITValuePublisher tList.Add(times[i]); } - return new TSeries(tList, new List(v)); + return new TSeries(tList, [.. v]); } } diff --git a/lib/momentum/aroon/Aroon.cs b/lib/momentum/aroon/Aroon.cs index 37542380..e3cd3a98 100644 --- a/lib/momentum/aroon/Aroon.cs +++ b/lib/momentum/aroon/Aroon.cs @@ -162,7 +162,7 @@ public sealed class Aroon : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; @@ -195,7 +195,7 @@ public sealed class Aroon : ITValuePublisher { if (destination.Length > 0) { - destination.Fill(0); + destination.Clear(); } return; } @@ -238,7 +238,7 @@ public sealed class Aroon : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; @@ -252,6 +252,6 @@ public sealed class Aroon : ITValuePublisher tList.Add(times[i]); } - return new TSeries(tList, new List(v)); + return new TSeries(tList, [.. v]); } } diff --git a/lib/momentum/aroonosc/AroonOsc.Validation.Tests.cs b/lib/momentum/aroonosc/AroonOsc.Validation.Tests.cs index 02e9b5f7..7410632c 100644 --- a/lib/momentum/aroonosc/AroonOsc.Validation.Tests.cs +++ b/lib/momentum/aroonosc/AroonOsc.Validation.Tests.cs @@ -97,12 +97,13 @@ public sealed class AroonOscValidationTests : IDisposable ValidationHelper.VerifyData(results, tulipOsc, lookback: 14); } - [Fact(Skip = "Ooples implementation deviates from standard even with adjustment")] + [Fact(Skip = "Ooples implementation deviates significantly from standard (TA-Lib, Tulip, Skender, QuanTAlib)")] public void MatchesOoples() { - // Note: OoplesFinance implementation of Aroon Oscillator differs by exactly 1 period (100/Period) - // from Skender, TA-Lib, Tulip, and QuanTAlib. - // We adjust Ooples results by adding 100/Period to match the standard implementation. + // Note: OoplesFinance implementation of Aroon Oscillator is an outlier. + // It deviates from the consensus of TA-Lib, Tulip, Skender, and QuanTAlib. + // The deviation is not a simple offset; it involves inconsistent steps and reversals, + // likely due to differences in how the high/low window indices are tracked. var aroon = new AroonOsc(14); var results = new List(); @@ -128,7 +129,6 @@ public sealed class AroonOscValidationTests : IDisposable // Ooples only provides CalculateAroonOscillator var aroonOscResults = stockData.CalculateAroonOscillator(14); var ooplesOsc = aroonOscResults.OutputValues["Aroon"] - .Select(x => x + (100.0 / 14.0)) .ToArray(); // Verify Oscillator diff --git a/lib/momentum/aroonosc/AroonOsc.cs b/lib/momentum/aroonosc/AroonOsc.cs index 1c1e13c0..5fdfd111 100644 --- a/lib/momentum/aroonosc/AroonOsc.cs +++ b/lib/momentum/aroonosc/AroonOsc.cs @@ -148,7 +148,7 @@ public sealed class AroonOsc : ITValuePublisher public TSeries Update(TBarSeries source) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; @@ -180,7 +180,7 @@ public sealed class AroonOsc : ITValuePublisher { if (destination.Length > 0) { - destination.Fill(0); + destination.Clear(); } return; @@ -224,7 +224,7 @@ public sealed class AroonOsc : ITValuePublisher [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { - if (source.Count == 0) return new TSeries(new List(), new List()); + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; @@ -238,6 +238,6 @@ public sealed class AroonOsc : ITValuePublisher tList.Add(times[i]); } - return new TSeries(tList, new List(v)); + return new TSeries(tList, [.. v]); } } diff --git a/lib/momentum/aroonosc/AroonOsc.md b/lib/momentum/aroonosc/AroonOsc.md index 2851f506..37b209cc 100644 --- a/lib/momentum/aroonosc/AroonOsc.md +++ b/lib/momentum/aroonosc/AroonOsc.md @@ -60,6 +60,10 @@ Validation is performed against **TA-Lib** and **Tushar Chande's original exampl - **Consistency**: Matches TA-Lib outputs exactly. - **Edge Cases**: Handles flat markets (where high/low are unchanged) correctly by prioritizing the *most recent* extreme. +### External Library Discrepancies + +- **OoplesFinance**: The Ooples implementation deviates significantly from the standard (TA-Lib, Tulip, Skender, QuanTAlib). It exhibits inconsistent steps and reversals, likely due to differences in windowing or index logic. Validation against Ooples is intentionally skipped. + ### Common Pitfalls - **Lag**: Because it looks back `Period` bars, it will not signal a reversal until the previous extreme "ages out" or is superseded. It is a lagging indicator of trend changes. diff --git a/lib/trends/sma/Sma.Tolerance.Tests.cs b/lib/trends/sma/Sma.Tolerance.Tests.cs new file mode 100644 index 00000000..46557930 --- /dev/null +++ b/lib/trends/sma/Sma.Tolerance.Tests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Skender.Stock.Indicators; +using Xunit; + +namespace QuanTAlib.Tests; + +public class SmaToleranceTests : IDisposable +{ + private readonly ValidationTestData _testData; + + public SmaToleranceTests() + { + _testData = new ValidationTestData(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _testData.Dispose(); + } + } + + [Fact] + public void Check_Skender_Tolerance() + { + int period = 20; + var sma = new Sma(period); + var qResult = sma.Update(_testData.Data); + var sResult = _testData.SkenderQuotes.GetSma(period).ToList(); + + ValidationHelper.VerifyData(qResult, sResult, (s) => s.Sma); + + // Add explicit assertion to satisfy SonarQube + Assert.True(qResult.Count > 0); + } +} diff --git a/lib/trends/trima/Trima.Tolerance.Tests.cs b/lib/trends/trima/Trima.Tolerance.Tests.cs new file mode 100644 index 00000000..53af4694 --- /dev/null +++ b/lib/trends/trima/Trima.Tolerance.Tests.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using TALib; +using Xunit; + +namespace QuanTAlib.Tests; + +public class TrimaToleranceTests : IDisposable +{ + private readonly ValidationTestData _testData; + + public TrimaToleranceTests() + { + _testData = new ValidationTestData(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _testData.Dispose(); + } + } + + [Fact] + public void Check_Talib_Tolerance() + { + int period = 20; + var trima = new Trima(period); + var qResult = trima.Update(_testData.Data); + + double[] output = new double[_testData.RawData.Length]; + var retCode = TALib.Functions.Trima(_testData.RawData.Span, 0..^0, output, out var outRange, period); + Assert.Equal(Core.RetCode.Success, retCode); + + int lookback = TALib.Functions.TrimaLookback(period); + + ValidationHelper.VerifyData(qResult, output, outRange, lookback, tolerance: ValidationHelper.OoplesTolerance); + + // Add explicit assertion to satisfy SonarQube + Assert.True(qResult.Count > 0); + } +} diff --git a/lib/volume/adosc/Adosc.Validation.Tests.cs b/lib/volume/adosc/Adosc.Validation.Tests.cs index b9921636..07780b49 100644 --- a/lib/volume/adosc/Adosc.Validation.Tests.cs +++ b/lib/volume/adosc/Adosc.Validation.Tests.cs @@ -74,7 +74,7 @@ public class AdoscValidationTests : IDisposable ValidationHelper.VerifyData(spanOutput, output, outRange, lookback: slowPeriod - 1); } - [Fact(Skip = "Tulip ADOSC implementation diverges significantly from TA-Lib and Skender")] + [Fact] public void Validate_Against_Tulip_Adosc() { int fastPeriod = 3; @@ -87,7 +87,8 @@ public class AdoscValidationTests : IDisposable var adoscIndicator = Tulip.Indicators.adosc; double[][] inputs = { high, low, close, volume }; double[] options = { fastPeriod, slowPeriod }; - double[][] outputs = { new double[close.Length - 1] }; // Tulip starts at 1? Need to check + int start = (int)adoscIndicator.Start(options); + double[][] outputs = { new double[close.Length - start] }; adoscIndicator.Run(inputs, options, outputs); double[] output = outputs[0]; @@ -95,7 +96,7 @@ public class AdoscValidationTests : IDisposable // 1. Batch Mode var adosc = new Adosc(fastPeriod, slowPeriod); var result = adosc.Update(_testData.Bars); - ValidationHelper.VerifyData(result, output, lookback: 1); + ValidationHelper.VerifyData(result, output, lookback: start); // 2. Streaming Mode var adoscStream = new Adosc(fastPeriod, slowPeriod); @@ -104,12 +105,12 @@ public class AdoscValidationTests : IDisposable { streamResults.Add(adoscStream.Update(bar).Value); } - ValidationHelper.VerifyData(streamResults, output, lookback: 1); + ValidationHelper.VerifyData(streamResults, output, lookback: start); // 3. Span Mode double[] spanOutput = new double[close.Length]; Adosc.Calculate(high, low, close, volume, spanOutput, fastPeriod, slowPeriod); - ValidationHelper.VerifyData(spanOutput, output, lookback: 1); + ValidationHelper.VerifyData(spanOutput, output, lookback: start); } [Fact] diff --git a/qodana.yaml b/qodana.yaml index c5aee4c9..fa78f7d9 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -96,7 +96,7 @@ exclude: - name: AutoPropertyCanBeMadeGetOnly.Global # Optional: common in libraries #Execute shell command before Qodana execution (Applied in CI/CD pipeline) -#bootstrap: sh ./prepare-qodana.sh +bootstrap: sh ./.github/prepare-qodana.sh #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) #plugins: @@ -105,10 +105,10 @@ exclude: #Specify Qodana linter for analysis (Applied in CI/CD pipeline) #linter: jetbrains/qodana-dotnet:2025.3 -solution: QuanTAlib.sln linter: qodana-cdnet-EAP dotnet: + solution: QuanTAlib.sln msbuild: properties: Qodana: true