Skip to main content

TailCoR — non-asymptotic, distribution-free decomposition of linear correlation vs non-linear tail contagion (Ricci, Tristani, Vergote 2023). Pure numpy+scipy.

Project description

tailcor

Non-asymptotic, distribution-free decomposition of linear correlation vs non-linear tail contagion — a Python implementation of Ricci, Tristani & Vergote (2023) "TailCoR" (Banco de España WP 1227, Journal of Empirical Finance). Pure numpy + scipy, no C++ deps, no copula fitting, no asymptotic limits.

What it does

Given two paired time series (X, Y), TailCoR returns three numbers per window:

  • Linear — always 1.0. Bivariate-Gaussian reference at the empirical Pearson correlation.
  • Composite — empirical tail inter-quantile range divided by the Gaussian reference. Equals 1.0 under exact Gaussian, >1 when the joint tail is fatter than Gaussian predicts.
  • Non-linearcomposite − linear. The "pure tail-contagion" component that correlation cannot explain.

Why it matters: during the 2008 + 2011 crises, linear correlations between Euro-area sovereign bonds collapsed to ~1 then dropped, while tail contagion spiked. Classical EVT was too slow to catch it (extremes are scarce at acceleration); TailCoR tracked it in real time because it does not need asymptotic-limit samples.

Install

pip install tailcor

Requires Python ≥ 3.9, numpy ≥ 1.23, scipy ≥ 1.10.

Quickstart

import numpy as np
from tailcor import tailcor, rolling_tailcor

rng = np.random.default_rng(0)
x = rng.standard_normal(1000)
y = 0.5 * x + rng.standard_normal(1000)

result = tailcor(x, y, q=0.95)
print(result.composite)   # ~1.0 under Gaussian
print(result.nonlinear)   # ~0.0 under Gaussian
print(result.rho)         # empirical Pearson

# rolling time series:
r = rolling_tailcor(x, y, window=252, q=0.95)
print(r.composite.shape)  # (1000 - 252 + 1,)

The math (1-minute version)

For bivariate (X, Y) with empirical Pearson correlation ρ, project onto the sum axis standardised by the Gaussian variance:

Z = (X + Y) / √(2(1 + ρ))

Under bivariate standard Gaussian at any ρ, Z is N(0, 1) — so its inter-quantile range at level q equals Φ⁻¹(q) − Φ⁻¹(1−q) (the Gaussian reference IQR at that quantile).

Define

S(q) = IQR_q(Z) / ( Φ⁻¹(q) − Φ⁻¹(1−q) )
  • S(q) ≈ 1 under exact bivariate Gaussian (any ρ)
  • S(q) > 1 when the empirical tail is fatter than Gaussian (tail contagion)
  • S(q) < 1 when the empirical tail is thinner (rare — anti-concentration)

The linear component is exactly 1.0 by construction (Gaussian reference). The non-linear component is S(q) − 1. "A few lines of basic coding", per the paper — and literally is (see tailcor/core.py).

Quantile profile

Fat-tailed joint distributions show TailCoR rising with q:

from tailcor import tailcor_profile
import numpy as np

rng = np.random.default_rng(1)
# Student-t copula with df=4 (canonical tail dependence)
g = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], size=15000).T
chi2 = rng.chisquare(4, size=15000)
t = g * np.sqrt(4 / chi2)
x, y = t[0], t[1]

for r in tailcor_profile(x, y):
    print(f"q={r.q:.2f}  composite={r.composite:.3f}  nonlinear={r.nonlinear:+.3f}")

Output: composite starts ~1 at q=0.6, grows past 1.5 at q=0.99 — the classic "tail contagion signature" that Ricci et al. use as a crisis early-warning.

Correctness

15 unit tests cover:

  • Bivariate Gaussian at six ρ levels (−0.5 … 0.8): composite within [0.9, 1.1] of 1, non-linear near 0
  • Student-t copula df=4: composite > 1.15 even at ρ=0 (tail contagion with zero Pearson)
  • Student-t copula df=30: nearly Gaussian (|non-linear| < 0.15)
  • ρ recovered to within 0.05 on n=10000 Gaussian samples
  • NaN-row handling (10% injection preserves a valid result)
  • q monotonicity on tail-dependent copulas (composite grows with q)
  • Input-validation error paths
  • Rolling: Gaussian → Student-t regime shift produces post-shift composite lift > 0.1

Use cases

  • Crisis early warning on bivariate index pairs (e.g. equity vs sovereign, or two credit spreads)
  • Risk decomposition: split pairwise CoVaR into what Gaussian explains and what it can't
  • Stress-test input: feed the non-linear component into correlation-matrix generators (CorrGAN and similar) as a fat-tail signal
  • Regime detection complementary to HMM / change-point methods: the non-linear component reacts to copula-shape changes that HMM trained on returns may miss

Roadmap

v0.2 (planned):

  • Alternative rotations (principal-component, anti-diagonal) — rotation="sum"|"pc"|"anti"
  • Tail-CoVaR helper — conditional-VaR-style wrapper on top of the core decomposition
  • Block-bootstrap confidence intervals for composite + non-linear
  • Multivariate extension via pairwise aggregation matrix

Authors

  • Pierre Samson (@darw007d) — idea, use-case, design decisions
  • Claude Opus (Anthropic) — implementation and tests

Originally motivated by the OMEGA Swarm project, Phase 19 Tier A #4. Sister package to phawkes (Hawkes processes) and fisherrao (information geometry). Same "small, tested, publishable" ethos.

Citation

If this library contributes to a published result, please cite the underlying paper:

  • Ricci, L., Tristani, O. & Vergote, O. (2023). TailCoR. Banco de España Working Paper 1227. Journal of Empirical Finance.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tailcor-0.1.0.tar.gz (10.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tailcor-0.1.0-py3-none-any.whl (9.2 kB view details)

Uploaded Python 3

File details

Details for the file tailcor-0.1.0.tar.gz.

File metadata

  • Download URL: tailcor-0.1.0.tar.gz
  • Upload date:
  • Size: 10.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for tailcor-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e30739e384d40ce0aa042c04a559291b6b3bc6c5989449327ae74a573b03815e
MD5 6c7f13b889d681b360713aadcf12ac19
BLAKE2b-256 902e196b0bbd621ce39eb547a790518968abb6ec8bfcd3077d8e35b1e13692f3

See more details on using hashes here.

File details

Details for the file tailcor-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tailcor-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 9.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for tailcor-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 97d7cf4a55f7061af7ea1194d852a2c756e0023ff6be5ba84af57b1e3dc5b297
MD5 9265a4b0357f4a7473fa71f27c0babf1
BLAKE2b-256 3d09e9b0c1f2a1e6d967ac215b3115b62254fe26f9ea67f8e3dec8f4f6e3cae0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page