Skip to main content
Ultra Feature. Requires an Ultra subscription. Get started at api.mathematicalcompany.com
What is this? When you need to model dependence across three or more markets simultaneously, vine copulas decompose the problem into a tree of pairwise copulas. This lets you capture complex multi-market dependencies that simple correlation matrices miss. Use it for portfolio-level tail risk analysis and multi-market stress testing.

Vine Copulas

Vine copulas extend bivariate copulas to model dependence among three or more markets. Where a single bivariate copula captures the relationship between two markets, a vine copula decomposes a high-dimensional dependence structure into a cascade of bivariate copulas arranged in a tree structure. Horizon implements both C-vine (canonical) and D-vine (drawable) constructions. All computation runs in Rust via PyO3.

C-Vine

Star-shaped tree with a central node. Best when one market dominates the dependence structure.

D-Vine

Path-shaped tree. Best when markets form a sequential dependence chain.

Tail Risk

Compute joint tail probabilities across many markets simultaneously.

Pipeline Integration

hz.vine_risk_monitor() tracks multi-market tail risk in real time within hz.run().

Why Vine Copulas?

Bivariate copulas work well for pairs of markets, but portfolio risk depends on the joint behavior of many markets simultaneously. The challenge is that most multivariate copula families (e.g., multivariate Gaussian, multivariate t) impose a single dependence structure across all pairs. Vine copulas solve this by:
  1. Pair-by-pair flexibility: each pair of markets can have its own copula family (Clayton for one pair, Gumbel for another)
  2. Tail dependence heterogeneity: some pairs can exhibit lower-tail dependence while others exhibit upper-tail dependence
  3. Scalability: the vine structure decomposes an n-dimensional problem into n*(n-1)/2 bivariate problems
Vine copulas build on the bivariate copula module. See the Copula Dependence page for details on the underlying copula families (Gaussian, Clayton, Gumbel, Frank).

API

VineType Enum

import horizon as hz

hz.VineType.CVine   # Canonical vine (star-shaped trees)
hz.VineType.DVine   # Drawable vine (path-shaped trees)
VariantTree StructureBest For
CVineStar-shaped: one central node per tree levelMarkets dominated by a single driver (e.g., BTC drives alts)
DVinePath-shaped: sequential chain per tree levelMarkets with sequential dependence (e.g., election timeline)

hz.fit_vine

Fit a vine copula to multivariate uniform data. The function selects the best bivariate copula family for each pair from Gaussian, Clayton, Gumbel, and Frank.
import horizon as hz

# Prepare uniform marginals for 4 markets
# Each inner list is one market's pseudo-uniform observations
data = [
    [0.1, 0.3, 0.5, 0.7, 0.9, 0.2, 0.4, 0.6, 0.8, 0.15],  # market A
    [0.2, 0.4, 0.6, 0.5, 0.8, 0.3, 0.5, 0.7, 0.9, 0.25],  # market B
    [0.9, 0.7, 0.5, 0.3, 0.1, 0.8, 0.6, 0.4, 0.2, 0.85],  # market C
    [0.15, 0.35, 0.55, 0.65, 0.85, 0.25, 0.45, 0.6, 0.75, 0.2],  # market D
]

result = hz.fit_vine(data, vine_type=hz.VineType.CVine)
print(f"Vine type: {result.vine_type}")
print(f"Dimensions: {result.n_dimensions}")
print(f"Log-likelihood: {result.log_likelihood:.4f}")
print(f"AIC: {result.aic:.4f}")

# Inspect pair copulas
for pc in result.pair_copulas:
    print(f"  Tree {pc.tree}, edge ({pc.var_a}, {pc.var_b}): "
          f"{pc.family} theta={pc.parameter:.3f} tau={pc.kendall_tau:.3f}")
ParameterTypeDescription
datalist[list[float]]N lists of uniform marginals, each of length T. All values in (0, 1).
vine_typeVineTypeCVine or DVine
Returns a VineCopulaResult.

VineCopulaResult Type

FieldTypeDescription
vine_typeVineTypeThe vine structure used
n_dimensionsintNumber of variables (markets)
pair_copulaslist[PairCopula]All fitted pair copulas in the vine
log_likelihoodfloatTotal log-likelihood of the vine
aicfloatAIC of the full vine model
tree_orderlist[int]Variable ordering used in the vine

PairCopula Type

Each pair copula in the vine decomposition.
FieldTypeDescription
treeintTree level (0-indexed; tree 0 has unconditional pairs)
var_aintFirst variable index
var_bintSecond variable index
conditioned_onlist[int]Variables conditioned on (empty for tree 0)
familystrBest-fit copula family name
parameterfloatFitted copula parameter
kendall_taufloatImplied Kendall’s tau
log_likelihoodfloatLog-likelihood for this pair

Sampling

hz.vine_sample

Generate multivariate correlated samples from a fitted vine copula.
import horizon as hz

result = hz.fit_vine(data, vine_type=hz.VineType.CVine)

samples = hz.vine_sample(
    vine=result,
    n=10000,
    seed=42,
)
# samples is a list of N-dimensional tuples
print(f"Generated {len(samples)} samples of dimension {len(samples[0])}")

# Verify marginals are approximately uniform
import statistics
for dim in range(result.n_dimensions):
    vals = [s[dim] for s in samples]
    print(f"  Dim {dim}: mean={statistics.mean(vals):.3f} std={statistics.stdev(vals):.3f}")
ParameterTypeDescription
vineVineCopulaResultA fitted vine copula from fit_vine()
nintNumber of samples to generate
seedintRandom seed for reproducibility (optional)
Returns list[tuple[float, ...]]: n samples, each of dimension vine.n_dimensions.

Tail Risk

hz.vine_tail_risk

Compute joint tail probabilities from a fitted vine copula. Estimates the probability that all (or a subset of) markets simultaneously fall below (or exceed) specified quantiles.
import horizon as hz

result = hz.fit_vine(data, vine_type=hz.VineType.CVine)

# Probability all 4 markets are below their 10th percentile simultaneously
tail_prob = hz.vine_tail_risk(
    vine=result,
    quantile=0.10,
    direction="lower",
    n_simulations=100000,
    seed=42,
)
print(f"Joint lower tail probability (10%): {tail_prob:.6f}")
# Under independence this would be 0.10^4 = 0.0001
# With positive dependence it will be higher

# Upper tail: probability all markets exceed their 90th percentile
upper_tail = hz.vine_tail_risk(
    vine=result,
    quantile=0.90,
    direction="upper",
    n_simulations=100000,
    seed=42,
)
print(f"Joint upper tail probability (90%): {upper_tail:.6f}")
ParameterTypeDescription
vineVineCopulaResultA fitted vine copula
quantilefloatQuantile threshold in (0, 1)
directionstr"lower" (all below quantile) or "upper" (all above quantile)
n_simulationsintMonte Carlo sample count (default: 100000)
seedintRandom seed (optional)
Returns float: estimated joint tail probability.
Joint tail probabilities are estimated via Monte Carlo simulation from the fitted vine. Increase n_simulations for more precise estimates of rare tail events. For a 4-market joint 5% tail event, 100,000 simulations typically provide 2 significant digits of precision.

Pipeline Integration

hz.vine_risk_monitor

Pipeline function that fits a vine copula across all monitored markets and injects tail risk metrics into ctx.params["vine_risk"].
import horizon as hz

def vine_risk_strategy(ctx):
    vine = ctx.params.get("vine_risk")
    if vine is None:
        return []

    mid = ctx.feed.price

    # Reduce position size when joint tail risk is elevated
    joint_tail = vine.get("joint_lower_tail_10pct", 0.0)
    independence_baseline = 0.10 ** vine.get("n_markets", 1)

    # Tail dependence ratio: how much worse than independence
    if independence_baseline > 0:
        tail_ratio = joint_tail / independence_baseline
    else:
        tail_ratio = 1.0

    # Scale down size when tail dependence is high
    base_size = 20
    if tail_ratio > 5.0:
        size = max(2, int(base_size / tail_ratio))
        spread = 0.06  # widen for crash risk
    else:
        size = base_size
        spread = 0.03

    return [
        hz.quote(ctx, hz.Side.Yes, hz.OrderSide.Buy, mid - spread, size),
        hz.quote(ctx, hz.Side.Yes, hz.OrderSide.Sell, mid + spread, size),
    ]

hz.run(
    name="vine-risk-mm",
    markets=["0xmarket_a...", "0xmarket_b...", "0xmarket_c..."],
    pipeline=[
        hz.vine_risk_monitor(refit_interval=300),  # refit every 5 minutes
        vine_risk_strategy,
    ],
    interval=1.0,
)
The ctx.params["vine_risk"] dict contains:
KeyTypeDescription
vine_typestrVine structure used ("CVine" or "DVine")
n_marketsintNumber of markets in the vine
joint_lower_tail_10pctfloatJoint probability all markets below 10th percentile
joint_upper_tail_90pctfloatJoint probability all markets above 90th percentile
max_pairwise_taufloatStrongest pairwise Kendall’s tau in the vine
aicfloatAIC of the fitted vine model
last_refitfloatTimestamp of the last vine refit

C-Vine vs D-Vine

When to Use C-Vine

C-vine (canonical vine) uses a star-shaped tree where one central variable is connected to all others at each level. Use C-vine when:
  • One market dominates the dependence structure (e.g., BTC in crypto prediction markets)
  • You want to condition all other pairwise relationships on the most connected market
  • The most correlated variable can be identified a priori
# C-vine: BTC is the central node, all others conditioned on BTC
result = hz.fit_vine(data, vine_type=hz.VineType.CVine)
# Tree 0: (BTC, ETH), (BTC, SOL), (BTC, AVAX)
# Tree 1: (ETH, SOL | BTC), (ETH, AVAX | BTC)
# Tree 2: (SOL, AVAX | BTC, ETH)

When to Use D-Vine

D-vine (drawable vine) uses a path-shaped tree where variables form a sequential chain. Use D-vine when:
  • Markets have a natural ordering (e.g., sequential election primaries)
  • Dependence is strongest between “neighboring” markets
  • No single variable dominates the dependence structure
# D-vine: sequential chain
result = hz.fit_vine(data, vine_type=hz.VineType.DVine)
# Tree 0: (A, B), (B, C), (C, D)
# Tree 1: (A, C | B), (B, D | C)
# Tree 2: (A, D | B, C)

Mathematical Background

Bedford and Cooke (2001) showed that any n-dimensional copula density can be decomposed into a product of n*(n-1)/2 bivariate copula densities arranged in a nested tree structure called a regular vine (R-vine).For n=4, the decomposition produces 6 pair copulas across 3 tree levels:
  • Tree 1: 3 unconditional pairs
  • Tree 2: 2 conditional pairs (conditioned on 1 variable)
  • Tree 3: 1 conditional pair (conditioned on 2 variables)
C-vine and D-vine are special cases of R-vine with specific tree topologies.
Beyond tree 0, pair copulas are fit to conditional distributions. The conditional CDF F(x|z) is computed using the h-function:h(u|v; theta) = dC(u,v; theta) / dvEach bivariate copula family has a closed-form h-function. The h-function transforms observations to conditional uniform marginals, which are then used to fit the next tree level.
At each edge of the vine, fit_vine evaluates all four copula families (Gaussian, Clayton, Gumbel, Frank) and selects the best by AIC. This means different edges can use different families, providing maximum flexibility. The total vine AIC is the sum of all pair copula AICs.
Joint tail probabilities are computed via Monte Carlo simulation from the fitted vine. The vine sampling algorithm (Aas et al., 2009) proceeds sequentially through the tree levels:
  1. Sample the first variable uniformly
  2. For each subsequent variable, sample conditionally using the inverse h-function
  3. Count the fraction of samples where all variables fall below (or above) the specified quantile
This captures the full heterogeneous tail dependence structure of the vine, unlike methods that assume a single copula family for all pairs.
Vine copula fitting requires at least n*(n-1)/2 parameters for n markets. With many markets (n > 10), the number of pair copulas grows quadratically. Ensure you have sufficient observations (at least 30 per pair copula) for reliable estimation.