2026-02-26 22:02:52 -08:00
|
|
|
|
using OoplesFinance.StockIndicators;
|
|
|
|
|
|
using OoplesFinance.StockIndicators.Models;
|
2026-02-12 19:43:09 -08:00
|
|
|
|
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);
|
2026-02-27 12:50:05 -08:00
|
|
|
|
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
2026-02-12 19:43:09 -08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-02-27 12:50:05 -08:00
|
|
|
|
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
2026-02-12 19:43:09 -08:00
|
|
|
|
|
|
|
|
|
|
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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 22:02:52 -08:00
|
|
|
|
|
|
|
|
|
|
// ── 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.");
|
|
|
|
|
|
}
|
2026-02-12 19:43:09 -08:00
|
|
|
|
}
|