mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-31 02:47:44 +00:00
060649192f
- 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
659 lines
21 KiB
C#
659 lines
21 KiB
C#
namespace QuanTAlib.Tests;
|
|
|
|
public class CointegrationTests
|
|
{
|
|
private const int DefaultPeriod = 20;
|
|
private const double Tolerance = 1e-10;
|
|
|
|
#region Constructor Tests
|
|
|
|
[Fact]
|
|
public void Constructor_WithValidPeriod_SetsProperties()
|
|
{
|
|
var indicator = new Cointegration(10);
|
|
|
|
Assert.Equal("Cointegration(10)", indicator.Name);
|
|
Assert.Equal(11, indicator.WarmupPeriod); // period + 1
|
|
Assert.False(indicator.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithDefaultPeriod_UsesTwenty()
|
|
{
|
|
var indicator = new Cointegration();
|
|
|
|
Assert.Equal("Cointegration(20)", indicator.Name);
|
|
Assert.Equal(21, indicator.WarmupPeriod);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithPeriodOne_ThrowsArgumentException()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Cointegration(1));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithPeriodZero_ThrowsArgumentException()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Cointegration(0));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithNegativePeriod_ThrowsArgumentException()
|
|
{
|
|
var ex = Assert.Throws<ArgumentException>(() => new Cointegration(-5));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Basic Calculation Tests
|
|
|
|
[Fact]
|
|
public void Update_ReturnsTValue()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
|
|
var result = indicator.Update(100.0, 100.0);
|
|
|
|
Assert.IsType<TValue>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_ReturnsNaN_BeforeWarmup()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
|
|
// First few updates should return NaN until warmup
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
var result = indicator.Update(100.0 + i, 100.0 + i);
|
|
Assert.True(double.IsNaN(result.Value));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_ReturnsFiniteValue_AfterWarmup()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
// Feed enough data to warm up
|
|
for (int i = 0; i < DefaultPeriod + 5; i++)
|
|
{
|
|
indicator.Update(gbmA.Next().Close, gbmB.Next().Close);
|
|
}
|
|
|
|
Assert.True(double.IsFinite(indicator.Last.Value));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_IsHot_BecomesTrueAfterWarmup()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
Assert.False(indicator.IsHot);
|
|
|
|
for (int i = 0; i < DefaultPeriod + 2; i++)
|
|
{
|
|
indicator.Update(gbmA.Next().Close, gbmB.Next().Close);
|
|
}
|
|
|
|
Assert.True(indicator.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_LastProperty_ReturnsLastCalculatedValue()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
TValue lastResult = default;
|
|
for (int i = 0; i < DefaultPeriod + 5; i++)
|
|
{
|
|
lastResult = indicator.Update(gbmA.Next().Close, gbmB.Next().Close);
|
|
}
|
|
|
|
Assert.Equal(lastResult.Value, indicator.Last.Value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region isNew Behavior Tests
|
|
|
|
[Fact]
|
|
public void Update_WithIsNewTrue_AdvancesState()
|
|
{
|
|
var indicator = new Cointegration(5);
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
// Build up state past warmup period (period + 1 = 6)
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
indicator.Update(gbmA.Next().Close, gbmB.Next().Close, isNew: true);
|
|
}
|
|
var result1 = indicator.Last;
|
|
|
|
// Next update with isNew=true should advance and produce different value
|
|
indicator.Update(gbmA.Next().Close, gbmB.Next().Close, isNew: true);
|
|
var result2 = indicator.Last;
|
|
|
|
// Values should differ (both should be finite after warmup)
|
|
Assert.True(double.IsFinite(result1.Value));
|
|
Assert.True(double.IsFinite(result2.Value));
|
|
Assert.NotEqual(result1.Value, result2.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_WithIsNewFalse_DoesNotAdvanceState()
|
|
{
|
|
var corrected = new Cointegration(5);
|
|
var direct = new Cointegration(5);
|
|
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
// Build identical state
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
double a = gbmA.Next().Close;
|
|
double b = gbmB.Next().Close;
|
|
corrected.Update(a, b, isNew: true);
|
|
direct.Update(a, b, isNew: true);
|
|
}
|
|
|
|
const double finalA = 105.0;
|
|
const double finalB = 55.0;
|
|
|
|
// Correction path: add + multiple rewrites + final rewrite to target value
|
|
corrected.Update(finalA, finalB, isNew: true);
|
|
corrected.Update(finalA + 10.0, finalB + 10.0, isNew: false);
|
|
corrected.Update(finalA - 3.0, finalB - 3.0, isNew: false);
|
|
corrected.Update(finalA, finalB, isNew: false);
|
|
|
|
// Direct path: only final new bar
|
|
direct.Update(finalA, finalB, isNew: true);
|
|
|
|
Assert.Equal(direct.Last.Value, corrected.Last.Value, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_BarCorrection_RestoresStateCorrectly()
|
|
{
|
|
var indicator1 = new Cointegration(5);
|
|
var indicator2 = new Cointegration(5);
|
|
|
|
// Build up identical state using stored values
|
|
var valuesA = new double[] { 100.0, 101.5, 99.8, 102.3, 100.9, 103.2, 98.7, 104.1, 99.5, 101.8 };
|
|
var valuesB = new double[] { 50.0, 51.2, 49.5, 52.0, 50.8, 51.9, 49.2, 52.5, 50.1, 51.5 };
|
|
|
|
for (int i = 0; i < valuesA.Length; i++)
|
|
{
|
|
indicator1.Update(valuesA[i], valuesB[i], isNew: true);
|
|
indicator2.Update(valuesA[i], valuesB[i], isNew: true);
|
|
}
|
|
|
|
// Both should have same state now
|
|
Assert.Equal(indicator1.Last.Value, indicator2.Last.Value, Tolerance);
|
|
|
|
// Indicator1: add new bar, then correct it, then another new bar
|
|
indicator1.Update(105.0, 53.0, isNew: true);
|
|
indicator1.Update(999.0, 999.0, isNew: false); // correction (overwrites previous)
|
|
indicator1.Update(106.0, 54.0, isNew: true);
|
|
|
|
// Indicator2: skip the 105/53 bar entirely, just add the 106/54 bar
|
|
indicator2.Update(106.0, 54.0, isNew: true);
|
|
|
|
// Both should have same result since the 105/53 was replaced by correction
|
|
// and then 106/54 was added as new - but indicator1 had an intermediate
|
|
// correction step that should be equivalent to indicator2 which never
|
|
// added the original value.
|
|
|
|
// Actually the test is wrong - indicator1 has 12 bars, indicator2 has 11 bars
|
|
// Let's verify the correction overwrites work correctly instead
|
|
|
|
var indicator3 = new Cointegration(5);
|
|
for (int i = 0; i < valuesA.Length; i++)
|
|
{
|
|
indicator3.Update(valuesA[i], valuesB[i], isNew: true);
|
|
}
|
|
|
|
// Add with correction pattern
|
|
indicator3.Update(105.0, 53.0, isNew: true); // bar 11
|
|
var afterFirstNew = indicator3.Last.Value;
|
|
|
|
indicator3.Update(110.0, 55.0, isNew: false); // correct bar 11
|
|
_ = indicator3.Last.Value; // afterCorrection - verify no exception
|
|
|
|
indicator3.Update(105.0, 53.0, isNew: false); // correct back to original
|
|
var afterSecondCorrection = indicator3.Last.Value;
|
|
|
|
// After correcting back to original values, should match first new
|
|
Assert.Equal(afterFirstNew, afterSecondCorrection, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_IterativeCorrections_ProduceSameResult()
|
|
{
|
|
var indicator = new Cointegration(5);
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
// Build up state
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
indicator.Update(gbmA.Next().Close, gbmB.Next().Close, isNew: true);
|
|
}
|
|
|
|
double finalA = 50.0;
|
|
double finalB = 55.0;
|
|
|
|
// Apply multiple corrections, each time with different intermediate values
|
|
indicator.Update(100.0, 105.0, isNew: true);
|
|
indicator.Update(200.0, 205.0, isNew: false);
|
|
indicator.Update(300.0, 305.0, isNew: false);
|
|
indicator.Update(finalA, finalB, isNew: false);
|
|
var resultWithCorrections = indicator.Last.Value;
|
|
|
|
// Reset and rebuild state using fresh GBMs
|
|
indicator.Reset();
|
|
var gbmA2 = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB2 = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
indicator.Update(gbmA2.Next().Close, gbmB2.Next().Close, isNew: true);
|
|
}
|
|
|
|
// Apply final value directly
|
|
indicator.Update(finalA, finalB, isNew: true);
|
|
var resultDirect = indicator.Last.Value;
|
|
|
|
Assert.Equal(resultDirect, resultWithCorrections, Tolerance);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Reset Tests
|
|
|
|
[Fact]
|
|
public void Reset_ClearsState()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
for (int i = 0; i < DefaultPeriod + 5; i++)
|
|
{
|
|
indicator.Update(gbmA.Next().Close, gbmB.Next().Close);
|
|
}
|
|
|
|
Assert.True(indicator.IsHot);
|
|
|
|
indicator.Reset();
|
|
|
|
Assert.False(indicator.IsHot);
|
|
Assert.Equal(default, indicator.Last);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_AllowsReuse()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
|
|
// First use
|
|
var gbmA1 = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB1 = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
for (int i = 0; i < DefaultPeriod + 5; i++)
|
|
{
|
|
indicator.Update(gbmA1.Next().Close, gbmB1.Next().Close);
|
|
}
|
|
var firstResult = indicator.Last.Value;
|
|
|
|
indicator.Reset();
|
|
|
|
// Second use with same seeds
|
|
var gbmA2 = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB2 = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
for (int i = 0; i < DefaultPeriod + 5; i++)
|
|
{
|
|
indicator.Update(gbmA2.Next().Close, gbmB2.Next().Close);
|
|
}
|
|
var secondResult = indicator.Last.Value;
|
|
|
|
Assert.Equal(firstResult, secondResult, Tolerance);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region NaN/Infinity Handling Tests
|
|
|
|
[Fact]
|
|
public void Update_WithNaN_UsesLastValidValue()
|
|
{
|
|
var indicator = new Cointegration(5);
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
indicator.Update(100.0 + i, 100.0 + i * 0.5);
|
|
}
|
|
|
|
_ = indicator.Last.Value; // beforeNaN - verify state before NaN
|
|
|
|
// Update with NaN
|
|
indicator.Update(double.NaN, double.NaN);
|
|
var afterNaN = indicator.Last.Value;
|
|
|
|
// Should still produce a valid (or NaN) result, not crash
|
|
Assert.True(double.IsFinite(afterNaN) || double.IsNaN(afterNaN));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_WithInfinity_UsesLastValidValue()
|
|
{
|
|
var indicator = new Cointegration(5);
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
indicator.Update(100.0 + i, 100.0 + i * 0.5);
|
|
}
|
|
|
|
// Update with infinity
|
|
indicator.Update(double.PositiveInfinity, double.NegativeInfinity);
|
|
var afterInfinity = indicator.Last.Value;
|
|
|
|
Assert.True(double.IsFinite(afterInfinity) || double.IsNaN(afterInfinity));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_BatchWithNaN_HandlesSafely()
|
|
{
|
|
var indicator = new Cointegration(5);
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
double a = i % 5 == 0 ? double.NaN : 100.0 + i;
|
|
double b = i % 7 == 0 ? double.NaN : 100.0 + i * 0.5;
|
|
indicator.Update(a, b);
|
|
}
|
|
|
|
// Should complete without exception
|
|
Assert.True(true);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Static Calculate Tests
|
|
|
|
[Fact]
|
|
public void Calculate_TSeries_ReturnsCorrectLength()
|
|
{
|
|
var seriesA = new TSeries();
|
|
var seriesB = new TSeries();
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
var barA = gbmA.Next();
|
|
var barB = gbmB.Next();
|
|
seriesA.Add(barA.Time, barA.Close);
|
|
seriesB.Add(barB.Time, barB.Close);
|
|
}
|
|
|
|
var result = Cointegration.Batch(seriesA, seriesB, DefaultPeriod);
|
|
|
|
Assert.Equal(seriesA.Count, result.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_TSeries_MatchesStreamingMode()
|
|
{
|
|
var seriesA = new TSeries();
|
|
var seriesB = new TSeries();
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var barA = gbmA.Next();
|
|
var barB = gbmB.Next();
|
|
seriesA.Add(barA.Time, barA.Close);
|
|
seriesB.Add(barB.Time, barB.Close);
|
|
}
|
|
|
|
// Batch calculation
|
|
var batchResult = Cointegration.Batch(seriesA, seriesB, DefaultPeriod);
|
|
|
|
// Streaming calculation
|
|
var streamingIndicator = new Cointegration(DefaultPeriod);
|
|
var streamingResult = new TSeries();
|
|
for (int i = 0; i < seriesA.Count; i++)
|
|
{
|
|
var result = streamingIndicator.Update(seriesA[i].Value, seriesB[i].Value);
|
|
streamingResult.Add(result);
|
|
}
|
|
|
|
// Compare last 10 values (after warmup)
|
|
for (int i = seriesA.Count - 10; i < seriesA.Count; i++)
|
|
{
|
|
if (double.IsNaN(batchResult[i].Value) && double.IsNaN(streamingResult[i].Value))
|
|
{
|
|
continue;
|
|
}
|
|
Assert.Equal(batchResult[i].Value, streamingResult[i].Value, Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_TSeries_ThrowsOnMismatchedLengths()
|
|
{
|
|
var seriesA = new TSeries();
|
|
var seriesB = new TSeries();
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var bar = gbmA.Next();
|
|
seriesA.Add(bar.Time, bar.Close);
|
|
}
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
var bar = gbmB.Next();
|
|
seriesB.Add(bar.Time, bar.Close);
|
|
}
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Cointegration.Batch(seriesA, seriesB, DefaultPeriod));
|
|
Assert.Equal("seriesB", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_Span_MatchesStreaming()
|
|
{
|
|
const int length = 50;
|
|
var seriesA = new double[length];
|
|
var seriesB = new double[length];
|
|
var output = new double[length];
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
seriesA[i] = gbmA.Next().Close;
|
|
seriesB[i] = gbmB.Next().Close;
|
|
}
|
|
|
|
// Span calculation
|
|
Cointegration.Batch(seriesA, seriesB, output, DefaultPeriod);
|
|
|
|
// Streaming calculation
|
|
var streamingIndicator = new Cointegration(DefaultPeriod);
|
|
var streamingOutput = new double[length];
|
|
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
var result = streamingIndicator.Update(seriesA[i], seriesB[i]);
|
|
streamingOutput[i] = result.Value;
|
|
}
|
|
|
|
// Compare last 10 values
|
|
for (int i = length - 10; i < length; i++)
|
|
{
|
|
if (double.IsNaN(output[i]) && double.IsNaN(streamingOutput[i]))
|
|
{
|
|
continue;
|
|
}
|
|
Assert.Equal(output[i], streamingOutput[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_Span_ThrowsOnMismatchedLengths()
|
|
{
|
|
var seriesA = new double[50];
|
|
var seriesB = new double[30];
|
|
var output = new double[50];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Cointegration.Batch(seriesA, seriesB, output, DefaultPeriod));
|
|
Assert.Equal("seriesB", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_Span_ThrowsOnMismatchedOutputLength()
|
|
{
|
|
var seriesA = new double[50];
|
|
var seriesB = new double[50];
|
|
var output = new double[30];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Cointegration.Batch(seriesA, seriesB, output, DefaultPeriod));
|
|
Assert.Equal("output", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_Span_ThrowsOnInvalidPeriod()
|
|
{
|
|
var seriesA = new double[50];
|
|
var seriesB = new double[50];
|
|
var output = new double[50];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Cointegration.Batch(seriesA, seriesB, output, 1));
|
|
Assert.Equal("period", ex.ParamName);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Unsupported Method Tests
|
|
|
|
[Fact]
|
|
public void Update_SingleTValue_ThrowsNotSupported()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
|
|
Assert.Throws<NotSupportedException>(() => indicator.Update(new TValue(DateTime.UtcNow, 100.0)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_SingleTSeries_ThrowsNotSupported()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
var series = new TSeries();
|
|
series.Add(DateTime.UtcNow, 100.0);
|
|
|
|
Assert.Throws<NotSupportedException>(() => indicator.Update(series));
|
|
}
|
|
|
|
[Fact]
|
|
public void Prime_ThrowsNotSupported()
|
|
{
|
|
var indicator = new Cointegration(DefaultPeriod);
|
|
var data = new double[] { 1.0, 2.0, 3.0 };
|
|
|
|
Assert.Throws<NotSupportedException>(() => indicator.Prime(data));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cointegration-Specific Tests
|
|
|
|
[Fact]
|
|
public void Update_CointegatedSeries_ProducesNegativeAdf()
|
|
{
|
|
// Create two cointegrated series: B = A + noise
|
|
var indicator = new Cointegration(20);
|
|
var random = new GBM(startPrice: 100.0, sigma: 1.0, seed: 42);
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
double a = 100.0 + i * 0.1;
|
|
double b = a + Math.Log(random.Next().Close / 100.0) * 0.1; // Highly correlated
|
|
|
|
indicator.Update(a, b);
|
|
}
|
|
|
|
// Cointegrated series should produce negative ADF statistic
|
|
Assert.True(indicator.Last.Value < 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_NonCointegatedSeries_ProducesLessNegativeAdf()
|
|
{
|
|
// Create two non-cointegrated series (random walks)
|
|
var indicatorCointegrated = new Cointegration(20);
|
|
var indicatorRandom = new Cointegration(20);
|
|
var random = new GBM(startPrice: 100.0, sigma: 1.0, seed: 42);
|
|
|
|
double walkA = 100.0;
|
|
double walkB = 100.0;
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
// Cointegrated pair
|
|
double a1 = 100.0 + i * 0.1;
|
|
double noise1 = Math.Log(random.Next().Close / 100.0);
|
|
double b1 = a1 + noise1 * 0.1;
|
|
indicatorCointegrated.Update(a1, b1);
|
|
|
|
// Random walks
|
|
walkA += Math.Log(random.Next().Close / 100.0);
|
|
walkB += Math.Log(random.Next().Close / 100.0);
|
|
indicatorRandom.Update(walkA, walkB);
|
|
}
|
|
|
|
// Note: Due to randomness, we just verify both produce finite values
|
|
Assert.True(double.IsFinite(indicatorCointegrated.Last.Value) || double.IsNaN(indicatorCointegrated.Last.Value));
|
|
Assert.True(double.IsFinite(indicatorRandom.Last.Value) || double.IsNaN(indicatorRandom.Last.Value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Tests
|
|
|
|
[Fact]
|
|
public void Pub_FiresOnUpdate()
|
|
{
|
|
var indicator = new Cointegration(5);
|
|
var gbmA = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 12345);
|
|
var gbmB = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.1, seed: 54321);
|
|
int eventCount = 0;
|
|
|
|
indicator.Pub += (sender, in args) => eventCount++;
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
indicator.Update(gbmA.Next().Close, gbmB.Next().Close);
|
|
}
|
|
|
|
Assert.Equal(10, eventCount);
|
|
}
|
|
|
|
#endregion
|
|
}
|