using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// GRANGER: Granger Causality Test /// A statistical test to determine whether one time series is useful in forecasting another. /// Tests if past values of X help predict future values of Y beyond Y's own past values. /// Returns a value between 0 and 1 representing the probability that X does not Granger-cause Y. /// /// /// The Granger Causality calculation process: /// 1. Fits two regression models: /// - Restricted model: Y(t) = α₀ + Σ(β₁ᵢY(t-i)) + ε(t) /// - Unrestricted model: Y(t) = α₀ + Σ(β₁ᵢY(t-i)) + Σ(β₂ᵢX(t-i)) + ε(t) /// 2. Calculates F-statistic comparing the models /// 3. Computes p-value from F-distribution /// /// Key characteristics: /// - Tests predictive causality, not true causation /// - Sensitive to lag selection /// - Assumes stationarity of time series /// - Useful for lead/lag relationship analysis /// /// Formula: /// F = ((RSS₁ - RSS₂)/p) / (RSS₂/(n-2p-1)) /// where: /// RSS₁ = residual sum of squares from restricted model /// RSS₂ = residual sum of squares from unrestricted model /// p = number of lags /// n = number of observations /// /// Market Applications: /// - Lead/lag analysis between markets /// - Price discovery analysis /// - Market efficiency testing /// - Intermarket analysis /// - Risk spillover detection /// /// Sources: /// https://en.wikipedia.org/wiki/Granger_causality /// "Investigating Causal Relations by Econometric Models and Cross-spectral Methods" - C.W.J. Granger /// /// Note: Assumes linear relationships and stationarity /// [SkipLocalsInit] public sealed class Granger : AbstractBase { private readonly int Lags; private readonly CircularBuffer _xValues; private readonly CircularBuffer _yValues; private const double Epsilon = 1e-10; private const int MinimumLags = 1; /// The number of lags to use in the Granger causality test. /// Thrown when lags is less than 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Granger(int lags) { if (lags < MinimumLags) { throw new ArgumentOutOfRangeException(nameof(lags), "Number of lags must be at least 1 for Granger causality test."); } Lags = lags; WarmupPeriod = lags + 1; _xValues = new CircularBuffer(lags * 2); // Need extra space for lagged values _yValues = new CircularBuffer(lags * 2); Name = $"Granger(lags={lags})"; Init(); } /// The data source object that publishes updates. /// The number of lags to use in the Granger causality test. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Granger(object source, int lags) : this(lags) { 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 double CalculateRSS(ReadOnlySpan y, ReadOnlySpan yhat) { double rss = 0; for (int i = 0; i < y.Length; i++) { double residual = y[i] - yhat[i]; rss += residual * residual; } return rss; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static void FitOLS(ReadOnlySpan y, ReadOnlySpan x, Span beta) { // Simple OLS implementation for y = Xβ + ε int n = y.Length; int k = beta.Length; // Create X matrix (including constant term) var X = new double[n, k]; for (int i = 0; i < n; i++) { X[i, 0] = 1.0; // Constant term for (int j = 1; j < k; j++) { X[i, j] = x[(i * (k - 1)) + (j - 1)]; } } // Calculate β = (X'X)⁻¹X'y var XtX = new double[k, k]; var Xty = new double[k]; // Calculate X'X and X'y for (int i = 0; i < k; i++) { for (int j = 0; j < k; j++) { double sum = 0; for (int l = 0; l < n; l++) { sum += X[l, i] * X[l, j]; } XtX[i, j] = sum; } double sum2 = 0; for (int l = 0; l < n; l++) { sum2 += X[l, i] * y[l]; } Xty[i] = sum2; } // Solve system of equations for (int i = 0; i < k; i++) { double pivot = XtX[i, i]; if (Math.Abs(pivot) > Epsilon) { for (int j = 0; j < k; j++) { XtX[i, j] /= pivot; } Xty[i] /= pivot; for (int j = 0; j < k; j++) { if (i != j) { double factor = XtX[j, i]; for (int l = 0; l < k; l++) { XtX[j, l] -= factor * XtX[i, l]; } Xty[j] -= factor * Xty[i]; } } } } // Copy results to beta for (int i = 0; i < k; i++) { beta[i] = Xty[i]; } } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private double CalculateFStatistic(double rss1, double rss2, int n, int p) { // Calculate F-statistic double numerator = (rss1 - rss2) / p; double denominator = rss2 / (n - (2 * p) - 1); return numerator / denominator; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double FDistributionPValue(double f, int df1, int df2) { // Approximate p-value from F-distribution // Using a simplified approximation for performance double v = df2 / (df2 + (df1 * f)); return Math.Pow(v, df2 / 2.0); } [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 pValue = 1.0; // Null hypothesis: X does not Granger-cause Y if (_xValues.Count >= WarmupPeriod && _yValues.Count >= WarmupPeriod) { int n = _xValues.Count - Lags; if (n > (2 * Lags) + 1) { ReadOnlySpan x = _xValues.GetSpan(); ReadOnlySpan y = _yValues.GetSpan(); // Prepare data for regression var yData = y.Slice(Lags, n).ToArray(); var restricted = new double[Lags + 1]; var unrestricted = new double[(2 * Lags) + 1]; // Fit restricted model (only Y lags) FitOLS(yData, y.Slice(0, n), restricted); // Calculate RSS for restricted model var yhatRestricted = new double[n]; for (int i = 0; i < n; i++) { yhatRestricted[i] = restricted[0]; for (int j = 0; j < Lags; j++) { yhatRestricted[i] += restricted[j + 1] * y[i + Lags - j - 1]; } } double rss1 = CalculateRSS(yData, yhatRestricted); // Fit unrestricted model (Y and X lags) FitOLS(yData, x.Slice(0, n), unrestricted); // Calculate RSS for unrestricted model var yhatUnrestricted = new double[n]; for (int i = 0; i < n; i++) { yhatUnrestricted[i] = unrestricted[0]; for (int j = 0; j < Lags; j++) { yhatUnrestricted[i] += unrestricted[j + 1] * y[i + Lags - j - 1]; yhatUnrestricted[i] += unrestricted[j + Lags + 1] * x[i + Lags - j - 1]; } } double rss2 = CalculateRSS(yData, yhatUnrestricted); // Calculate F-statistic and p-value if (rss2 > Epsilon) { double f = CalculateFStatistic(rss1, rss2, n, Lags); pValue = FDistributionPValue(f, Lags, n - (2 * Lags) - 1); } } } IsHot = _xValues.Count >= WarmupPeriod && _yValues.Count >= WarmupPeriod; return pValue; } }