Momentum EP - Comentários Adicionais

Bernardo Reckziegel 2022-07-19 5 min read

Hoje adiciono alguns comentários sobre a estratégia implementada nos últimos dois posts, aqui e aqui.

Para evitar ter que estimar tudo novamente, carrego uma tibble que contém apenas os resultados finais:

library(curl)
library(broom)
library(ggdist)
library(readxl)
library(lubridate)
library(tidyverse)
library(distributional)

url  <- "https://github.com/Reckziegel/site/raw/master/data/momentum_ep"
data <- read_rds(url)
data
## # A tibble: 590 x 4
##    .date      `Momentum-EP`     IBOV `One Over N`
##    <date>             <dbl>    <dbl>        <dbl>
##  1 2011-02-04      -0.0142  -0.0216     -0.0199  
##  2 2011-02-11       0.00556  0.00742     0.0121  
##  3 2011-02-18       0.0325   0.0345      0.0356  
##  4 2011-02-25      -0.0214  -0.0172     -0.0297  
##  5 2011-03-04       0.0267   0.0165      0.0214  
##  6 2011-03-11       0.00138 -0.0197     -0.0126  
##  7 2011-03-18      -0.00765  0.00292     0.000207
##  8 2011-03-25       0.0211   0.0132      0.0130  
##  9 2011-04-01       0.0327   0.0219      0.0316  
## 10 2011-04-08      -0.0148  -0.00797    -0.0148  
## # ... with 580 more rows

Esse dadaset apresenta os retornos líquidos de custos operacionais - \(1,5\%\) ao ano - tanto da estratégia de momentum entropy-pooling, quanto da famosa \(1/N\).

A performance cumulativa desses dois portfolios mais o Ibovespa pode ser vista com o comando abaixo:

data |> 

# compound
  mutate(across(where(is.numeric), ~ cumprod(1 + .x) * 100)) |>
  
  # tidy data
  pivot_longer(cols = -.date) |>
  
  # plot
  ggplot(aes(x = .date, y = value, color = name)) +
  geom_line() + 
  scale_y_log10() + 
  scale_color_viridis_d(end = 0.75, option = "C") + 
  labs(title = "Corrida de Cavalos", 
       subtitle = "Momentum Entropy-Pooling vs. 1 / N", 
       x = NULL, y = NULL, color = NULL) + 
  theme(legend.position = "bottom")

As estratégias “batem” o índice dentro do período selecionado, mas, como quase sempre, o portfolio \(1/N\) é difícil de ser superado por uma larga margem.

Nesse caso, sempre há a possibilidade de que o excesso de retorno esteja vinculado a um nível de volatilidade mais elevado. Dessa forma, calculo o retorno médio por unidade de risco (uma medida unificada performance):

data |> 
  pivot_longer(cols = -.date, names_to = "portfolio") |> 
  group_by(portfolio) |> 
  summarise(sharpe = mean(value)/ sd(value)) |> 
  arrange(desc(sharpe))
## # A tibble: 3 x 2
##   portfolio   sharpe
##   <chr>        <dbl>
## 1 Momentum-EP 0.0520
## 2 One Over N  0.0429
## 3 IBOV        0.0247

Novamente, entropy-pooling e \(1/N\) saem melhores do que o Ibovespa.

Se houvesse o desejo de controlar mais de perto a volatilidade, a função ffp::view_on_volatility() poderia ser utilizada- veja o post Opiniões nas Volatilidades. Outra alternativa seria controlar a “Vol” por meio da calibragem do indicador de aversão à risco, \(\lambda\), ou ainda, utilizando uma função de utilidade diferente da quadrática.

Infelizmente, muitos portfolios com tilt em momentum são conhecidos por apresentarem Tail Risk. Assim, calculo também o VaR histórico para cada estratégia aos níveis de 95% e 99% de confiança:

data |> 
  pivot_longer(cols = -.date, names_to = "portfolio") |> 
  group_by(portfolio) |> 
  summarise(`VaR (0.05%)` = quantile(value, 0.05), 
            `VaR (0.01%)` = quantile(value, 0.01)) |> 
  arrange(desc(`VaR (0.05%)`))
## # A tibble: 3 x 3
##   portfolio   `VaR (0.05%)` `VaR (0.01%)`
##   <chr>               <dbl>         <dbl>
## 1 Momentum-EP       -0.0366       -0.0601
## 2 One Over N        -0.0396       -0.0766
## 3 IBOV              -0.0473       -0.0805

E, mais uma vez, entropy-pooling e \(1/N\) possuem estatisticas melhores do que o Ibovespa.

É legítimo se questionar o porquê de investir tempo em research se a estratégia \(1/N\) performa quase tão bem quanto entropy-pooling e não requer otimização, nem pesquisa adicional. Acho que a resposta para esse ponto passa pelo fato de que nem todos os retornos são feitos iguais.

Abaixo faço uma análise ex-post dos fatores de risco de cada estratégia para toda a amostra:

urls <- list(market = "https://nefin.com.br/resources/risk_factors/Market_Factor.xls",
             smb    = "https://nefin.com.br/resources/risk_factors/SMB_Factor.xls",
             hml    = "https://nefin.com.br/resources/risk_factors/HML_Factor.xls",
             wml    = "https://nefin.com.br/resources/risk_factors/WML_Factor.xls",
             iml    = "https://nefin.com.br/resources/risk_factors/IML_Factor.xls")

destfiles <- list("market.xls", "smb.xls", "hml.xls", "wml.xls", "iml.xls")

risk_factors <- map2(.x = urls,
                     .y = destfiles,
                     .f = ~ curl_download(url = .x, destfile = .y)) |> 
  map(read_excel) |> 
  reduce(left_join, by = c("year", "month", "day")) |> 
  mutate(date = make_date(year = year, month = month, day = day)) |> 
  rename_all(str_to_lower) |> 
  rename(rm = "rm_minus_rf") |> 
  select(date, everything(), -year, -month, -day)

data <- left_join(data, risk_factors, by = c(".date" = "date")) |>
  na.omit()

# OLS Formulation
fml_ibov <- as.formula(IBOV ~ rm + smb + hml + wml + iml)
fml_ep   <- as.formula(`Momentum-EP`  ~ rm + smb + hml + wml + iml)
fml_oon  <- as.formula(`One Over N`  ~ rm + smb + hml + wml + iml)

unconditional <- lm(formula = fml_ibov, data = data) |> 
  tidy() |> 
  mutate(Cenário = "Ibovespa")

momentum_ep <- lm(formula = fml_ep, data = data) |> 
  tidy() |> 
  mutate(Cenário = "Momentum EP")

one_over_n <- lm(formula = fml_oon, data = data) |> 
  tidy() |> 
  mutate(Cenário = "One Over N")

regression <- bind_rows(unconditional, momentum_ep, one_over_n)
regression  |> 
  mutate(term = fct_reorder(as_factor(term), estimate)) |> 
  filter(term != "(Intercept)") |> 
  ggplot(aes(y = term, group = `Cenário`, color = `Cenário`, fill = `Cenário`) ) +
  stat_halfeye(aes(
    xdist = dist_student_t(df = 2784, mu = estimate, sigma = std.error)), alpha = 0.75
  ) + 
  geom_vline(xintercept = 0, size = 1, color = "grey", linetype = 2) + 
  scale_fill_viridis_d(end = 0.75, option = "C") + 
  scale_color_viridis_d(end = 0.75, option = "C") + 
  scale_x_continuous(labels = scales::percent_format()) + 
  theme(legend.position = "bottom") + 
  labs(title    = "Exposições Ex-Post",
       subtitle = "Risk Drivers das Diferentes Estratégias",
       x        = NULL, 
       y        = NULL)

Como se esperava, a estratégia via entropy-pooling é mais bem sucedida do que \(1/N\) no sentido de “puxar” os retornos em direção do fator de momentum, embora a exposição média ainda seja negativa (um problema potencialmente relacionado ao dataset de pequena dimensão que foi utilizado).

Uma agradável surpresa vem do fato que o risco de mercado é menor em entropy-pooling. Esse é justamente o tipo de benefício que se espera de uma estratégia sistemática.