Implement ZTEST: One-Sample t-Test Statistic with validation tests

- Added Ztest class to compute the one-sample t-statistic using sample standard deviation with Bessel correction.
- Implemented validation tests for Ztest to ensure accuracy against manual calculations and PineScript.
- Updated documentation for Ztest, detailing its mathematical foundation, performance profile, and common pitfalls.
- Adjusted NDepend badges to reflect changes in code metrics after implementation.
- Updated missing indicators report to reflect the completion of statistical indicators, including ZTEST.
This commit is contained in:
Miha Kralj
2026-02-16 16:54:36 -08:00
parent 09ffd31a40
commit b3a64f18fa
73 changed files with 13041 additions and 88 deletions
@@ -0,0 +1,136 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public sealed class KendallIndicatorTests
{
[Fact]
public void KendallIndicator_Constructor_SetsDefaults()
{
var indicator = new KendallIndicator();
Assert.Equal(20, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.Equal(SourceType.Open, indicator.Source2);
Assert.True(indicator.ShowColdValues);
Assert.Equal("KENDALL - Kendall Tau-a Rank Correlation", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void KendallIndicator_MinHistoryDepths_EqualsTwo()
{
var indicator = new KendallIndicator();
Assert.Equal(2, KendallIndicator.MinHistoryDepths);
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void KendallIndicator_ShortName_IncludesPeriodAndSources()
{
var indicator = new KendallIndicator { Period = 20 };
Assert.Contains("KENDALL", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void KendallIndicator_Initialize_CreatesInternalKendall()
{
var indicator = new KendallIndicator { Period = 10 };
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void KendallIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new KendallIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
Assert.Equal(1, indicator.LinesSeries[0].Count);
}
[Fact]
public void KendallIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new KendallIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void KendallIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new KendallIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsNaN(firstValue) || double.IsFinite(firstValue));
Assert.True(double.IsNaN(secondValue) || double.IsFinite(secondValue));
}
[Fact]
public void KendallIndicator_MultipleUpdates_ProducesSequence()
{
var indicator = new KendallIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] opens = [100, 101, 102, 103, 104, 105];
double[] closes = [100, 101, 102, 103, 104, 105];
for (int i = 0; i < opens.Length; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), opens[i], opens[i] + 5, opens[i] - 5, closes[i]);
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
}
Assert.Equal(opens.Length, indicator.LinesSeries[0].Count);
}
[Fact]
public void KendallIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new KendallIndicator { Period = 5, Source = source, Source2 = SourceType.Close };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Should not throw and should produce output
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.Equal(1, indicator.LinesSeries[0].Count);
}
}
}
@@ -0,0 +1,79 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
/// <summary>
/// Quantower adapter for Kendall Tau-a Rank Correlation indicator.
/// Measures ordinal association between two price sources from the same symbol.
/// </summary>
/// <remarks>
/// This adapter compares two different price sources from the same symbol (e.g., Close vs Open,
/// Close vs Volume, High vs Low). For cross-symbol correlation, use the core
/// Kendall class directly.
///
/// Output is the Kendall Tau-a coefficient, ranging from -1 to +1.
/// Values near +1 indicate strong concordance, near -1 strong discordance.
/// </remarks>
[SkipLocalsInit]
public sealed class KendallIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 0, minimum: 2, maximum: 10000)]
public int Period { get; set; } = 20;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Source 2 Type", sortIndex: 2)]
public SourceType Source2 { get; set; } = SourceType.Open;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Kendall _kendall = null!;
private readonly LineSeries _series;
private string _sourceName = null!;
private Func<IHistoryItem, double> _priceSelector = null!;
private Func<IHistoryItem, double> _priceSelector2 = null!;
public static int MinHistoryDepths => 2;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"KENDALL({Period}):{_sourceName}/{Source2}";
public KendallIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "KENDALL - Kendall Tau-a Rank Correlation";
Description = "Measures ordinal association between two price sources. Range: -1 (discordant) to +1 (concordant).";
_series = new LineSeries(name: "Kendall", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
protected override void OnInit()
{
_priceSelector = Source.GetPriceSelector();
_priceSelector2 = Source2.GetPriceSelector();
_sourceName = Source.ToString();
_kendall = new Kendall(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
bool isNew = args.IsNewBar();
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
double valueA = _priceSelector(item);
double valueB = _priceSelector2(item);
var tvalA = new TValue(item.TimeLeft.Ticks, valueA);
var tvalB = new TValue(item.TimeLeft.Ticks, valueB);
double value = _kendall.Update(tvalA, tvalB, isNew).Value;
_series.SetValue(value, _kendall.IsHot, ShowColdValues);
}
}
+587
View File
@@ -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
}
+283
View File
@@ -0,0 +1,283 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Computes the Kendall Tau-a Rank Correlation Coefficient, which measures the ordinal
/// association between two series by counting concordant and discordant pairs.
/// </summary>
/// <remarks>
/// Kendall Tau-a Formula:
/// <c>τ = (C - D) / (n × (n - 1) / 2)</c>,
/// where <c>C</c> = concordant pairs, <c>D</c> = discordant pairs, <c>n</c> = window size.
///
/// A concordant pair (i,j) has both x_i &gt; x_j and y_i &gt; y_j (or both less).
/// A discordant pair has opposite ordering. Tied pairs contribute zero.
/// Output ranges from -1 (perfect disagreement) to +1 (perfect agreement).
///
/// This implementation recalculates pairwise comparisons from circular buffers each update.
/// The algorithm is O(n²) per update; no running-sum shortcut exists for rank statistics.
/// Non-finite inputs (NaN/±Inf) are sanitized by substituting the last finite value observed.
///
/// For the authoritative algorithm reference, full rationale, and behavioral contracts, see the
/// companion files in the same directory.
/// </remarks>
/// <seealso href="Kendall.md">Detailed documentation</seealso>
/// <seealso href="kendall.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Kendall : AbstractBase
{
private readonly RingBuffer _bufferX;
private readonly RingBuffer _bufferY;
private double _lastValidX, _lastValidY;
private const double Epsilon = 1e-10;
public override bool IsHot => _bufferX.Count >= 2;
/// <summary>
/// Creates a new Kendall Tau-a indicator.
/// </summary>
/// <param name="period">Lookback period for calculation (must be &gt; 1)</param>
public Kendall(int period = 20)
{
if (period <= 1)
{
throw new ArgumentException("Period must be greater than 1", nameof(period));
}
_bufferX = new RingBuffer(period);
_bufferY = new RingBuffer(period);
Name = $"Kendall({period})";
WarmupPeriod = period;
}
/// <summary>
/// Updates the Kendall indicator with new values from both series.
/// </summary>
/// <param name="seriesX">First series value</param>
/// <param name="seriesY">Second series value</param>
/// <param name="isNew">Whether this is a new bar</param>
/// <returns>Kendall Tau-a coefficient (-1 to +1)</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue seriesX, TValue seriesY, bool isNew = true)
{
double x = SanitizeX(seriesX.Value);
double y = SanitizeY(seriesY.Value);
if (isNew || _bufferX.Count == 0)
{
_bufferX.Add(x);
_bufferY.Add(y);
}
else
{
_bufferX.UpdateNewest(x);
_bufferY.UpdateNewest(y);
}
double tau = CalculateTau();
Last = new TValue(seriesX.Time, tau);
PubEvent(Last);
return Last;
}
/// <summary>
/// Updates with raw double values.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(double seriesX, double seriesY, bool isNew = true)
{
return Update(new TValue(DateTime.UtcNow, seriesX), new TValue(DateTime.UtcNow, seriesY), isNew);
}
/// <inheritdoc/>
/// <remarks>Not supported for dual-input indicator. Use Update(seriesX, seriesY) instead.</remarks>
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException("Kendall requires two inputs (seriesX and seriesY). Use Update(seriesX, seriesY).");
}
/// <inheritdoc/>
/// <remarks>Not supported for dual-input indicator. Use Batch(seriesX, seriesY, period) instead.</remarks>
public override TSeries Update(TSeries source)
{
throw new NotSupportedException("Kendall requires two inputs. Use Batch(seriesX, seriesY, period).");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double SanitizeX(double value)
{
if (double.IsFinite(value))
{
_lastValidX = value;
return value;
}
return double.IsFinite(_lastValidX) ? _lastValidX : 0.0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double SanitizeY(double value)
{
if (double.IsFinite(value))
{
_lastValidY = value;
return value;
}
return double.IsFinite(_lastValidY) ? _lastValidY : 0.0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateTau()
{
int n = _bufferX.Count;
if (n < 2)
{
return double.NaN;
}
int concordant = 0;
int discordant = 0;
for (int i = 0; i < n - 1; i++)
{
double xi = _bufferX[i];
double yi = _bufferY[i];
for (int j = i + 1; j < n; j++)
{
double diffX = xi - _bufferX[j];
double diffY = yi - _bufferY[j];
double product = diffX * diffY;
if (product > 0)
{
concordant++;
}
else if (product < 0)
{
discordant++;
}
// product == 0 means tie — contributes nothing to Tau-a
}
}
double denominator = (double)n * (n - 1) * 0.5;
if (denominator < Epsilon)
{
return double.NaN;
}
return (concordant - discordant) / denominator;
}
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
throw new NotSupportedException("Kendall requires two inputs.");
}
public override void Reset()
{
_bufferX.Clear();
_bufferY.Clear();
_lastValidX = 0;
_lastValidY = 0;
Last = default;
}
/// <summary>
/// Calculates Kendall Tau-a for two time series.
/// </summary>
public static TSeries Batch(TSeries seriesX, TSeries seriesY, int period = 20, Kendall? indicator = null)
{
if (seriesX.Count != seriesY.Count)
{
throw new ArgumentException("Series must have the same length", nameof(seriesY));
}
indicator ??= new Kendall(period);
var result = new TSeries(seriesX.Count);
var timesX = seriesX.Times;
var valuesX = seriesX.Values;
var valuesY = seriesY.Values;
for (int i = 0; i < seriesX.Count; i++)
{
var tvalX = new TValue(timesX[i], valuesX[i]);
var tvalY = new TValue(timesX[i], valuesY[i]);
result.Add(indicator.Update(tvalX, tvalY, isNew: true));
}
return result;
}
/// <summary>
/// Static batch calculation for span-based processing with NaN sanitization.
/// </summary>
public static void Batch(
ReadOnlySpan<double> seriesX,
ReadOnlySpan<double> seriesY,
Span<double> output,
int period = 20)
{
if (seriesX.Length != seriesY.Length)
{
throw new ArgumentException("Series must have the same length", nameof(seriesY));
}
if (seriesX.Length != output.Length)
{
throw new ArgumentException("Output must have the same length as input", nameof(output));
}
if (period <= 1)
{
throw new ArgumentException("Period must be greater than 1", nameof(period));
}
var indicator = new Kendall(period);
double lastValidX = 0;
double lastValidY = 0;
for (int i = 0; i < seriesX.Length; i++)
{
double x = seriesX[i];
double y = seriesY[i];
if (double.IsFinite(x))
{
lastValidX = x;
}
else
{
x = lastValidX;
}
if (double.IsFinite(y))
{
lastValidY = y;
}
else
{
y = lastValidY;
}
var result = indicator.Update(x, y, isNew: true);
output[i] = result.Value;
}
}
public static (TSeries Results, Kendall Indicator) Calculate(TSeries seriesX, TSeries seriesY, int period = 20)
{
var indicator = new Kendall(period);
TSeries results = Batch(seriesX, seriesY, period, indicator);
return (results, indicator);
}
}
+182
View File
@@ -0,0 +1,182 @@
# KENDALL: Kendall Tau-a Rank Correlation Coefficient
> "The rank is the message." -- adapted from Marshall McLuhan
<!-- QUICK REFERENCE CARD (scan in 5 seconds) -->
| Property | Value |
|--------------|-------|
| Category | Statistic |
| Inputs | Two source series (e.g., Close vs Open, or two separate instruments) |
| Parameters | `period` (int, default: 20, valid: >= 2) |
| Outputs | double (single value) |
| Output range | -1 to +1 |
| Warmup | 2 bars (produces finite output), `period` bars (full window) |
### Key takeaways
- Kendall Tau measures ordinal (rank-based) association between two series by counting concordant and discordant pairs.
- Primary use case: detecting monotonic relationships that Pearson correlation misses, because Kendall ignores magnitude.
- Unlike Pearson, Kendall is robust to outliers and non-linear monotonic relationships.
- O(n^2) per update makes it unsuitable for very large lookback periods (>60 recommended max).
- A Tau-a value of 0 does not mean independence; it means no net concordance/discordance among pairs.
## Historical Context
Maurice Kendall introduced the Tau coefficient in 1938 as a non-parametric measure of ordinal association. While Pearson's correlation (1896) measures linear relationships using raw values, Kendall recognized that many real-world relationships are monotonic but not linear. His approach counts concordant and discordant pairs without any distributional assumptions.
The Tau-a variant is the simplest form that does not adjust for tied pairs. Tau-b and Tau-c provide tie corrections, but for continuous financial data, ties are rare enough that Tau-a suffices. The Pine Script reference implementation uses Tau-a, and this implementation follows that convention.
In quantitative finance, Kendall Tau finds use in pairs trading (measuring rank agreement between two instruments), regime detection (tracking how ordinal relationships change over time), and risk management (capturing non-linear dependence structures that Pearson misses).
## What It Measures and Why It Matters
Kendall Tau answers a specific question: when one series goes up, does the other tend to go up (concordance) or down (discordance)? It does this by examining every possible pair of observations within the lookback window and classifying each as concordant, discordant, or tied.
This matters because financial returns often exhibit monotonic but non-linear relationships. Two assets might move in the same direction without proportional magnitudes. Pearson correlation weights large moves heavily (it uses raw differences from means), while Kendall treats every directional agreement equally. This makes Kendall more robust when you care about directional consistency rather than magnitude scaling.
The coefficient ranges from -1 (every pair disagrees) through 0 (no net tendency) to +1 (every pair agrees). For financial data, values beyond +/-0.5 indicate strong rank association.
## Mathematical Foundation
### Core Formula
$$
\tau_a = \frac{C - D}{\binom{n}{2}} = \frac{C - D}{\frac{n(n-1)}{2}}
$$
where:
- $C$ = number of concordant pairs
- $D$ = number of discordant pairs
- $n$ = number of observations in the lookback window
- $\binom{n}{2}$ = total number of distinct pairs
### Pair Classification
For observations $(x_i, y_i)$ and $(x_j, y_j)$ where $i < j$:
$$
\text{Concordant if } (x_i - x_j)(y_i - y_j) > 0
$$
$$
\text{Discordant if } (x_i - x_j)(y_i - y_j) < 0
$$
$$
\text{Tied if } (x_i - x_j)(y_i - y_j) = 0
$$
### Parameter Mapping
| Parameter | Symbol | Default | Constraint |
|-----------|--------|---------|------------|
| `period` | $n$ | 20 | $n \geq 2$ |
### Warmup Period
$$
\text{WarmupPeriod} = n
$$
The indicator produces finite values after 2 observations, but the full window requires $n$ bars.
## Architecture and Physics
The implementation uses two `RingBuffer` instances (one per series) to maintain the sliding window. On each update, the entire O(n^2) pairwise comparison is recalculated from the buffers.
### Why No Running Sums
Unlike Pearson correlation (which maintains running sums of x, y, xy, x^2, y^2), Kendall Tau cannot be incrementally updated when the window slides. Adding or removing a single observation affects its relationship with every other observation in the window. There is no algebraic shortcut to adjust concordant/discordant counts when one element enters and another leaves.
### Update Flow
1. Sanitize inputs (NaN/Infinity substitution with last valid value)
2. Add to buffers (`isNew=true`) or update newest (`isNew=false`)
3. Iterate all $\binom{n}{2}$ pairs, counting concordant and discordant
4. Compute $\tau = (C - D) / \binom{n}{2}$
### Edge Cases
- **NaN/Infinity inputs**: Substituted with last valid value per series. If no valid value exists, 0.0 is used.
- **Constant series**: All pair products are 0, resulting in $\tau = 0$.
- **Single observation**: Returns NaN (need at least 2 for a pair).
- **Division by zero**: Denominator $n(n-1)/2$ is zero only when $n < 2$, which returns NaN.
## Interpretation and Signals
### Signal Zones
| Zone | Level | Interpretation |
|------|-------|----------------|
| Strong concordance | > 0.5 | Series consistently move in same direction |
| Weak concordance | 0.1 to 0.5 | Mild directional agreement |
| No association | -0.1 to 0.1 | No consistent directional pattern |
| Weak discordance | -0.5 to -0.1 | Mild directional disagreement |
| Strong discordance | < -0.5 | Series consistently move in opposite directions |
### Signal Patterns
- **Regime detection**: Track $\tau$ over time. Sudden drops from positive to negative suggest relationship breakdown, common before market dislocations.
- **Pairs trading**: High positive $\tau$ between two instruments suggests directional co-movement suitable for mean-reversion strategies.
- **Divergence**: When Pearson correlation and Kendall $\tau$ disagree meaningfully, it signals that the relationship is driven by a few large moves (outliers) rather than consistent directional agreement.
### Practical Notes
Kendall Tau works best with moderate lookback periods (10-30). Very short periods yield noisy estimates. Very long periods (>60) become computationally expensive at O(n^2) per bar. For real-time streaming, keep the period under 60 to avoid latency.
## Related Indicators
- **[Correlation](../correlation/Correlation.md)**: Pearson coefficient. Measures linear (not just monotonic) relationships. Faster O(1) updates but sensitive to outliers.
- **[Covariance](../covariance/Covariance.md)**: Unstandardized measure of joint variability. Building block for Pearson but not rank-based.
## Validation
Validated against known mathematical results and properties in `Kendall.Validation.Tests.cs`.
No standard TA library (TA-Lib, Skender, Tulip, Ooples) implements Kendall Tau directly.
| Library | Batch | Streaming | Span | Notes |
|---------|:-----:|:---------:|:----:|-------|
| **TA-Lib** | -- | -- | -- | Not available |
| **Skender** | -- | -- | -- | Not available |
| **Tulip** | -- | -- | -- | Not available |
| **Ooples** | -- | -- | -- | Not available |
| **Math properties** | ✓ | ✓ | ✓ | Known-value, symmetry, antisymmetry validated |
## Performance Profile
### Key Optimizations
- **No SIMD**: The pairwise comparison involves data-dependent branching (concordant vs discordant), making vectorization impractical.
- **Aggressive inlining**: `CalculateTau()` and `SanitizeX/Y` are inlined.
- **No heap allocation**: All state lives in `RingBuffer`; no per-update allocations.
### Operation Count (Streaming Mode)
| Operation | Count | Cost (cycles) | Subtotal |
|-----------|------:|:-------------:|:--------:|
| CMP (pair) | $n(n-1)/2$ | 1 | $\binom{n}{2}$ |
| MUL (product) | $n(n-1)/2$ | 3 | $3\binom{n}{2}$ |
| ADD (counters) | $n(n-1)/2$ | 1 | $\binom{n}{2}$ |
| DIV (final) | 1 | 15 | 15 |
| **Total** | -- | -- | **~5n^2/2** |
For period=20: ~1000 cycles per update. For period=60: ~9000 cycles.
## Common Pitfalls
1. **O(n^2) complexity**: Each `Update()` call performs $n(n-1)/2$ pair comparisons. Keep `period` reasonable (<60) for real-time use.
2. **Tau-a vs Tau-b**: This implements Tau-a, which does not adjust for ties. For discrete data with many ties, Tau-b would be more appropriate (divide by geometric mean of non-tied pairs instead).
3. **isNew parameter**: When `isNew=false`, the newest buffer entries are overwritten (bar correction). Since Kendall recalculates from buffers, this is inherently safe.
4. **Not a test of independence**: $\tau = 0$ means no net concordance/discordance, not statistical independence.
5. **Confidence interpretation**: For small samples ($n < 10$), $\tau$ has high variance. Values should be interpreted cautiously without additional hypothesis testing.
6. **Comparison with Pearson**: Kendall $\tau$ values are typically smaller in magnitude than Pearson $r$ for the same data. Do not compare them directly.
7. **NaN handling**: Non-finite inputs are replaced with the last valid value per series. Extended NaN sequences cause the buffer to fill with repeated values, reducing effective sample size.
## References
- Kendall, M. G. (1938). "A New Measure of Rank Correlation." *Biometrika*, 30(1/2), 81-93.
- Kendall, M. G. (1948). *Rank Correlation Methods*. Charles Griffin & Company.
- Abdi, H. (2007). "Kendall Rank Correlation." In *Encyclopedia of Measurement and Statistics*, Sage Publications.
- [Wikipedia: Kendall rank correlation coefficient](https://en.wikipedia.org/wiki/Kendall_rank_correlation_coefficient)