Files
Miha Kralj 060649192f docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- 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
2026-03-12 12:34:16 -07:00

475 lines
18 KiB
C#
Raw Permalink 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 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.");
}
}