mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 17:18:05 +00:00
257 lines
7.9 KiB
C#
257 lines
7.9 KiB
C#
// NLMA Validation Tests: Cross-mode consistency and mathematical properties
|
|
using System;
|
|
using System.Linq;
|
|
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public class NlmaValidationTests
|
|
{
|
|
private const double Epsilon = 1e-6;
|
|
|
|
[Fact]
|
|
public void Batch_Matches_Streaming()
|
|
{
|
|
int period = 10;
|
|
int flen = 5 * period - 1; // 49
|
|
int len = flen + 30;
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 100 + Math.Sin(i) * 20));
|
|
}
|
|
|
|
var batchResult = Nlma.Batch(src, period);
|
|
|
|
var streaming = new Nlma(period);
|
|
for (int i = 0; i < src.Count; i++)
|
|
{
|
|
var streamVal = streaming.Update(src[i]);
|
|
Assert.Equal(streamVal.Value, batchResult[i].Value, 6);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Span_Matches_Streaming()
|
|
{
|
|
int period = 8;
|
|
int flen = 5 * period - 1; // 39
|
|
int len = flen + 20;
|
|
double[] values = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
values[i] = 50 + i * 0.7;
|
|
}
|
|
|
|
double[] spanOutput = new double[len];
|
|
Nlma.Batch(values, spanOutput, period);
|
|
|
|
var streaming = new Nlma(period);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
var result = streaming.Update(new TValue(DateTime.MinValue.AddDays(i), values[i]));
|
|
Assert.Equal(result.Value, spanOutput[i], 6);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_Matches_Batch()
|
|
{
|
|
int period = 12;
|
|
int flen = 5 * period - 1; // 59
|
|
int len = flen + 20;
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 200 + i * 0.3));
|
|
}
|
|
|
|
var batchResult = Nlma.Batch(src, period);
|
|
var (calcResult, _) = Nlma.Calculate(src, period);
|
|
|
|
for (int i = 0; i < src.Count; i++)
|
|
{
|
|
Assert.Equal(batchResult[i].Value, calcResult[i].Value, 6);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ConstantInput_ProducesConstant()
|
|
{
|
|
int period = 15;
|
|
int flen = 5 * period - 1; // 74
|
|
int len = flen + 20;
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 42.0));
|
|
}
|
|
|
|
var result = Nlma.Batch(src, period);
|
|
|
|
// DC gain = 1: constant input → output = constant (after warmup, and during warmup returns price)
|
|
for (int i = 0; i < result.Count; i++)
|
|
{
|
|
Assert.Equal(42.0, result[i].Value, 8);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NaN_PreservedBeforeValidData()
|
|
{
|
|
var nlma = new Nlma(5);
|
|
var first = nlma.Update(new TValue(DateTime.MinValue, double.NaN));
|
|
Assert.True(double.IsNaN(first.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void LargePeriod_Handles()
|
|
{
|
|
int period = 200;
|
|
int flen = 5 * period - 1; // 999
|
|
int len = flen + 100;
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 100 + Math.Sin(i * 0.1) * 10));
|
|
}
|
|
|
|
var result = Nlma.Batch(src, period);
|
|
Assert.Equal(len, result.Count);
|
|
|
|
for (int i = flen; i < result.Count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(result[i].Value), $"Output at {i} should be finite");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DifferentPeriods_ProduceDifferentResults()
|
|
{
|
|
int maxFlen = 5 * 20 - 1; // 99 for period=20
|
|
int len = maxFlen + 30;
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 100 + i));
|
|
}
|
|
|
|
var result5 = Nlma.Batch(src, 5);
|
|
var result20 = Nlma.Batch(src, 20);
|
|
|
|
// Different periods must produce different results after both warmups
|
|
bool anyDifferent = false;
|
|
for (int i = maxFlen; i < len; i++)
|
|
{
|
|
if (Math.Abs(result5[i].Value - result20[i].Value) > 0.01)
|
|
{
|
|
anyDifferent = true;
|
|
break;
|
|
}
|
|
}
|
|
Assert.True(anyDifferent, "Different periods should produce different output");
|
|
}
|
|
|
|
[Fact]
|
|
public void AllNaN_Input_ReturnsNaN()
|
|
{
|
|
var nlma = new Nlma(5);
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), double.NaN));
|
|
}
|
|
Assert.True(double.IsNaN(nlma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void BarCorrection_MultipleCorrections_Stable()
|
|
{
|
|
var nlma = new Nlma(5);
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 100 + i));
|
|
}
|
|
|
|
double beforeCorrection = nlma.Last.Value;
|
|
|
|
// Multiple corrections should not drift
|
|
for (int c = 0; c < 10; c++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(29), 129.0 + c * 0.001), isNew: false);
|
|
}
|
|
|
|
// Final correction with original value
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(29), 129.0), isNew: false);
|
|
Assert.Equal(beforeCorrection, nlma.Last.Value, 8);
|
|
}
|
|
|
|
[Fact]
|
|
public void NLMA_HasNegativeWeights()
|
|
{
|
|
// NLMA with Igorad kernel contains negative weights that create the lag
|
|
// cancellation effect. Verify this by checking that NLMA on sinusoidal data
|
|
// differs from SMA and shows phase lead (less phase lag than SMA).
|
|
int period = 10;
|
|
int flen = 5 * period - 1; // 49
|
|
int len = 3 * flen;
|
|
var src = new TSeries([], []);
|
|
// Sinusoidal signal with period matching the filter period
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 100 + 10 * Math.Sin(2 * Math.PI * i / 20)));
|
|
}
|
|
|
|
var nlmaResult = Nlma.Batch(src, period);
|
|
var smaResult = Sma.Batch(src, period);
|
|
|
|
// After warmup, NLMA and SMA should produce different results (different kernel)
|
|
bool anyDifferent = false;
|
|
for (int i = flen; i < len; i++)
|
|
{
|
|
if (Math.Abs(nlmaResult[i].Value - smaResult[i].Value) > 0.01)
|
|
{
|
|
anyDifferent = true;
|
|
break;
|
|
}
|
|
}
|
|
Assert.True(anyDifferent, "NLMA should produce different output than SMA (negative weights effect)");
|
|
|
|
// NLMA's output should track closer to the original sinusoidal peaks
|
|
// because its negative weights reduce smoothing lag on oscillating signals
|
|
double nlmaMaxPeak = double.MinValue;
|
|
double smaMaxPeak = double.MinValue;
|
|
for (int i = flen; i < len; i++)
|
|
{
|
|
nlmaMaxPeak = Math.Max(nlmaMaxPeak, nlmaResult[i].Value);
|
|
smaMaxPeak = Math.Max(smaMaxPeak, smaResult[i].Value);
|
|
}
|
|
// NLMA should preserve more of the signal amplitude than SMA(period)
|
|
Assert.True(nlmaMaxPeak > smaMaxPeak,
|
|
$"NLMA peak ({nlmaMaxPeak:F2}) should be higher than SMA peak ({smaMaxPeak:F2}) on sinusoidal input");
|
|
}
|
|
|
|
[Fact]
|
|
public void NLMA_CanOvershoot()
|
|
{
|
|
// NLMA's negative weights can cause output to exceed input range
|
|
int period = 14;
|
|
int flen = 5 * period - 1; // 69
|
|
var nlma = new Nlma(period);
|
|
|
|
// Step function: all 0s then all 100s — enough data for full kernel
|
|
for (int i = 0; i < flen; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 0.0));
|
|
}
|
|
// Switch to 100
|
|
for (int i = flen; i < 2 * flen; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 100.0));
|
|
}
|
|
|
|
// After the step, early values may overshoot above 100
|
|
double lastVal = nlma.Last.Value;
|
|
Assert.True(double.IsFinite(lastVal), "NLMA output should be finite");
|
|
}
|
|
}
|