Files
QuanTAlib/lib/trends_FIR/nlma/tests/Nlma.Validation.Tests.cs
T
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

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");
}
}