mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-01 19:27:44 +00:00
060649192f
- 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
370 lines
12 KiB
C#
370 lines
12 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 };
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
// Calculate QuanTAlib AFIRMA (batch TSeries)
|
|
var afirma = new Afirma(period);
|
|
var qResult = afirma.Update(_testData.Data);
|
|
|
|
// Verify all results are finite
|
|
foreach (var val in qResult)
|
|
{
|
|
Assert.True(double.IsFinite(val.Value),
|
|
$"AFIRMA({period}) 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 };
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
// Calculate QuanTAlib AFIRMA (streaming)
|
|
var afirma = new Afirma(period);
|
|
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}) 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 };
|
|
|
|
// Prepare data for Span API
|
|
double[] sourceData = _testData.RawData.ToArray();
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
// Calculate QuanTAlib AFIRMA (Span API)
|
|
double[] qOutput = new double[sourceData.Length];
|
|
Afirma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
|
|
|
|
// Verify all results are finite
|
|
foreach (var val in qOutput)
|
|
{
|
|
Assert.True(double.IsFinite(val),
|
|
$"AFIRMA({period}) span produced non-finite value");
|
|
}
|
|
}
|
|
_output.WriteLine("AFIRMA Span internal consistency validated");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_BatchStreamingConsistency()
|
|
{
|
|
int[] periods = { 5, 10, 20 };
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
// Batch calculation
|
|
var afirmaBatch = new Afirma(period);
|
|
var batchResult = afirmaBatch.Update(_testData.Data);
|
|
|
|
// Streaming calculation
|
|
var afirmaStream = new Afirma(period);
|
|
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 };
|
|
|
|
double[] sourceData = _testData.RawData.ToArray();
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
// TSeries Batch
|
|
var afirma = new Afirma(period);
|
|
var tseriesResult = afirma.Update(_testData.Data);
|
|
|
|
// Span Batch
|
|
double[] spanOutput = new double[sourceData.Length];
|
|
Afirma.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period);
|
|
|
|
// 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
|
|
};
|
|
|
|
const int period = 10;
|
|
|
|
foreach (var window in windows)
|
|
{
|
|
// Batch
|
|
var afirmaBatch = new Afirma(period, window);
|
|
var batchResult = afirmaBatch.Update(_testData.Data);
|
|
|
|
// Streaming
|
|
var afirmaStream = new Afirma(period, 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;
|
|
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);
|
|
var result = afirma.Update(flatSeries);
|
|
|
|
// After warmup, all values should equal the constant
|
|
for (int i = period; 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 = 21;
|
|
|
|
// Calculate variance of input
|
|
var rawData = _testData.RawData.ToArray();
|
|
double inputMean = rawData.Average();
|
|
double inputVariance = rawData.Average(x => Math.Pow(x - inputMean, 2));
|
|
|
|
// Calculate AFIRMA
|
|
var afirma = new Afirma(period);
|
|
var result = afirma.Update(_testData.Data);
|
|
|
|
// Calculate variance of output (after warmup)
|
|
var outputValues = result.Skip(period).Select(v => v.Value).ToList();
|
|
double outputMean = outputValues.Average();
|
|
double outputVariance = outputValues.Average(x => Math.Pow(x - outputMean, 2));
|
|
|
|
// 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_LargerPeriod_MoreSmoothing()
|
|
{
|
|
// Calculate with different periods (which implies different tap counts)
|
|
var afirma5 = new Afirma(5);
|
|
var afirma11 = new Afirma(11);
|
|
var afirma21 = new Afirma(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.Average(x => Math.Pow(x - mean, 2));
|
|
}
|
|
|
|
double var5 = GetVariance(result5, 5);
|
|
double var11 = GetVariance(result11, 11);
|
|
double var21 = GetVariance(result21, 21);
|
|
|
|
// Larger period should generally produce smoother output (lower variance)
|
|
// This is a statistical property, not guaranteed for all data
|
|
_output.WriteLine($"Variance by period: 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;
|
|
|
|
var rectangularResult = Afirma.Batch(_testData.Data, period, Afirma.WindowType.Rectangular);
|
|
var blackmanHarrisResult = Afirma.Batch(_testData.Data, period, 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));
|
|
}
|
|
|
|
[Fact]
|
|
public void Afirma_LeastSquares_Streaming_Matches_Batch()
|
|
{
|
|
int[] periods = { 5, 10, 20 };
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
// Batch calculation with leastSquares=true
|
|
var afirmaBatch = new Afirma(period, leastSquares: true);
|
|
var batchResult = afirmaBatch.Update(_testData.Data);
|
|
|
|
// Streaming calculation with leastSquares=true
|
|
var afirmaStream = new Afirma(period, leastSquares: true);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Afirma_Correction_Recomputes()
|
|
{
|
|
var ind = new Afirma(20);
|
|
var t0 = DateTime.MinValue;
|
|
|
|
// Build state well past warmup
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
ind.Update(new TValue(t0.AddSeconds(i), 100.0 + (i * 0.5)));
|
|
}
|
|
|
|
// Anchor bar
|
|
var anchorTime = t0.AddSeconds(50);
|
|
const double anchorValue = 125.0;
|
|
ind.Update(new TValue(anchorTime, anchorValue), isNew: true);
|
|
double anchorResult = ind.Last.Value;
|
|
|
|
// Correction with dramatically different value — must yield different result
|
|
ind.Update(new TValue(anchorTime, anchorValue * 10), isNew: false);
|
|
Assert.NotEqual(anchorResult, ind.Last.Value);
|
|
|
|
// Correction back to original — must exactly restore original result
|
|
ind.Update(new TValue(anchorTime, anchorValue), isNew: false);
|
|
Assert.Equal(anchorResult, ind.Last.Value, 1e-9);
|
|
}
|
|
}
|