Files
QuanTAlib/lib/statistics/cointegration/Cointegration.Validation.Tests.cs
T
2026-02-10 21:33:16 -08:00

330 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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
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
}