Files
QuanTAlib/lib/trends/afirma/Afirma.Validation.Tests.cs
T
Miha Kralj 78a3a25ada Add AFIRMA indicator implementation with validation tests and documentation
- Implemented AFIRMA (Autoregressive Finite Impulse Response Moving Average) class with support for various window types and batch processing.
- Created unit tests for AFIRMA to validate internal consistency, streaming, and batch processing.
- Added comprehensive documentation for AFIRMA, including usage examples, performance profile, and parameter selection guide.
- Removed obsolete omnisharp.json configuration file.
2025-12-30 20:42:15 -08:00

340 lines
11 KiB
C#

using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for AFIRMA indicator.
/// AFIRMA is a specialized FIR filter with windowed sinc coefficients.
/// Since no external library implements this exact algorithm, validation
/// focuses on internal consistency and mathematical properties.
/// </summary>
public sealed class AfirmaValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public AfirmaValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_InternalConsistency_Batch()
{
int[] periods = { 5, 10, 20, 50 };
int[] taps = { 5, 11, 21 };
foreach (var period in periods)
{
foreach (var tap in taps)
{
// Calculate QuanTAlib AFIRMA (batch TSeries)
var afirma = new Afirma(period, tap);
var qResult = afirma.Update(_testData.Data);
// Verify all results are finite
foreach (var val in qResult)
{
Assert.True(double.IsFinite(val.Value),
$"AFIRMA({period},{tap}) produced non-finite value");
}
// Verify count matches input
Assert.Equal(_testData.Data.Count, qResult.Count);
}
}
_output.WriteLine("AFIRMA Batch(TSeries) internal consistency validated");
}
[Fact]
public void Validate_InternalConsistency_Streaming()
{
int[] periods = { 5, 10, 20, 50 };
int[] taps = { 5, 11, 21 };
foreach (var period in periods)
{
foreach (var tap in taps)
{
// Calculate QuanTAlib AFIRMA (streaming)
var afirma = new Afirma(period, tap);
var qResults = new List<double>();
foreach (var item in _testData.Data)
{
qResults.Add(afirma.Update(item).Value);
}
// Verify all results are finite
foreach (var val in qResults)
{
Assert.True(double.IsFinite(val),
$"AFIRMA({period},{tap}) streaming produced non-finite value");
}
// Verify count matches input
Assert.Equal(_testData.Data.Count, qResults.Count);
}
}
_output.WriteLine("AFIRMA Streaming internal consistency validated");
}
[Fact]
public void Validate_InternalConsistency_Span()
{
int[] periods = { 5, 10, 20, 50 };
int[] taps = { 5, 11, 21 };
// Prepare data for Span API
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
foreach (var tap in taps)
{
// Calculate QuanTAlib AFIRMA (Span API)
double[] qOutput = new double[sourceData.Length];
Afirma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period, tap);
// Verify all results are finite
foreach (var val in qOutput)
{
Assert.True(double.IsFinite(val),
$"AFIRMA({period},{tap}) span produced non-finite value");
}
}
}
_output.WriteLine("AFIRMA Span internal consistency validated");
}
[Fact]
public void Validate_BatchStreamingConsistency()
{
int[] periods = { 5, 10, 20 };
int[] taps = { 5, 11 };
foreach (var period in periods)
{
foreach (var tap in taps)
{
// Batch calculation
var afirmaBatch = new Afirma(period, tap);
var batchResult = afirmaBatch.Update(_testData.Data);
// Streaming calculation
var afirmaStream = new Afirma(period, tap);
var streamResults = new List<double>();
foreach (var item in _testData.Data)
{
streamResults.Add(afirmaStream.Update(item).Value);
}
// Compare last 100 values
int compareCount = Math.Min(100, batchResult.Count);
for (int i = 0; i < compareCount; i++)
{
int idx = batchResult.Count - compareCount + i;
Assert.Equal(batchResult[idx].Value, streamResults[idx], 1e-10);
}
}
}
_output.WriteLine("AFIRMA Batch/Streaming consistency validated");
}
[Fact]
public void Validate_SpanBatchConsistency()
{
int[] periods = { 5, 10, 20 };
int[] taps = { 5, 11 };
double[] sourceData = _testData.RawData.ToArray();
foreach (var period in periods)
{
foreach (var tap in taps)
{
// TSeries Batch
var afirma = new Afirma(period, tap);
var tseriesResult = afirma.Update(_testData.Data);
// Span Batch
double[] spanOutput = new double[sourceData.Length];
Afirma.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period, tap);
// Compare
for (int i = 0; i < sourceData.Length; i++)
{
Assert.Equal(tseriesResult[i].Value, spanOutput[i], 1e-10);
}
}
}
_output.WriteLine("AFIRMA Span/Batch consistency validated");
}
[Fact]
public void Validate_WindowTypes_Consistency()
{
var windows = new[]
{
Afirma.WindowType.Rectangular,
Afirma.WindowType.Hanning,
Afirma.WindowType.Hamming,
Afirma.WindowType.Blackman,
Afirma.WindowType.BlackmanHarris
};
int period = 10;
int taps = 11;
foreach (var window in windows)
{
// Batch
var afirmaBatch = new Afirma(period, taps, window);
var batchResult = afirmaBatch.Update(_testData.Data);
// Streaming
var afirmaStream = new Afirma(period, taps, window);
foreach (var item in _testData.Data)
{
afirmaStream.Update(item);
}
// Compare last values
Assert.Equal(batchResult.Last.Value, afirmaStream.Last.Value, 1e-10);
_output.WriteLine($"Window {window}: Batch={batchResult.Last.Value:F6}, Stream={afirmaStream.Last.Value:F6}");
}
_output.WriteLine("AFIRMA Window types consistency validated");
}
[Fact]
public void Validate_FlatInput_ReturnsConstant()
{
int period = 10;
int taps = 11;
double constantValue = 100.0;
// Create flat input
var flatSeries = new TSeries();
for (int i = 0; i < 100; i++)
{
flatSeries.Add(DateTime.UtcNow.AddSeconds(i), constantValue);
}
var afirma = new Afirma(period, taps);
var result = afirma.Update(flatSeries);
// After warmup, all values should equal the constant
for (int i = taps; i < result.Count; i++)
{
Assert.Equal(constantValue, result[i].Value, 1e-9);
}
_output.WriteLine($"AFIRMA flat input returns constant: {result.Last.Value:F9}");
}
[Fact]
public void Validate_Smoothing_ReducesVariance()
{
int period = 10;
int taps = 21;
// Calculate variance of input
var rawData = _testData.RawData.ToArray();
double inputMean = rawData.Average();
double inputVariance = rawData.Select(x => Math.Pow(x - inputMean, 2)).Average();
// Calculate AFIRMA
var afirma = new Afirma(period, taps);
var result = afirma.Update(_testData.Data);
// Calculate variance of output (after warmup)
var outputValues = result.Skip(taps).Select(v => v.Value).ToList();
double outputMean = outputValues.Average();
double outputVariance = outputValues.Select(x => Math.Pow(x - outputMean, 2)).Average();
// Output variance should be less than input variance (smoothing effect)
Assert.True(outputVariance < inputVariance,
$"AFIRMA should reduce variance. Input: {inputVariance:F4}, Output: {outputVariance:F4}");
_output.WriteLine($"AFIRMA smoothing effect: Input variance={inputVariance:F4}, Output variance={outputVariance:F4}");
}
[Fact]
public void Validate_LargerTaps_MoreSmoothing()
{
const int period = 10;
// Calculate with different tap counts
var afirma5 = new Afirma(period, 5);
var afirma11 = new Afirma(period, 11);
var afirma21 = new Afirma(period, 21);
var result5 = afirma5.Update(_testData.Data);
var result11 = afirma11.Update(_testData.Data);
var result21 = afirma21.Update(_testData.Data);
// Calculate variance of each
double GetVariance(TSeries series, int skip)
{
var values = series.Skip(skip).Select(v => v.Value).ToList();
double mean = values.Average();
return values.Select(x => Math.Pow(x - mean, 2)).Average();
}
double var5 = GetVariance(result5, 5);
double var11 = GetVariance(result11, 11);
double var21 = GetVariance(result21, 21);
// More taps should generally produce smoother output (lower variance)
// This is a statistical property, not guaranteed for all data
_output.WriteLine($"Variance by taps: 5={var5:F4}, 11={var11:F4}, 21={var21:F4}");
// At minimum, all should be finite
Assert.True(double.IsFinite(var5));
Assert.True(double.IsFinite(var11));
Assert.True(double.IsFinite(var21));
}
[Fact]
public void Validate_DifferentWindows_DifferentCharacteristics()
{
int period = 10;
int taps = 21;
var rectangularResult = Afirma.Batch(_testData.Data, period, taps, Afirma.WindowType.Rectangular);
var blackmanHarrisResult = Afirma.Batch(_testData.Data, period, taps, Afirma.WindowType.BlackmanHarris);
// Results should be different (different window characteristics)
double rectLast = rectangularResult.Last.Value;
double bhLast = blackmanHarrisResult.Last.Value;
// They should generally not be exactly equal
// (unless input happens to be perfectly constant)
_output.WriteLine($"Rectangular: {rectLast:F6}, Blackman-Harris: {bhLast:F6}");
// Both should be finite and reasonable
Assert.True(double.IsFinite(rectLast));
Assert.True(double.IsFinite(bhLast));
}
}