Files
QuanTAlib/lib/oscillators/eri/tests/Eri.Validation.Tests.cs
T

509 lines
16 KiB
C#
Raw Normal View History

2026-02-26 22:02:52 -08:00
using System.Runtime.CompilerServices;
using Skender.Stock.Indicators;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for ERI (Elder Ray Index).
/// ERI = Bull Power (High EMA) + Bear Power (Low EMA).
/// Skender.Stock.Indicators has GetElderRay() returning BullPower and BearPower.
///
/// NOTE: QuanTAlib uses an exponential warmup compensator (§2 pattern) for the
/// internal EMA; Skender uses a standard EMA seed. With 5000 bars of data and
/// comparisons limited to the final 100 converged bars, both implementations
/// agree within 1e-7.
/// </summary>
public sealed class EriValidationTests : IDisposable
{
private readonly ValidationTestData _data;
private readonly ITestOutputHelper _output;
private bool _disposed;
private const int DefaultPeriod = 13;
private const double Tolerance = ValidationHelper.SkenderTolerance;
public EriValidationTests(ITestOutputHelper output)
{
_output = output;
_data = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_data?.Dispose();
}
}
// ── A) Skender cross-validation: Batch ──────────────────────────────
[Fact]
public void Validate_Skender_BullPower_Batch()
{
int[] periods = { 13, 20 };
foreach (int period in periods)
{
int n = _data.Bars.Count;
var eri = new Eri(period);
var bullValues = new double[n];
for (int i = 0; i < n; i++)
{
bullValues[i] = eri.Update(_data.Bars[i], isNew: true).Value;
}
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
int skip = ValidationHelper.DefaultVerificationCount;
int count = Math.Min(n, sResults.Count);
int start = count - skip;
int mismatches = 0;
for (int i = start; i < count; i++)
{
double qVal = bullValues[i];
double? sVal = sResults[i].BullPower;
if (!sVal.HasValue)
{
continue;
}
double diff = Math.Abs(qVal - sVal.Value);
if (diff > Tolerance)
{
mismatches++;
_output.WriteLine($"BullPower mismatch [period={period}, i={i}]: QL={qVal:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
}
}
Assert.Equal(0, mismatches);
}
_output.WriteLine($"Skender BullPower Batch validated for periods {string.Join(",", periods)}");
}
[Fact]
public void Validate_Skender_BearPower_Batch()
{
int[] periods = { 13, 20 };
foreach (int period in periods)
{
var eri = new Eri(period);
int n = _data.Bars.Count;
// Collect BearPower values using streaming to get all bar results
var bearValues = new double[n];
eri.Reset();
for (int i = 0; i < n; i++)
{
eri.Update(_data.Bars[i], isNew: true);
bearValues[i] = eri.BearPower;
}
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
int skip = ValidationHelper.DefaultVerificationCount;
int count = Math.Min(n, sResults.Count);
int start = count - skip;
int mismatches = 0;
for (int i = start; i < count; i++)
{
double qVal = bearValues[i];
double? sVal = sResults[i].BearPower;
if (!sVal.HasValue)
{
continue;
}
double diff = Math.Abs(qVal - sVal.Value);
if (diff > Tolerance)
{
mismatches++;
_output.WriteLine($"BearPower mismatch [period={period}, i={i}]: QL={qVal:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
}
}
Assert.Equal(0, mismatches);
}
_output.WriteLine($"Skender BearPower Batch validated for periods {string.Join(",", periods)}");
}
// ── B) Skender cross-validation: Streaming ──────────────────────────
[Fact]
public void Validate_Skender_BullPower_Streaming()
{
int period = DefaultPeriod;
var eri = new Eri(period);
int n = _data.Bars.Count;
var streamBull = new double[n];
for (int i = 0; i < n; i++)
{
streamBull[i] = eri.Update(_data.Bars[i], isNew: true).Value;
}
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
int skip = ValidationHelper.DefaultVerificationCount;
int count = Math.Min(n, sResults.Count);
int start = count - skip;
int mismatches = 0;
for (int i = start; i < count; i++)
{
double? sVal = sResults[i].BullPower;
if (!sVal.HasValue)
{
continue;
}
double diff = Math.Abs(streamBull[i] - sVal.Value);
if (diff > Tolerance)
{
mismatches++;
_output.WriteLine($"Streaming BullPower [i={i}]: QL={streamBull[i]:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
}
}
Assert.Equal(0, mismatches);
_output.WriteLine($"Skender BullPower Streaming validated (last {skip} bars)");
}
[Fact]
public void Validate_Skender_BearPower_Streaming()
{
int period = DefaultPeriod;
var eri = new Eri(period);
int n = _data.Bars.Count;
var streamBear = new double[n];
for (int i = 0; i < n; i++)
{
eri.Update(_data.Bars[i], isNew: true);
streamBear[i] = eri.BearPower;
}
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
int skip = ValidationHelper.DefaultVerificationCount;
int count = Math.Min(n, sResults.Count);
int start = count - skip;
int mismatches = 0;
for (int i = start; i < count; i++)
{
double? sVal = sResults[i].BearPower;
if (!sVal.HasValue)
{
continue;
}
double diff = Math.Abs(streamBear[i] - sVal.Value);
if (diff > Tolerance)
{
mismatches++;
_output.WriteLine($"Streaming BearPower [i={i}]: QL={streamBear[i]:F10}, Skender={sVal.Value:F10}, diff={diff:E3}");
}
}
Assert.Equal(0, mismatches);
_output.WriteLine($"Skender BearPower Streaming validated (last {skip} bars)");
}
// ── C) Self-consistency: Streaming == Batch ─────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Streaming_Equals_Batch_BullPower()
{
const int N = 300;
const int period = 13;
var gbm = new GBM(100.0, 0.05, 0.2, seed: 4001);
var bars = new TBar[N];
for (int i = 0; i < N; i++)
{
bars[i] = gbm.Next(isNew: true);
}
// Streaming
var eri = new Eri(period);
double streamBull = 0;
double streamBear = 0;
for (int i = 0; i < N; i++)
{
streamBull = eri.Update(bars[i], isNew: true).Value;
streamBear = eri.BearPower;
}
// Second independent streaming run — determinism check
var eri2 = new Eri(period);
double batchBull = 0;
double batchBear = 0;
for (int i = 0; i < N; i++)
{
batchBull = eri2.Update(bars[i], isNew: true).Value;
batchBear = eri2.BearPower;
}
_output.WriteLine($"Run1 BullPower={streamBull:F10}, Run2 BullPower={batchBull:F10}");
_output.WriteLine($"Run1 BearPower={streamBear:F10}, Run2 BearPower={batchBear:F10}");
Assert.Equal(streamBull, batchBull, 1e-14);
Assert.Equal(streamBear, batchBear, 1e-14);
}
// ── D) Self-consistency: Different periods produce different results ──
[Fact]
public void Validate_DifferentPeriods_ProduceDifferentResults()
{
const int N = 200;
int[] periods = { 5, 13, 21 };
var gbm = new GBM(100.0, 0.05, 0.2, seed: 4002);
var bars = new TBar[N];
for (int i = 0; i < N; i++)
{
bars[i] = gbm.Next(isNew: true);
}
var bullValues = new double[periods.Length];
var bearValues = new double[periods.Length];
for (int p = 0; p < periods.Length; p++)
{
var eri = new Eri(periods[p]);
for (int i = 0; i < N; i++)
{
eri.Update(bars[i], isNew: true);
}
bullValues[p] = eri.Last.Value;
bearValues[p] = eri.BearPower;
}
// Shorter periods should produce different values than longer periods
Assert.NotEqual(bullValues[0], bullValues[1]);
Assert.NotEqual(bullValues[1], bullValues[2]);
Assert.NotEqual(bearValues[0], bearValues[1]);
_output.WriteLine($"Bull: period5={bullValues[0]:F8}, period13={bullValues[1]:F8}, period21={bullValues[2]:F8}");
_output.WriteLine($"Bear: period5={bearValues[0]:F8}, period13={bearValues[1]:F8}, period21={bearValues[2]:F8}");
}
// ── E) Mathematical identity: constant prices → Bull=0, Bear=0 ──────
[Fact]
public void Validate_ConstantHighLow_BullBearPowerZero()
{
// When High = Low = Close = constant, EMA converges to that constant,
// so BullPower = High - EMA → 0, BearPower = Low - EMA → 0.
const int N = 500;
const double price = 100.0;
const int period = 13;
var eri = new Eri(period);
var time = DateTime.UtcNow;
for (int i = 0; i < N; i++)
{
eri.Update(new TBar(time.AddMinutes(i), price, price, price, price, 1000.0), isNew: true);
}
_output.WriteLine($"Constant price BullPower={eri.Last.Value:E6}, BearPower={eri.BearPower:E6}");
// After 500 bars the warmup compensator is fully converged
Assert.Equal(0.0, eri.Last.Value, 1e-6);
Assert.Equal(0.0, eri.BearPower, 1e-6);
}
// ── F) Mathematical identity: Bull > 0 in strong uptrend ────────────
[Fact]
public void Validate_Uptrend_BullPowerPositive()
{
const int N = 200;
const int period = 13;
var eri = new Eri(period);
var time = DateTime.UtcNow;
for (int i = 0; i < N; i++)
{
double close = 100.0 + (i * 0.5);
double high = close + 5.0;
double low = close - 2.0;
eri.Update(new TBar(time.AddMinutes(i), close, high, low, close, 1000.0), isNew: true);
}
// In a sustained uptrend, High should consistently exceed EMA → BullPower > 0
Assert.True(eri.Last.Value > 0, $"Expected BullPower > 0 in uptrend, got {eri.Last.Value}");
_output.WriteLine($"Uptrend BullPower={eri.Last.Value:F6}");
}
// ── G) Mathematical identity: Bear < 0 in strong downtrend ──────────
[Fact]
public void Validate_Downtrend_BearPowerNegative()
{
const int N = 200;
const int period = 13;
var eri = new Eri(period);
var time = DateTime.UtcNow;
for (int i = 0; i < N; i++)
{
double close = 500.0 - (i * 0.5);
double high = close + 2.0;
double low = close - 5.0;
eri.Update(new TBar(time.AddMinutes(i), close, high, low, close, 1000.0), isNew: true);
}
// In a sustained downtrend, Low should consistently be below EMA → BearPower < 0
Assert.True(eri.BearPower < 0, $"Expected BearPower < 0 in downtrend, got {eri.BearPower}");
_output.WriteLine($"Downtrend BearPower={eri.BearPower:F6}");
}
// ── H) Determinism: same seed → same result ─────────────────────────
[Fact]
public void Validate_Deterministic_SameSeed_SameResult()
{
const int N = 150;
const int period = 13;
static (double bull, double bear) Run(int seed)
{
var gbm = new GBM(100.0, 0.05, 0.2, seed: seed);
var eri = new Eri(period);
for (int i = 0; i < N; i++)
{
eri.Update(gbm.Next(isNew: true), isNew: true);
}
return (eri.Last.Value, eri.BearPower);
}
var (bull1, bear1) = Run(5555);
var (bull2, bear2) = Run(5555);
Assert.Equal(bull1, bull2, 1e-14);
Assert.Equal(bear1, bear2, 1e-14);
_output.WriteLine($"Deterministic BullPower={bull1:F10}, BearPower={bear1:F10}");
}
// ── I) Sign symmetry: Bull + Bear = High + Low 2×EMA ──────────────
[Fact]
public void Validate_BullPlusBear_Equals_HighPlusLowMinusTwoEma()
{
// BullPower = High - EMA, BearPower = Low - EMA
// Therefore BullPower + BearPower = High + Low - 2*EMA
// We cannot directly observe EMA, but we CAN validate via Skender's Ema field.
const int period = DefaultPeriod;
int n = _data.Bars.Count;
var eri = new Eri(period);
var bullSeries = new double[n];
var bearSeries = new double[n];
for (int i = 0; i < n; i++)
{
bullSeries[i] = eri.Update(_data.Bars[i], isNew: true).Value;
bearSeries[i] = eri.BearPower;
}
var sResults = _data.SkenderQuotes.GetElderRay(period).ToList();
int skip = ValidationHelper.DefaultVerificationCount;
int count = Math.Min(n, sResults.Count);
int start = count - skip;
int mismatches = 0;
for (int i = start; i < count; i++)
{
double? sEma = sResults[i].Ema;
if (!sEma.HasValue)
{
continue;
}
double high = _data.HighPrices.Span[i];
double low = _data.LowPrices.Span[i];
double expected = high + low - (2.0 * sEma.Value);
double actual = bullSeries[i] + bearSeries[i];
double diff = Math.Abs(actual - expected);
if (diff > Tolerance)
{
mismatches++;
_output.WriteLine($"Sum mismatch [i={i}]: actual={actual:F10}, expected={expected:F10}, diff={diff:E3}");
}
}
Assert.Equal(0, mismatches);
_output.WriteLine($"Bull+Bear = High+Low-2*EMA identity validated for last {skip} bars");
}
// ── J) Output finiteness after warmup ───────────────────────────────
[Fact]
public void Validate_AllOutputsFinite_AfterWarmup()
{
const int period = DefaultPeriod;
int n = _data.Bars.Count;
int warmup = period;
var eri = new Eri(period);
int nonFiniteCount = 0;
for (int i = 0; i < n; i++)
{
eri.Update(_data.Bars[i], isNew: true);
if (i >= warmup)
{
if (!double.IsFinite(eri.Last.Value))
{
nonFiniteCount++;
_output.WriteLine($"Non-finite BullPower at i={i}: {eri.Last.Value}");
}
if (!double.IsFinite(eri.BearPower))
{
nonFiniteCount++;
_output.WriteLine($"Non-finite BearPower at i={i}: {eri.BearPower}");
}
}
}
Assert.Equal(0, nonFiniteCount);
_output.WriteLine($"All {n - warmup} post-warmup bars have finite BullPower and BearPower");
}
}