Files
QuanTAlib/lib/oscillators/trix/Trix.Validation.Tests.cs
T
Miha Kralj 951842acca Add validation tests for various volume and momentum indicators
- Introduced Massi validation tests to ensure mathematical properties hold for the Mass Index indicator.
- Added Va validation tests for Volume Accumulation, checking for finite outputs and correct accumulation behavior.
- Implemented Vf validation tests for Volume Force, verifying outputs for rising and falling prices, and ensuring batch and streaming results match.
- Created Vo validation tests for Volume Oscillator, confirming behavior with constant, increasing, and decreasing volumes.
- Developed Vroc validation tests for Volume Rate of Change, validating outputs for constant volume and changes in volume.
- Updated project file to include new momentum indicators (MACD and RSI) in the compilation.
2026-02-12 19:43:09 -08:00

434 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Skender.Stock.Indicators;
using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class TrixValidationTests(ITestOutputHelper output) : IDisposable
{
private readonly ValidationTestData _testData = new();
private readonly ITestOutputHelper _output = output;
private bool _disposed;
public void Dispose()
{
Dispose(disposing: true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
// ── A) Skender Batch ─────────────────────────────────────────────────────
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = [9, 14, 25];
foreach (var period in periods)
{
var trix = new global::QuanTAlib.Trix(period);
var qResult = trix.Update(_testData.Data);
var sResult = _testData.SkenderQuotes.GetTrix(period).ToList();
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Trix);
}
_output.WriteLine("TRIX Batch(TSeries) validated successfully against Skender");
}
// ── B) Skender Streaming ─────────────────────────────────────────────────
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = [9, 14, 25];
foreach (var period in periods)
{
var trix = new global::QuanTAlib.Trix(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(trix.Update(item).Value);
}
var sResult = _testData.SkenderQuotes.GetTrix(period).ToList();
ValidationHelper.VerifyData(qResults, sResult, (s) => s.Trix);
}
_output.WriteLine("TRIX Streaming validated successfully against Skender");
}
// ── C) Skender Span ──────────────────────────────────────────────────────
[Fact]
public void Validate_Skender_Span()
{
int[] periods = [9, 14, 25];
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Trix.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
var sResult = _testData.SkenderQuotes.GetTrix(period).ToList();
ValidationHelper.VerifyData(qOutput, sResult, (s) => s.Trix);
}
_output.WriteLine("TRIX Span validated successfully against Skender");
}
// ── D) TA-Lib Span ───────────────────────────────────────────────────────
[Fact]
public void Validate_Talib_Span()
{
int[] periods = [14, 20, 50, 100];
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
double[] qOutput = new double[tData.Length];
global::QuanTAlib.Trix.Batch(tData.AsSpan(), qOutput.AsSpan(), period);
double[] tOutput = new double[tData.Length];
var retCode = TALib.Functions.Trix<double>(tData, 0..^0, tOutput, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.TrixLookback(period);
ValidationHelper.VerifyData(qOutput, tOutput, outRange, lookback);
}
_output.WriteLine("TRIX Span validated against TA-Lib");
}
// ── E) TA-Lib Streaming ──────────────────────────────────────────────────
[Fact]
public void Validate_Talib_Streaming()
{
int[] periods = [9, 14, 25];
double[] tData = _testData.RawData.ToArray();
double[] tOutput = new double[tData.Length];
foreach (var period in periods)
{
var trix = new global::QuanTAlib.Trix(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(trix.Update(item).Value);
}
var retCode = TALib.Functions.Trix<double>(tData, 0..^0, tOutput, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.TrixLookback(period);
ValidationHelper.VerifyData(qResults, tOutput, outRange, lookback);
}
_output.WriteLine("TRIX Streaming validated successfully against TA-Lib");
}
// ── F) Tulip Batch ───────────────────────────────────────────────────────
[Fact]
public void Validate_Tulip_Batch()
{
int[] periods = [9, 14, 25];
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
var trix = new global::QuanTAlib.Trix(period);
var qResult = trix.Update(_testData.Data);
var trixIndicator = Tulip.Indicators.trix;
double[][] inputs = [tData];
double[] options = [period];
int lookback = trixIndicator.Start(options);
double[][] outputs = [new double[tData.Length - lookback]];
trixIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Tulip uses non-compensated EMA; warmup compensation causes persistent diffs
// TRIX amplifies by 100×, so small EMA diffs become noticeable in TRIX
ValidationHelper.VerifyData(qResult, tResult, lookback, tolerance: 1e-3);
}
_output.WriteLine("TRIX Batch(TSeries) validated successfully against Tulip");
}
// ── G) Tulip Span ────────────────────────────────────────────────────────
[Fact]
public void Validate_Tulip_Span()
{
int[] periods = [14, 20, 50, 100];
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
double[] qOutput = new double[tData.Length];
global::QuanTAlib.Trix.Batch(tData.AsSpan(), qOutput.AsSpan(), period);
var trixIndicator = Tulip.Indicators.trix;
double[][] inputs = [tData];
double[] options = [period];
int lookback = trixIndicator.Start(options);
double[][] outputs = [new double[tData.Length - lookback]];
trixIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Tulip uses non-compensated EMA; warmup compensation causes minor convergence diffs
// TRIX amplifies by 100×, so EMA diffs of ~1e-6 become ~1e-4 in TRIX
ValidationHelper.VerifyData(qOutput, tResult, lookback, tolerance: 5e-4);
}
_output.WriteLine("TRIX Span validated against Tulip");
}
// ── H) Tulip Streaming ───────────────────────────────────────────────────
[Fact]
public void Validate_Tulip_Streaming()
{
int[] periods = [9, 14, 25];
double[] tData = _testData.RawData.ToArray();
foreach (var period in periods)
{
var trix = new global::QuanTAlib.Trix(period);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(trix.Update(item).Value);
}
var trixIndicator = Tulip.Indicators.trix;
double[][] inputs = [tData];
double[] options = [period];
int lookback = trixIndicator.Start(options);
double[][] outputs = [new double[tData.Length - lookback]];
trixIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Tulip uses non-compensated EMA; warmup compensation causes persistent diffs
// TRIX amplifies by 100×, so small EMA diffs become noticeable in TRIX
ValidationHelper.VerifyData(qResults, tResult, lookback, tolerance: 1e-3);
}
_output.WriteLine("TRIX Streaming validated successfully against Tulip");
}
// ── I) Self-Consistency: All Modes ────────────────────────────────────────
[Fact]
public void Validate_AllModes_ProduceIdenticalResults()
{
int[] periods = [5, 10, 20, 50];
foreach (var period in periods)
{
// 1. Batch Mode (TSeries)
var batchTrix = new global::QuanTAlib.Trix(period);
var batchResult = batchTrix.Update(_testData.Data);
// 2. Span Mode
double[] sourceData = _testData.RawData.ToArray();
double[] spanOutput = new double[sourceData.Length];
global::QuanTAlib.Trix.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period);
// 3. Streaming Mode
var streamingTrix = new global::QuanTAlib.Trix(period);
var streamingResults = new List<double>();
foreach (var item in _testData.Data)
{
streamingResults.Add(streamingTrix.Update(item).Value);
}
// Compare all modes
for (int i = 0; i < _testData.Data.Count; i++)
{
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-8);
Assert.Equal(batchResult[i].Value, streamingResults[i], 1e-8);
}
}
_output.WriteLine("All modes validated to produce identical results");
}
// ── J) Self-Consistency: Convergence ──────────────────────────────────────
[Fact]
public void Validate_Convergence_AfterWarmup()
{
int[] periods = [5, 10, 20, 50];
foreach (var period in periods)
{
var trix = new global::QuanTAlib.Trix(period);
int warmup = trix.WarmupPeriod; // period * 3
Assert.False(trix.IsHot);
for (int i = 0; i < warmup - 1; i++)
{
trix.Update(_testData.Data[i]);
Assert.False(trix.IsHot);
}
trix.Update(_testData.Data[warmup - 1]);
Assert.True(trix.IsHot);
}
}
// ── K) NaN Robustness ────────────────────────────────────────────────────
[Fact]
public void Validate_HandlesNaN_Gracefully()
{
var trix = new global::QuanTAlib.Trix(10);
for (int i = 0; i < 20; i++)
{
trix.Update(_testData.Data[i]);
}
var result = trix.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
for (int i = 20; i < 30; i++)
{
var r = trix.Update(_testData.Data[i]);
Assert.True(double.IsFinite(r.Value));
}
}
// ── L) Infinity Robustness ───────────────────────────────────────────────
[Fact]
public void Validate_HandlesInfinity_Gracefully()
{
var trix = new global::QuanTAlib.Trix(10);
for (int i = 0; i < 20; i++)
{
trix.Update(_testData.Data[i]);
}
var resultPos = trix.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultPos.Value));
var resultNeg = trix.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultNeg.Value));
}
// ── M) Zero Crossing Behavior ────────────────────────────────────────────
[Fact]
public void Validate_ZeroCrossing_DetectsDirectionChange()
{
var trix = new global::QuanTAlib.Trix(3);
// Feed a long sustained uptrend to ensure TRIX stabilizes positive
for (int i = 0; i < 50; i++)
{
trix.Update(new TValue(DateTime.UtcNow, 100 + i * 2));
}
double uptrendTrix = trix.Last.Value;
Assert.True(uptrendTrix > 0, $"Sustained uptrend should produce positive TRIX, got {uptrendTrix}");
// Feed a long sustained downtrend
for (int i = 0; i < 50; i++)
{
trix.Update(new TValue(DateTime.UtcNow, 200 - i * 2));
}
double downtrendTrix = trix.Last.Value;
Assert.True(downtrendTrix < 0, $"Sustained downtrend should produce negative TRIX, got {downtrendTrix}");
}
// ── N) Flat Line ─────────────────────────────────────────────────────────
[Fact]
public void Validate_FlatLine_ProducesZeroTrix()
{
var trix = new global::QuanTAlib.Trix(10);
for (int i = 0; i < 200; i++)
{
trix.Update(new TValue(DateTime.UtcNow, 100));
}
// After sufficient warmup with flat data, TRIX ≈ 0
// Warmup compensation introduces tiny residual; 1e-4 is sufficient
Assert.True(Math.Abs(trix.Last.Value) < 1e-4,
$"Expected TRIX ≈ 0 for flat line, got {trix.Last.Value}");
}
// ── O) Large Dataset Precision ───────────────────────────────────────────
[Fact]
public void Validate_LargeDataset_MaintainsPrecision()
{
const int period = 20;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
var bars = gbm.Fetch(10_000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Compare batch vs streaming on last 100 points of large dataset
var batchResult = global::QuanTAlib.Trix.Batch(bars.Close, period);
var streamTrix = new global::QuanTAlib.Trix(period);
for (int i = 0; i < bars.Close.Count; i++)
{
streamTrix.Update(bars.Close[i]);
}
// Verify final values match
Assert.Equal(batchResult.Last.Value, streamTrix.Last.Value, 1e-9);
}
// ── P) Different Periods ─────────────────────────────────────────────────
[Fact]
public void Validate_DifferentPeriods_ProduceDifferentSensitivity()
{
var trix5 = new global::QuanTAlib.Trix(5);
var trix20 = new global::QuanTAlib.Trix(20);
var trix50 = new global::QuanTAlib.Trix(50);
for (int i = 0; i < _testData.Data.Count; i++)
{
trix5.Update(_testData.Data[i]);
trix20.Update(_testData.Data[i]);
trix50.Update(_testData.Data[i]);
}
Assert.True(double.IsFinite(trix5.Last.Value));
Assert.True(double.IsFinite(trix20.Last.Value));
Assert.True(double.IsFinite(trix50.Last.Value));
}
// ── Q) Batch Span NaN ────────────────────────────────────────────────────
[Fact]
public void Validate_BatchSpan_HandlesNaN_InMiddle()
{
double[] data = new double[100];
var gbm = new GBM(startPrice: 100, seed: 42);
for (int i = 0; i < 100; i++)
{
data[i] = gbm.Next().Close;
}
data[50] = double.NaN;
double[] result = new double[100];
global::QuanTAlib.Trix.Batch(data.AsSpan(), result.AsSpan(), 10);
foreach (var value in result)
{
Assert.True(double.IsFinite(value), $"Expected finite value, got {value}");
}
}
}