Statistical Analysis·Return Distribution Analysis·Advanced

Copula Dependency Model

Model complex non-linear cross-asset dependency structures using copula functions including Gaussian, Student-t, Clayton, and Gumbel copulas to capture asymmetric tail dependence patterns that simple linear correlation matrices completely miss in portfolio risk modeling.

distributionsmachine-learningquant-analysis

Copula Dependency Model — Statistical Analysis

Category: Statistical Analysis | Subcategory: Distributions


What This Notebook Does

A copula separates the marginal distributions of individual assets from their joint dependence structure. This is Sklar's Theorem:

F(x₁, x₂) = C(F₁(x₁), F₂(x₂))

Why copulas matter for crypto trading:

  • Linear correlation only captures symmetric dependence — copulas capture tail dependence
  • Tail dependence: assets that move together in crashes (lower tail) but independently in good times
  • Clayton copula: strong lower tail dependence — models crisis co-movement
  • Gaussian copula: symmetric — underestimates crash co-movement (this caused the 2008 crisis)

This notebook:

  1. Fits Gaussian and Clayton copulas to BTC-ETH returns
  2. Estimates upper and lower tail dependence coefficients
  3. Simulates joint scenarios from each copula
  4. Compares portfolio risk under Gaussian vs tail-dependent copula
  5. Visualises scatter plots and tail concordance
  6. Exports results
[1]
!pip install numpy pandas matplotlib seaborn scipy --quiet
[2]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from scipy.optimize import minimize_scalar
import warnings

warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')
Imports ready.

Setup and Configuration

This section handles the initial setup of the notebook, including installing necessary libraries and defining key parameters for simulations and analysis.

Section 1 — Configuration

[3]
SIMULATION_DAYS = 1000
N_SIMULATIONS   = 10_000
TAIL_QUANTILE   = 0.10   # lower 10% for tail dependence estimation
print('Config ready.')
Config ready.

Data Generation and Tail Dependence Estimation

This section defines functions to generate synthetic BTC-ETH returns using Gaussian and Clayton copulas, and then estimates their lower tail dependence coefficients. The data simulates scenarios with and without strong tail dependence.

Section 2 — Data

[4]
def generate_tail_dependent_returns(n=1000, rho=0.7, lower_tail_dep=0.4, seed=42):
    """
    Generate BTC-ETH returns with strong lower tail dependence.

    Normal periods: correlated (rho) but independent tails.
    Crash periods: both assets crash together (lower tail dep).
    """
    rng = np.random.default_rng(seed)

    # Gaussian copula sample
    mean = [0, 0]
    cov  = [[1, rho], [rho, 1]]
    u_gauss = stats.multivariate_normal.rvs(mean, cov, size=n, random_state=seed)
    u_gauss = stats.norm.cdf(u_gauss)  # convert to uniform marginals

    # Clayton copula (lower tail dependence)
    theta = 2 * lower_tail_dep / (1 - lower_tail_dep)  # Clayton param from lower tail dep
    theta = max(theta, 0.1)
    v1 = rng.uniform(size=n)
    v2 = rng.uniform(size=n)
    u1 = v1
    u2 = (v1**(-theta) * (v2**(-theta/(theta+1)) - 1) + 1)**(-1/theta)
    u_clayton = np.column_stack([u1, np.clip(u2, 1e-6, 1-1e-6)])

    # Map uniforms to returns using t-distribution marginals
    def uniform_to_return(u, df, scale):
        return stats.t.ppf(u, df=df, scale=scale)

    rets_gauss   = np.column_stack([
        uniform_to_return(u_gauss[:,0], 4, 0.02),
        uniform_to_return(u_gauss[:,1], 4, 0.025)])
    rets_clayton = np.column_stack([
        uniform_to_return(u_clayton[:,0], 4, 0.02),
        uniform_to_return(u_clayton[:,1], 4, 0.025)])

    idx = pd.date_range('2020-01-01', periods=n, freq='D')
    gauss_df   = pd.DataFrame(rets_gauss   * 100, columns=['BTC', 'ETH'], index=idx)
    clayton_df = pd.DataFrame(rets_clayton * 100, columns=['BTC', 'ETH'], index=idx)
    return gauss_df, clayton_df


gauss_df, clayton_df = generate_tail_dependent_returns(SIMULATION_DAYS)

def tail_dependence(r1, r2, quantile):
    q1 = np.percentile(r1, quantile * 100)
    q2 = np.percentile(r2, quantile * 100)
    both_below = ((r1 <= q1) & (r2 <= q2)).sum()
    return both_below / (quantile * len(r1))

td_gauss   = tail_dependence(gauss_df['BTC'], gauss_df['ETH'], TAIL_QUANTILE)
td_clayton = tail_dependence(clayton_df['BTC'], clayton_df['ETH'], TAIL_QUANTILE)
print(f'Lower tail dependence (Gaussian copula): {td_gauss:.3f}')
print(f'Lower tail dependence (Clayton copula):  {td_clayton:.3f}')
Lower tail dependence (Gaussian copula): 0.460
Lower tail dependence (Clayton copula):  0.580

Portfolio Risk Comparison

Here, we analyze the risk of an equally-weighted BTC-ETH portfolio under both the Gaussian and Clayton copula-generated returns. We calculate Value at Risk (VaR) and Conditional Value at Risk (CVaR) to highlight the impact of tail dependence on portfolio downside risk.

Section 3 — Portfolio Risk Comparison

[5]
# Equal-weight portfolio (50% BTC, 50% ETH)
portfolio_gauss   = (gauss_df['BTC']   + gauss_df['ETH']) / 2
portfolio_clayton = (clayton_df['BTC'] + clayton_df['ETH']) / 2

for name, port in [('Gaussian copula', portfolio_gauss), ('Clayton copula', portfolio_clayton)]:
    var_99  = np.percentile(port, 1)
    cvar_99 = port[port <= var_99].mean()
    print(f'{name}: 99% VaR = {var_99:.2f}% | CVaR = {cvar_99:.2f}%')
Gaussian copula: 99% VaR = -7.19% | CVaR = -9.05%
Clayton copula: 99% VaR = -9.45% | CVaR = -10.98%

Visualization of Copula Dependencies

This section provides visual comparisons of the BTC-ETH return distributions generated by the Gaussian and Clayton copulas. Scatter plots are used to illustrate the differences in their dependence structures, particularly in the lower tail.

Section 4 — Visualization

[6]
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
fig.suptitle('Copula Dependency Models', fontsize=13, fontweight='bold')

for ax, df_plot, title in [
    (axes[0], gauss_df, f'Gaussian Copula (tail dep ≈ {td_gauss:.2f})'),
    (axes[1], clayton_df, f'Clayton Copula (tail dep ≈ {td_clayton:.2f})')
]:
    # Color by tail events
    btc_q = np.percentile(df_plot['BTC'], 10)
    eth_q = np.percentile(df_plot['ETH'], 10)
    both_tail = (df_plot['BTC'] <= btc_q) & (df_plot['ETH'] <= eth_q)
    colors = np.where(both_tail, 'red', 'steelblue')
    ax.scatter(df_plot['BTC'], df_plot['ETH'], c=colors, s=3, alpha=0.4)
    ax.axvline(btc_q, color='red', ls='--', lw=0.8, alpha=0.5)
    ax.axhline(eth_q, color='red', ls='--', lw=0.8, alpha=0.5)
    ax.set_xlabel('BTC Return (%)')
    ax.set_ylabel('ETH Return (%)')
    ax.set_title(title)

plt.tight_layout()
plt.show()
cell output

Exporting Simulation Results

This final section saves the simulated return dataframes for both the Gaussian and Clayton copulas to CSV files, allowing for further external analysis or use.

Section 5 — Export

[7]
gauss_df.to_csv('copula_gaussian_returns.csv')
clayton_df.to_csv('copula_clayton_returns.csv')
print('Saved: copula_gaussian_returns.csv, copula_clayton_returns.csv')
Saved: copula_gaussian_returns.csv, copula_clayton_returns.csv

Conclusion

This notebook demonstrates the importance of using appropriate copula models to capture complex dependence structures in financial assets, particularly tail dependence. The comparison between Gaussian and Clayton copulas highlights how a model ignoring tail dependence can significantly underestimate downside risk, as seen in the VaR and CVaR metrics. Understanding and modeling tail dependence is crucial for robust risk management and portfolio optimization, especially in volatile markets like cryptocurrency.