using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// KENDALL: Kendall's Rank Correlation Coefficient (Tau) /// A nonparametric measure that evaluates the degree of similarity between two sets /// of rankings by analyzing concordant and discordant pairs. Unlike Spearman correlation, /// Kendall's tau measures the ordinal association between two variables. /// /// /// The Kendall calculation process: /// 1. Compares each pair of observations /// 2. Counts concordant and discordant pairs /// 3. Handles ties in both variables /// /// Key characteristics: /// - Measures ordinal association /// - Range: -1 to +1 /// - Robust to outliers /// - More intuitive probabilistic interpretation /// - Less sensitive to error than Spearman /// /// Formula: /// τ = (nc - nd) / sqrt((n0 - n1)(n0 - n2)) /// where: /// nc = number of concordant pairs /// nd = number of discordant pairs /// n0 = n(n-1)/2 /// n1 = sum(u(u-1)/2) for ties in x /// n2 = sum(v(v-1)/2) for ties in y /// /// Market Applications: /// - Rank correlation analysis /// - Portfolio diversification /// - Risk assessment /// - Market trend analysis /// - Pattern recognition /// /// Sources: /// https://en.wikipedia.org/wiki/Kendall_rank_correlation_coefficient /// "Rank Correlation Methods" - Maurice G. Kendall /// /// Note: More robust to outliers and errors than other correlation measures /// [SkipLocalsInit] public sealed class Kendall : AbstractBase { private readonly int Period; private readonly CircularBuffer _xValues; private readonly CircularBuffer _yValues; private const double Epsilon = 1e-10; private const int MinimumPoints = 2; /// The number of points to consider for Kendall correlation calculation. /// Thrown when period is less than 2. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Kendall(int period) { if (period < MinimumPoints) { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2 for Kendall correlation calculation."); } Period = period; WarmupPeriod = MinimumPoints; _xValues = new CircularBuffer(period); _yValues = new CircularBuffer(period); Name = $"Kendall(period={period})"; Init(); } /// The data source object that publishes updates. /// The number of points to consider for Kendall correlation calculation. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Kendall(object source, int period) : this(period) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Init() { base.Init(); _xValues.Clear(); _yValues.Clear(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) { _lastValidValue = Input.Value; _index++; } } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static (int concordant, int discordant, int tiesX, int tiesY) CountPairs(ReadOnlySpan x, ReadOnlySpan y) { int n = x.Length; int concordant = 0; int discordant = 0; int tiesX = 0; int tiesY = 0; for (int i = 0; i < n - 1; i++) { if (double.IsNaN(x[i]) || double.IsNaN(y[i])) continue; for (int j = i + 1; j < n; j++) { if (double.IsNaN(x[j]) || double.IsNaN(y[j])) continue; double xDiff = x[i] - x[j]; double yDiff = y[i] - y[j]; if (Math.Abs(xDiff) < Epsilon && Math.Abs(yDiff) < Epsilon) { tiesX++; tiesY++; } else if (Math.Abs(xDiff) < Epsilon) { tiesX++; } else if (Math.Abs(yDiff) < Epsilon) { tiesY++; } else { int xSign = xDiff > 0 ? 1 : -1; int ySign = yDiff > 0 ? 1 : -1; if (xSign == ySign) { concordant++; } else { discordant++; } } } } return (concordant, discordant, tiesX, tiesY); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(Input.IsNew); _xValues.Add(Input.Value, Input.IsNew); _yValues.Add(Input2.Value, Input.IsNew); double correlation = 0; if (_xValues.Count >= MinimumPoints && _yValues.Count >= MinimumPoints) { ReadOnlySpan xValues = _xValues.GetSpan(); ReadOnlySpan yValues = _yValues.GetSpan(); var (concordant, discordant, tiesX, tiesY) = CountPairs(xValues, yValues); int n = xValues.Length; int n0 = (n * (n - 1)) / 2; // Calculate denominator considering ties double denominator = Math.Sqrt((n0 - tiesX) * (n0 - tiesY)); if (denominator > Epsilon) { correlation = (concordant - discordant) / denominator; } } IsHot = _xValues.Count >= Period && _yValues.Count >= Period; return correlation; } }