A construção de portfólios de investimento eficientes é um pilar central na gestão financeira. O modelo de otimização de portfólio de Markowitz, baseado na análise de média-variância, busca maximizar o retorno esperado para um determinado nível de risco ou minimizar o risco para um determinado retorno esperado. No entanto, a aplicação "cega" da otimização pode levar a soluções extremas e pouco robustas. Este artigo explora a implementação de tais modelos em R, com foco em estratégias de momentum e na aplicação de restrições para garantir a estabilidade das soluções.
Para controlar as soluções de otimização média-variância e torná-las mais práticas, é comum introduzir restrições como:
- Um limite máximo para o peso de cada ativo no portfólio.
- Uma restrição sobre a volatilidade alvo do portfólio.
Consideraremos um conjunto de ativos hipotéticos para ilustrar os conceitos. O primeiro passo é carregar os dados históricos de preços dos ativos.
Carregamento e Preparação dos Dados Históricos
Utilizamos o pacote quantmod para buscar e gerenciar dados financeiros. Para este exemplo, buscaremos dados de algumas ETFs representativas.
# Carregar pacotes necessários
if (!requireNamespace("quantmod", quietly = TRUE)) install.packages("quantmod")
if (!requireNamespace("PerformanceAnalytics", quietly = TRUE)) install.packages("PerformanceAnalytics")
if (!requireNamespace("quadprog", quietly = TRUE)) install.packages("quadprog")
library(quantmod)
library(PerformanceAnalytics)
library(quadprog)
# Lista de tickers para os ativos (exemplo de 6 ETFs)
tickers_ativos <- c("SPY", "EFA", "GLD", "TLT", "VNQ", "QQQ")
# Definir período para os dados
data_inicial <- "2010-01-01"
data_final <- Sys.Date()
# Criar ambiente para armazenar os dados
dados_historicos <- new.env()
# Baixar dados do Yahoo Finance
getSymbols(tickers_ativos, env = dados_historicos, from = data_inicial, to = data_final)
# Extrair os preços de fechamento ajustados e combiná-los em um único objeto xts
precos_fechamento <- do.call(merge, eapply(dados_historicos, Ad))
colnames(precos_fechamento) <- gsub("\\.Adjusted", "", colnames(precos_fechamento))
# Calcular retornos diários (usando Return.calculate do PerformanceAnalytics)
retornos_diarios <- na.omit(Return.calculate(precos_fechamento))
# Opcional: Visualizar os retornos acumulados
# chart.CumReturns(retornos_diarios, main = "Retornos Acumulados dos Ativos", legend.loc = "topleft")
Implementação do Modelo de Otimização de Markowitz
O modelo de Markowitz busca um portfólio que minimize a variância para um dado nível de retorno esperado, ou maximize o retorno para um dado nível de risco. Isso é tipciamente formulado como um problema de programação quadrática.
Para otimizar o portfólio, precisamos de:
- Vetor de retornos esperados para cada ativo.
- Matriz de covariância dos retornos dos ativos.
- Restrições (por exemplo, a soma dos pesos deve ser 1, e os pesos individuais devem estar dentro de certos limites).
Vamos calcular esses insumos com base nos dados históricos. Para simplificar, usaremos os retornos médios históricos como retornos esperados e a matriz de covariância histórica.
# Calcular retornos esperados (média histórica simples)
retornos_esperados <- colMeans(retornos_diarios)
# Calcular matriz de covariância dos retornos
matriz_covariancia <- cov(retornos_diarios)
# Número de ativos no portfólio
num_ativos <- ncol(retornos_diarios)
# Configurar o problema de programação quadrática usando 'quadprog'
# A função 'solve.QP' minimiza '-dvec' * x + 0.5 * x' * Dmat * x sujeito a A'x >= b.
# Para otimização de mínima variância pura, definimos 'dvec' como um vetor de zeros.
Dmat <- 2 * matriz_covariancia # Dmat é a matriz de covariância multiplicada por 2.
dvec <- rep(0, num_ativos) # Vetor zero para minimizar apenas a variância.
# Matriz de restrições (Amat) e vetor de limites (bvec)
# Restrição 1: A soma dos pesos deve ser 1 (igualdade)
# Restrição 2: Pesos individuais não-negativos (desigualdade)
# Restrição 3: Pesos individuais não podem exceder um limite máximo (desigualdade)
# Construir Amat e bvec
# Para Ax = b (igualdade), incluímos a linha correspondente em Amat e bvec,
# e indicamos o número de igualdades em 'meq'.
# Amat inicial: soma dos pesos = 1 e pesos >= 0
Amat_base <- cbind(matrix(1, num_ativos, 1), diag(num_ativos)) # Coluna 1: soma dos pesos; Colunas 2-num_ativos+1: pesos >= 0
bvec_base <- c(1, rep(0, num_ativos)) # 1 para a soma; 0 para pesos >= 0
meq_base <- 1 # Uma restrição de igualdade (a soma dos pesos)
# Otimização de Mínima Variância Global (sem limites superiores de peso)
otimizacao_min_var <- solve.QP(Dmat, dvec, Amat_base, bvec_base, meq = meq_base)
pesos_min_var <- round(otimizacao_min_var$solution, 4)
names(pesos_min_var) <- tickers_ativos
cat("Pesos do Portfólio de Mínima Variância Global:\n")
print(pesos_min_var[pesos_min_var > 1e-6]) # Exibir pesos significativos
cat("Soma dos pesos:", sum(pesos_min_var), "\n")
# Otimização com um limite máximo para o peso de cada ativo (ex: 30%)
peso_maximo <- 0.30
# Adicionar restrições de limite superior: -pesos >= -peso_maximo (equivalente a pesos <= peso_maximo)
Amat_com_limites <- cbind(Amat_base, -diag(num_ativos))
bvec_com_limites <- c(bvec_base, rep(-peso_maximo, num_ativos))
# O número de restrições de igualdade (meq) permanece 1
otimizacao_com_limites <- solve.QP(Dmat, dvec, Amat_com_limites, bvec_com_limites, meq = meq_base)
pesos_com_limites <- round(otimizacao_com_limites$solution, 4)
names(pesos_com_limites) <- tickers_ativos
cat("\nPesos do Portfólio com Limite Máximo de 30% por Ativo:\n")
print(pesos_com_limites[pesos_com_limites > 1e-6]) # Exibir pesos significativos
cat("Soma dos pesos:", sum(pesos_com_limites), "\n")
Estratégias de Momnetum no Contexto do Markowitz
Estratégias de momentum buscam capitalizar a tendência de ativos com bom desempenho recente continuarem a ter um bom desempenho no futuro (ou o inverso). Em vez de usar retornos médios históricos simples, podemos incorporar o momentum para estimar os retornos esperados, que podem servir como input para o modelo de Markowitz.
Por exemplo, podemos definir o "retorno esperado" de um ativo como o retorno acumulado nos últimos 3, 6 ou 12 meses, dando pesos maiores a ativos com momentum positivo. Esses retornos de momentum seriam então usados no vetor dvec do problema de otimização para guiar a seleção de ativos em direção àqueles com maior momentum, enquanto ainda controlamos o risco via matriz de covariância.
Uma abordagem mais robusta envolve a ponderação de diferentes horizontes de tempo ou a utilização de "premissas de entrada médias", como sugerido por Pierre Chretien. Isso significa que, em vez de usar um único período de lookback para calcular os retornos esperados, combinamos informações de múltiplos períodos (ex: 1, 3, 6 e 12 meses) para suavizar as estimativas e reduzir a sensibilidade a ruídos.
# Exemplo conceitual de cálculo de retornos de momentum para inputs do Markowitz
# A função 'calcular_momentum_retorno' obtém o retorno acumulado em um período específico.
calcular_momentum_retorno <- function(retornos_diarios, lookback_meses) {
# Converter retornos diários para mensais (para o cálculo do momentum)
retornos_mensais <- apply.monthly(retornos_diarios, Return.cumulative)
# Assegurar que há dados suficientes para o período de lookback
if (nrow(retornos_mensais) < lookback_meses) {
# Retorna vetor de zeros se não houver dados suficientes
return(setNames(rep(0, ncol(retornos_diarios)), colnames(retornos_diarios)))
}
# Seleciona os retornos mensais do período de lookback
retornos_passados_periodo <- tail(retornos_mensais, lookback_meses)
# Calcula o retorno acumulado para cada ativo no período de lookback
momentum_retornos <- apply(retornos_passados_periodo, 2, Return.cumulative)
return(momentum_retornos)
}
# Usando uma premissa de "entradas médias" (similar ao conceito de Pierre Chretien)
# Combinar momentum de diferentes janelas de tempo
momentum_1m <- calcular_momentum_retorno(retornos_diarios, 1)
momentum_3m <- calcular_momentum_retorno(retornos_diarios, 3)
momentum_6m <- calcular_momentum_retorno(retornos_diarios, 6)
momentum_12m <- calcular_momentum_retorno(retornos_diarios, 12)
# Média simples dos retornos de momentum para formar o vetor de retornos esperados
# Na prática, ponderações mais sofisticadas podem ser aplicadas.
retornos_esperados_momentum <- (momentum_1m + momentum_3m + momentum_6m + momentum_12m) / 4
cat("\nRetornos Esperados baseados em Momentum Médio (últimos valores calculados):\n")
print(retornos_esperados_momentum)
# Estes 'retornos_esperados_momentum' poderiam então ser usados como o vetor 'dvec'
# em 'solve.QP' para otimização que visa maximizar o retorno para um dado nível de risco.
# Para isso, a função objetivo seria modificada para incluir o termo 'dvec' e,
# possivelmente, uma restrição de retorno alvo para o portfólio.
Análise dos Resultados e Considerações Finais
Após executar as otimizações, é crucial analisar os portfólios resultantes. A visualização dos pesos, retorno e risco do portfólio, bem como métricas como o turnover (rotatividade do portfólio), são essenciais. Um alto turnover implica em maiores custos de transação e, portanto, menor rentabilidade líquida.
A principal lição é que a otimização não deve ser um processo cego. A aplicação de restrições realistas (limites de peso, volatilidade alvo) e o uso de inputs mais sofisticados (como as premissas de entradas médias de momentum) são fundamentais para gerar soluções de portfólio que sejam robustas, implementáveis e que performem melhor em ambientes de mercado dinâmicos.