mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-13 08:08:05 +00:00
225 lines
6.4 KiB
C#
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);
|
|
}
|
|
}
|