Opiniões nas Copulas

Bernardo Reckziegel 2022-05-03 6 min read

Vamos assumir que o time de gestão esteja receoso com a performance futura do mercado de ações e deseje simular um cenário de “estresse” para medir o impacto ex-ante sobre o P&L.

Uma das maneiras de simular esse tipo de comportamento é colocando as opiniões nas correlações - como fiz aqui - ou manipulando as copulas diretamente. No post de hoje mostro como implementar o segundo approach.

Como de praxe, análise é conduzida com o dataset EuStockMarkets, que vem com a instalação do R:

# invariance
x <- matrix(diff(log(EuStockMarkets)), ncol = 4)
colnames(x) <- colnames(EuStockMarkets)
head(x)
##               DAX          SMI          CAC         FTSE
## [1,] -0.009326550  0.006178360 -0.012658756  0.006770286
## [2,] -0.004422175 -0.005880448 -0.018740638 -0.004889587
## [3,]  0.009003794  0.003271184 -0.005779182  0.009027020
## [4,] -0.001778217  0.001483372  0.008743353  0.005771847
## [5,] -0.004676712 -0.008933417 -0.005120160 -0.007230164
## [6,]  0.012427042  0.006737244  0.011714353  0.008517217

No gráfico abaixo coloco o índice SMI no eixo x e o DAX no eixo y. Perceba que é a combinação das distribuições marginais que nos permite fazer inferência sobre a associação linear dessas variáveis:

Ou seja, são as informações individuais - as “margens” - que ditam a estrutura de correlação dos ativos financeiros.

Distribuição Multivariada = Margens + Copulas

As margens carregam as informações puramente índividuais (exclusivas de cada ativo), enquanto as copulas levam as informações puramente conjuntas (de dependência entre as variáveis)1.

Manipulando a expressão acima obtemos:

Copulas = Distribuição Multivariada - Margens

Em outras palavras, a copula é a informação que sobra uma vez que tenhamos “limpado” a informação puramente individual contida nas margens.

Abaixo a copula empírica dos índices SMI e DAX:

Perceba que essa copula, em particular, apresenta dois pontos de aglomeração nos extremos: quando o SMI cai muito (perto de \(0\)), o DAX também cai muito (perto de \(0\)); quando SMI sobe muito (perto de \(1\)), o DAX também sobe muito (perto de \(1\)). Ou seja, essa copula está revelando que em momentos de euforia e pânico os índices SMI e DAX andam juntos!

Obviamente, nem todas as copulas são iguais. Abaixo mostro quatro tipos de copulas bastante conhecidas, que fazem parte do mundo arquimediano.


A copula de Clayton geralmente é utilizada para modelar eventos de pânico, porque uma enorme quantidade de pontos se aglomeram à esquerda na parte inferior (quando x cai, y também cai). Já a copula de Gumbel é utilizada para modelar eventos de euforia, pois muitos pontos se concentram à direita na parte superior (quando x sobe, y também sobe).

Na prática, não há uma única forma de “decompor” as distribuições entre margens e copulas. Aqui, sigo o approach das probabilidades flexíveis e manipulo esses elementos com o algoritmo CMA, que oferece uma receita não-paramétrica de dois passos para “separar” e “combinar” distribuições multivariadas.

O pacote cma não está no CRAN, então para reproduzir os códigos abaixo você deve rodar o comando: devtools::install_github("Reckziegel/CMA") no console:

# devtools::install_github("Reckziegel/CMA")
library(cma)

sep <- cma_separation(x = x)
sep
## # CMA Decomposition
## marginal: << tbl 1859 x 4 >>
## cdf     : << tbl 1859 x 4 >>
## copula  : << tbl 1859 x 4 >>

Para objetos da classe cma_separation o pacote cma disponibiliza a família de funções fit_copula_*(). Como comentei, a copula de clayton é um candidado natural para modelar eventos de estresse:

clayton_fit <- fit_copula_clayton(copula = sep, method = "ml")
clayton_fit
## # New Copula
## Conveged:       0
## Dimension:      4
## Log-Likelihood: 1615.755
## Model:          claytonCopula

Perceba que nessa copula apenas um parâmetro precisa ser estimado:

clayton_fit$estimate
## [1] 1.066038

Quando maior for o parâmetro \(\alpha\), mais aglomerados os dados ficam a esquerda da distribuição. Veja:

Para precificar um cenário de “sell-off” ex-ante, adiciono uma perturbação no parâmetro \(\alpha\). Em particular, uso \(\alpha = 5\) e com essa nova estimativa simulo um painel com \(1.000.000\) de linhas e \(4\) colunas que guardam as mesmas propriedades estatísticas do objeto clayton_fit.

Esse processo é realizado com a função generate_copulas:

# lock environment
set.seed(2)

# "twick" the alpha parameter
clayton_fit$estimate <- 5

# generate new scenarios
simul_clayton <- generate_copulas(model = clayton_fit, n = 1000000)

Para colocar opiniões nas copulas, o pacote ffp disponibiliza a função view_on_copula:

library(ffp)

prior_for_simul <- rep(1 / 1000000, 1000000)

views_on_cop <- view_on_copula(
  x     = sep$copula, 
  simul = simul_clayton, 
  p     = prior_for_simul
)
views_on_cop
## # ffp view
## Type:  View On Copula
## Aeq :  Dim 34 x 1859 
## beq :  Dim 34 x 1

Formalmente, o objetivo é minimizar a expressão:

$$ min \sum_{i=1}^I x_i(ln(x_i) - ln(p_i)) $$ \(s.a.\) $$ \sum_{i=1}^I \hat{p_i} U_{j,k}U_{j,l} = \sum_{i=1}^I p_i \hat{U}_{j,k}\hat{U}_{j,l} $$ $$ \sum_{i=1}^I \hat{p_i} U_{j,k}U_{j,l}U_{j,i} = \sum_{i=1}^I p_i \hat{U}_{j,k}\hat{U}_{j,l}\hat{U}_{j,i} $$ $$ \sum_{i=1}^I \hat{p_i} U_{j,k} = 0.5 $$ $$ \sum_{i=1}^I \hat{p_i} U_{j,k}^2 = 0.33 $$

No qual \(p_i \hat{U}_{j,k}\hat{U}_{j,l}\) e \(p_i \hat{U}_{j,k}\hat{U}_{j,l}\hat{U}_{j,i}\) atuam como restrições nos momentos cruzados e \(0.5\) e \(0.33\) condicionam os dois primeiros momentos da distribuição uniforme.

Esse sistema é solucionado com a função entropy_pooling:

prior_from_data <- rep(1 / nrow(x), nrow(x))

ep <- entropy_pooling(
  p      = prior_from_data, 
  Aeq    = views_on_cop$Aeq, 
  beq    = views_on_cop$beq, 
  solver = "nloptr"
)
ep
## <ffp[1859]>
## 2.675292e-12 0.0002722608 6.850905e-06 5.248376e-05 0.001002795 ... 0.0005913308

O vetor de probabilidades ep é aquele que consegue atender as opiniões do econometrista distorcendo ao mínimo as probabilidades uniformes:

library(ggplot2)

autoplot(ep) + 
  scale_color_viridis_c(option = "C", end = 0.75) + 
  labs(title    = "Distribuição de Probabilidades Posteriores", 
       subtitle = "Perturbação na Copula de Clayton", 
       x        = NULL, 
       y        = NULL)

Dessas probabilidades, deriva-se os momentos condicionais, que são o principal insumo para construção de uma fronteira eficiente bayesiana:

ffp_moments(x = x, p = ep)
## $mu
##          DAX          SMI          CAC         FTSE 
## 0.0005338149 0.0004894317 0.0004274987 0.0004413356 
## 
## $sigma
##               DAX          SMI          CAC         FTSE
## DAX  1.194373e-04 9.354658e-05 1.070850e-04 7.536304e-05
## SMI  9.354658e-05 9.929703e-05 9.467923e-05 6.690249e-05
## CAC  1.070850e-04 9.467923e-05 1.314587e-04 8.035356e-05
## FTSE 7.536304e-05 6.690249e-05 8.035356e-05 7.180174e-05

Uma maneira simples de analisar o impacto dessas opiniões nos ativos da carteira é combinando o output da função empirical_stats com o ggplot2:

library(dplyr)

prior <- empirical_stats(x = x, p = as_ffp(prior_from_data)) |> 
  mutate(type = "Prior")
posterior <- empirical_stats(x = x, p = ep) |> 
  mutate(type = "Posterior")

bind_rows(prior, posterior) |> 
  ggplot(aes(x = name, y = value, color = type, fill = type)) +
  geom_col(position = "dodge") +
  facet_wrap(~stat, scales = "free") +
  scale_fill_viridis_d(end = 0.75, option = "C") + 
  scale_color_viridis_d(end = 0.75, option = "C") + 
  labs(title = "Análise de 'Estresse' ex-ante via Entropy-Pooling", 
       subtitle = "Perturbação na Copula de Clayton",
       x = NULL, y = NULL, color = NULL, fill = NULL) 

Perceba que o impacto da perturbação vai na direção esperada: sob regime de “stress” os retornos são menores, as volatilidades mais elevadas, as margens mais assimétricas, as caudas mais largas e, por fim, o VaR e Expected Shortfall também são maiores.

Manipulando os objetos prior e posterior é possível computar o impacto exato sobre qualquer uma dessas estatísticas. Por exemplo, o incremento no Value-at-Risk (VaR), ao nível de \(99\%\), pode ser calculado da seguinte forma:

library(tidyr)

bind_rows(prior, posterior) |> 
  filter(stat == "VaR") |> 
  select(name, value, type) |> 
  pivot_wider(names_from = "type", values_from = "value") |> 
  transmute(
    Ativo = name, 
    `VaR: Diferença Anualizada` = paste0(round(100 * sqrt(252) * (Posterior - Prior), 2), "%")
  )
## # A tibble: 4 x 2
##   Ativo `VaR: Diferença Anualizada`
##   <chr> <chr>                      
## 1 DAX   5.12%                      
## 2 SMI   7.01%                      
## 3 CAC   0.02%                      
## 4 FTSE  2.28%

Por hoje é isso e no próximo post falarei sobre como modelar eventos de pânico com mixtures.


  1. A New Breed of Copulas for Risk and Portfolio Management ↩︎