Files
Miha Kralj 060649192f docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- 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
2026-03-12 12:34:16 -07:00

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);
}
}