mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +00:00
200 lines
6.6 KiB
C#
200 lines
6.6 KiB
C#
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");
|
|||
|
|
}
|
|||
|
|
}
|