2026-02-02 19:47:21 -08:00
|
|
|
|
using System.Runtime.CompilerServices;
|
|
|
|
|
|
using static System.Math;
|
|
|
|
|
|
|
|
|
|
|
|
namespace QuanTAlib;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Cointegration: Measures the statistical equilibrium relationship between two price series
|
|
|
|
|
|
/// using the Engle-Granger two-step method with Augmented Dickey-Fuller test.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// Cointegration tests whether two non-stationary time series have a long-run equilibrium
|
|
|
|
|
|
/// relationship. The indicator returns the ADF test statistic for the regression residuals.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Algorithm:
|
|
|
|
|
|
/// 1. Estimate linear regression: A = α + β*B + ε
|
|
|
|
|
|
/// - β = correlation(A,B) × (σA/σB)
|
|
|
|
|
|
/// - α = mean(A) - β × mean(B)
|
|
|
|
|
|
/// 2. Calculate residuals: ε = A - (α + β×B)
|
|
|
|
|
|
/// 3. Run ADF test on residuals:
|
|
|
|
|
|
/// - Δε_t = γ × ε_{t-1} + u_t
|
|
|
|
|
|
/// - ADF statistic = γ / SE(γ)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Interpretation:
|
|
|
|
|
|
/// - More negative ADF values indicate stronger evidence of cointegration
|
|
|
|
|
|
/// - Critical values (approx): -3.43 (1%), -2.86 (5%), -2.57 (10%)
|
|
|
|
|
|
/// - Values more negative than critical values reject null hypothesis of no cointegration
|
2026-03-13 22:01:31 -07:00
|
|
|
|
///
|
|
|
|
|
|
/// Uses Kahan compensated summation for numerical stability over long streams.
|
2026-02-02 19:47:21 -08:00
|
|
|
|
/// </remarks>
|
|
|
|
|
|
[SkipLocalsInit]
|
|
|
|
|
|
public sealed class Cointegration : AbstractBase
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly RingBuffer _bufferA;
|
|
|
|
|
|
private readonly RingBuffer _bufferB;
|
|
|
|
|
|
|
|
|
|
|
|
// Running sums for O(1) statistics
|
|
|
|
|
|
private double _sumA, _sumB;
|
|
|
|
|
|
private double _sumA2, _sumB2;
|
|
|
|
|
|
private double _sumAB;
|
|
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// Kahan compensation for main sums
|
|
|
|
|
|
private double _sumAComp, _sumBComp;
|
|
|
|
|
|
private double _sumA2Comp, _sumB2Comp;
|
|
|
|
|
|
private double _sumABComp;
|
|
|
|
|
|
|
2026-02-02 19:47:21 -08:00
|
|
|
|
// Residual tracking
|
|
|
|
|
|
private double _prevResidual;
|
|
|
|
|
|
private double _p_prevResidual;
|
|
|
|
|
|
private bool _hasPrevResidual;
|
|
|
|
|
|
private bool _p_hasPrevResidual;
|
|
|
|
|
|
|
|
|
|
|
|
// ADF regression running sums (period-1 window)
|
|
|
|
|
|
private readonly RingBuffer _deltaResiduals;
|
|
|
|
|
|
private readonly RingBuffer _laggedResiduals;
|
2026-02-27 12:50:05 -08:00
|
|
|
|
private double _sumDeltaLagged, _sumLagged2, _sumDelta2;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// Kahan compensation for ADF sums
|
|
|
|
|
|
private double _sumDeltaLaggedComp, _sumLagged2Comp, _sumDelta2Comp;
|
|
|
|
|
|
|
|
|
|
|
|
// Previous compensation state for rollback
|
|
|
|
|
|
private double _p_sumAComp, _p_sumBComp;
|
|
|
|
|
|
private double _p_sumA2Comp, _p_sumB2Comp;
|
|
|
|
|
|
private double _p_sumABComp;
|
|
|
|
|
|
private double _p_sumDeltaLaggedComp, _p_sumLagged2Comp, _p_sumDelta2Comp;
|
|
|
|
|
|
|
2026-02-02 19:47:21 -08:00
|
|
|
|
// Last valid values for NaN handling
|
|
|
|
|
|
private double _lastValidA, _lastValidB;
|
|
|
|
|
|
private double _p_lastValidA, _p_lastValidB;
|
|
|
|
|
|
|
|
|
|
|
|
private const double Epsilon = 1e-10;
|
|
|
|
|
|
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
/// <inheritdoc />
|
2026-02-02 19:47:21 -08:00
|
|
|
|
public override bool IsHot => _bufferA.IsFull && _hasPrevResidual;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Creates a new Cointegration indicator.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="period">Lookback period for regression and ADF test (must be > 1)</param>
|
|
|
|
|
|
public Cointegration(int period = 20)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (period <= 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException("Period must be greater than 1", nameof(period));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_bufferA = new RingBuffer(period);
|
|
|
|
|
|
_bufferB = new RingBuffer(period);
|
|
|
|
|
|
_deltaResiduals = new RingBuffer(period - 1);
|
|
|
|
|
|
_laggedResiduals = new RingBuffer(period - 1);
|
|
|
|
|
|
|
|
|
|
|
|
Name = $"Cointegration({period})";
|
|
|
|
|
|
WarmupPeriod = period + 1; // Need extra bar for first delta
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Updates the Cointegration indicator with new values from both series.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="seriesA">First series value (dependent variable)</param>
|
|
|
|
|
|
/// <param name="seriesB">Second series value (independent variable)</param>
|
|
|
|
|
|
/// <param name="isNew">Whether this is a new bar</param>
|
|
|
|
|
|
/// <returns>The ADF test statistic (more negative = stronger cointegration)</returns>
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
public TValue Update(TValue seriesA, TValue seriesB, bool isNew = true)
|
|
|
|
|
|
{
|
|
|
|
|
|
double a = SanitizeA(seriesA.Value);
|
|
|
|
|
|
double b = SanitizeB(seriesB.Value);
|
|
|
|
|
|
|
|
|
|
|
|
if (isNew)
|
|
|
|
|
|
{
|
|
|
|
|
|
ProcessNewBar(a, b);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
ProcessBarCorrection(a, b);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double adfStat = CalculateAdfStatistic();
|
|
|
|
|
|
|
|
|
|
|
|
Last = new TValue(seriesA.Time, adfStat);
|
|
|
|
|
|
PubEvent(Last);
|
|
|
|
|
|
return Last;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Updates with raw double values.
|
|
|
|
|
|
/// </summary>
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// Stamps both inputs with <c>DateTime.UtcNow</c> as their timestamp. For
|
|
|
|
|
|
/// deterministic or replay-safe sequences use
|
|
|
|
|
|
/// <see cref="Update(TValue, TValue, bool)"/> with explicit timestamps instead.
|
|
|
|
|
|
/// </remarks>
|
2026-02-02 19:47:21 -08:00
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
public TValue Update(double seriesA, double seriesB, bool isNew = true)
|
|
|
|
|
|
{
|
2026-03-13 22:01:31 -07:00
|
|
|
|
DateTime now = DateTime.UtcNow;
|
|
|
|
|
|
return Update(new TValue(now, seriesA), new TValue(now, seriesB), isNew);
|
2026-02-02 19:47:21 -08:00
|
|
|
|
}
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
/// <summary>Not supported. This indicator requires two inputs; use <see cref="Update(TValue, TValue, bool)"/> instead.</summary>
|
2026-02-02 19:47:21 -08:00
|
|
|
|
/// <remarks>Not supported for bi-input indicator. Use Update(seriesA, seriesB) instead.</remarks>
|
|
|
|
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new NotSupportedException("Cointegration requires two inputs (seriesA and seriesB). Use Update(seriesA, seriesB).");
|
|
|
|
|
|
}
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
/// <summary>Not supported. This indicator requires two inputs; use <see cref="Batch(TSeries, TSeries, int)"/> instead.</summary>
|
2026-02-02 19:47:21 -08:00
|
|
|
|
/// <remarks>Not supported for bi-input indicator. Use Calculate(seriesA, seriesB, period) instead.</remarks>
|
|
|
|
|
|
public override TSeries Update(TSeries source)
|
|
|
|
|
|
{
|
2026-02-10 21:33:16 -08:00
|
|
|
|
throw new NotSupportedException("Cointegration requires two inputs. Use Batch(seriesA, seriesB, period).");
|
2026-02-02 19:47:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private double SanitizeA(double value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (double.IsFinite(value))
|
|
|
|
|
|
{
|
|
|
|
|
|
_lastValidA = value;
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return double.IsFinite(_lastValidA) ? _lastValidA : 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private double SanitizeB(double value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (double.IsFinite(value))
|
|
|
|
|
|
{
|
|
|
|
|
|
_lastValidB = value;
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return double.IsFinite(_lastValidB) ? _lastValidB : 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private void ProcessNewBar(double a, double b)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Save state for bar correction
|
|
|
|
|
|
_p_lastValidA = _lastValidA;
|
|
|
|
|
|
_p_lastValidB = _lastValidB;
|
|
|
|
|
|
_p_prevResidual = _prevResidual;
|
|
|
|
|
|
_p_hasPrevResidual = _hasPrevResidual;
|
2026-03-13 22:01:31 -07:00
|
|
|
|
_p_sumAComp = _sumAComp;
|
|
|
|
|
|
_p_sumBComp = _sumBComp;
|
|
|
|
|
|
_p_sumA2Comp = _sumA2Comp;
|
|
|
|
|
|
_p_sumB2Comp = _sumB2Comp;
|
|
|
|
|
|
_p_sumABComp = _sumABComp;
|
|
|
|
|
|
_p_sumDeltaLaggedComp = _sumDeltaLaggedComp;
|
|
|
|
|
|
_p_sumLagged2Comp = _sumLagged2Comp;
|
|
|
|
|
|
_p_sumDelta2Comp = _sumDelta2Comp;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
|
|
|
|
|
// Update main buffers
|
|
|
|
|
|
if (_bufferA.IsFull)
|
|
|
|
|
|
{
|
|
|
|
|
|
double oldA = _bufferA.Oldest;
|
|
|
|
|
|
double oldB = _bufferB.Oldest;
|
2026-03-13 22:01:31 -07:00
|
|
|
|
|
|
|
|
|
|
// Kahan subtract oldA from _sumA
|
|
|
|
|
|
{ double y = -oldA - _sumAComp; double t = _sumA + y; _sumAComp = (t - _sumA) - y; _sumA = t; }
|
|
|
|
|
|
// Kahan subtract oldB from _sumB
|
|
|
|
|
|
{ double y = -oldB - _sumBComp; double t = _sumB + y; _sumBComp = (t - _sumB) - y; _sumB = t; }
|
|
|
|
|
|
// Kahan subtract oldA² from _sumA2
|
|
|
|
|
|
{ double y = -(oldA * oldA) - _sumA2Comp; double t = _sumA2 + y; _sumA2Comp = (t - _sumA2) - y; _sumA2 = t; }
|
|
|
|
|
|
// Kahan subtract oldB² from _sumB2
|
|
|
|
|
|
{ double y = -(oldB * oldB) - _sumB2Comp; double t = _sumB2 + y; _sumB2Comp = (t - _sumB2) - y; _sumB2 = t; }
|
|
|
|
|
|
// Kahan subtract oldA*oldB from _sumAB
|
|
|
|
|
|
{ double y = -(oldA * oldB) - _sumABComp; double t = _sumAB + y; _sumABComp = (t - _sumAB) - y; _sumAB = t; }
|
2026-02-02 19:47:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_bufferA.Add(a);
|
|
|
|
|
|
_bufferB.Add(b);
|
|
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// Kahan add a to _sumA
|
|
|
|
|
|
{ double y = a - _sumAComp; double t = _sumA + y; _sumAComp = (t - _sumA) - y; _sumA = t; }
|
|
|
|
|
|
// Kahan add b to _sumB
|
|
|
|
|
|
{ double y = b - _sumBComp; double t = _sumB + y; _sumBComp = (t - _sumB) - y; _sumB = t; }
|
|
|
|
|
|
// Kahan add a² to _sumA2
|
|
|
|
|
|
{ double y = (a * a) - _sumA2Comp; double t = _sumA2 + y; _sumA2Comp = (t - _sumA2) - y; _sumA2 = t; }
|
|
|
|
|
|
// Kahan add b² to _sumB2
|
|
|
|
|
|
{ double y = (b * b) - _sumB2Comp; double t = _sumB2 + y; _sumB2Comp = (t - _sumB2) - y; _sumB2 = t; }
|
|
|
|
|
|
// Kahan add a*b to _sumAB
|
|
|
|
|
|
{ double y = (a * b) - _sumABComp; double t = _sumAB + y; _sumABComp = (t - _sumAB) - y; _sumAB = t; }
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
|
|
|
|
|
// Calculate current residual
|
|
|
|
|
|
double residual = CalculateResidual(a, b);
|
|
|
|
|
|
|
|
|
|
|
|
// Update ADF regression buffers
|
|
|
|
|
|
if (_hasPrevResidual)
|
|
|
|
|
|
{
|
|
|
|
|
|
double delta = residual - _prevResidual;
|
|
|
|
|
|
double lagged = _prevResidual;
|
|
|
|
|
|
|
|
|
|
|
|
if (_deltaResiduals.IsFull)
|
|
|
|
|
|
{
|
|
|
|
|
|
double oldDelta = _deltaResiduals.Oldest;
|
|
|
|
|
|
double oldLagged = _laggedResiduals.Oldest;
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// Kahan subtract from ADF sums
|
|
|
|
|
|
{ double y = -(oldDelta * oldLagged) - _sumDeltaLaggedComp; double t = _sumDeltaLagged + y; _sumDeltaLaggedComp = (t - _sumDeltaLagged) - y; _sumDeltaLagged = t; }
|
|
|
|
|
|
{ double y = -(oldLagged * oldLagged) - _sumLagged2Comp; double t = _sumLagged2 + y; _sumLagged2Comp = (t - _sumLagged2) - y; _sumLagged2 = t; }
|
|
|
|
|
|
{ double y = -(oldDelta * oldDelta) - _sumDelta2Comp; double t = _sumDelta2 + y; _sumDelta2Comp = (t - _sumDelta2) - y; _sumDelta2 = t; }
|
2026-02-02 19:47:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_deltaResiduals.Add(delta);
|
|
|
|
|
|
_laggedResiduals.Add(lagged);
|
|
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// Kahan add to ADF sums
|
|
|
|
|
|
{ double y = (delta * lagged) - _sumDeltaLaggedComp; double t = _sumDeltaLagged + y; _sumDeltaLaggedComp = (t - _sumDeltaLagged) - y; _sumDeltaLagged = t; }
|
|
|
|
|
|
{ double y = (lagged * lagged) - _sumLagged2Comp; double t = _sumLagged2 + y; _sumLagged2Comp = (t - _sumLagged2) - y; _sumLagged2 = t; }
|
|
|
|
|
|
{ double y = (delta * delta) - _sumDelta2Comp; double t = _sumDelta2 + y; _sumDelta2Comp = (t - _sumDelta2) - y; _sumDelta2 = t; }
|
2026-02-02 19:47:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_prevResidual = residual;
|
|
|
|
|
|
_hasPrevResidual = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private void ProcessBarCorrection(double a, double b)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Restore state
|
|
|
|
|
|
_lastValidA = _p_lastValidA;
|
|
|
|
|
|
_lastValidB = _p_lastValidB;
|
|
|
|
|
|
_prevResidual = _p_prevResidual;
|
|
|
|
|
|
_hasPrevResidual = _p_hasPrevResidual;
|
2026-03-13 22:01:31 -07:00
|
|
|
|
_sumAComp = _p_sumAComp;
|
|
|
|
|
|
_sumBComp = _p_sumBComp;
|
|
|
|
|
|
_sumA2Comp = _p_sumA2Comp;
|
|
|
|
|
|
_sumB2Comp = _p_sumB2Comp;
|
|
|
|
|
|
_sumABComp = _p_sumABComp;
|
|
|
|
|
|
_sumDeltaLaggedComp = _p_sumDeltaLaggedComp;
|
|
|
|
|
|
_sumLagged2Comp = _p_sumLagged2Comp;
|
|
|
|
|
|
_sumDelta2Comp = _p_sumDelta2Comp;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
|
|
|
|
|
// Update newest values in main buffers
|
2026-02-27 12:50:05 -08:00
|
|
|
|
if (_bufferA.Count == 0)
|
2026-02-02 19:47:21 -08:00
|
|
|
|
{
|
2026-02-27 12:50:05 -08:00
|
|
|
|
// Nothing to correct yet; no current bar exists
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
double oldA = _bufferA.Newest;
|
|
|
|
|
|
double oldB = _bufferB.Newest;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// Kahan subtract old + add new for main sums
|
|
|
|
|
|
{ double y = (-oldA + a) - _sumAComp; double t = _sumA + y; _sumAComp = (t - _sumA) - y; _sumA = t; }
|
|
|
|
|
|
{ double y = (-oldB + b) - _sumBComp; double t = _sumB + y; _sumBComp = (t - _sumB) - y; _sumB = t; }
|
|
|
|
|
|
{ double y = (-(oldA * oldA) + (a * a)) - _sumA2Comp; double t = _sumA2 + y; _sumA2Comp = (t - _sumA2) - y; _sumA2 = t; }
|
|
|
|
|
|
{ double y = (-(oldB * oldB) + (b * b)) - _sumB2Comp; double t = _sumB2 + y; _sumB2Comp = (t - _sumB2) - y; _sumB2 = t; }
|
|
|
|
|
|
{ double y = (-(oldA * oldB) + (a * b)) - _sumABComp; double t = _sumAB + y; _sumABComp = (t - _sumAB) - y; _sumAB = t; }
|
2026-02-27 12:50:05 -08:00
|
|
|
|
|
|
|
|
|
|
_bufferA.UpdateNewest(a);
|
|
|
|
|
|
_bufferB.UpdateNewest(b);
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
|
|
|
|
|
// Calculate current residual
|
|
|
|
|
|
double residual = CalculateResidual(a, b);
|
|
|
|
|
|
|
|
|
|
|
|
// Update ADF regression buffers
|
|
|
|
|
|
if (_hasPrevResidual)
|
|
|
|
|
|
{
|
|
|
|
|
|
double delta = residual - _prevResidual;
|
|
|
|
|
|
double lagged = _prevResidual;
|
|
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
if (_deltaResiduals.Count == 0)
|
2026-02-02 19:47:21 -08:00
|
|
|
|
{
|
2026-02-27 12:50:05 -08:00
|
|
|
|
// Nothing to correct yet in ADF buffers; no current entry exists
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
double oldDelta = _deltaResiduals.Newest;
|
|
|
|
|
|
double oldLagged = _laggedResiduals.Newest;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
// Kahan subtract old + add new for ADF sums
|
|
|
|
|
|
{ double y = (-(oldDelta * oldLagged) + (delta * lagged)) - _sumDeltaLaggedComp; double t = _sumDeltaLagged + y; _sumDeltaLaggedComp = (t - _sumDeltaLagged) - y; _sumDeltaLagged = t; }
|
|
|
|
|
|
{ double y = (-(oldLagged * oldLagged) + (lagged * lagged)) - _sumLagged2Comp; double t = _sumLagged2 + y; _sumLagged2Comp = (t - _sumLagged2) - y; _sumLagged2 = t; }
|
|
|
|
|
|
{ double y = (-(oldDelta * oldDelta) + (delta * delta)) - _sumDelta2Comp; double t = _sumDelta2 + y; _sumDelta2Comp = (t - _sumDelta2) - y; _sumDelta2 = t; }
|
2026-02-27 12:50:05 -08:00
|
|
|
|
|
|
|
|
|
|
_deltaResiduals.UpdateNewest(delta);
|
|
|
|
|
|
_laggedResiduals.UpdateNewest(lagged);
|
2026-02-02 19:47:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_prevResidual = residual;
|
|
|
|
|
|
_hasPrevResidual = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private double CalculateResidual(double a, double b)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = _bufferA.Count;
|
|
|
|
|
|
if (n < 2)
|
|
|
|
|
|
{
|
|
|
|
|
|
return 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate means
|
|
|
|
|
|
double meanA = _sumA / n;
|
|
|
|
|
|
double meanB = _sumB / n;
|
|
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
// Calculate variance of B and covariance
|
2026-02-02 19:47:21 -08:00
|
|
|
|
double varB = Max(0.0, (_sumB2 / n) - (meanB * meanB));
|
|
|
|
|
|
double cov = (_sumAB / n) - (meanA * meanB);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate beta and alpha
|
|
|
|
|
|
double beta = 0.0;
|
2026-02-27 12:50:05 -08:00
|
|
|
|
if (varB > Epsilon)
|
2026-02-02 19:47:21 -08:00
|
|
|
|
{
|
2026-02-27 12:50:05 -08:00
|
|
|
|
beta = cov / varB;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
double alpha = meanA - (beta * meanB);
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate residual
|
|
|
|
|
|
return a - (alpha + (beta * b));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private double CalculateAdfStatistic()
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = _deltaResiduals.Count;
|
|
|
|
|
|
if (n < 2)
|
|
|
|
|
|
{
|
|
|
|
|
|
return double.NaN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
if (_sumLagged2 < Epsilon)
|
2026-02-02 19:47:21 -08:00
|
|
|
|
{
|
|
|
|
|
|
return double.NaN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
// No-intercept ADF regression: Δε_t = γ × ε_{t-1} + u_t
|
|
|
|
|
|
double gamma = _sumDeltaLagged / _sumLagged2;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
// Calculate sum of squared regression errors in O(1)
|
|
|
|
|
|
// Sum((Δε_t - γ ε_{t-1})^2) = Sum(Δε_t^2) - 2γ Sum(Δε_t ε_{t-1}) + γ^2 Sum(ε_{t-1}^2)
|
|
|
|
|
|
double sumErrorSq = _sumDelta2 - (2.0 * gamma * _sumDeltaLagged) + (gamma * gamma * _sumLagged2);
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
// Ensure non-negative due to floating point errors
|
|
|
|
|
|
sumErrorSq = Max(0.0, sumErrorSq);
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-02-27 12:50:05 -08:00
|
|
|
|
double varError = sumErrorSq / (n - 1);
|
|
|
|
|
|
double seGammaSq = varError / _sumLagged2;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
|
|
|
|
|
if (seGammaSq <= 0 || !double.IsFinite(seGammaSq))
|
|
|
|
|
|
{
|
|
|
|
|
|
return double.NaN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double seGamma = Sqrt(seGammaSq);
|
2026-02-27 12:50:05 -08:00
|
|
|
|
if (seGamma < Epsilon)
|
2026-02-02 19:47:21 -08:00
|
|
|
|
{
|
|
|
|
|
|
return double.NaN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return gamma / seGamma;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
/// <summary>Not supported. This indicator requires two input spans.</summary>
|
2026-02-02 19:47:21 -08:00
|
|
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new NotSupportedException("Cointegration requires two inputs.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
/// <inheritdoc />
|
2026-02-02 19:47:21 -08:00
|
|
|
|
public override void Reset()
|
|
|
|
|
|
{
|
|
|
|
|
|
_bufferA.Clear();
|
|
|
|
|
|
_bufferB.Clear();
|
|
|
|
|
|
_deltaResiduals.Clear();
|
|
|
|
|
|
_laggedResiduals.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
_sumA = 0;
|
|
|
|
|
|
_sumB = 0;
|
|
|
|
|
|
_sumA2 = 0;
|
|
|
|
|
|
_sumB2 = 0;
|
|
|
|
|
|
_sumAB = 0;
|
|
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
_sumAComp = 0;
|
|
|
|
|
|
_sumBComp = 0;
|
|
|
|
|
|
_sumA2Comp = 0;
|
|
|
|
|
|
_sumB2Comp = 0;
|
|
|
|
|
|
_sumABComp = 0;
|
|
|
|
|
|
|
2026-02-02 19:47:21 -08:00
|
|
|
|
_sumDeltaLagged = 0;
|
|
|
|
|
|
_sumLagged2 = 0;
|
2026-02-27 12:50:05 -08:00
|
|
|
|
_sumDelta2 = 0;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
2026-03-13 22:01:31 -07:00
|
|
|
|
_sumDeltaLaggedComp = 0;
|
|
|
|
|
|
_sumLagged2Comp = 0;
|
|
|
|
|
|
_sumDelta2Comp = 0;
|
|
|
|
|
|
|
2026-02-02 19:47:21 -08:00
|
|
|
|
_prevResidual = 0;
|
|
|
|
|
|
_p_prevResidual = 0;
|
|
|
|
|
|
_hasPrevResidual = false;
|
|
|
|
|
|
_p_hasPrevResidual = false;
|
|
|
|
|
|
|
|
|
|
|
|
_lastValidA = 0;
|
|
|
|
|
|
_lastValidB = 0;
|
|
|
|
|
|
_p_lastValidA = 0;
|
|
|
|
|
|
_p_lastValidB = 0;
|
|
|
|
|
|
|
|
|
|
|
|
Last = default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Calculates cointegration for two time series.
|
|
|
|
|
|
/// </summary>
|
2026-02-10 21:33:16 -08:00
|
|
|
|
public static TSeries Batch(TSeries seriesA, TSeries seriesB, int period = 20)
|
2026-02-27 12:50:05 -08:00
|
|
|
|
=> Calculate(seriesA, seriesB, period).Results;
|
2026-02-02 19:47:21 -08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Static batch calculation for span-based processing.
|
|
|
|
|
|
/// </summary>
|
2026-02-10 21:33:16 -08:00
|
|
|
|
public static void Batch(
|
2026-02-02 19:47:21 -08:00
|
|
|
|
ReadOnlySpan<double> seriesA,
|
|
|
|
|
|
ReadOnlySpan<double> seriesB,
|
|
|
|
|
|
Span<double> output,
|
|
|
|
|
|
int period = 20)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (seriesA.Length != seriesB.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException("Series must have the same length", nameof(seriesB));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (seriesA.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 Cointegration(period);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < seriesA.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = indicator.Update(seriesA[i], seriesB[i], isNew: true);
|
|
|
|
|
|
output[i] = result.Value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings
2026-03-09 13:45:46 -07:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Calculates the ADF cointegration statistic for two time series and returns both the result series and the live indicator instance.
|
|
|
|
|
|
/// </summary>
|
2026-02-10 21:33:16 -08:00
|
|
|
|
public static (TSeries Results, Cointegration Indicator) Calculate(TSeries seriesA, TSeries seriesB, int period = 20)
|
|
|
|
|
|
{
|
2026-02-27 12:50:05 -08:00
|
|
|
|
if (seriesA.Count != seriesB.Count)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentException("Series must have the same length", nameof(seriesB));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 21:33:16 -08:00
|
|
|
|
var indicator = new Cointegration(period);
|
2026-02-27 12:50:05 -08:00
|
|
|
|
var result = new TSeries(seriesA.Count);
|
|
|
|
|
|
|
|
|
|
|
|
var timesA = seriesA.Times;
|
|
|
|
|
|
var valuesA = seriesA.Values;
|
|
|
|
|
|
var valuesB = seriesB.Values;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < seriesA.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Add(indicator.Update(new TValue(timesA[i], valuesA[i]), new TValue(timesA[i], valuesB[i]), isNew: true));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (result, indicator);
|
2026-02-10 21:33:16 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|