Files
QuanTAlib/lib/trends_FIR/fwma/Fwma.Validation.Tests.cs
T
2026-02-23 17:27:35 -08:00

225 lines
6.4 KiB
C#

namespace QuanTAlib.Tests;
public class FwmaValidationTests
{
private static TSeries MakeSeries(int count = 500)
{
var gbm = new GBM(startPrice: 100, seed: 42);
var series = new TSeries();
for (int i = 0; i < count; i++)
{
series.Add(gbm.Next());
}
return series;
}
// === Self-consistency validation (no external library implements FWMA) ===
[Fact]
public void Batch_Matches_Streaming()
{
int period = 10;
var src = MakeSeries(200);
var batchResult = Fwma.Batch(src, period);
var streaming = new Fwma(period);
for (int i = 0; i < src.Count; i++)
{
streaming.Update(src[i]);
}
// Compare last value
Assert.Equal(streaming.Last.Value, batchResult.Values[^1], 1e-10);
// Compare all values after warmup
var streaming2 = new Fwma(period);
for (int i = 0; i < src.Count; i++)
{
double streamVal = streaming2.Update(src[i]).Value;
Assert.Equal(streamVal, batchResult.Values[i], 1e-10);
}
}
[Fact]
public void Span_Matches_Streaming()
{
int period = 10;
var src = MakeSeries(200);
double[] spanOutput = new double[src.Count];
Fwma.Batch(src.Values, spanOutput.AsSpan(), period);
var streaming = new Fwma(period);
for (int i = 0; i < src.Count; i++)
{
double streamVal = streaming.Update(src[i]).Value;
Assert.Equal(streamVal, spanOutput[i], 1e-10);
}
}
[Fact]
public void Calculate_Matches_Batch()
{
int period = 10;
var src = MakeSeries(200);
var batchResult = Fwma.Batch(src, period);
var (calcResult, _) = Fwma.Calculate(src, period);
for (int i = 0; i < src.Count; i++)
{
Assert.Equal(batchResult.Values[i], calcResult.Values[i], 1e-10);
}
}
[Fact]
public void OutputBounded_ByInputRange()
{
// FIR with all-positive weights: output must be within [min, max] of input window
int period = 10;
var src = MakeSeries(200);
var result = Fwma.Batch(src, period);
for (int i = period - 1; i < src.Count; i++)
{
double min = double.MaxValue;
double max = double.MinValue;
for (int k = 0; k < period; k++)
{
double v = src.Values[i - k];
if (v < min)
{
min = v;
}
if (v > max)
{
max = v;
}
}
Assert.True(result.Values[i] >= min - 1e-10, $"Output at {i} below min");
Assert.True(result.Values[i] <= max + 1e-10, $"Output at {i} above max");
}
}
[Fact]
public void FWMA_MoreResponsive_ThanSMA()
{
// FWMA should have less lag than SMA (lower center of gravity)
// Test with a step function: FWMA should reach the step faster
int period = 10;
var fwma = new Fwma(period);
var sma = new Sma(period);
// Feed constant 100 to fill buffers
for (int i = 0; i < period; i++)
{
fwma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
sma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
}
// Step to 200
fwma.Update(new TValue(DateTime.UtcNow.AddSeconds(period), 200.0));
sma.Update(new TValue(DateTime.UtcNow.AddSeconds(period), 200.0));
// FWMA should be closer to 200 than SMA (more responsive)
double fwmaVal = fwma.Last.Value;
double smaVal = sma.Last.Value;
Assert.True(fwmaVal > smaVal, $"FWMA ({fwmaVal}) should be more responsive than SMA ({smaVal})");
}
[Fact]
public void FWMA_MoreResponsive_ThanWMA()
{
// FWMA (Fibonacci weights) should be more responsive than WMA (linear weights)
int period = 10;
var fwma = new Fwma(period);
var wma = new Wma(period);
for (int i = 0; i < period; i++)
{
fwma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
wma.Update(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0));
}
fwma.Update(new TValue(DateTime.UtcNow.AddSeconds(period), 200.0));
wma.Update(new TValue(DateTime.UtcNow.AddSeconds(period), 200.0));
double fwmaVal = fwma.Last.Value;
double wmaVal = wma.Last.Value;
Assert.True(fwmaVal > wmaVal, $"FWMA ({fwmaVal}) should be more responsive than WMA ({wmaVal})");
}
[Fact]
public void DifferentPeriods_ProduceDifferentResults()
{
var src = MakeSeries(100);
var r5 = Fwma.Batch(src, 5);
var r10 = Fwma.Batch(src, 10);
// After both are hot, at least some values should differ
bool anyDifferent = false;
for (int i = 10; i < src.Count; i++)
{
if (Math.Abs(r5.Values[i] - r10.Values[i]) > 1e-10)
{
anyDifferent = true;
break;
}
}
Assert.True(anyDifferent);
}
[Fact]
public void LargePeriod_Handles()
{
int period = 50;
var src = MakeSeries(200);
var result = Fwma.Batch(src, period);
Assert.Equal(src.Count, result.Count);
Assert.True(double.IsFinite(result.Values[^1]));
}
[Fact]
public void AllNaN_Input_ReturnsNaN()
{
double[] source = [double.NaN, double.NaN, double.NaN];
double[] output = new double[3];
Fwma.Batch(source.AsSpan(), output.AsSpan(), period: 3);
for (int i = 0; i < output.Length; i++)
{
Assert.True(double.IsNaN(output[i]), $"All-NaN input should produce NaN at index {i}");
}
}
[Fact]
public void BarCorrection_MultipleCorrections_Stable()
{
// Apply multiple corrections and verify stability
int period = 5;
var fwma = new Fwma(period);
var series = MakeSeries(20);
for (int i = 0; i < series.Count; i++)
{
fwma.Update(series[i], isNew: true);
}
double baseValue = fwma.Last.Value;
// Apply 10 corrections with the same value
for (int c = 0; c < 10; c++)
{
_ = fwma.Update(series[^1], isNew: false);
}
Assert.Equal(baseValue, fwma.Last.Value, 1e-10);
}
}