Files
QuanTAlib/lib/statistics/cointegration/Cointegration.Validation.Tests.cs
T

330 lines
10 KiB
C#
Raw Normal View History

namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Cointegration indicator.
/// Note: Cointegration is not commonly implemented in standard TA libraries.
/// These tests validate against expected statistical properties rather than
/// external library comparisons.
/// </summary>
public class CointegrationValidationTests
{
private const double Tolerance = 1e-6;
#region Statistical Property Validation
[Fact]
public void Cointegration_PerfectlyCointegrated_ProducesStrongNegativeAdf()
{
// Two series with near-perfect linear relationship should show strong cointegration
// Adding small noise to avoid zero-variance residuals
var indicator = new Cointegration(20);
var random = new Random(42);
for (int i = 0; i < 100; i++)
{
double a = 100.0 + i * 0.5 + (random.NextDouble() - 0.5) * 0.1;
double b = 2.0 * a + 10.0 + (random.NextDouble() - 0.5) * 0.1;
indicator.Update(a, b);
}
// Near-perfect cointegration should produce strongly negative ADF statistic
Assert.True(indicator.Last.Value < -2.0, $"ADF should be strongly negative for cointegrated series, got {indicator.Last.Value}");
}
[Fact]
public void Cointegration_IdenticalSeries_ProducesNegativeOrNaN()
{
// Two identical series produce zero residuals, which is mathematically correct
// but results in zero variance for ADF test (division by zero → NaN)
var indicator = new Cointegration(20);
for (int i = 0; i < 100; i++)
{
double value = 100.0 + Math.Sin(i * 0.1) * 10.0;
indicator.Update(value, value);
}
// Identical series produce zero residuals → NaN ADF (mathematically correct)
// This is expected behavior: perfect cointegration with no estimation error
Assert.True(double.IsNaN(indicator.Last.Value) || indicator.Last.Value < 0,
$"ADF should be NaN or negative for identical series, got {indicator.Last.Value}");
}
[Fact]
public void Cointegration_ProportionalSeries_WithNoise_ProducesNegativeAdf()
{
// B = k * A + small noise (near-proportional relationship)
var indicator = new Cointegration(20);
var random = new Random(42);
for (int i = 0; i < 100; i++)
{
double a = 50.0 + i * 0.3 + Math.Sin(i * 0.2) * 5.0;
double noise = (random.NextDouble() - 0.5) * 0.5;
double b = 1.5 * a + noise;
indicator.Update(a, b);
}
Assert.True(indicator.Last.Value < 0, $"ADF should be negative for near-proportional series, got {indicator.Last.Value}");
}
[Fact]
public void Cointegration_LinearWithNoise_StillDetectsCointegration()
{
// B = α + β*A + small_noise
var indicator = new Cointegration(20);
var random = new Random(42);
for (int i = 0; i < 100; i++)
{
double a = 100.0 + i * 0.2;
double noise = (random.NextDouble() - 0.5) * 0.5; // Small noise
double b = 25.0 + 0.8 * a + noise;
indicator.Update(a, b);
}
// Should still detect cointegration despite small noise
Assert.True(indicator.Last.Value < 0, $"ADF should be negative even with small noise, got {indicator.Last.Value}");
}
[Fact]
public void Cointegration_DifferentPeriods_ProduceDifferentResults()
{
var indicator10 = new Cointegration(10);
var indicator30 = new Cointegration(30);
for (int i = 0; i < 100; i++)
{
double a = 100.0 + i * 0.3;
double b = 50.0 + 0.5 * a + Math.Sin(i * 0.1);
indicator10.Update(a, b);
indicator30.Update(a, b);
}
// Different periods should yield different ADF values
Assert.NotEqual(indicator10.Last.Value, indicator30.Last.Value);
}
#endregion
#region Consistency Tests
[Fact]
public void Cointegration_BatchMatchesStreaming()
{
var seriesA = new TSeries();
var seriesB = new TSeries();
var baseTime = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
double a = 100.0 + i * 0.2 + Math.Sin(i * 0.1) * 3.0;
double b = 30.0 + 0.7 * a + Math.Cos(i * 0.15) * 2.0;
seriesA.Add(baseTime.AddMinutes(i), a);
seriesB.Add(baseTime.AddMinutes(i), b);
}
// Batch calculation
2026-02-10 21:33:16 -08:00
var batchResult = Cointegration.Batch(seriesA, seriesB, 20);
// Streaming calculation
var streamingIndicator = new Cointegration(20);
for (int i = 0; i < seriesA.Count; i++)
{
streamingIndicator.Update(seriesA[i].Value, seriesB[i].Value);
}
// Last values should match
if (double.IsNaN(batchResult.Last.Value) && double.IsNaN(streamingIndicator.Last.Value))
{
Assert.True(true);
}
else
{
Assert.Equal(batchResult.Last.Value, streamingIndicator.Last.Value, Tolerance);
}
}
[Fact]
public void Cointegration_SpanMatchesStreaming()
{
const int length = 50;
var seriesA = new double[length];
var seriesB = new double[length];
var output = new double[length];
for (int i = 0; i < length; i++)
{
seriesA[i] = 100.0 + i * 0.2 + Math.Sin(i * 0.1) * 3.0;
seriesB[i] = 30.0 + 0.7 * seriesA[i] + Math.Cos(i * 0.15) * 2.0;
}
// Span calculation
2026-02-10 21:33:16 -08:00
Cointegration.Batch(seriesA, seriesB, output, 20);
// Streaming calculation
var streamingIndicator = new Cointegration(20);
for (int i = 0; i < length; i++)
{
streamingIndicator.Update(seriesA[i], seriesB[i]);
}
// Last values should match
if (double.IsNaN(output[length - 1]) && double.IsNaN(streamingIndicator.Last.Value))
{
Assert.True(true);
}
else
{
Assert.Equal(output[length - 1], streamingIndicator.Last.Value, Tolerance);
}
}
[Fact]
public void Cointegration_ResetProducesSameResults()
{
var indicator = new Cointegration(20);
// First run
for (int i = 0; i < 50; i++)
{
double a = 100.0 + i * 0.3;
double b = 50.0 + 0.5 * a;
indicator.Update(a, b);
}
var firstResult = indicator.Last.Value;
indicator.Reset();
// Second run with same data
for (int i = 0; i < 50; i++)
{
double a = 100.0 + i * 0.3;
double b = 50.0 + 0.5 * a;
indicator.Update(a, b);
}
var secondResult = indicator.Last.Value;
Assert.Equal(firstResult, secondResult, Tolerance);
}
#endregion
#region Edge Cases
[Fact]
public void Cointegration_ConstantSeries_HandlesGracefully()
{
var indicator = new Cointegration(10);
// Both series are constant
for (int i = 0; i < 20; i++)
{
indicator.Update(100.0, 50.0);
}
// Should handle constant series without crashing (result may be NaN due to zero variance)
Assert.True(double.IsNaN(indicator.Last.Value) || double.IsFinite(indicator.Last.Value));
}
[Fact]
public void Cointegration_OneConstantOneTrending_HandlesGracefully()
{
var indicator = new Cointegration(10);
for (int i = 0; i < 20; i++)
{
indicator.Update(100.0, 50.0 + i); // A constant, B trending
}
// Should handle mixed constant/trending without crashing
Assert.True(double.IsNaN(indicator.Last.Value) || double.IsFinite(indicator.Last.Value));
}
[Fact]
public void Cointegration_SmallPeriod_WorksCorrectly()
{
var indicator = new Cointegration(3); // Minimum practical period
var random = new Random(42);
for (int i = 0; i < 20; i++)
{
double a = 100.0 + i + (random.NextDouble() - 0.5) * 0.1;
double b = 50.0 + 0.5 * a + (random.NextDouble() - 0.5) * 0.1;
indicator.Update(a, b);
}
Assert.True(indicator.IsHot);
// With small periods and noise, result may be finite or NaN
Assert.True(double.IsFinite(indicator.Last.Value) || double.IsNaN(indicator.Last.Value));
}
[Fact]
public void Cointegration_LargePeriod_WorksCorrectly()
{
var indicator = new Cointegration(100);
var random = new Random(42);
for (int i = 0; i < 150; i++)
{
double a = 100.0 + i * 0.1 + (random.NextDouble() - 0.5) * 0.1;
double b = 30.0 + 0.8 * a + (random.NextDouble() - 0.5) * 0.1;
indicator.Update(a, b);
}
Assert.True(indicator.IsHot);
// Should produce finite or NaN value (both acceptable for edge cases)
Assert.True(double.IsFinite(indicator.Last.Value) || double.IsNaN(indicator.Last.Value));
}
#endregion
#region Numerical Stability
[Fact]
public void Cointegration_LargeValues_MaintainsStability()
{
var indicator = new Cointegration(20);
for (int i = 0; i < 50; i++)
{
double a = 1e8 + i * 1e5;
double b = 2e8 + 2.0 * a;
indicator.Update(a, b);
}
Assert.True(double.IsFinite(indicator.Last.Value) || double.IsNaN(indicator.Last.Value));
}
[Fact]
public void Cointegration_SmallValues_MaintainsStability()
{
var indicator = new Cointegration(20);
for (int i = 0; i < 50; i++)
{
double a = 1e-6 + i * 1e-8;
double b = 2e-6 + 1.5 * a;
indicator.Update(a, b);
}
Assert.True(double.IsFinite(indicator.Last.Value) || double.IsNaN(indicator.Last.Value));
}
[Fact]
public void Cointegration_MixedMagnitudes_HandlesCorrectly()
{
var indicator = new Cointegration(20);
for (int i = 0; i < 50; i++)
{
double a = 1000.0 + i;
double b = 0.001 + 0.000001 * a; // Much smaller scale
indicator.Update(a, b);
}
Assert.True(double.IsFinite(indicator.Last.Value) || double.IsNaN(indicator.Last.Value));
}
#endregion
}