mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-04 04:07:42 +00:00
330 lines
10 KiB
C#
330 lines
10 KiB
C#
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
|
||
} |