mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-30 02:27:43 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
475 lines
18 KiB
C#
475 lines
18 KiB
C#
using OoplesFinance.StockIndicators;
|
||
using OoplesFinance.StockIndicators.Models;
|
||
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(TALib.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(TALib.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}");
|
||
}
|
||
}
|
||
|
||
// ── Cross-library: OoplesFinance ──────────────────────────────────────────
|
||
[Fact]
|
||
public void Trix_MatchesOoples_Structural()
|
||
{
|
||
const int period = 14;
|
||
var ooplesData = _testData.SkenderQuotes.Select(static q => new TickerData
|
||
{
|
||
Date = q.Date,
|
||
Open = (double)q.Open,
|
||
High = (double)q.High,
|
||
Low = (double)q.Low,
|
||
Close = (double)q.Close,
|
||
Volume = (double)q.Volume
|
||
}).ToList();
|
||
|
||
var stockData = new StockData(ooplesData);
|
||
var oResult = stockData.CalculateTrix(length: period);
|
||
var oValues = oResult.OutputValues.Values.First();
|
||
|
||
var trix = new global::QuanTAlib.Trix(period);
|
||
var qValues = new List<double>();
|
||
foreach (var item in _testData.Data)
|
||
{
|
||
qValues.Add(trix.Update(item).Value);
|
||
}
|
||
|
||
Assert.True(oValues.Count > 0, "Ooples Trix must produce output");
|
||
int finiteCount = 0;
|
||
for (int i = period; i < Math.Min(oValues.Count, qValues.Count); i++)
|
||
{
|
||
if (double.IsFinite(oValues[i]) && double.IsFinite(qValues[i]))
|
||
{
|
||
finiteCount++;
|
||
}
|
||
}
|
||
Assert.True(finiteCount > 100, $"Expected >100 finite Trix pairs, got {finiteCount}");
|
||
_output.WriteLine($"Trix Ooples structural: {finiteCount} finite pairs verified.");
|
||
}
|
||
}
|