Skip to content

GrangerCausality

The Granger-causality F-statistic over a rolling window: does series b help predict series a beyond a's own past?

Quick reference

FieldValue
FamilyPrice Statistics
Input type(f64, f64) (a pair a, b)
Output typef64 (F-statistic)
Output range>= 0; larger = stronger predictive causality
Default parametersperiod, lag both required (lag >= 1, period >= 3·lag + 2)
Warmup periodperiod
InterpretationLarge F → b leads a; near 0b adds nothing. Predictive, not structural cause.

Formula

Each update takes one (a, b) pair. Over the trailing window of period observations the indicator fits two autoregressions of a and compares them with an F-test:

restricted:    aₜ = c + Σ φᵢ·aₜ₋ᵢ                       (a's own lags only)
unrestricted:  aₜ = c + Σ φᵢ·aₜ₋ᵢ + Σ ψᵢ·bₜ₋ᵢ          (+ b's lags)
F = ((RSSᵣ − RSSᵤ) / lag) / (RSSᵤ / (n − 2·lag − 1))

If adding b's lags significantly reduces the residual sum of squares, bGranger-causes a: past values of b carry information about the future of a beyond what a's own past holds. A larger F means stronger predictive causality (lead–lag structure a stat-arb model can trade); a value near 0 means b adds nothing. Note Granger causality is purely predictive — it is not structural cause and effect. The statistic is 0 when a regression is degenerate (a collinear or flat window makes the normal equations singular). The output is always >= 0.

Source: crates/wickra-core/src/indicators/granger_causality.rs.

Parameters

NameTypeDefaultValid rangeSourceDescription
periodusizenone>= 3·lag + 2granger_causality.rs:65Look-back window of observations.
lagusizenone>= 1granger_causality.rs:65Number of autoregressive lags. Bad parameters error with Error::InvalidPeriod.

Inputs / Outputs

From crates/wickra-core/src/indicators/granger_causality.rs:

rust
use wickra::{GrangerCausality, Indicator};
// GrangerCausality: Input = (f64, f64), Output = f64
const _: fn(&mut GrangerCausality, (f64, f64)) -> Option<f64> =
    <GrangerCausality as Indicator>::update;

Python streams as update(a, b) -> float | None and batches over two equal-length arrays. Node streams as update(a, b) and batches over a[], b[].

Warmup

warmup_period() == period. The unit test accessors_and_metadata pins warmup_period() == 60 for period = 60; warmup_returns_none pins the first Some at the period-th pair.

Edge cases

  • b leads a. When a is driven by b's prior values, the F-statistic is large and positive; pinned by b_leading_a_has_positive_statistic.
  • Singular window. A constant b makes its lag columns collinear with the intercept, so the unrestricted system is singular and the output is 0; pinned by constant_b_is_singular_and_returns_zero.
  • Constant a. A flat a makes the restricted regression singular and also returns 0; pinned by constant_a_restricted_singular_returns_zero.
  • Reset. reset() clears the window.

Examples

Rust

rust
use wickra::{BatchExt, GrangerCausality, Indicator};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut g = GrangerCausality::new(60, 1)?;
    // a today is driven by b yesterday (plus a little of its own past) -> b helps.
    let mut prev_drive = 0.0;
    let pairs: Vec<(f64, f64)> = (0..120)
        .map(|t| {
            let drive = (f64::from(t) * 0.3).sin() + 0.4 * (f64::from(t) * 0.11).cos();
            let a = 0.8 * prev_drive + 0.05 * (f64::from(t) * 0.7).sin();
            prev_drive = drive;
            (a, drive)
        })
        .collect();
    let last = g.batch(&pairs).into_iter().flatten().last().unwrap();
    println!("{last:.4}");
    Ok(())
}

Output:

1335.5963

Python

python
import numpy as np
import wickra as ta

t = np.arange(120)
drive = np.sin(t * 0.3) + 0.4 * np.cos(t * 0.11)
a = np.empty(120)
a[0] = 0.0
a[1:] = 0.8 * drive[:-1] + 0.05 * np.sin(t[1:] * 0.7)
print(round(ta.GrangerCausality(60, 1).batch(a, drive)[-1], 4))

Output:

1335.5963

Node

javascript
const ta = require('wickra');
const drive = Array.from({ length: 120 }, (_, t) => Math.sin(t * 0.3) + 0.4 * Math.cos(t * 0.11));
const a = drive.map((_, t) => (t === 0 ? 0 : 0.8 * drive[t - 1] + 0.05 * Math.sin(t * 0.7)));
console.log(new ta.GrangerCausality(60, 1).batch(a, drive).at(-1).toFixed(4));

Output:

1335.5963

Interpretation

GrangerCausality quantifies lead–lag structure: a large F means b's history predicts a's future, the asymmetry a stat-arb model trades by acting on the leading leg. Run it both ways — b → a and a → b — to find the direction of information flow; the larger statistic points to the leader. Because the test is purely predictive, treat a high F as a tradeable forecasting edge, not proof of an economic mechanism. Combine with Cointegration to confirm the legs share a long-run equilibrium, not just short-run predictability.

Common pitfalls

  • Predictive, not causal. Granger causality detects forecast usefulness, not structural cause. A confounding third series can produce a high F.
  • Lag and window tuning. Too few lags miss the lead structure; too many inflate the window requirement and the variance of the estimate. Pick lag from the suspected lead time.

References

Granger, C. W. J. (1969), Investigating Causal Relations by Econometric Models and Cross-spectral Methods, Econometrica.

See also