mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- 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
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class KendallConstructorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidPeriod_CreatesIndicator()
|
||||
{
|
||||
var indicator = new Kendall(20);
|
||||
Assert.Equal("Kendall(20)", indicator.Name);
|
||||
Assert.Equal(20, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MinimumValidPeriod_CreatesIndicator()
|
||||
{
|
||||
var indicator = new Kendall(2);
|
||||
Assert.Equal("Kendall(2)", indicator.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_IsTwenty()
|
||||
{
|
||||
var indicator = new Kendall();
|
||||
Assert.Equal("Kendall(20)", indicator.Name);
|
||||
Assert.Equal(20, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex1 = Assert.Throws<ArgumentException>(() => new Kendall(1));
|
||||
Assert.Equal("period", ex1.ParamName);
|
||||
|
||||
var ex2 = Assert.Throws<ArgumentException>(() => new Kendall(0));
|
||||
Assert.Equal("period", ex2.ParamName);
|
||||
|
||||
var ex3 = Assert.Throws<ArgumentException>(() => new Kendall(-5));
|
||||
Assert.Equal("period", ex3.ParamName);
|
||||
}
|
||||
}
|
||||
|
||||
public class KendallBasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void Update_SingleValue_ReturnsNaN()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
var result = indicator.Update(100.0, 200.0, true);
|
||||
Assert.True(double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TwoValues_ReturnsFinite()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
indicator.Update(100.0, 200.0, true);
|
||||
var result = indicator.Update(102.0, 204.0, true);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PerfectPositiveCorrelation_ReturnsOne()
|
||||
{
|
||||
var indicator = new Kendall(10);
|
||||
|
||||
// Monotonically increasing both series — all pairs concordant
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 + (2 * i);
|
||||
indicator.Update(x, y, true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_PerfectNegativeCorrelation_ReturnsMinusOne()
|
||||
{
|
||||
var indicator = new Kendall(10);
|
||||
|
||||
// x increasing, y decreasing — all pairs discordant
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 - (2 * i);
|
||||
indicator.Update(x, y, true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(-1.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantX_ReturnsZero()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
// Constant x means all x differences are 0 → product is 0 → no concordant/discordant
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(100.0, 200.0 + i, true);
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantY_ReturnsZero()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0, true);
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownSequence_CorrectTau()
|
||||
{
|
||||
// Known example: x = [1,2,3,4,5], y = [1,3,2,5,4]
|
||||
// Concordant pairs: (1,2),(1,3),(1,4),(1,5),(2,4),(2,5),(3,4),(3,5) = 8
|
||||
// Discordant pairs: (2,3),(4,5) = 2
|
||||
// Tau-a = (8-2)/(5*4/2) = 6/10 = 0.6
|
||||
var indicator = new Kendall(5);
|
||||
indicator.Update(1.0, 1.0, true);
|
||||
indicator.Update(2.0, 3.0, true);
|
||||
indicator.Update(3.0, 2.0, true);
|
||||
indicator.Update(4.0, 5.0, true);
|
||||
var result = indicator.Update(5.0, 4.0, true);
|
||||
|
||||
Assert.Equal(0.6, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ResultAlwaysInRange()
|
||||
{
|
||||
var indicator = new Kendall(10);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 12345);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.5, seed: 54321);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
var result = indicator.Update(x, y, true);
|
||||
|
||||
if (double.IsFinite(result.Value))
|
||||
{
|
||||
Assert.InRange(result.Value, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class KendallStateCorrectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Update_BarCorrection_RestoresState()
|
||||
{
|
||||
var indicator1 = new Kendall(5);
|
||||
var indicator2 = new Kendall(5);
|
||||
|
||||
// Feed same initial data
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 + (i * 0.5);
|
||||
indicator1.Update(x, y, true);
|
||||
indicator2.Update(x, y, true);
|
||||
}
|
||||
|
||||
// indicator1: Add another bar
|
||||
indicator1.Update(110.0, 205.0, true);
|
||||
|
||||
// indicator2: Add wrong bar, then correct
|
||||
indicator2.Update(999.0, 999.0, true);
|
||||
indicator2.Update(110.0, 205.0, false);
|
||||
|
||||
Assert.Equal(indicator1.Last.Value, indicator2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoreState()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
// Feed initial data
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
double x = 100.0 + i;
|
||||
double y = 200.0 + (i * 2);
|
||||
indicator.Update(x, y, true);
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
indicator.Update(108.0, 216.0, true);
|
||||
|
||||
// Make multiple corrections
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
double x = 108.0 + (j * 0.1);
|
||||
double y = 216.0 + (j * 0.2);
|
||||
_ = indicator.Update(x, y, false);
|
||||
}
|
||||
|
||||
// Final correction back to original
|
||||
indicator.Update(108.0, 216.0, false);
|
||||
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesBuffer()
|
||||
{
|
||||
var indicator = new Kendall(3);
|
||||
|
||||
indicator.Update(1.0, 10.0, true);
|
||||
indicator.Update(2.0, 20.0, true);
|
||||
indicator.Update(3.0, 30.0, true);
|
||||
|
||||
// All concordant: tau = 1.0
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-10);
|
||||
|
||||
// Add a 4th bar — buffer rolls, oldest drops
|
||||
indicator.Update(4.0, 40.0, true);
|
||||
Assert.Equal(1.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_DoesNotAdvanceBuffer()
|
||||
{
|
||||
var indicator = new Kendall(3);
|
||||
|
||||
indicator.Update(1.0, 10.0, true);
|
||||
indicator.Update(2.0, 20.0, true);
|
||||
indicator.Update(3.0, 30.0, true);
|
||||
|
||||
double beforeValue = indicator.Last.Value;
|
||||
|
||||
// Correct the last bar to same values — result unchanged
|
||||
indicator.Update(3.0, 30.0, false);
|
||||
Assert.Equal(beforeValue, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + (i * 2), true);
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
indicator.Reset();
|
||||
Assert.False(indicator.IsHot);
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
}
|
||||
|
||||
public class KendallWarmupTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsHot_BelowTwo_ReturnsFalse()
|
||||
{
|
||||
var indicator = new Kendall(10);
|
||||
indicator.Update(100.0, 200.0, true);
|
||||
Assert.False(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AtLeastTwoValues_ReturnsTrue()
|
||||
{
|
||||
var indicator = new Kendall(10);
|
||||
indicator.Update(100.0, 200.0, true);
|
||||
indicator.Update(101.0, 201.0, true);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesConstructorPeriod()
|
||||
{
|
||||
var indicator = new Kendall(15);
|
||||
Assert.Equal(15, indicator.WarmupPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
public class KendallRobustnessTests
|
||||
{
|
||||
[Fact]
|
||||
public void Update_NaNInputX_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + i, true);
|
||||
}
|
||||
|
||||
var result = indicator.Update(double.NaN, 205.0, true);
|
||||
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInputY_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + i, true);
|
||||
}
|
||||
|
||||
var result = indicator.Update(105.0, double.NaN, true);
|
||||
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNBothInputs_UsesLastValidValues()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + i, true);
|
||||
}
|
||||
|
||||
var result = indicator.Update(double.NaN, double.NaN, true);
|
||||
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(100.0 + i, 200.0 + i, true);
|
||||
}
|
||||
|
||||
var result = indicator.Update(double.PositiveInfinity, double.NegativeInfinity, true);
|
||||
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LargeDataset_NoOverflow()
|
||||
{
|
||||
var indicator = new Kendall(20);
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.05, sigma: 0.4, seed: 42);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.03, sigma: 0.3, seed: 84);
|
||||
|
||||
for (int i = 0; i < 5000; i++)
|
||||
{
|
||||
double x = gbmX.Next().Close;
|
||||
double y = gbmY.Next().Close;
|
||||
var result = indicator.Update(x, y, true);
|
||||
|
||||
if (double.IsFinite(result.Value))
|
||||
{
|
||||
Assert.InRange(result.Value, -1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class KendallConsistencyTests
|
||||
{
|
||||
[Fact]
|
||||
public void StreamingVsBatch_TSeries_Match()
|
||||
{
|
||||
int period = 10;
|
||||
int length = 100;
|
||||
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 42);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.4, seed: 123);
|
||||
|
||||
var seriesX = new TSeries(length);
|
||||
var seriesY = new TSeries(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var now = DateTime.UtcNow.AddMinutes(i);
|
||||
seriesX.Add(new TValue(now, gbmX.Next().Close));
|
||||
seriesY.Add(new TValue(now, gbmY.Next().Close));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streamIndicator = new Kendall(period);
|
||||
double[] streamResults = new double[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
streamResults[i] = streamIndicator.Update(
|
||||
seriesX.Values[i], seriesY.Values[i], true).Value;
|
||||
}
|
||||
|
||||
// Batch TSeries
|
||||
var batchResults = Kendall.Batch(seriesX, seriesY, period);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
if (double.IsFinite(streamResults[i]) && double.IsFinite(batchResults.Values[i]))
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StreamingVsBatch_Span_Match()
|
||||
{
|
||||
int period = 10;
|
||||
int length = 100;
|
||||
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 42);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.4, seed: 123);
|
||||
|
||||
double[] xData = new double[length];
|
||||
double[] yData = new double[length];
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
xData[i] = gbmX.Next().Close;
|
||||
yData[i] = gbmY.Next().Close;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var indicator = new Kendall(period);
|
||||
double[] streamResults = new double[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
streamResults[i] = indicator.Update(xData[i], yData[i], true).Value;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
double[] spanResults = new double[length];
|
||||
Kendall.Batch(xData, yData, spanResults, period);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
if (double.IsFinite(streamResults[i]) && double.IsFinite(spanResults[i]))
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
int period = 5;
|
||||
var seriesX = new TSeries(20);
|
||||
var seriesY = new TSeries(20);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var now = DateTime.UtcNow.AddMinutes(i);
|
||||
seriesX.Add(new TValue(now, 100.0 + i));
|
||||
seriesY.Add(new TValue(now, 200.0 + (i * 2)));
|
||||
}
|
||||
|
||||
var (results, indicator) = Kendall.Calculate(seriesX, seriesY, period);
|
||||
|
||||
Assert.Equal(20, results.Count);
|
||||
Assert.NotNull(indicator);
|
||||
}
|
||||
}
|
||||
|
||||
public class KendallSpanTests
|
||||
{
|
||||
[Fact]
|
||||
public void Batch_Span_ReturnsCorrectLength()
|
||||
{
|
||||
double[] seriesX = new double[20];
|
||||
double[] seriesY = new double[20];
|
||||
double[] output = new double[20];
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
seriesX[i] = 100.0 + i;
|
||||
seriesY[i] = 200.0 + (i * 2);
|
||||
}
|
||||
|
||||
Kendall.Batch(seriesX, seriesY, output, 5);
|
||||
|
||||
Assert.True(double.IsNaN(output[0]));
|
||||
Assert.True(double.IsFinite(output[19]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_DifferentLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] seriesX = new double[10];
|
||||
double[] seriesY = new double[15];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Kendall.Batch(seriesX, seriesY, output, 5));
|
||||
Assert.Equal("seriesY", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_OutputWrongLength_ThrowsArgumentException()
|
||||
{
|
||||
double[] seriesX = new double[20];
|
||||
double[] seriesY = new double[20];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Kendall.Batch(seriesX, seriesY, output, 5));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
double[] seriesX = new double[20];
|
||||
double[] seriesY = new double[20];
|
||||
double[] output = new double[20];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Kendall.Batch(seriesX, seriesY, output, 1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_DifferentLengths_ThrowsArgumentException()
|
||||
{
|
||||
var seriesX = new TSeries(10);
|
||||
var seriesY = new TSeries(15);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
seriesX.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
seriesY.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 200.0 + i));
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Kendall.Batch(seriesX, seriesY, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_NaN_Handled()
|
||||
{
|
||||
double[] seriesX = [100, 101, double.NaN, 103, 104, 105, 106, 107, 108, 109];
|
||||
double[] seriesY = [200, 201, 202, 203, double.NaN, 205, 206, 207, 208, 209];
|
||||
double[] output = new double[10];
|
||||
|
||||
Kendall.Batch(seriesX, seriesY, output, 5);
|
||||
|
||||
// After warmup, results should be finite
|
||||
for (int i = 5; i < 10; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"output[{i}] should be finite but was {output[i]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class KendallNotSupportedTests
|
||||
{
|
||||
[Fact]
|
||||
public void Update_TValue_ThrowsNotSupportedException()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
Assert.Throws<NotSupportedException>(() => indicator.Update(new TValue(DateTime.UtcNow, 100.0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ThrowsNotSupportedException()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
var series = new TSeries(10);
|
||||
Assert.Throws<NotSupportedException>(() => indicator.Update(series));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_ThrowsNotSupportedException()
|
||||
{
|
||||
var indicator = new Kendall(5);
|
||||
Assert.Throws<NotSupportedException>(() => indicator.Prime(new double[] { 1, 2, 3 }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Kendall Tau-a Rank Correlation Coefficient.
|
||||
/// Validates against known mathematical results and properties since
|
||||
/// no standard TA library implements Kendall Tau directly.
|
||||
/// </summary>
|
||||
public sealed class KendallValidationTests : IDisposable
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public KendallValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#region Mathematical Property Validation
|
||||
|
||||
[Fact]
|
||||
public void Validate_PerfectConcordance_TauEqualsOne()
|
||||
{
|
||||
// When both series are monotonically increasing with no ties,
|
||||
// all n(n-1)/2 pairs are concordant → τ = 1.0
|
||||
const int period = 10;
|
||||
var indicator = new Kendall(period);
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
indicator.Update((double)i, (double)i, true);
|
||||
}
|
||||
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Perfect concordance: τ = {indicator.Last.Value:G17} (expected 1.0)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_PerfectDiscordance_TauEqualsMinusOne()
|
||||
{
|
||||
// When one series is ascending and the other descending,
|
||||
// all pairs are discordant → τ = -1.0
|
||||
const int period = 10;
|
||||
var indicator = new Kendall(period);
|
||||
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
indicator.Update((double)i, (double)(period - 1 - i), true);
|
||||
}
|
||||
|
||||
Assert.Equal(-1.0, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Perfect discordance: τ = {indicator.Last.Value:G17} (expected -1.0)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownSequence_TauA()
|
||||
{
|
||||
// x = [1, 2, 3, 4, 5], y = [1, 3, 2, 5, 4]
|
||||
// Pairs: (1,2)(1,3)(1,4)(1,5)(2,3)(2,4)(2,5)(3,4)(3,5)(4,5) = 10 total
|
||||
// Concordant: (1,2)✓(1,3)✓(1,4)✓(1,5)✓(2,4)✓(2,5)✓(3,4)✓(3,5)✓ = 8
|
||||
// Discordant: (2,3)✗(4,5)✗ = 2
|
||||
// τ = (8-2)/10 = 0.6
|
||||
var indicator = new Kendall(5);
|
||||
indicator.Update(1.0, 1.0, true);
|
||||
indicator.Update(2.0, 3.0, true);
|
||||
indicator.Update(3.0, 2.0, true);
|
||||
indicator.Update(4.0, 5.0, true);
|
||||
indicator.Update(5.0, 4.0, true);
|
||||
|
||||
Assert.Equal(0.6, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Known sequence τ = {indicator.Last.Value:G17} (expected 0.6)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReverseKnownSequence_NegativeTau()
|
||||
{
|
||||
// x = [5, 4, 3, 2, 1], y = [1, 3, 2, 5, 4]
|
||||
// This reverses x → should yield τ = -0.6 (same magnitude, opposite sign)
|
||||
var indicator = new Kendall(5);
|
||||
indicator.Update(5.0, 1.0, true);
|
||||
indicator.Update(4.0, 3.0, true);
|
||||
indicator.Update(3.0, 2.0, true);
|
||||
indicator.Update(2.0, 5.0, true);
|
||||
indicator.Update(1.0, 4.0, true);
|
||||
|
||||
Assert.Equal(-0.6, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Reverse sequence τ = {indicator.Last.Value:G17} (expected -0.6)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllTied_TauEqualsZero()
|
||||
{
|
||||
// When all x values are identical, every pair has diffX=0 → product=0
|
||||
// No concordant or discordant pairs → τ = 0
|
||||
var indicator = new Kendall(5);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.Update(42.0, (double)i, true);
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"All-tied x: τ = {indicator.Last.Value:G17} (expected 0.0)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SymmetryProperty()
|
||||
{
|
||||
// τ(X,Y) should equal τ(Y,X)
|
||||
const int n = 20;
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 42);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.4, seed: 84);
|
||||
|
||||
double[] xData = new double[n];
|
||||
double[] yData = new double[n];
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
xData[i] = gbmX.Next().Close;
|
||||
yData[i] = gbmY.Next().Close;
|
||||
}
|
||||
|
||||
// τ(X,Y)
|
||||
var ind1 = new Kendall(10);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
ind1.Update(xData[i], yData[i], true);
|
||||
}
|
||||
|
||||
// τ(Y,X)
|
||||
var ind2 = new Kendall(10);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
ind2.Update(yData[i], xData[i], true);
|
||||
}
|
||||
|
||||
Assert.Equal(ind1.Last.Value, ind2.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Symmetry: τ(X,Y) = {ind1.Last.Value:G17}, τ(Y,X) = {ind2.Last.Value:G17}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AntisymmetryProperty()
|
||||
{
|
||||
// τ(X, -Y) should equal -τ(X, Y)
|
||||
const int n = 30;
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 55);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.4, seed: 77);
|
||||
|
||||
double[] xData = new double[n];
|
||||
double[] yData = new double[n];
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
xData[i] = gbmX.Next().Close;
|
||||
yData[i] = gbmY.Next().Close;
|
||||
}
|
||||
|
||||
// τ(X,Y)
|
||||
var ind1 = new Kendall(10);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
ind1.Update(xData[i], yData[i], true);
|
||||
}
|
||||
|
||||
// τ(X,-Y)
|
||||
var ind2 = new Kendall(10);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
ind2.Update(xData[i], -yData[i], true);
|
||||
}
|
||||
|
||||
Assert.Equal(-ind1.Last.Value, ind2.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Antisymmetry: τ(X,Y) = {ind1.Last.Value:G17}, τ(X,-Y) = {ind2.Last.Value:G17}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch vs Streaming Consistency
|
||||
|
||||
[Fact]
|
||||
public void Validate_BatchTSeries_MatchesStreaming()
|
||||
{
|
||||
const int period = 10;
|
||||
const int length = 200;
|
||||
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 42);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.4, seed: 123);
|
||||
|
||||
var seriesX = new TSeries(length);
|
||||
var seriesY = new TSeries(length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var now = DateTime.UtcNow.AddMinutes(i);
|
||||
seriesX.Add(new TValue(now, gbmX.Next().Close));
|
||||
seriesY.Add(new TValue(now, gbmY.Next().Close));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var indicator = new Kendall(period);
|
||||
double[] streamResults = new double[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
streamResults[i] = indicator.Update(
|
||||
seriesX.Values[i], seriesY.Values[i], true).Value;
|
||||
}
|
||||
|
||||
// Batch TSeries
|
||||
var batchResults = Kendall.Batch(seriesX, seriesY, period);
|
||||
|
||||
int matched = 0;
|
||||
for (int i = period; i < length; i++)
|
||||
{
|
||||
if (double.IsFinite(streamResults[i]) && double.IsFinite(batchResults.Values[i]))
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults.Values[i], Tolerance);
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(matched > 100, $"Only matched {matched} values (expected > 100)");
|
||||
_output.WriteLine($"Batch TSeries vs Streaming: {matched} values matched");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BatchSpan_MatchesStreaming()
|
||||
{
|
||||
const int period = 10;
|
||||
const int length = 200;
|
||||
|
||||
var gbmX = new GBM(startPrice: 100, mu: 0.02, sigma: 0.3, seed: 42);
|
||||
var gbmY = new GBM(startPrice: 200, mu: 0.01, sigma: 0.4, seed: 123);
|
||||
|
||||
double[] xData = new double[length];
|
||||
double[] yData = new double[length];
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
xData[i] = gbmX.Next().Close;
|
||||
yData[i] = gbmY.Next().Close;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var indicator = new Kendall(period);
|
||||
double[] streamResults = new double[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
streamResults[i] = indicator.Update(xData[i], yData[i], true).Value;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
double[] spanResults = new double[length];
|
||||
Kendall.Batch(xData, yData, spanResults, period);
|
||||
|
||||
int matched = 0;
|
||||
for (int i = period; i < length; i++)
|
||||
{
|
||||
if (double.IsFinite(streamResults[i]) && double.IsFinite(spanResults[i]))
|
||||
{
|
||||
Assert.Equal(streamResults[i], spanResults[i], Tolerance);
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(matched > 100, $"Only matched {matched} values (expected > 100)");
|
||||
_output.WriteLine($"Batch Span vs Streaming: {matched} values matched");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Known Analytical Values
|
||||
|
||||
[Fact]
|
||||
public void Validate_ThreeElements_KnownTau()
|
||||
{
|
||||
// x = [1, 2, 3], y = [3, 1, 2]
|
||||
// Pairs: (1,2): x↑y↓ disc, (1,3): x↑y↓ disc, (2,3): x↑y↑ conc
|
||||
// τ = (1-2)/3 = -1/3
|
||||
var indicator = new Kendall(3);
|
||||
indicator.Update(1.0, 3.0, true);
|
||||
indicator.Update(2.0, 1.0, true);
|
||||
indicator.Update(3.0, 2.0, true);
|
||||
|
||||
Assert.Equal(-1.0 / 3.0, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Three elements: τ = {indicator.Last.Value:G17} (expected {-1.0 / 3.0:G17})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_FourElements_AllConcordant()
|
||||
{
|
||||
// x = [1,2,3,4], y = [10,20,30,40]
|
||||
// All 6 pairs concordant → τ = 6/6 = 1.0
|
||||
var indicator = new Kendall(4);
|
||||
indicator.Update(1.0, 10.0, true);
|
||||
indicator.Update(2.0, 20.0, true);
|
||||
indicator.Update(3.0, 30.0, true);
|
||||
indicator.Update(4.0, 40.0, true);
|
||||
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Four elements all concordant: τ = {indicator.Last.Value:G17}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_FourElements_MixedPairs()
|
||||
{
|
||||
// x = [1,2,3,4], y = [2,4,1,3]
|
||||
// Pairs analysis:
|
||||
// (1,2): x↑ y↑ C (2,3): x↑ y↓ D (3,4): x↑ y↑ C
|
||||
// (1,3): x↑ y↓ D (2,4): x↑ y↓ D
|
||||
// (1,4): x↑ y↑ C
|
||||
// C=3, D=3 → τ = 0/6 = 0.0
|
||||
var indicator = new Kendall(4);
|
||||
indicator.Update(1.0, 2.0, true);
|
||||
indicator.Update(2.0, 4.0, true);
|
||||
indicator.Update(3.0, 1.0, true);
|
||||
indicator.Update(4.0, 3.0, true);
|
||||
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
_output.WriteLine($"Four elements mixed: τ = {indicator.Last.Value:G17} (expected 0.0)");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user