mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 17:18:05 +00:00
- 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
471 lines
14 KiB
C#
471 lines
14 KiB
C#
// NLMA Unit Tests
|
|
using System;
|
|
using System.Linq;
|
|
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public class NlmaTests
|
|
{
|
|
private const double Epsilon = 1e-10;
|
|
|
|
// ── Constructor tests ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Constructor_DefaultPeriod_Is14()
|
|
{
|
|
var nlma = new Nlma();
|
|
Assert.Equal("Nlma(14)", nlma.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_CustomPeriod_SetsCorrectly()
|
|
{
|
|
var nlma = new Nlma(20);
|
|
Assert.Equal("Nlma(20)", nlma.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_Period2_IsMinValid()
|
|
{
|
|
// Igorad kernel requires period >= 2
|
|
var nlma = new Nlma(2);
|
|
var result = nlma.Update(new TValue(DateTime.MinValue, 42.0));
|
|
Assert.Equal(42.0, result.Value, 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_Period1_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Nlma(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NegativePeriod_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Nlma(-1));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_PeriodZero_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new Nlma(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_SetsWarmupPeriod()
|
|
{
|
|
// WarmupPeriod = flen = 5*period - 1
|
|
var nlma = new Nlma(10);
|
|
Assert.Equal(49, nlma.WarmupPeriod); // 5*10 - 1 = 49
|
|
}
|
|
|
|
[Fact]
|
|
public void Name_IsAccessible()
|
|
{
|
|
var nlma = new Nlma(7);
|
|
Assert.StartsWith("Nlma(", nlma.Name, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmupPeriod_IsFlen()
|
|
{
|
|
// WarmupPeriod = 5*period - 1 (Igorad kernel length)
|
|
var nlma = new Nlma(25);
|
|
Assert.Equal(124, nlma.WarmupPeriod); // 5*25 - 1 = 124
|
|
}
|
|
|
|
// ── Value computation tests ────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Update_ConstantInput_ReturnsConstant()
|
|
{
|
|
// DC gain = 1: constant input → output must equal that constant after warmup
|
|
var nlma = new Nlma(10);
|
|
int flen = 5 * 10 - 1; // 49
|
|
TValue result = default;
|
|
for (int i = 0; i < flen + 10; i++)
|
|
{
|
|
result = nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 50.0));
|
|
}
|
|
Assert.Equal(50.0, result.Value, 8);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Period2_ReturnsInput()
|
|
{
|
|
// period=2, flen=9. After warmup, constant input → output = input
|
|
var nlma = new Nlma(2);
|
|
int flen = 5 * 2 - 1; // 9
|
|
TValue result = default;
|
|
for (int i = 0; i < flen + 5; i++)
|
|
{
|
|
result = nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 123.456));
|
|
}
|
|
Assert.Equal(123.456, result.Value, 8);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_KnownValues_Igorad_ConstantDCGain()
|
|
{
|
|
// Igorad kernel with any period: constant input must produce constant output
|
|
// This validates that signed-sum normalization preserves DC gain = 1
|
|
var nlma = new Nlma(4);
|
|
int flen = 5 * 4 - 1; // 19
|
|
TValue result = default;
|
|
for (int i = 0; i < flen + 5; i++)
|
|
{
|
|
result = nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 100.0));
|
|
}
|
|
Assert.Equal(100.0, result.Value, 6);
|
|
}
|
|
|
|
[Fact]
|
|
public void IgoradWeights_HasNegativeWeights()
|
|
{
|
|
// Igorad kernel with period 14 should have negative weights for lag cancellation
|
|
// Test: feed a step function and verify responsiveness
|
|
var nlma = new Nlma(14);
|
|
int flen = 5 * 14 - 1; // 69
|
|
|
|
// Feed flen bars of 100, then flen bars of 200
|
|
for (int i = 0; i < flen; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 100.0));
|
|
}
|
|
for (int i = flen; i < 2 * flen; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 200.0));
|
|
}
|
|
// After enough 200s, the NLMA should converge near 200
|
|
double val = nlma.Last.Value;
|
|
Assert.True(val > 190.0, $"NLMA should track step to ~200, got {val}");
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Last_IsAccessible()
|
|
{
|
|
var nlma = new Nlma(5);
|
|
nlma.Update(new TValue(DateTime.MinValue, 100.0));
|
|
Assert.True(double.IsFinite(nlma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_ReturnsTValue()
|
|
{
|
|
var nlma = new Nlma(5);
|
|
var result = nlma.Update(new TValue(DateTime.MinValue, 100.0));
|
|
Assert.True(double.IsFinite(result.Value));
|
|
}
|
|
|
|
// ── State management tests ─────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void IsHot_FlipsWhenBufferFull()
|
|
{
|
|
// period=3, flen = 5*3-1 = 14
|
|
var nlma = new Nlma(3);
|
|
int flen = 5 * 3 - 1; // 14
|
|
Assert.False(nlma.IsHot);
|
|
|
|
for (int i = 0; i < flen - 1; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), i + 1));
|
|
}
|
|
Assert.False(nlma.IsHot);
|
|
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(flen - 1), flen));
|
|
Assert.True(nlma.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsNew_True_AdvancesState()
|
|
{
|
|
var nlma = new Nlma(3);
|
|
nlma.Update(new TValue(DateTime.MinValue, 10), isNew: true);
|
|
Assert.True(nlma.IsNew);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsNew_False_Rewrites()
|
|
{
|
|
var nlma = new Nlma(3);
|
|
nlma.Update(new TValue(DateTime.MinValue, 10), isNew: true);
|
|
nlma.Update(new TValue(DateTime.MinValue, 20), isNew: false);
|
|
Assert.False(nlma.IsNew);
|
|
}
|
|
|
|
[Fact]
|
|
public void IterativeCorrections_Restore()
|
|
{
|
|
// After correction (isNew=false), next isNew=true should advance normally
|
|
var nlma = new Nlma(5);
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 100 + i));
|
|
}
|
|
_ = nlma.Last.Value;
|
|
|
|
// Correct last bar
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(29), 110), isNew: false);
|
|
double afterCorrection = nlma.Last.Value;
|
|
Assert.True(double.IsFinite(afterCorrection));
|
|
|
|
// Add new bar — should restore from previous state
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(30), 105), isNew: true);
|
|
Assert.True(double.IsFinite(nlma.Last.Value));
|
|
Assert.NotEqual(afterCorrection, nlma.Last.Value);
|
|
}
|
|
|
|
// ── NaN / Infinity handling ────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void NaN_UsesLastValid()
|
|
{
|
|
var nlma = new Nlma(3);
|
|
nlma.Update(new TValue(DateTime.MinValue, 10));
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(1), 20));
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(2), double.NaN));
|
|
// Should use last valid value (20) in place of NaN
|
|
Assert.True(double.IsFinite(nlma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Infinity_UsesLastValid()
|
|
{
|
|
var nlma = new Nlma(3);
|
|
nlma.Update(new TValue(DateTime.MinValue, 10));
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(1), 20));
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(2), double.PositiveInfinity));
|
|
Assert.True(double.IsFinite(nlma.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void BatchNaN_Safe()
|
|
{
|
|
var nlma = new Nlma(3);
|
|
nlma.Update(new TValue(DateTime.MinValue, double.NaN));
|
|
// First value NaN should return NaN
|
|
Assert.True(double.IsNaN(nlma.Last.Value));
|
|
}
|
|
|
|
// ── Event-based chaining ───────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void EventBased_Chaining_Works()
|
|
{
|
|
var source = new Nlma(3);
|
|
var chained = new Nlma(source, 5);
|
|
|
|
for (int i = 0; i < 250; i++)
|
|
{
|
|
source.Update(new TValue(DateTime.MinValue.AddDays(i), 100 + i));
|
|
}
|
|
|
|
Assert.True(double.IsFinite(chained.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pub_Fires()
|
|
{
|
|
var nlma = new Nlma(3);
|
|
bool fired = false;
|
|
nlma.Pub += (object? _, in TValueEventArgs _) => fired = true;
|
|
nlma.Update(new TValue(DateTime.MinValue, 100.0));
|
|
Assert.True(fired);
|
|
}
|
|
|
|
// ── Batch TSeries ──────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void AllModes_ProduceSameResults()
|
|
{
|
|
int period = 10;
|
|
int flen = 5 * period - 1; // 49
|
|
int len = flen + 30; // ensure enough bars for full kernel
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 100 + Math.Sin(i) * 10));
|
|
}
|
|
|
|
// Mode 1: streaming
|
|
var streaming = new Nlma(period);
|
|
var streamResults = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
streamResults[i] = streaming.Update(src[i]).Value;
|
|
}
|
|
|
|
// Mode 2: Batch(TSeries)
|
|
var batchResult = Nlma.Batch(src, period);
|
|
|
|
// Mode 3: Batch(span)
|
|
var spanInput = new double[len];
|
|
var spanOutput = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
spanInput[i] = src[i].Value;
|
|
}
|
|
Nlma.Batch(spanInput, spanOutput, period);
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
Assert.Equal(streamResults[i], batchResult[i].Value, 6);
|
|
Assert.Equal(streamResults[i], spanOutput[i], 6);
|
|
}
|
|
}
|
|
|
|
// ── Batch Span API ─────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Batch_Span_ValidatesPeriod()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Nlma.Batch(new double[5], new double[5], 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_ValidatesLengths()
|
|
{
|
|
Assert.Throws<ArgumentException>(() =>
|
|
Nlma.Batch(new double[5], new double[3], 3));
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_EmptyInput_NoError()
|
|
{
|
|
Nlma.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5);
|
|
Assert.True(true, "Empty span batch should not throw");
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_MatchesTSeries()
|
|
{
|
|
int period = 7;
|
|
int flen = 5 * period - 1; // 34
|
|
int len = flen + 20;
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 50 + i * 0.5));
|
|
}
|
|
|
|
var tsBatch = Nlma.Batch(src, period);
|
|
|
|
var spanInput = new double[len];
|
|
var spanOutput = new double[len];
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
spanInput[i] = src[i].Value;
|
|
}
|
|
Nlma.Batch(spanInput, spanOutput, period);
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
Assert.Equal(tsBatch[i].Value, spanOutput[i], 6);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_HandlesNaN()
|
|
{
|
|
double[] source = [1, 2, double.NaN, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
|
double[] output = new double[source.Length];
|
|
Nlma.Batch(source, output, 3);
|
|
|
|
for (int i = 1; i < output.Length; i++)
|
|
{
|
|
Assert.True(double.IsFinite(output[i]));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_Span_LargeData_NoStackOverflow()
|
|
{
|
|
int count = 10000;
|
|
double[] source = new double[count];
|
|
double[] output = new double[count];
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
source[i] = 100.0 + i * 0.1;
|
|
}
|
|
Nlma.Batch(source, output, 300);
|
|
|
|
// flen = 5*300 - 1 = 1499
|
|
int flen = 5 * 300 - 1;
|
|
for (int i = flen; i < count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(output[i]), $"Output at index {i} should be finite");
|
|
}
|
|
}
|
|
|
|
// ── Calculate ──────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Calculate_ReturnsIndicatorAndResults()
|
|
{
|
|
int period = 5;
|
|
int flen = 5 * period - 1; // 24
|
|
int len = flen + 20;
|
|
var src = new TSeries([], []);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
src.Add(new TValue(DateTime.MinValue.AddDays(i), 100 + i));
|
|
}
|
|
|
|
var (results, indicator) = Nlma.Calculate(src, period);
|
|
Assert.Equal(len, results.Count);
|
|
Assert.NotNull(indicator);
|
|
Assert.True(indicator.IsHot);
|
|
}
|
|
|
|
// ── Reset / Dispose ────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Reset_ClearsState()
|
|
{
|
|
int period = 5;
|
|
int flen = 5 * period - 1; // 24
|
|
var nlma = new Nlma(period);
|
|
for (int i = 0; i < flen + 10; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 100 + i));
|
|
}
|
|
Assert.True(nlma.IsHot);
|
|
|
|
nlma.Reset();
|
|
Assert.False(nlma.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_UnsubscribesFromSource()
|
|
{
|
|
var source = new Nlma(3);
|
|
var chained = new Nlma(source, 5);
|
|
|
|
source.Update(new TValue(DateTime.MinValue, 100));
|
|
Assert.True(double.IsFinite(chained.Last.Value));
|
|
|
|
chained.Dispose();
|
|
|
|
// After dispose, source updates should not propagate
|
|
source.Update(new TValue(DateTime.MinValue.AddDays(1), 200));
|
|
// chained.Last should remain unchanged after dispose
|
|
}
|
|
|
|
[Fact]
|
|
public void LargePeriod_Handles()
|
|
{
|
|
int period = 500;
|
|
int flen = 5 * period - 1; // 2499
|
|
var nlma = new Nlma(period);
|
|
for (int i = 0; i < flen + 100; i++)
|
|
{
|
|
nlma.Update(new TValue(DateTime.MinValue.AddDays(i), 100 + i * 0.01));
|
|
}
|
|
Assert.True(double.IsFinite(nlma.Last.Value));
|
|
Assert.True(nlma.IsHot);
|
|
}
|
|
}
|