feat: enhance validation tests for Aroon Oscillator and ADOSC

This commit is contained in:
Miha Kralj
2025-12-22 15:31:38 -08:00
parent d2a622bff9
commit 6bd0096297
15 changed files with 147 additions and 33 deletions
+4 -3
View File
@@ -3,10 +3,11 @@
"isRoot": true,
"tools": {
"gitversion.tool": {
"version": "5.12.0",
"version": "6.5.1",
"commands": [
"dotnet-gitversion"
]
],
"rollForward": false
}
}
}
}
+10
View File
@@ -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
+3
View File
@@ -413,3 +413,6 @@ ilspy/
# Ignore insiders AI rules
.github/instructions/codacy.instructions.md
# Ignore dotnet install script
dotnet-install.sh
+1 -1
View File
@@ -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 | - | - | - | - |
+3 -3
View File
@@ -277,7 +277,7 @@ public sealed class Adx : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
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<long>(), new List<double>());
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);
+3 -3
View File
@@ -142,7 +142,7 @@ public sealed class Adxr : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
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<long>(), new List<double>());
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new double[len];
+3 -3
View File
@@ -128,7 +128,7 @@ public sealed class Ao : ITValuePublisher
/// <returns>The AO series</returns>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
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<long>(), new List<double>());
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<double>(v));
return new TSeries(tList, [.. v]);
}
}
+4 -4
View File
@@ -162,7 +162,7 @@ public sealed class Aroon : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
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<long>(), new List<double>());
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<double>(v));
return new TSeries(tList, [.. v]);
}
}
@@ -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<double>();
@@ -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
+4 -4
View File
@@ -148,7 +148,7 @@ public sealed class AroonOsc : ITValuePublisher
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
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<long>(), new List<double>());
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<double>(v));
return new TSeries(tList, [.. v]);
}
}
+4
View File
@@ -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.
+45
View File
@@ -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);
}
}
+50
View File
@@ -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<double>(_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);
}
}
+6 -5
View File
@@ -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]
+2 -2
View File
@@ -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