Files

200 lines
6.6 KiB
C#
Raw Permalink Normal View History

2026-02-23 17:27:35 -08:00
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class NyqmaValidationTests : IDisposable
{
private const int DefaultPeriod = 10;
private const int DefaultNyquistPeriod = 4;
private const double ValidationTolerance = 1e-9;
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
public NyqmaValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (disposing)
{
_testData.Dispose();
}
}
// NYQMA has no direct external library equivalent, so we validate
// by verifying NYQMA = (1+α)·WMA(src,N1) α·WMA(WMA(src,N1),N2) (component consistency)
[Fact]
public void Validate_NyqmaEqualsComponentFormula_Batch()
{
int period = DefaultPeriod;
int nyquistPeriod = DefaultNyquistPeriod;
double alpha = (double)nyquistPeriod / (period - nyquistPeriod);
var source = _testData.Data;
// Compute WMA1 and WMA2(WMA1) separately
var wma1Result = Wma.Batch(source, period);
var wma2Result = Wma.Batch(wma1Result, nyquistPeriod);
// Compute NYQMA
var nyqmaResult = Nyqma.Batch(source, period, nyquistPeriod);
// NYQMA = (1+α)·WMA1 α·WMA2
int count = nyqmaResult.Count;
int warmup = period + nyquistPeriod - 1;
for (int i = warmup; i < count; i++)
{
double expected = Math.FusedMultiplyAdd(1.0 + alpha, wma1Result[i].Value, -alpha * wma2Result[i].Value);
Assert.Equal(expected, nyqmaResult[i].Value, ValidationTolerance);
}
_output.WriteLine($"NYQMA({period},{nyquistPeriod}) component consistency: PASS ({count - warmup} bars validated at {ValidationTolerance})");
}
[Fact]
public void Validate_NyqmaEqualsComponentFormula_Streaming()
{
int period = DefaultPeriod;
int nyquistPeriod = DefaultNyquistPeriod;
double alpha = (double)nyquistPeriod / (period - nyquistPeriod);
var source = _testData.Data;
var wma1 = new Wma(period);
var wma2 = new Wma(nyquistPeriod);
var nyqma = new Nyqma(period, nyquistPeriod);
int warmup = period + nyquistPeriod - 1;
int validated = 0;
for (int i = 0; i < source.Count; i++)
{
var w1Val = wma1.Update(source[i]);
var w2Val = wma2.Update(w1Val);
var nyqmaVal = nyqma.Update(source[i]);
if (i >= warmup)
{
double expected = Math.FusedMultiplyAdd(1.0 + alpha, w1Val.Value, -alpha * w2Val.Value);
Assert.Equal(expected, nyqmaVal.Value, ValidationTolerance);
validated++;
}
}
_output.WriteLine($"NYQMA({period},{nyquistPeriod}) streaming component consistency: PASS ({validated} bars validated)");
}
[Fact]
public void Validate_NyqmaEqualsComponentFormula_Span()
{
int period = DefaultPeriod;
int nyquistPeriod = DefaultNyquistPeriod;
double alpha = (double)nyquistPeriod / (period - nyquistPeriod);
var rawData = _testData.RawData;
var wma1Output = new double[rawData.Length];
var wma2Output = new double[rawData.Length];
var nyqmaOutput = new double[rawData.Length];
Wma.Batch(rawData.Span, wma1Output.AsSpan(), period);
Wma.Batch(wma1Output.AsSpan(), wma2Output.AsSpan(), nyquistPeriod);
Nyqma.Batch(rawData.Span, nyqmaOutput.AsSpan(), period, nyquistPeriod);
int warmup = period + nyquistPeriod - 1;
int validated = 0;
for (int i = warmup; i < rawData.Length; i++)
{
double expected = Math.FusedMultiplyAdd(1.0 + alpha, wma1Output[i], -alpha * wma2Output[i]);
Assert.Equal(expected, nyqmaOutput[i], ValidationTolerance);
validated++;
}
_output.WriteLine($"NYQMA({period},{nyquistPeriod}) span component consistency: PASS ({validated} bars validated)");
}
[Fact]
public void Validate_BatchAndStreamingMatch()
{
int period = DefaultPeriod;
int nyquistPeriod = DefaultNyquistPeriod;
var source = _testData.Data;
// Batch
var batchResult = Nyqma.Batch(source, period, nyquistPeriod);
// Streaming
var streaming = new Nyqma(period, nyquistPeriod);
for (int i = 0; i < source.Count; i++)
{
streaming.Update(source[i]);
}
Assert.Equal(batchResult.Last.Value, streaming.Last.Value, 1e-6);
_output.WriteLine($"NYQMA({period},{nyquistPeriod}) batch/streaming match: PASS");
}
[Fact]
public void Validate_BatchAndSpanMatch()
{
int period = DefaultPeriod;
int nyquistPeriod = DefaultNyquistPeriod;
var source = _testData.Data;
var rawData = _testData.RawData;
var batchResult = Nyqma.Batch(source, period, nyquistPeriod);
var spanOutput = new double[rawData.Length];
Nyqma.Batch(rawData.Span, spanOutput.AsSpan(), period, nyquistPeriod);
Assert.Equal(batchResult.Last.Value, spanOutput[^1], ValidationTolerance);
_output.WriteLine($"NYQMA({period},{nyquistPeriod}) batch/span match: PASS");
}
[Theory]
[InlineData(5, 2)]
[InlineData(10, 3)]
[InlineData(21, 8)]
[InlineData(89, 21)]
public void Validate_DifferentPeriods_ConstantInputConverges(int period, int nyquistPeriod)
{
double constant = 55.0;
var nyqma = new Nyqma(period, nyquistPeriod);
double last = 0;
for (int i = 0; i < period * 3; i++)
{
last = nyqma.Update(new TValue(DateTime.UtcNow, constant)).Value;
}
Assert.Equal(constant, last, 1e-6);
_output.WriteLine($"NYQMA({period},{nyquistPeriod}) constant convergence: PASS (converged to {last:F9})");
}
[Fact]
public void Validate_NyquistClamping_AlphaBoundsCorrect()
{
// When nyquistPeriod is at maximum (period/2), alpha = (N1/2)/(N1 - N1/2) = 1
var nyqma = new Nyqma(10, 5);
// alpha = 5/(10-5) = 1.0
// NYQMA = 2*WMA1 - WMA2 (same as PMA when nyquistPeriod = period)
double constant = 100.0;
double last = 0;
for (int i = 0; i < 50; i++)
{
last = nyqma.Update(new TValue(DateTime.UtcNow, constant)).Value;
}
Assert.Equal(constant, last, 1e-9);
_output.WriteLine("NYQMA(10,5) alpha=1.0 convergence: PASS");
}
}