Files
QuanTAlib/lib/trends_FIR/sgma/Sgma.Validation.Tests.cs
T
2026-02-10 21:33:16 -08:00

298 lines
10 KiB
C#

namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for SGMA (Savitzky-Golay Moving Average).
/// Note: SGMA is not commonly available in other libraries (TA-Lib, Skender, Tulip, Ooples)
/// as a standard indicator. These tests validate against known mathematical properties
/// and internal consistency rather than external library comparisons.
/// </summary>
public class SgmaValidationTests
{
private const double Tolerance = 1e-9;
[Fact]
public void Sgma_Degree0_MatchesSma_Batch()
{
// SGMA with degree=0 should produce identical results to SMA (uniform weights)
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
var sgmaResults = Sgma.Batch(series, 9, 0);
var smaResults = Sma.Batch(series, 9);
for (int i = 0; i < sgmaResults.Count; i++)
{
Assert.Equal(smaResults[i].Value, sgmaResults[i].Value, Tolerance);
}
}
[Fact]
public void Sgma_Degree0_MatchesSma_Streaming()
{
var sgma = new Sgma(5, 0);
var sma = new Sma(5);
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
var tv = new TValue(bar.Time, bar.Close);
var sgmaResult = sgma.Update(tv);
var smaResult = sma.Update(tv);
Assert.Equal(smaResult.Value, sgmaResult.Value, Tolerance);
}
}
[Fact]
public void Sgma_Degree0_MatchesSma_Span()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
double[] input = series.Values.ToArray();
double[] sgmaOutput = new double[input.Length];
double[] smaOutput = new double[input.Length];
Sgma.Batch(input.AsSpan(), sgmaOutput.AsSpan(), 9, 0);
Sma.Batch(input.AsSpan(), smaOutput.AsSpan(), 9);
for (int i = 0; i < input.Length; i++)
{
Assert.Equal(smaOutput[i], sgmaOutput[i], Tolerance);
}
}
[Fact]
public void Sgma_ConstantInput_ReturnsConstant_AllDegrees()
{
const double constantValue = 100.0;
const int period = 9;
for (int degree = 0; degree <= 4; degree++)
{
var sgma = new Sgma(period, degree);
for (int i = 0; i < 20; i++)
{
var result = sgma.Update(new TValue(DateTime.UtcNow, constantValue));
Assert.Equal(constantValue, result.Value, Tolerance);
}
}
}
[Fact]
public void Sgma_LinearTrend_PreservesSlope_LowDegree()
{
// For a perfectly linear input, SGMA should follow the trend
// Higher degrees should give more accurate mid-point values
const int period = 5;
double[] prices = new double[20];
for (int i = 0; i < 20; i++)
{
prices[i] = 100.0 + i * 10.0; // Linear: 100, 110, 120, ..., 290
}
var sgma0 = new Sgma(period, 0);
var sgma2 = new Sgma(period, 2);
// After warmup, results should track the trend
for (int i = 0; i < 20; i++)
{
sgma0.Update(new TValue(DateTime.UtcNow, prices[i]));
sgma2.Update(new TValue(DateTime.UtcNow, prices[i]));
}
// Both should produce reasonable values within the data range
Assert.True(sgma0.Last.Value >= 250 && sgma0.Last.Value <= 290);
Assert.True(sgma2.Last.Value >= 250 && sgma2.Last.Value <= 290);
}
[Fact]
public void Sgma_WeightSymmetry_ProducesSymmetricResponse()
{
// SGMA weights are symmetric around the center
// Test by feeding symmetric data and verifying symmetric output
const int period = 5;
var sgma = new Sgma(period, 2);
// Symmetric pattern: 100, 110, 120, 110, 100
double[] symmetric = [100, 110, 120, 110, 100];
TValue result = default;
foreach (var val in symmetric)
{
result = sgma.Update(new TValue(DateTime.UtcNow, val));
}
// Center value is 120, symmetric weights should produce value close to weighted average
// with center weighted higher
Assert.True(result.Value >= 100 && result.Value <= 120);
}
[Fact]
public void Sgma_HigherDegree_MoreResponsive()
{
// Higher polynomial degrees preserve shape better (more responsive to changes)
var gbm = new GBM(startPrice: 100.0, mu: 0.1, sigma: 0.3, seed: 42);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
var sgma0 = new Sgma(9, 0);
var sgma4 = new Sgma(9, 4);
var results0 = sgma0.Update(series);
var results4 = sgma4.Update(series);
// Calculate variance of differences from actual values
double sumSqDiff0 = 0, sumSqDiff4 = 0;
for (int i = 9; i < results0.Count; i++)
{
double actual = series[i].Value;
sumSqDiff0 += (results0[i].Value - actual) * (results0[i].Value - actual);
sumSqDiff4 += (results4[i].Value - actual) * (results4[i].Value - actual);
}
// Higher degree should track actual values more closely (lower sum of squared differences)
// But this is not guaranteed for all data, so just verify both are reasonable
Assert.True(double.IsFinite(sumSqDiff0));
Assert.True(double.IsFinite(sumSqDiff4));
}
[Fact]
public void Sgma_EvenPeriodAdjustment_ProducesOddPeriod()
{
// Even periods should be adjusted to odd
var sgma6 = new Sgma(6, 2);
var sgma10 = new Sgma(10, 2);
var sgma100 = new Sgma(100, 2);
Assert.Contains("7", sgma6.Name, StringComparison.Ordinal);
Assert.Contains("11", sgma10.Name, StringComparison.Ordinal);
Assert.Contains("101", sgma100.Name, StringComparison.Ordinal);
}
[Fact]
public void Sgma_AllModes_Match_AllDegrees()
{
// Verify batch, streaming, span, and eventing modes produce identical results
// for all polynomial degrees
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
var series = new TSeries();
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
for (int degree = 0; degree <= 4; degree++)
{
// Batch
var batchResults = Sgma.Batch(series, 9, degree);
double expected = batchResults.Last.Value;
// Span
double[] input = series.Values.ToArray();
double[] output = new double[input.Length];
Sgma.Batch(input.AsSpan(), output.AsSpan(), 9, degree);
Assert.Equal(expected, output[^1], Tolerance);
// Streaming
var streaming = new Sgma(9, degree);
foreach (var item in series)
{
streaming.Update(item);
}
Assert.Equal(expected, streaming.Last.Value, Tolerance);
// Eventing
var pubSource = new TSeries();
var eventing = new Sgma(pubSource, 9, degree);
foreach (var item in series)
{
pubSource.Add(item);
}
Assert.Equal(expected, eventing.Last.Value, Tolerance);
}
}
[Fact]
public void Sgma_NaN_Handling_Consistent_AllModes()
{
// Verify NaN handling is consistent across all modes
double[] sourceWithNaN = [100, 110, 120, double.NaN, 140, 150, 160, 170, 180];
double[] output = new double[sourceWithNaN.Length];
Sgma.Batch(sourceWithNaN.AsSpan(), output.AsSpan(), 5, 2);
// All outputs should be finite
foreach (var val in output)
{
Assert.True(double.IsFinite(val));
}
// Streaming should match span
var sgma = new Sgma(5, 2);
for (int i = 0; i < sourceWithNaN.Length; i++)
{
sgma.Update(new TValue(DateTime.UtcNow, sourceWithNaN[i]));
Assert.Equal(output[i], sgma.Last.Value, Tolerance);
}
}
[Fact]
public void Sgma_KnownValues_Degree2_Period5()
{
// Test with known input and verify mathematical correctness
// Period=5, Degree=2: weights follow w = 1 - normX^2
// positions: [-2, -1, 0, 1, 2] / 2 = [-1, -0.5, 0, 0.5, 1]
// weights: 1-1=0, 1-0.25=0.75, 1-0=1, 1-0.25=0.75, 1-1=0
// Sum of non-zero weights = 0.75 + 1 + 0.75 = 2.5
var sgma = new Sgma(5, 2);
// Feed 5 values: 100, 200, 300, 400, 500
sgma.Update(new TValue(DateTime.UtcNow, 100));
sgma.Update(new TValue(DateTime.UtcNow, 200));
sgma.Update(new TValue(DateTime.UtcNow, 300));
sgma.Update(new TValue(DateTime.UtcNow, 400));
sgma.Update(new TValue(DateTime.UtcNow, 500));
// Expected: (0*100 + 0.75*200 + 1*300 + 0.75*400 + 0*500) / 2.5
// = (0 + 150 + 300 + 300 + 0) / 2.5
// = 750 / 2.5 = 300
Assert.Equal(300.0, sgma.Last.Value, Tolerance);
}
[Fact]
public void Sgma_KnownValues_Degree0_Period5()
{
// Degree=0: all weights = 1.0 (equivalent to SMA)
var sgma = new Sgma(5, 0);
sgma.Update(new TValue(DateTime.UtcNow, 100));
sgma.Update(new TValue(DateTime.UtcNow, 200));
sgma.Update(new TValue(DateTime.UtcNow, 300));
sgma.Update(new TValue(DateTime.UtcNow, 400));
sgma.Update(new TValue(DateTime.UtcNow, 500));
// Expected: (100 + 200 + 300 + 400 + 500) / 5 = 1500 / 5 = 300
Assert.Equal(300.0, sgma.Last.Value, Tolerance);
}
}