DCC-GARCH: Correlações Dinâmicas para Pares e Risco de Portfólio
Pergunte para a maioria das mesas de operação cripto qual é a correlação entre BTC e ETH e você receberá um número único — 0,8, talvez 0,75 — calculado sobre alguma janela que ninguém se lembra de ter escolhido. Esse número é uma mentira, ou pelo menos uma simplificação perigosa. A correlação amostral é uma média sobre um período durante o qual a verdadeira estrutura de dependência estava em movimento constante. Em mercados calmos, BTC e ETH se distanciam o suficiente para fazer um par market-neutral parecer atraente. Em uma cascata de liquidações, eles se travam um ao outro e a tudo mais, e a diversificação pela qual você pagou evapora exatamente no momento em que você mais precisa dela.
Esse não é um efeito sutil. Pegue qualquer drawdown de 2022 — o colapso da LUNA em maio, o desmonte da 3AC em junho, a implosão da FTX em novembro — e você verá a correlação média entre pares nos 20 principais tokens marchar da faixa de 0,4-0,6 em direção a 0,9+ em questão de dias. Correlação não é uma constante que ocasionalmente é mal estimada; é uma série temporal com dinâmica própria, clustering próprio e regimes próprios. Tratá-la como um escalar é o equivalente multivariado de assumir volatilidade constante — um erro que já passamos a Parte 1 desta série desmontando para um único ativo.
Este artigo é a Parte 3 de uma série de quatro partes sobre volatilidade. A Parte 1 construiu o GARCH(1,1) univariado com a biblioteca arch e mostrou como a volatilidade se agrupa em clusters e reverte à média. A Parte 2 adicionou assimetria (GJR-GARCH, EGARCH) e inovações Student-t para capturar o efeito alavancagem e as caudas pesadas. Aqui vamos para o multivariado: modelamos toda a matriz de covariância condicional conforme ela evolui, usando o modelo de Correlação Condicional Dinâmica (DCC) de Engle. Isso nos dá duas coisas que uma correlação escalar nunca pode — uma razão de hedge dinâmica para pairs trading, e uma variância de portfólio honesta e variável no tempo para alocação baseada em risco. A Parte 4 encerra a série com um backtest de targeting de volatilidade que amarra as previsões univariadas e multivariadas em uma regra de dimensionamento de posição.
Assumimos que você leu as Partes 1 e 2, então não vamos re-derivar o GARCH univariado. Se você quer o comportamento de cauda conjunta — a probabilidade de dois ativos romperem seu quantil de 1% juntos — isso é uma questão de cópulas, e cobrimos isso em Modelos de Cópula para Risco Conjunto. DCC e cópulas são complementares: a cópula dá a você uma estrutura de dependência de cauda estática-mas-flexível, enquanto o DCC dá a você uma série temporal tratável de toda a matriz de correlação. Este artigo trata do segundo caso.
Por Que a Correlação Estática Falha em Cripto
Antes da maquinaria, seja preciso sobre o que falha. Uma única correlação amostral sobre uma janela estima
Isso carrega três suposições implícitas, todas falsas para cripto:
- Estacionariedade da dependência. A janela tem um único verdadeiro. Na realidade a dependência tem regimes — um regime de mercado tranquilo próximo de 0,5 e um regime de estresse próximo de 0,95 — e os mistura em uma média sem sentido.
- Volatilidade marginal constante. A correlação de Pearson é uma covariância normalizada. Se e estão eles próprios em movimento (estão — essa é toda a premissa das Partes 1 e 2), então mesmo uma covariância constante produz uma correlação variável no tempo, e vice-versa. Você não consegue separar os dois sem um modelo de volatilidade por baixo.
- Simetria entre as direções de mercado. A correlação sobe mais em drawdowns do que em rallies. Este é o primo multivariado do efeito alavancagem. Uma janela móvel não consegue expressar isso sem se tornar tão curta que vira puro ruído.
O ajuste de janela móvel — recalcular nos últimos 30 ou 60 dias — troca um problema por outro. Janelas curtas são responsivas mas ruidosas e ficam atrasadas em relação à quebra real; janelas longas são estáveis mas obsoletas. Pior, uma matriz de correlação móvel sobre ativos não tem garantia de permanecer semidefinida positiva assim que você começa a fazer shrinkage ou remendos, o que quebra todo otimizador downstream. Queremos um modelo que (a) seja guiado por um processo de volatilidade adequado por ativo, (b) produza uma matriz de correlação válida em cada passo por construção, e (c) tenha parâmetros que possamos estimar por máxima verossimilhança em vez de escolher o comprimento de uma janela de forma arbitrária. Esse modelo é o DCC-GARCH.
O Problema Multivariado: A Matriz de Covariância Condicional
Seja o vetor de retornos para ativos no tempo , com média condicional (frequentemente apenas uma constante ou um pequeno termo AR) e resíduo . Assumimos
onde é a matriz de covariância condicional dado o conjunto de informação , e é alguma distribuição condicional (Gaussiana ou, melhor para cripto, Student-t multivariada). Tudo em modelagem de volatilidade multivariada é uma resposta diferente para uma única pergunta: como parametrizar a dinâmica de para que ela permaneça simétrica e definida positiva a cada passo sem uma explosão de parâmetros?
Duas respostas clássicas mostram por que o problema é difícil.
VECH
O modelo VECH (Bollerslev, Engle, Wooldridge 1988) escreve a semi-vetorização de como uma função linear dos resíduos quadrados passados e das covariâncias passadas:
onde empilha o triângulo inferior de uma matriz simétrica em um vetor de comprimento . Isso é maximamente geral — cada variância e covariância depende de cada variância e covariância passada — e maximamente inútil além de . Para ativos, e são cada um . Em isso é duas matrizes , aproximadamente 450 parâmetros, além de restrições de definição positiva dolorosas até de expressar. A superfície de verossimilhança é um pântano.
BEKK
O modelo BEKK (Engle & Kroner 1995) garante a definição positiva por construção usando uma forma quadrática:
com triangular superior. Como cada termo é uma forma quadrática, automaticamente desde que . O BEKK é mais parcimonioso que o VECH mas ainda escala como parâmetros — as matrizes e são cada uma . Para você está estimando algo na ordem de 200+ parâmetros conjuntamente por MLE, em dados diários ruidosos de cripto, sem garantia de que o otimizador converge para algo significativo. Na prática, o BEKK completo fica confinado a , e mesmo assim as pessoas usam as restrições "diagonal" ou "escalar" que descartam a maior parte da dinâmica cruzada.
Essa é a maldição da dimensionalidade para o GARCH multivariado: o número de parâmetros cresce quadraticamente, mas a quantidade de informação nos dados não. Você fica sem graus de liberdade muito antes de ficar sem ativos com os quais se importa. Qualquer book cripto com 10-30 tokens está completamente fora de alcance para VECH ou BEKK.
A saída, devida a Engle, é parar de tentar modelar diretamente e, em vez disso, fatorá-la em peças que já sabemos como estimar de forma barata.
O DCC de Engle (2002): A Decomposição em Dois Passos
O modelo de Correlação Condicional Constante (CCC) de Bollerslev (1990) foi a primeira fatoração parcimoniosa. Ele escreve
onde é a matriz diagonal dos desvios-padrão condicionais — um GARCH univariado por ativo — e é uma matriz de correlação constante. Isso é uma enorme simplificação: você ajusta modelos GARCH univariados independentes, depois estima uma única matriz de correlação amostral dos resíduos padronizados. A definição positiva é automática desde que seja uma matriz de correlação válida e todos os .
O problema do CCC está bem ali no nome — a correlação é constante, exatamente a suposição que abrimos este artigo rejeitando. A Correlação Condicional Dinâmica de Engle (2002) mantém a bela fatoração do CCC mas deixa a matriz de correlação respirar:
Agora é variável no tempo. O genial é que as volatilidades e as correlações são estimadas em dois passos separados, então nunca enfrentamos a otimização conjunta completa de .
Passo 1: GARCH univariado por ativo
Para cada ativo , ajuste um modelo GARCH univariado exatamente como nas Partes 1 e 2 — GARCH(1,1), GJR-GARCH ou EGARCH com inovações Student-t, o que se ajustar melhor para aquela série. Isso dá as variâncias condicionais e, portanto, .
Dos modelos ajustados extraímos os resíduos padronizados:
Por construção, cada tem (aproximadamente) variância condicional unitária. Empilhe-os em um vetor . Esses resíduos padronizados são a matéria-prima para o passo de correlação — eles tiveram suas dinâmicas de volatilidade individuais removidas, então qualquer co-movimento remanescente é dependência pura, não um artefato de volatilidade. (Essa é a mesma lógica estilo PIT que o artigo de cópulas usa antes de ajustar as margens; aqui paramos na padronização em vez de ir até o fim para uniformes.)
Passo 2: A recursão de correlação DCC
Modelamos um processo auxiliar , uma matriz simétrica definida positiva , com uma recursão estilo GARCH guiada pelos produtos externos dos resíduos padronizados:
onde:
- é a matriz de correlação incondicional dos resíduos padronizados (isso é targeting de correlação — mais sobre isso abaixo),
- governa o quanto o choque de hoje puxa a correlação,
- governa a persistência — quanto do de ontem carrega adiante,
- e a restrição de reversão à média é (com ), diretamente análoga a no GARCH univariado.
Note que a estrutura é idêntica a uma recursão GARCH(1,1) escalar, mas em matrizes: uma âncora de longo prazo , um termo de choque e um termo de persistência. Como é uma combinação convexa de matrizes semidefinidas positivas (, o produto externo de rank 1, e o anterior), permanece definida positiva desde que e os pesos sejam não negativos. É isso que nos compra matrizes de covariância válidas garantidas de graça.
é quase uma matriz de correlação mas não exatamente — sua diagonal não é exatamente 1. Então normalizamos:
Elemento a elemento, a correlação condicional entre os ativos e é
Este é uma matriz de correlação adequada — diagonal unitária, elementos fora da diagonal em , definida positiva — a cada passo de tempo, por construção. Remontando a covariância condicional completa:
Essa última forma elemento a elemento é a que você usará constantemente: a covariância condicional de dois ativos é sua correlação dinâmica multiplicada por cada uma de suas volatilidades dinâmicas. Cada ingrediente do lado direito é variável no tempo e vem de um modelo que você pode estimar.
O modelo inteiro tem apenas dois parâmetros de correlação, e , independentemente de ou . O lado da volatilidade escala linearmente (um GARCH univariado por ativo, cada um com ~4-5 parâmetros, todos ajustados independentemente e embaraçosamente paralelos). É por isso que o DCC escala onde BEKK e VECH não conseguem: a maldição da dimensionalidade fica confinada a , que é targeted (inserida como uma estimativa amostral) em vez de otimizada.
A restrição escalar e seu custo
Os escalares significam que todo par de ativos compartilha a mesma dinâmica de correlação — a mesma velocidade de ajuste e a mesma persistência. A correlação BTC-ETH e a correlação DOGE-SHIB se movem no mesmo ritmo mesmo que suas economias sejam diferentes. Esse é o preço da tratabilidade, e geralmente é um preço aceitável. Generalizações (DCC Generalizado com matrizes ; o DCC assimétrico de Cappiello-Engle-Sheppard) relaxam isso ao custo de parâmetros e estabilidade de estimação. Mencionamos o aDCC abaixo.
A Quase-Log-Verossimilhança do DCC
Para estimar e precisamos da verossimilhança. O resultado chave de Engle é que a log-verossimilhança Gaussiana se separa em uma parte de volatilidade e uma parte de correlação, o que justifica o estimador em dois passos. Assumindo , a contribuição da log-verossimilhança no tempo é
Substitua . Então e , e usando :
Agora divida somando e subtraindo :
A parte de volatilidade depende apenas dos parâmetros GARCH univariados (através de ) — maximizá-la é exatamente ajustar modelos GARCH univariados independentes, o que fizemos no Passo 1. A parte de correlação depende de e (através de ), dados os resíduos padronizados do Passo 1. Então no Passo 2 maximizamos apenas
(o termo não depende de , então o descartamos). Essa é uma otimização de dois parâmetros independentemente de quantos ativos existam — esse é todo o ponto. É chamada de quase-verossimilhança porque o estimador em dois passos é consistente mas não totalmente eficiente; os erros padrão precisam de uma correção (Engle & Sheppard 2001), mas para geração de sinal as estimativas pontuais são o que importa.
Para cripto, as inovações Gaussianas subestimam o risco de cauda. Trocar para a verossimilhança Student-t multivariada é uma mudança direta em (substitua o kernel Gaussiano pela densidade multivariada e adicione um parâmetro de graus de liberdade ). Mantemos a quase-verossimilhança Gaussiana no estimador abaixo por clareza e observamos onde entra — a padronização das Partes 1-2 já usou inovações t nas margens, o que captura a maior parte do benefício de cauda.
Implementação em Python
Um fato contundente mas importante: a biblioteca arch não faz GARCH multivariado ou DCC. arch é um excelente motor univariado (nos apoiamos nela exatamente para isso), mas não há dcc_model nela. Suas opções práticas são:
- Construir seu próprio DCC em cima do
arch— ajustar modelos univariados comarch, extrair resíduos padronizados, implementar a recursão de e a quase-verossimilhança de correlação em NumPy/SciPy, e otimizar os dois escalares. É o que fazemos abaixo. São cerca de 60 linhas e completamente transparente. - O pacote PyPI
mgarch— uma implementação leve em Python puro de DCC-GARCH. Conveniente para um ajuste rápido, menos flexível se você quiser margens GJR ou inovações t com precisão. - O
rmgarchdo R (Alexios Galanos) — a implementação de referência.dccspec/dccfitsuportam DCC, aDCC, GARCH-copula, Student-t e erros padrão adequados. Se você está fazendo pesquisa séria de volatilidade multivariada, ormgarch(chamado do Python viarpy2se necessário) é o padrão ouro.
Construímos a opção 1 porque torna cada parte móvel explícita e reutiliza as habilidades univariadas das Partes 1-2.
Passo 1: Ajustar margens GARCH univariadas com arch
import numpy as np
import pandas as pd
from arch import arch_model
from scipy.optimize import minimize
def fetch_returns(symbols, start="2022-01-01", end="2025-12-31"):
"""
Daily log returns for a list of crypto symbols.
Replace with your data source (ccxt for 24/7 exchange data,
yfinance for a quick sketch).
"""
import yfinance as yf
px = yf.download([f"{s}-USD" for s in symbols],
start=start, end=end)["Close"]
px.columns = symbols
rets = np.log(px / px.shift(1)).dropna()
return rets
symbols = ["BTC", "ETH", "SOL", "BNB"]
returns = fetch_returns(symbols)
def fit_univariate(series, dist="t"):
"""
Fit GJR-GARCH(1,1) with Student-t innovations (Part 2 model).
Returns the fitted result and the *scaled* series it was fit on.
We scale returns by 100 for the optimizer's numerical health,
exactly as in Parts 1 and 2.
"""
scaled = series * 100.0
model = arch_model(scaled, mean="Constant",
vol="GARCH", p=1, o=1, q=1, dist=dist)
res = model.fit(disp="off")
return res
fits = {s: fit_univariate(returns[s]) for s in symbols}
Z = pd.DataFrame({s: fits[s].std_resid for s in symbols}).dropna()
Sigma = pd.DataFrame({s: fits[s].conditional_volatility
for s in symbols}).loc[Z.index]
print(Z.describe())
Uma verificação rápida de sanidade nos resíduos padronizados importa. Se qualquer coluna tiver um desvio padrão muito distante de 1, ou autocorrelação remanescente pesada em seu quadrado (Ljung-Box em ), a margem univariada está mal especificada e o passo DCC herdará esse erro. Corrija a margem primeiro — era para isso que servia a Parte 2.
Passo 2: A recursão DCC e a quase-log-verossimilhança
def dcc_negloglik(params, Z):
"""
Negative DCC quasi-log-likelihood (Gaussian) in (a, b).
Z : (T, d) array of standardized residuals from Step 1.
Implements:
Q_t = (1-a-b) Qbar + a z_{t-1} z_{t-1}' + b Q_{t-1}
R_t = diag(Q_t)^{-1/2} Q_t diag(Q_t)^{-1/2}
LL = -1/2 sum_t ( log|R_t| + z_t' R_t^{-1} z_t )
with correlation targeting: Qbar = sample corr of Z.
"""
a, b = params
if a <= 0 or b <= 0 or a + b >= 1.0:
return 1e10
Z = np.asarray(Z)
T, d = Z.shape
Qbar = np.cov(Z, rowvar=False, bias=True) # unconditional (targeted)
dinv = np.diag(1.0 / np.sqrt(np.diag(Qbar)))
Qbar = dinv @ Qbar @ dinv
Q = Qbar.copy() # initialize Q_1 at the target
ll = 0.0
for t in range(T):
q_diag = np.sqrt(np.diag(Q))
R = Q / np.outer(q_diag, q_diag)
sign, logdet = np.linalg.slogdet(R)
if sign <= 0:
return 1e10
z = Z[t]
Rinv_z = np.linalg.solve(R, z)
ll += -0.5 * (logdet + z @ Rinv_z)
Q = (1 - a - b) * Qbar + a * np.outer(z, z) + b * Q
return -ll
def fit_dcc(Z):
"""Estimate (a, b) by maximizing the DCC quasi-log-likelihood."""
res = minimize(
dcc_negloglik, x0=[0.03, 0.94], args=(np.asarray(Z),),
method="L-BFGS-B",
bounds=[(1e-6, 0.5), (1e-6, 0.999)],
)
a, b = res.x
print(f"DCC estimates: a = {a:.4f}, b = {b:.4f}, a+b = {a+b:.4f}")
print(f"log-likelihood: {-res.fun:.2f}")
return a, b
a_hat, b_hat = fit_dcc(Z)
Rodar isso em um book BTC/ETH/SOL/BNB ao longo de alguns anos de dados diários produz saída na seguinte forma (os números abaixo são ilustrativos, não de um experimento datado específico — rode nos seus próprios dados):
DCC estimates: a = 0.0287, b = 0.9401, a+b = 0.9688
log-likelihood: -3812.44
Como interpretar:
- é pequeno — a matriz de correlação não dispara com o choque de um único dia. Cada dia empurra em direção ao produto externo em apenas ~3%.
- é grande — as correlações são altamente persistentes. Uma vez que o book se acopla em um evento de estresse, ele permanece acoplado por um tempo, decaindo de volta em direção a lentamente. Isso corresponde à experiência vivida dos drawdowns em cripto: as correlações não voltam ao normal no momento em que o preço se estabiliza.
- confirma a reversão à média. O processo de correlação tem um nível de longo prazo estacionário () ao qual retorna, com uma meia-vida de aproximadamente dias. Se você alguma vez estimar essencialmente igual a 1, o processo de correlação está integrado — não tem âncora de longo prazo, geralmente um sintoma de uma quebra estrutural dentro da sua amostra que o modelo está absorvendo como persistência infinita.
A persistência quase unitária e o pequeno carregamento de choque são a assinatura canônica do DCC em diversas classes de ativos, e cripto não é exceção. É também por isso que uma correlação móvel de 30 dias é um substituto tão pobre: uma janela móvel implicitamente assume e que não correspondem em nada a essa estrutura de decaimento.
Algumas notas de implementação que economizam tempo real de depuração:
- Inicialização. Começar em
[0.03, 0.94]reflete a estimativa típica de cripto: pequeno (correlações reagem a choques mas não violentamente), grande (correlações são persistentes). Se seu otimizador vagar para , o processo de correlação está integrado — geralmente um sinal de uma quebra estrutural na amostra (uma mudança de regime que o modelo está se esforçando para ajustar como persistência). - Convenção de timing. Dentro do loop pontuamos contra e depois atualizamos com para o próximo passo. Isso mantém como função apenas da informação até — sem look-ahead. Errar esse deslocamento de um é o bug mais comum do DCC, e ele infla silenciosamente o ajuste in-sample.
- Targeting de correlação. Inserimos como a correlação amostral em vez de estimá-la. Isso é o que torna a otimização bidimensional. O custo é que usa a amostra completa, então em um walk-forward estrito você deve reestimar isso apenas na janela de treino (veja abaixo).
Passo 3: Reconstruir os caminhos de correlação e covariância
Uma vez que estejam fixos, rode a recursão mais uma vez, desta vez armazenando o caminho completo de (e ) para que estratégias downstream possam usá-lo.
def dcc_filter(Z, Sigma, a, b):
"""
Run the DCC recursion with fixed (a,b) and return the full paths:
R_path : (T, d, d) conditional correlation matrices
H_path : (T, d, d) conditional covariance matrices (scaled units)
Sigma : (T, d) conditional volatilities aligned with Z.
"""
Z = np.asarray(Z); Sig = np.asarray(Sigma)
T, d = Z.shape
Qbar = np.cov(Z, rowvar=False, bias=True)
dinv = np.diag(1.0 / np.sqrt(np.diag(Qbar)))
Qbar = dinv @ Qbar @ dinv
Q = Qbar.copy()
R_path = np.empty((T, d, d))
H_path = np.empty((T, d, d))
for t in range(T):
q_diag = np.sqrt(np.diag(Q))
R = Q / np.outer(q_diag, q_diag)
R_path[t] = R
Dt = np.diag(Sig[t]) # D_t = diag(sigma_i,t)
H_path[t] = Dt @ R @ Dt # H_t = D_t R_t D_t
z = Z[t]
Q = (1 - a - b) * Qbar + a * np.outer(z, z) + b * Q
return R_path, H_path
R_path, H_path = dcc_filter(Z, Sigma, a_hat, b_hat)
i, j = symbols.index("BTC"), symbols.index("ETH")
rho_btc_eth = pd.Series(R_path[:, i, j], index=Z.index, name="rho_BTC_ETH")
print(rho_btc_eth.describe())
print("min/max correlation:", rho_btc_eth.min().round(3),
rho_btc_eth.max().round(3))
A série rho_btc_eth é a recompensa de todo o exercício: em vez de um número, agora você tem uma correlação diária que pode plotar, limitar ou alimentar em uma estratégia. Em dados reais de cripto você tipicamente verá ela variar de aproximadamente 0,5 em períodos tranquilos até acima de 0,9 durante estresse — exatamente o espectro que uma correlação amostral única faz desaparecer na média.
Previsão um passo à frente
Para negociação ao vivo você precisa de um passo à frente a partir da informação disponível agora. O lado da volatilidade vem da previsão de um passo de cada modelo arch; o lado da correlação é mais uma volta da recursão:
def dcc_forecast_next(Z, a, b, fits, symbols):
"""One-step-ahead H_{t+1} using info through the last observation."""
Z = np.asarray(Z); T, d = Z.shape
Qbar = np.cov(Z, rowvar=False, bias=True)
dinv = np.diag(1.0 / np.sqrt(np.diag(Qbar)))
Qbar = dinv @ Qbar @ dinv
Q = Qbar.copy()
for t in range(T):
z = Z[t]
Q = (1 - a - b) * Qbar + a * np.outer(z, z) + b * Q
q_diag = np.sqrt(np.diag(Q))
R_next = Q / np.outer(q_diag, q_diag)
sig_next = np.array([
np.sqrt(fits[s].forecast(horizon=1, reindex=False)
.variance.iloc[-1, 0])
for s in symbols
])
Dt = np.diag(sig_next)
H_next = Dt @ R_next @ Dt
return R_next, H_next, sig_next
R_next, H_next, sig_next = dcc_forecast_next(Z, a_hat, b_hat, fits, symbols)
Lembre-se de que tudo está em unidades escaladas (×100) porque ajustamos arch em series * 100. Divida as volatilidades por 100 (e as covariâncias por ) para voltar às unidades de retorno bruto antes de alimentar uma estratégia. Manter a escala em ordem é tedioso mas uma fonte frequente de bugs silenciosos.
Aplicação 1: Uma Razão de Hedge Dinâmica para Pairs Trading
O par market-neutral clássico — comprado em um ativo, vendido em uma quantidade beta-ponderada de outro — vive ou morre com a razão de hedge . Estime-a por OLS estático sobre uma janela de treino e você herda exatamente o problema de correlação obsoleta sobre o qual é todo este artigo: o hedge que neutralizou a exposição de mercado no trimestre passado está errado neste trimestre.
O DCC dá a você a razão de hedge como uma série temporal. O hedge de variância mínima da exposição ETH usando BTC é o coeficiente de regressão condicional
Cada termo do lado direito é uma saída do DCC. A razão de hedge se move por duas razões distintas, e o DCC as separa de forma limpa: a correlação muda (os ativos se acoplam ou desacoplam), e a razão de volatilidade muda (um ativo fica relativamente mais volátil). Um beta de OLS móvel borra os dois efeitos juntos com um atraso; o DCC os atribui.
def dynamic_hedge_ratio(R_path, Sigma, base="BTC", target="ETH",
symbols=symbols, index=Z.index):
"""
beta_t to hedge `target` exposure with `base`:
beta_t = rho_t * sigma_target,t / sigma_base,t
(Scaling cancels in the ratio, so scaled units are fine here.)
"""
i = symbols.index(target)
j = symbols.index(base)
rho = R_path[:, i, j]
sig = np.asarray(Sigma)
beta = rho * sig[:, i] / sig[:, j]
return pd.Series(beta, index=index, name=f"beta_{target}_{base}")
beta_t = dynamic_hedge_ratio(R_path, Sigma)
print(beta_t.describe())
spread = (returns["ETH"] - beta_t.shift(1) * returns["BTC"]).dropna()
Alimente o spread no motor de pares que você roda. O hedge dinâmico não cria uma vantagem por si só — ele torna o spread negociado genuinamente market-neutral ao longo do tempo, de modo que seu sinal de reversão à média não seja contaminado por exposição direcional à deriva. Se você constrói estratégias de pares, isso se encaixa diretamente nos frameworks de Arbitragem Estatística e Pairs Trading em Cripto e a abordagem de distância para pares, substituindo sua razão de hedge fixa. A própria série de correlação também é uma entrada mais limpa para um sinal de par baseado em correlação do que qualquer janela móvel — você obtém um suavizado e consistente com o modelo em vez de uma estimativa em janela ruidosa.
Duas precauções específicas ao usar ao vivo. Primeiro, atrase-o — negocie com , nunca o contemporâneo, ou você estará espiando o futuro. Segundo, uma razão de hedge que oscila todo dia gera turnover e taxas; no mercado 24/7 de cripto com custos de funding na perna vendida, um hedge hiper-reativo pode sangrar mais do que a deriva que corrige. Suavize (um EWMA, ou rebalanceie o hedge apenas quando ele se move além de uma banda) e dimensione tudo isso com sensatez — dimensionamento de posição a partir de um sinal ruidoso é sua própria disciplina, coberta em dimensionamento pelo critério de Kelly.
Aplicação 2: Variância de Portfólio Variável no Tempo
Para um portfólio com vetor de pesos , a variância condicional é
Com uma matriz de covariância estática — o padrão de Markowitz — esse número é uma constante que você calculou uma vez e finge que ainda é verdadeira. Não é. O risco de portfólio respira com o mercado, e ele respira mais forte exatamente quando as correlações disparam, porque em um drawdown tanto os termos quanto os termos sobem juntos e se multiplicam. Um portfólio que parecia ter 40% de volatilidade anualizada em mercados calmos pode estar rodando 80%+ em uma semana de estresse, e uma matriz de covariância estática dirá a você que nada mudou.
def portfolio_vol_path(H_path, weights, index=Z.index, unscale=1e4):
"""
Conditional portfolio volatility sigma_{p,t} = sqrt(w' H_t w).
H_path is in scaled (x100) units, so covariances carry a 100^2
factor: divide by unscale=1e4 to return to raw-return variance.
"""
w = np.asarray(weights)
var_t = np.einsum("i,tij,j->t", w, H_path, w) / unscale
return pd.Series(np.sqrt(var_t), index=index, name="port_vol")
w = np.array([0.4, 0.3, 0.2, 0.1]) # BTC, ETH, SOL, BNB
pv = portfolio_vol_path(H_path, w)
pv_annual = pv * np.sqrt(365)
print(pv_annual.describe())
Esse variável no tempo é a entrada honesta que a alocação baseada em risco precisa. A otimização média-variância (Markowitz para cripto) com uma covariância amostral estática está otimizando contra uma ficção; alimentá-la com (ou sua previsão de curto prazo) torna a própria fronteira eficiente variável no tempo e força o otimizador a reduzir risco em regimes de correlação crescente em vez de depois deles. Abordagens de risk-parity e hierárquicas — o pipeline HRP + CVaR — são ainda mais sensíveis à entrada de covariância, já que toda a alocação é uma função da matriz de risco. E se você está comparando alocadores lado a lado, como em algoritmos de otimização de portfólio comparados, se eles consomem covariância estática ou dinâmica costuma ser um fator mais determinante do risco realizado do que a escolha do algoritmo.
A aplicação direta é o targeting de volatilidade de todo o portfólio: escolha uma volatilidade anualizada alvo , e escale a exposição bruta por a cada período para que o risco realizado permaneça aproximadamente constante em vez de disparar em crises. Isso fecha o ciclo com a Parte 4, que constrói e faz backtest exatamente dessa regra.
Aplicação 3: Correlação como Sinal de Regime
Além de hedging e dimensionamento, a matriz de correlação carrega um sinal macro. O escalar único mais útil que você pode extrair é a correlação média entre pares:
Quando sobe em todo o book, o mercado está entrando em um regime de risk-off — histórias idiossincráticas param de importar e tudo negocia como um único beta macro. Essa é a assinatura quantitativa de "as correlações vão para 1 em uma crise". Ela tende a liderar ou coincidir com drawdowns, o que a torna um indicador de regime utilizável em vez de uma autópsia atrasada.
def avg_pairwise_corr(R_path, index=Z.index):
T, d, _ = R_path.shape
iu = np.triu_indices(d, k=1) # upper-triangle off-diagonals
avg = R_path[:, iu[0], iu[1]].mean(axis=1)
return pd.Series(avg, index=index, name="avg_corr")
avg_corr = avg_pairwise_corr(R_path)
roll_q = avg_corr.rolling(365, min_periods=90).quantile(0.80)
risk_off = (avg_corr > roll_q)
Você pode usar risk_off como um regulador autônomo (corte exposição bruta, amplie stops, suspenda estratégias de reversão à média que são atropeladas quando tudo tende junto) ou como uma feature em um modelo de regime mais formal. Ele se combina naturalmente com a abordagem de cadeia de Markov oculta em detecção de regime com HMMs: a correlação média do DCC é uma das variáveis de observação mais informativas que você pode entregar a um HMM, porque ela é prospectiva sobre estresse sistêmico de uma forma que os retornos passados não são. A ressalva honesta: correlação crescente informa que a diversificação está falhando, não para qual direção o mercado vai. É um sinal de risco, não um sinal de alfa, e deve ser dimensionado como tal — veja a assimetria entre perdas e lucros para entender por que tratar um regime de risco como uma aposta direcional termina mal.
Considerações Práticas
Estabilidade de estimação e número de ativos
O DCC escala muito melhor que o BEKK, mas "escalar" não é "grátis". A matriz de targeting de correlação é uma correlação amostral , e matrizes de correlação amostral ficam mal condicionadas conforme se aproxima do número de observações. Com 4 ativos e 1000 dias você está bem. Com 60 ativos e 400 dias, está quase singular, sua inversa na verossimilhança explode, e pode vagar para não-PD por ruído numérico. Mitigações, aproximadamente na ordem de quanto você vai precisar delas:
- Aplique shrinkage em em direção a um alvo estruturado (Ledoit-Wolf, ou em direção à identidade / uma matriz de correlação constante) antes de rodar a recursão. Esse é o ajuste de maior alavancagem para books grandes.
- Agrupe ativos em um punhado de setores (principais, L1s, DeFi, memes), modele dentro e entre no nível setorial, ou rode o DCC em fatores de componentes principais em vez de ativos brutos.
- Prefira mais dados a mais ativos. O DCC tem um apetite insaciável por um histórico longo, limpo e contemporâneo — que é exatamente o que os tokens mais jovens não têm.
Realisticamente, mantenha o DCC direto a algumas dezenas de ativos no máximo. Para um universo grande, DCC sobre retornos de fatores mais resíduos idiossincráticos é a solução padrão.
O targeting de correlação é um atalho com custo
Fazer targeting de torna a estimação tratável mas embute a correlação incondicional da amostra completa em cada . Em um backtest estrito isso é um vazamento de look-ahead: sua matriz de correlação do dia "conhece" a correlação média de toda a amostra, incluindo o futuro. Para uma avaliação honesta você deve reestimar apenas na janela de treino e mantê-la fixa fora da amostra, ou rolá-la para frente. Essa é a mesma disciplina que todo o framework de otimização walk-forward impõe, e é fácil de violar acidentalmente com um np.cov(Z) conveniente sobre o array completo — como o nosso código didático acima faz. Corrija isso antes de confiar em um único número de P&L.
Cadência de reajuste e disciplina contra look-ahead
Você não precisa reotimizar todo dia — eles são parâmetros estáveis. Uma cadência de produção sensata:
- Reestime e os parâmetros GARCH univariados semanalmente ou mensalmente.
- Rode o filtro (atualize , ) a cada período com os parâmetros congelados para obter e atualizados. Filtrar é barato; ajustar não é.
- Sempre preveja, nunca suavize. Use construído a partir de informação até para negociar em . A estrutura de dois passos (ajustar em uma janela, depois filtrar para frente) é o que mantém você honesto.
A lacuna entre um backtest do DCC e a performance ao vivo é quase sempre um vazamento de look-ahead — de amostra completa, contemporâneo, ou reajuste em dados que incluem a operação que você está avaliando. A disciplina de fazer o backtest corresponder às condições ao vivo é um tópico próprio em paridade backtest-ao vivo, e o DCC é um modelo que pune descuido aqui mais do que a maioria. Se, após uma avaliação walk-forward limpa, a correlação dinâmica não adicionar nada sobre uma estimativa móvel simples para sua estratégia, esse é um resultado negativo real e publicável — a mentalidade em resultados negativos honestos se aplica diretamente.
DCC Assimétrico (aDCC)
Assim como o efeito alavancagem univariado (Parte 2) significa que notícias ruins elevam a volatilidade mais do que notícias boas, as correlações sobem mais após choques negativos conjuntos do que após choques positivos conjuntos. Cappiello, Engle & Sheppard (2006) capturam isso com o DCC assimétrico, adicionando um termo guiado pelo produto externo dos resíduos padronizados de parte negativa :
onde e mede o impulso extra de correlação vindo de movimentos conjuntos de baixa. Para cripto, onde a correlação de crash é o risco dominante, o termo de assimetria costuma ser significativo e vale o parâmetro extra. O rmgarch ajusta o aDCC nativamente (model="aDCC"); adicionar o termo ao nosso estimador NumPy é um exercício direto.
Comparação: DCC Contra as Alternativas
Onde o DCC se encaixa entre as formas de obter uma matriz de covariância para um book cripto? O resumo honesto:
| Abordagem | Parâmetros | Escala até | variável no tempo? | PD garantida? | Dependência de cauda? |
|---|---|---|---|---|---|
| Covariância amostral / móvel | 0 (comprimento da janela) | qualquer | grosseiramente (com atraso, ruidosa) | não (precisa de remendo) | não |
| EWMA (RiskMetrics) | 1 () | qualquer | sim (decaimento único) | sim | não |
| CCC-GARCH | margens + | dezenas | não ( constante) | sim | não |
| DCC-GARCH | margens + 2 | dezenas | sim | sim | não |
| aDCC-GARCH | margens + 3 | dezenas | sim, assimétrico | sim | parcial |
| BEKK | sim (rico) | sim | não | ||
| VECH | sim (mais rico) | difícil | não | ||
| GARCH-copula | margens + cópula | dezenas (vines) | cópula estática | sim | sim |
Algumas leituras dessa tabela:
- EWMA é a base barata que todos deveriam superar antes de alegar que o DCC ajuda. É um caso especial de um parâmetro em espírito — um único decaimento exponencial aplicado diretamente à covariância — e para muitos books é surpreendentemente difícil de melhorar out-of-sample. Se o DCC não superar o EWMA em walk-forward limpo, use o EWMA.
- CCC vs DCC é todo o ponto deste artigo: mesma fatoração, mas o CCC congela e o DCC o deixa se mover. Os dois parâmetros extras () são a diferença inteira, e em cripto eles ganham seu lugar.
- BEKK/VECH compram dinâmicas mais ricas — cada covariância pode responder a cada choque passado — mas o custo de parâmetros os confina a books minúsculos. Para qualquer coisa além de 4 ativos eles não são uma opção real.
- GARCH-copula é a única linha com "sim" na dependência de cauda. Essa é a complementaridade de novo: o DCC modela o centro dinâmico da distribuição conjunta, as cópulas modelam suas caudas estáticas. Se sua questão de risco é "o que acontece quando tudo quebra ao mesmo tempo", recorra ao pipeline de cópulas; se é "qual é minha razão de hedge / variância de portfólio agora", recorra ao DCC.
O padrão prático para uma mesa cripto sistemática: DCC (ou aDCC) para razões de hedge e covariância dinâmica no corpo, uma camada de cópula para risco de cauda e CVaR, e EWMA como a linha de base de verificação de sanidade que mantém você honesto sobre se a maquinaria extra está valendo a pena.
Limitações
- Dinâmica escalar. Um único e um único para todos os pares é uma restrição forte. BTC-ETH e dois altcoins obscuros compartilham a mesma velocidade de ajuste. O DCC Generalizado relaxa isso mas reintroduz a explosão de parâmetros que o DCC foi projetado para evitar.
- Perda de eficiência em dois passos. O estimador de quase-verossimilhança é consistente mas não totalmente eficiente, e erros padrão ingênuos estão errados. Use a correção de Engle-Sheppard se você se importa com inferência; para geração de sinal as estimativas pontuais bastam.
- Caudas Gaussianas por padrão. A quase-verossimilhança Gaussiana simples subestima o risco de cauda conjunto. Inovações Student-t ajudam; para dependência de cauda genuína (a probabilidade de movimentos extremos simultâneos), o DCC é a ferramenta errada e um modelo de cópula é o certo. O DCC dá a você o corpo dinâmico da correlação; cópulas dão a você a cauda estática. Mesas sérias usam ambos.
- Correlação não é causalidade, nem direção. crescente avisa que a diversificação está falhando; não diz nada sobre a direção do mercado. Não sobrecarregue um sinal de risco com expectativas direcionais.
- Fome de dados. Tudo acima assume históricos longos, limpos e sincronizados. Os tokens mais novos e interessantes de cripto violam todos os três.
Resumo
- A correlação estática é uma mentira em cripto. As correlações se agrupam, persistem e disparam em direção a 1 em drawdowns — exatamente quando a diversificação deveria ajudar. Um único amostral tira a média de um processo de mudança de regime em algo sem sentido.
- O GARCH multivariado completo (VECH, BEKK) não escala. A contagem de parâmetros cresce como ; ambos ficam confinados a um punhado de ativos na prática.
- O DCC (Engle 2002) fatora o problema: , com vindo de ajustes GARCH univariados independentes (reutilizando as Partes 1-2) e vindo de uma recursão de dois parâmetros. Ele escala para dezenas de ativos porque apenas são otimizados.
- A recursão , normalizada para , produz uma matriz de correlação definida positiva válida a cada passo, com , .
- O
archnão faz DCC. Ajuste as margens comarch, depois implemente o estimador NumPy/SciPy de ~60 linhas aqui, ou usemgarch(Python) ourmgarch(R, a referência). - Três retornos concretos: uma razão de hedge dinâmica para pairs trading; uma variância de portfólio honesta e variável no tempo para alocação baseada em risco; e a correlação média entre pares como sinal de regime risk-off.
- A disciplina é tudo. O targeting de correlação vaza a média da amostra completa, então reestime apenas nos dados de treino; atrase toda razão de hedge; filtre para frente, nunca suavize. A avaliação walk-forward é inegociável.
- O aDCC adiciona um termo de assimetria de baixa e geralmente vale a pena em cripto, onde a correlação de crash domina.
- A Parte 4 usa essas previsões para construir e fazer backtest de uma estratégia com targeting de volatilidade.
Referências:
- Engle, R. (2002). Dynamic Conditional Correlation: A Simple Class of Multivariate Generalized Autoregressive Conditional Heteroskedasticity Models. Journal of Business & Economic Statistics, 20(3), 339-350. DOI
- Engle, R. & Sheppard, K. (2001). Theoretical and Empirical Properties of Dynamic Conditional Correlation Multivariate GARCH. NBER Working Paper 8554. DOI
- Bollerslev, T. (1990). Modelling the Coherence in Short-Run Nominal Exchange Rates: A Multivariate Generalized ARCH Model. Review of Economics and Statistics, 72(3), 498-505. DOI
- Cappiello, L., Engle, R., & Sheppard, K. (2006). Asymmetric Dynamics in the Correlations of Global Equity and Bond Returns. Journal of Financial Econometrics, 4(4), 537-572. DOI
- Engle, R. & Kroner, K. (1995). Multivariate Simultaneous Generalized ARCH. Econometric Theory, 11(1), 122-150. DOI
- Bollerslev, T., Engle, R., & Wooldridge, J. (1988). A Capital Asset Pricing Model with Time-Varying Covariances. Journal of Political Economy, 96(1), 116-131. DOI
- Galanos, A. (2022). rmgarch: Multivariate GARCH Models. R package. CRAN.
MarketMaker.cc Team
Quantitative Research & Strategy