Opiniões com Ordenação

Bernardo Reckziegel 2022-04-07 4 min read

Nem sempre os gestores possuem opiniões precisas sobre os parâmetros de locação e dispersão. Às vezes, as opiniões são mais sutis: a inclinação da curva de juros vai aumentar, o Ibovespa vai cair, o dólar vai ficar de lado, etc.

Para mostrar como entropy-pooling acomoda essas opiniões de posição (ou “rankiamento”) mais uma vez utilizo o dataset EuStockMarkets, que vem junto com a instalação do R:

x <- diff(log(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

Com base nesses dados, vamos supor que o time de gestão acredite que os retornos dos índices \(SMI\) e \(DAX\) estarão encadeados da seguinte maneira:

$$ SMI \leq DAX $$ Ou seja, o \(DAX\) performará melhor do que \(SMI\). Já em relação ao para o \(CAC\) e \(FTSE\), a postura é passiva e não há opinião formada.

Para montar esse tipo de opinião fraca, o pacote ffp disponibiliza a função view_on_rank:

library(ffp)

views <- view_on_rank(x = x, rank = c(2, 1))
views
## # ffp view
## Type:  View On Rank
## A :  Dim 1 x 1859 
## b :  Dim 1 x 1

No argumento rank devemos indicar o número da coluna de cada ativo em x, de maneira que aqueles com as melhores perspectivas de retorno estejam localizados à direita dos demais. Como achamos que o \(DAX\) (1 coluna) irá performar melhor do que o \(SMI\) (2 coluna) usamos rank = c(2, 1).

Matematicamente, estamos buscando minimizar a expressão:

$$ \sum_{i=1}^I x_i(ln(x_i) - ln(p_i)) $$ sujeito a restrição:

$$ p_i (SMI - DAX) \leq 0 $$ A função que soluciona esse problema é entropy_pooling:

prior <- rep(1 / nrow(x), nrow(x))
ep <- entropy_pooling(p = prior, A = views$A, b = views$b, solver = "nloptr")
ep
## <ffp[1859]>
## 0.0005145646 0.0005403155 0.0005470049 0.0005330238 0.0005446858 ... 0.0005469164

Note que pela primeira vez abandonamos o solver = "nlmimb", pois problemas com restrições de desigualdade devem ser resolvidos com os otimizadores solnl ou nloptr.

As probabilidades posteriores (que solucionam o problema da entropia mínima relativa) são visualizados com o método autoplot do pacote ggplot2:

library(ggplot2)

autoplot(ep) + 
  scale_color_viridis_c(option = "C", end = 0.75) + 
  labs(title    = "Distribuição de Probabilidades Posteriores", 
       subtitle = "Opiniões de Ordenação - 'Rankiamento'", 
       x        = NULL, 
       y        = NULL)

A razão entre os momentos - condicionais vs. incondicionais - de locação mostra que a restrição foi atendida: o retorno esperado do \(DAX\) aumentou em \(17\%\) (em relação a prior) e o retorno do índice \(SMI\) foi rebaixado em \(6,5\%\).

cond_moments <- ffp_moments(x = x, p = ep)

cond_moments$mu / colMeans(x) - 1
##         DAX         SMI         CAC        FTSE 
##  0.17274275 -0.06507207  0.13576463  0.06261163

Note que embora não fosse a intenção, os retornos projetados para os índices \(CAC\) e \(FTSE\) também foram alterados. Nesse caso, a função view_on_mean poderia ser utlizada para “redirecionar” a locação de volta para as médias amostrais, como mostra o post Opiniões nos retornos esperados.

A fronteira eficiente condicional se situa abaixo da fronteira incondicional porque a opinião coloca um teto na performance do índice mais rentável - \(SMI \leq DAX\):

(1 + colMeans(x)) ^ 252 - 1
##       DAX       SMI       CAC      FTSE 
## 0.1785218 0.2287857 0.1164048 0.1149803

Como já mencionei outras vezes, é fácil estender esse tipo de análise para o VaR, Expected Shortfall, etc. De fato, ffp oferece um atalho para esses cálculos com empirical_stats:

library(dplyr)

prior_stats <- empirical_stats(x = x, p = as_ffp(prior), level = 0.05) |> 
  mutate(Regime = "Prior")
posterior_stats <- empirical_stats(x = x, p = ep, level = 0.05) |> 
  mutate(Regime = "Posterior")

# Plot
bind_rows(posterior_stats, prior_stats) |> 
  ggplot(aes(x = name, y = value, fill = Regime, color = Regime)) + 
  geom_col(position = "dodge") + 
  facet_wrap(~ stat, scales = "free") + 
  scale_fill_viridis_d(option = "C", end = 0.75) + 
  scale_color_viridis_d(option = "C", end = 0.75) + 
  theme(legend.position = "bottom") + 
  labs(title    = "Análise de Sensibilidade", 
       subtitle = "Opiniões com Ordenação via Entropy-Pooling", 
       x        = "Índice", 
       y        = "Estatística", 
       fill     = NULL,
       color    = NULL)