Disciplina: Investimentos (PDEC7095) / Mercado de
Capitais (GOLD7027)
Professor: Adalto Acir Althaus
Junior (adalto.althaus@ufpr.br)
Curitiba - PR
2026
A construção da base histórica de preços cobrindo o período de 2000 a 2025 exigiu procedimentos rigorosos de higienização, consolidação e alinhamento com indicadores macroeconômicos centrais, tais como o Índice Bovespa (^BVSP, extraído via Yahoo Finance) e a taxa CDI (obtida por meio da API do Banco Central do Brasil). O recorte temporal definitivo para a modelagem analítica foi fixado entre 2005 e 2025, de modo a garantir a consistência estatística, viabilizar o correto tratamento de lacunas operacionais (gaps) e a conversão padronizada de dados numéricos para o cômputo dos retornos diários e compostos.Durante essa triagem de consistência — cujo objetivo foi descartar ativos com descontinuidade de dados ou prevalência crítica de valores ausentes (missing values) —, exatamente 71 ações satisfizeram integralmente os critérios de integridade contínua. Essa amostragem consolidada representa o repositório analítico definitivo deste trabalho, preservado sem distorções para a calibração de modelos estatísticos, mensuração de métricas de volatilidade, estimativas do risco sistemático e composição dos painéis interativos.
A validação desse conjunto de ativos possibilitou a extração das estatísticas descritivas anuais representadas na Figura 1, evidenciando a dinâmica individual e agregada da média e do desvio-padrão ao longo do horizonte estudado.2. Dinâmica de Risco e Distribuição dos RetornosA investigação aprofundada da relação risco-retorno ganha corpo ao se examinar o comportamento temporal dos ativos, onde os recursos visuais desempenham papel determinante na identificação de regimes de volatilidade e dispersão estrutural. Complementando a análise descritiva, a Figura 2 detalha a trajetória do retorno acumulado anual para cada um dos 71 papéis selecionados no período de 2005 a 2025.
A leitura conjunta dessa dinâmica explicita os ciclos de expansão e retração de liquidez característicos do mercado de capitais brasileiro, permitindo avaliar o grau de resiliência dos ativos perante o ambiente macroeconômico e o benchmark de mercado.A avaliação do binômio risco-retorno na Bovespa (2005–2025) coloca em evidência uma característica estatística marcante: os desvios-padrões superam sistematicamente os retornos médios na quase totalidade dos papéis. Longe de se restringir a distorções pontuais ou setores específicos, tal fenômeno manifesta-se de forma recorrente em toda a série temporal.
A magnitude da variação diária e anual reflete a acentuada sensibilidade do mercado acionário doméstico a choques externos, revisões macroeconômicas e ciclos de liquidez, gerando oscilações substancialmente maiores do que o retorno médio gerado pelas companhias.Essa assimetria entre o risco (expresso pelo desvio-padrão) e o retorno médio impõe uma reflexão crítica sobre a eficiência e a atratividade do mercado acionário frente a alternativas de renda fixa de reduzido risco, como o CDI. Em séries temporais baseadas em preços de fechamento, a volatilidade é um traço intrínseco à renda variável. Não obstante, quando o desvio-padrão permanece multianualmente superior às médias de retorno, o prêmio de risco oferecido ao investidor passa a ser questionável, desafiando a premissa teórica clássica de que exposições a riscos elevados são compensadas por retornos proporcionalmente superiores e previsíveis.3. Renda Variável vs. Benchmark de Renda Fixa (CDI)Ao ponderar o papel da Bovespa frente ao CDI — indicador que reflete a taxa básica de juros (Selic) com liquidez diária e previsibilidade nominal —, a comparação torna-se incontornável.
Enquanto o CDI atua como uma âncora de rentabilidade livre de volatilidade de preço, o investimento em ações exige a aceitação de desvios-padrões expressivos que raramente se traduzem em prêmios médios equivalentes no longo prazo.
Isso evidencia que a alocação no mercado acionário ultrapassa a mera busca por ganhos nominais maiores; trata-se de assumir assimetrias relevantes entre o risco suportado e a rentabilidade efetivamente realizada.Compreender a razão pela qual os desvios-padrões superam ostensivamente as médias requer olhar além dos dados agregados e reconhecer a estrutura de mercado na qual as empresas listadas operaram nas últimas duas décadas. Esse cenário reforça a urgência de adotar métricas avançadas de mensuração de desempenho e risco — como os Índices de Sharpe, Betas ajustados e estimativas em janelas móveis desenvolvidos nesta pesquisa —, pois revelam que o êxito na renda variável decorre menos da busca por médias elevadas e muito mais do manejo da volatilidade extrema em relação à estabilidade do CDI.
Para subsidiar essa análise, os comportamentos de média e desvio-padrão das janelas anuais são ilustrados na Figura 1, ao passo que a Figura 2 apresenta a evolução gráfica dos retornos acumulados dos 71 ativos. Vale destacar que as rotinas computacionais em linguagem R desenvolvidas neste estudo automatizam a criação e a consolidação de todos esses painéis visuais e tabelas interativas.O gráfico da Figura 2 contrapõe visualmente o retorno acumulado anual (2005–2025) das ações validadas em relação à curva de rentabilidade do CDI.
Enquanto a trajetória da renda fixa se mantém linear e ascendente, o conjunto de ações desenha um panorama de acentuada volatilidade e alternância de ciclos ao longo de 20 anos. Esse contraste sintetiza o risco de mercado, no qual os ativos acionários registram períodos de forte valorização intercalados por contrazões severas. Um marco notável na Figura 2 é o pico observado em meados de 2007, exemplificado pela BBDC4, que ultrapassou 1.200% de retorno acumulado anual. Contudo, essa fase de euforia pré-crise foi sucedida pela retração de 2008, marcada por reversões acentuadas rumo ao campo negativo (-50% a -100%).
Recuperações subsequentes em 2009 e 2016 (com picos entre 200% e 300%) e o desempenho da ALPA4 em 2025 (próximo a 900%) confirmam que os ciclos de alto retorno no mercado brasileiro são invariavelmente acompanhados por elevado desvio-padrão.
Para mensurar o risco sistemático e a sensibilidade dos 71 ativos em relação ao mercado acionário brasileiro, estimou-se o coeficiente Beta por meio de modelos de regressão linear MCO (Ordinary Least Squares), utilizando o retorno do Índice Bovespa (\(Ret\_IBOV\)) como variável explicativa do benchmark. O coeficiente angular da regressão — correspondente à inclinação da reta ajustada — reflete a magnitude da resposta do ativo às variações do mercado. Essa estimativa é acompanhada pelo p-valor (\(Pr(>\vert{}t\vert{})\)), utilizado para testar a significância estatística sob a hipótese nula de igualdade a zero (\(\beta = 0\)).A análise comparativa desses coeficientes em distintas janelas temporais (1 ano, além das médias móveis de 3 e 5 anos) revela nuances relevantes sobre a sensibilidade das companhias listadas. Enquanto determinados ativos apresentam Betas iguais ou superiores a \(1,0\) — sinalizando maior volatilidade e sensibilidade aos ciclos econômicos —, outros exibem perfis defensivos com Betas inferiores à unidade.
No entanto, a verificação sistemática dos p-valores indica que essa sensibilidade não conserva significância estatística em todas as janelas observadas, ressaltando a necessidade de discernir entre movimentações estruturais e oscilações decorrentes de ruídos de curto prazo ou iliquidez temporária.Para explorar os valores de Beta e seus respectivos p-valores para cada ação e categoria analítica, disponibiliza-se um painel interativo no relatório executivo. A Tabela 1 reúne os coeficientes estimados e seus níveis de significância, permitindo ao leitor selecionar ativos específicos ou navegar por categorias de ranking.O Efeito da Expansão da Janela Temporal (Janela Móvel de 5 Anos)A ampliação da janela de estimação para o horizonte móvel de 5 anos (Tabela 2) gera mudanças expressivas na convergência estatística quando comparada aos horizontes mais curtos.
Em janelas anuais de 1 ano, as estimativas do Beta sofrem distorções frequentes causadas por choques pontuais de liquidez, volatilidade idiossincrática e ruídos conjunturais. Em contrapartida, ao se expandir a amostra para abarcar os 5 anos antecedentes a cada ponto do tempo, observa-se uma atenuação nos valores extremos, favorecendo a convergência dos coeficientes em direção ao parâmetro central de mercado (\(\beta = 1,0\)).Sob o ponto de vista econométrico, o aumento no tamanho da amostra ao longo do horizonte de 5 anos amplia os graus de liberdade das regressões e reduz o erro-padrão das estimativas. Como consequência direta, observa-se uma expressiva redução nos p-valores, que passam a indicar alta significância estatística (\(p < 0,05\)).
Assim, flutuações que pareciam aleatórias ou desprovidas de significância em janelas anuais revelam, no horizonte de 5 anos, uma relação estatisticamente consistente com o Índice Bovespa.Dessa forma, a janela móvel de 5 anos funciona como um filtro para a identificação da real exposição ao risco sistemático.
Ao isolar surtos conjunturais de volatilidade registrados ao longo das duas últimas décadas, essa especificação consolida o Beta como uma métrica mais estável para a tomada de decisão, permitindo mensurar com maior precisão a exposição do ativo em comparação com a estabilidade do CDI.Análise dos Grupos Extremos (Maiores e Menores Desempenhos)Análise das 10 Maiores Médias de RetornoA Tabela 3 consolida o painel interativo de coeficientes e p-valores para as diversas janelas analíticas. O grupo representativo das 10 maiores médias de retorno anual em 21 anos é composto por empresas de destaque histórico na Bovespa, incluindo companhias de setores diversos como WEGE3, RENT3, SAPR4 e UNIP6. O desempenho desse grupo mostra que a consistência nas médias anuais (que variam de \(28,82\%\) para WEGE3 a \(69,25\%\) para TELB4) resulta em valorização acumulada exponencial no longo prazo: a WEGE3 registrou \(5.954,04\%\) de retorno total acumulado, enquanto a RENT3 atingiu \(4.596,98\%\).Em termos da relação risco-retorno, o grupo expõe comportamentos heterogêneos. Empresas com sólida execução operacional (como WEGE3, RENT3 e SAPR4) conciliam elevadas médias de retorno com desvios-padrões contidos (entre \(2\%\) e \(3\%\)).
Por outro lado, ativos como TELB4 e GOLL4 figuram no topo das médias aritméticas em razão de volatilidades extremas causadas por eventos de reestruturação ou forte oscilação de mercado, apresentando desvios-padrões elevados (\(0,2843\) no caso da GOLL4) e retornos acumulados totais severamente reduzidos ou negativos (\(-59,70\%\)).A avaliação econométrica dos Betas para esse grupo confirma o ganho de consistência com a expansão da janela temporal.
Embora horizontes de 1 ano apresentem pontualmente baixa significância estatística (ex.: p-valor de \(0,7147\) para GOLL4 e \(0,0788\) para TELB4), as janelas de 3 e 5 anos demonstram convergência dos p-valores para \(p < 0,05\) (com diversos casos próximos a \(0,0000\)), confirmando que o risco sistemático desses papéis guarda relação estrutural com o Índice Bovespa.Análise dos 10 Menores RendimentosO grupo dos 10 menores rendimentos anuais (Tabela 3) exibe um perfil analítico distinto no período de 2005 a 2025. Diferente da dinâmica observada em papéis altamente especulativos ou cíclicos, este grupo é formado predominantemente por companhias de setores regulados e defensivos — como utilities (EGIE3, CPFE3, TRPL4 e CGAS5), instituições financeiras consolidadas (ITUB3) e seguradoras (PSSA3). Seus retornos acumulados totais e médias anuais situam-se em patamares moderados, com retornos acumulados variando de \(7,2\%\) (COCE5) a \(41,8\%\) (PSSA3) e médias anuais entre \(13\%\) e \(25\%\).No tocante ao risco, a característica marcante do grupo é a reduzida dispersão dos desvios-padrões, que se mantêm contidos na faixa de \(1,8\%\) a \(2,0\%\) ao longo de todo o período. Essa estabilidade aproxima esses papéis da previsibilidade da renda fixa (\(CDI\_Diario\)), cuja volatilidade é residual (\(5,26 \times 10^{-7}\)).
Assim, esses ativos desempenham papel amortecedor em carteiras de investimento, reduzindo a volatilidade global do portfólio diante dos ciclos de mercado.A análise econométrica reafirma a significância de longo prazo dos coeficientes estimados. Com exceção de oscilações pontuais em janelas de 1 ano (como o p-valor de \(0,0784\) para COCE5), as janelas de 3 e 5 anos apresentam p-valores tendentes a zero (\(0,0000\)), confirmando que tais empresas possuem risco sistemático controlado (\(\beta < 1,0\)) e comportamento previsível em relação ao benchmark acionário.
A comparação direta entre o grupo de menor volatilidade e o de maior desvio-padrão evidencia a dualidade do mercado brasileiro. De um lado, ativos defensivos (ABEV3, VIVT3, EGIE3, CPFE3 e ITUB3) combinam baixos desvios-padrões (\(0,017\) a \(0,020\)) a retornos acumulados consistentes (superando \(4.100\%\) em PSSA3) e Betas módicos. De outro lado, o grupo de maior risco — composto por companhias em recuperação judicial ou setores altamente estressados (GOLL4, AMER3, OIBR4 e RSID3) — atinge níveis extremos de desvio-padrão (\(0,035\) a \(0,284\)) associados a perdas expressivas de capital (destruição de patrimônio entre \(-90\%\) e \(-99\%\)).Sob a ótica da Teoria Moderna de Carteiras, essa discrepância questiona a premissa de que maior volatilidade resulta em maior prêmio de retorno no mercado local.
Enquanto o grupo defensivo gera retornos atrativos mantendo volatilidade próxima a níveis moderados, papéis de risco extremo falham em remunerar o capital investido. Isso demonstra que desvios-padrões elevados no mercado acionário brasileiro frequentemente traduzem risco idiossincrático e estresse financeiro, em vez de prêmio de risco sistemático.Por fim, os coeficientes de Beta nas janelas móveis destacam a necessidade de horizontes ampliados de análise. O grupo defensivo apresenta estabilidade estatística e p-valores nulos (\(0,0000\)) em praticamente todos os prazos. Já os papéis de maior volatilidade exibem instabilidade no curto prazo (como o Beta de \(6,1506\) da GOLL4 em 1 ano, com \(p = 0,7147\)).
Apenas na janela móvel de 5 anos esses coeficientes convergem para parâmetros estatisticamente válidos (\(p < 0,05\)), consolidando as métricas apresentadas na Tabela 5.ConclusãoAs evidências empíricas e econométricas reunidas neste trabalho revisam premissas sobre a dinâmica de risco e retorno na Bovespa no período de 2005 a 2025. A análise da base de 71 ações validadas indica que a máxima teórica de que maior volatilidade é acompanhada por maior retorno não se aplica de maneira uniforme no mercado brasileiro.
A assimetria observada, na qual o desvio-padrão supera de forma consistente a média dos retornos, indica que a volatilidade em diversos papéis decorre de risco idiossincrático e estresse operacional, culminando em perda de valor e ausência de prêmio de risco em relação ao CDI.Por outro lado, o mapeamento dos grupos extremos demonstra a presença de companhias de elevada eficiência operacional (como utilities, grandes bancos e líderes industriais) capazes de gerar valorização acumulada expressiva com volatilidade moderada. Adicionalmente, a aplicação de modelos de regressão em janelas móveis confirmou que o horizonte de 5 anos é essencial para filtrar ruídos conjunturais, estabilizar as estimativas de Beta e assegurar alta significância estatística (\(p < 0,05\)).
Assim, os instrumentos analíticos e dashboards desenvolvidos oferecem suporte à tomada de decisão na gestão de portfólios. Os resultados reforçam que o sucesso da alocação em renda variável na Bovespa depende da seleção de ativos com risco sistemático bem definido, do uso de janelas temporais condizentes com a maturidade das companhias e do gerenciamento consciente da volatilidade intrínseca ao ambiente macroeconômico brasileiro.
• Elimine dados dos finais de semana ou de dias sem negociação (sugestão: alternativamente a análise do calendário de funcionamento da bolsa, tome como referência ações que possuem alta liquidez) • Estime os retornos diários de cada ação em uma nova planilha ou “aba” do excel • Estime a média e o desvio padrão dos retornos diários de cada ativo durante todo o período
# ==============================================================================
# 1. CARREGAR PACOTES NECESSÁRIOS
# ==============================================================================
library(readxl)
library(quantmod)
library(dplyr)
library(httr)
library(jsonlite)
# ==============================================================================
# 2. IMPORTAR E TRATAR DADOS DE PREÇOS (ECONOMATICA)
# ==============================================================================
df <- read_excel("C:/Users/Usuario/Desktop/noticia/economatica-precos_ativos-2000-2025.xlsx", sheet = 2)
df$Data <- as.Date(df$Data, format = "%Y-%m-%d") # Ajuste se necessário para "%d/%m/%Y"
df$Ano <- format(df$Data, "%Y")
# ==============================================================================
# 3. BAIXAR O ÍNDICE BOVESPA (IBOV) DO YAHOO FINANCE
# ==============================================================================
getSymbols("^BVSP", src = "yahoo", from = "2000-01-01", to = "2025-12-30", auto.assign = TRUE)
## [1] "BVSP"
df_ibov <- data.frame(
Data = index(BVSP),
IBOV_Fechamento = as.numeric(Cl(BVSP))
)
df_ibov <- na.omit(df_ibov)
# ==============================================================================
# 4. BAIXAR A TAXA CDI DO BANCO CENTRAL DO BRASIL (BCB)
# ==============================================================================
url_bloco1 <- "https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=01/01/2000&dataFinal=31/12/2009"
url_bloco2 <- "https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=01/01/2010&dataFinal=31/12/2019"
url_bloco3 <- "https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=01/01/2020&dataFinal=30/12/2025"
baixar_bcb <- function(url) {
resp <- GET(url, user_agent("Mozilla/5.0"))
txt <- content(resp, as = "text", encoding = "UTF-8")
fromJSON(txt)
}
df1 <- baixar_bcb(url_bloco1)
df2 <- baixar_bcb(url_bloco2)
df3 <- baixar_bcb(url_bloco3)
# Definindo o caminho da pasta solicitada
#pasta_destino <- "C:/Users/Usuario/Desktop/noticia/mercado de acoes"
# ==============================================================================
# 2. COMANDO PARA PUXAR / ABRIR OS DATAFRAMES DESSA MESMA PASTA
# ==============================================================================
#df1 <- readRDS(file.path(pasta_destino, "df1_bloco.rds"))
#df2 <- readRDS(file.path(pasta_destino, "df2_bloco.rds"))
#df3 <- readRDS(file.path(pasta_destino, "df3_bloco.rds"))
df_cdi <- bind_rows(df1, df2, df3)
df_cdi$Data <- as.Date(df_cdi$data, format = "%d/%m/%Y")
df_cdi$CDI_Diario <- as.numeric(df_cdi$valor) / 100
df_cdi <- df_cdi %>% select(Data, CDI_Diario)
# ==============================================================================
# 5. INTEGRAR BASES DE MERCADO E FILTRAR PERÍODO (2005 - 2025)
# ==============================================================================
base_mercado <- merge(df_ibov, df_cdi, by = "Data", all.x = TRUE)
base_mercado$Data <- as.Date(base_mercado$Data)
df_atualizado <- df %>%
left_join(base_mercado, by = "Data")
df_filtrado <- df_atualizado %>%
filter(Data >= as.Date("2005-01-01") & Data <= as.Date("2025-12-30")) %>%
filter(!is.na(IBOV_Fechamento) & !is.na(CDI_Diario))
# ==============================================================================
# 6. FILTRAGEM RIGOROSA E CONVERSÃO NUMÉRICA DOS ATIVOS
# ==============================================================================
colunas_candidatas <- setdiff(colnames(df_filtrado), c("Data", "Ano", "IBOV_Fechamento", "CDI_Diario"))
nas_totais <- sapply(df_filtrado[colunas_candidatas], function(x) sum(is.na(as.numeric(gsub(",", ".", x)))))
acoes_com_ate_3_nas <- names(which(nas_totais <= 150))
colunas_essenciais <- intersect(c("Data", "Ano", "IBOV_Fechamento", "CDI_Diario"), colnames(df_filtrado))
colunas_finais_base <- unique(c(colunas_essenciais, acoes_com_ate_3_nas))
df_base_rigorosa <- df_filtrado %>%
select(all_of(colunas_finais_base))
# Identificar dinamicamente as colunas de ativos na base rigorosa
colunas_ativos <- setdiff(colnames(df_base_rigorosa), c("Data", "Ano", "IBOV_Fechamento", "CDI_Diario"))
# FORÇAR A CONVERSÃO DE TODAS AS COLUNAS DE ATIVOS PARA NUMÉRICO COM SEGURANÇA
df_base_rigorosa[colunas_ativos] <- lapply(df_base_rigorosa[colunas_ativos], function(x) {
as.numeric(gsub(",", ".", as.character(x)))
})
df <- na.omit(df_base_rigorosa)
# ==============================================================================
# 7. CÁLCULO DE RETORNOS E RANKING DE SHARPE (TOP 10 ATIVOS)
# ==============================================================================
df_retornos <- df %>%
mutate(
Ret_IBOV = (IBOV_Fechamento / lag(IBOV_Fechamento)) - 1,
CDI_Dia = (1 + CDI_Diario)^(1/252) - 1,
across(all_of(colunas_ativos), ~ (. / lag(.)) - 1)
) %>%
slice(-1) # Remove a primeira linha gerada pelo lag
# Função para limpar o nome do ativo
limpar_nome_ativo <- function(nome) {
partes <- unlist(strsplit(as.character(nome), "[\r\n]+"))
trimws(tail(partes, 1))
}
df_retornos <- df_retornos %>%
rename_with(~ sapply(.x, limpar_nome_ativo))
• Estime janelas de 1 ano para cada papel e as respectivas médias e desvio padrão de cada ativo para cada janela de 1 ano. Comece essas janelas anuais a partir de 2005. (serão 21 janelas) • Estime os retornos acumulados para todos os ativos durante todo o período e em cada janela de 1 ano no período de 2005 a 2025. • Complemente sua base de dados com os valores do índice Bovespa (IBOV) e do CDI, sempre em base diária, informando a fonte de consulta. Isso será necessário para calcular o Beta das ações, performance, fronteira de eficiência além de fazer comparativos e análise adicionais.
library(dplyr)
library(knitr)
# 1. Definir as colunas que devem ser ignoradas
colunas_excluir <- c("Data", "Ano", "IBOV_Fechamento", "CDI_Diario", "Em moeda orig")
# 2. Selecionar o restante das colunas dinamicamente
colunas_alvo <- setdiff(names(df_retornos), colunas_excluir)
# 3. Aplicar o agrupamento e o summarise
estatisticas_geral <- df_retornos %>% filter(Ano >= 2005) %>%
group_by(Ano) %>%
summarise(
across(
all_of(colunas_alvo),
list(
Média = ~ mean(.x, na.rm = TRUE),
`Desvio-padrão` = ~ sd(.x, na.rm = TRUE)
),
.names = "{.col}_{.fn}"
),
.groups = "drop"
) %>%
mutate(
across(
where(is.numeric),
~ round(.x, 4)
)
)
• Estime os retornos acumulados para todos os ativos durante todo o período e em cada janela de 1 ano no período de 2005 a 2025.
library(tidyverse)
library(plotly)
# 1. Filtrar o período de 2005 a 2025 (assumindo que a coluna de data se chama 'Data' ou similar)
# Certifique-se de ajustar 'Data' e as colunas alvo de retornos conforme sua base original.
colunas_excluir <- c("Data", "Ano", "IBOV_Fechamento", "CDI_diario", "Em moeda orig")
colunas_ativos <- setdiff(names(df_retornos), colunas_excluir)
# 2. Retorno Acumulado Total por Ativo (de 2005 até 2025)
# Fórmula padrão para retorno acumulado composto: Prod(1 + r) - 1
retorno_total <- df_retornos %>%
filter(Ano >= 2005 & Ano <= 2025) %>%
summarise(
across(
all_of(colunas_ativos),
~ prod(1 + .x, na.rm = TRUE) - 1,
.names = "{.col}_Acumulado_Total"
)
)
# 3. Retorno Acumulado por Janela de 1 Ano (agrupado por Ano)
retorno_anual <- df_retornos %>%
filter(Ano >= 2005 & Ano <= 2025) %>%
group_by(Ano) %>%
summarise(
across(
all_of(colunas_ativos),
~ prod(1 + .x, na.rm = TRUE) - 1,
.names = "{.col}"
),
.groups = "drop"
)
# 4. Transformar a base de retornos anuais para o formato longo (para o Plotly)
df_long_retorno <- retorno_anual %>%
pivot_longer(
cols = -Ano,
names_to = "Ativo",
values_to = "Retorno_Acumulado"
) %>%
mutate(
Ativo = sapply(Ativo, limpar_nome_ativo),
Retorno_Acumulado = round(Retorno_Acumulado * 100, 2) # Opcional: transformar em percentual
)
# Lista de ativos únicos
ativos_ret <- unique(df_long_retorno$Ativo)
n_ativos_ret <- length(ativos_ret)
# 5. Criar o Gráfico Interativo com Plotly para o Retorno Acumulado
p_ret <- plot_ly()
for (ativo in ativos_ret) {
df_sub <- df_long_retorno %>% filter(Ativo == ativo)
p_ret <- p_ret %>% add_lines(
data = df_sub,
x = ~Ano,
y = ~Retorno_Acumulado,
name = paste(ativo, "- Retorno 1 Ano"),
legendgroup = ativo,
visible = TRUE
)
}
# Criar o Menu Suspenso de Ativos
botoes_ativos_ret <- list(
list(
method = "restyle",
args = list("visible", TRUE),
label = "Todos os Ativos"
)
)
for (i in seq_along(ativos_ret)) {
vis_vector <- rep(FALSE, n_ativos_ret)
vis_vector[i] <- TRUE
botoes_ativos_ret[[i + 1]] <- list(
method = "restyle",
args = list("visible", vis_vector),
label = ativos_ret[i]
)
}
# Configurar Layout e remover a barra de ferramentas
p_ret <- p_ret %>% layout(
title = "Retorno Acumulado Anual (2005 - 2025)",
xaxis = list(title = "Ano", dtick = 1),
yaxis = list(title = "Retorno Acumulado (%)"),
updatemenus = list(
list(
type = "dropdown",
active = 0,
x = 0.1,
y = 1.15,
buttons = botoes_ativos_ret
)
)
) %>%
config(displayModeBar = FALSE)
# Exibir o gráfico de retornos acumulados
p_ret
• Estime os betas das ações para todos os ativos durante todo o período e em cada janela de 1 ano no período de 2005 a 2025.
library(tidyverse)
library(broom)
# Desativa a notação científica (ex: exibe 0.0001 em vez de 1e-04)
options(scipen = 999)
# 1. Definir as colunas que devem ser ignoradas
colunas_excluir <- c("Data", "Ano", "IBOV_Fechamento", "Ret_IBOV", "CDI_Dia", "Em moeda orig","CDI_Diario")
# 2. Pegar todas as colunas restantes de df_retornos
colunas_ativos_geral <- setdiff(names(df_retornos), colunas_excluir)
# ==============================================================================
# 3. CALCULAR O BETA E P-VALOR GLOBAL (Todo o período) para TODAS as ações
# ==============================================================================
beta_global_list <- map_dfr(colunas_ativos_geral, function(acao) {
modelo <- tryCatch({
lm(df_retornos[[acao]] ~ df_retornos$Ret_IBOV, data = df_retornos)
}, error = function(e) { NULL })
if (is.null(modelo)) return(tibble(Ativo = acao, Beta = NA_real_, P_Valor = NA_real_))
resumo <- summary(modelo)$coefficients
if (!"df_retornos$Ret_IBOV" %in% rownames(resumo) && nrow(resumo) < 2) {
return(tibble(Ativo = acao, Beta = NA_real_, P_Valor = NA_real_))
}
# Pega o coeficiente e o p-valor da variável de mercado (linha 2)
tibble(
Ativo = acao,
Beta = round(resumo[2, "Estimate"], 4),
P_Valor = round(resumo[2, "Pr(>|t|)"], 4)
)
})
df_beta_global <- beta_global_list %>%
mutate(Ativo = sapply(Ativo, function(x) {
partes <- unlist(strsplit(x, "[\r\n\\.]+"))
ticker <- partes[grepl("^[A-Z0-9]{4,6}$", partes)]
if(length(ticker) == 0) ticker <- tail(partes, 2)[1]
return(trimws(ticker))
}))
# ==============================================================================
# 4. CALCULAR O BETA E P-VALOR POR JANELAS ANUAIS (2005 a 2025)
# ==============================================================================
# Como o summarise tradicional com across() dificulta extrair o p-valor simultaneamente,
# a forma mais segura e limpa para janelas anuais com p-valor é iterar por Ano e Ativo via map_dfr:
df_periodo <- df_retornos %>% filter(Ano >= 2005 & Ano <= 2025)
anos_unicos <- sort(unique(df_periodo$Ano))
beta_anuais_longo <- map_dfr(anos_unicos, function(ano_alvo) {
dados_ano <- df_periodo %>% filter(Ano == ano_alvo)
map_dfr(colunas_ativos_geral, function(acao) {
# Verifica se há dados suficientes
subset_dados <- dados_ano %>% select(all_of(c(acao, "Ret_IBOV"))) %>% drop_na()
if(nrow(subset_dados) > 5) {
modelo <- tryCatch({
lm(subset_dados[[acao]] ~ subset_dados$Ret_IBOV)
}, error = function(e) { NULL })
if(!is.null(modelo)) {
resumo <- summary(modelo)$coefficients
if(nrow(resumo) >= 2) {
return(tibble(
Ano = ano_alvo,
Ativo = acao,
Beta = round(resumo[2, "Estimate"], 4),
P_Valor = round(resumo[2, "Pr(>|t|)"], 4)
))
}
}
}
tibble(Ano = ano_alvo, Ativo = acao, Beta = NA_real_, P_Valor = NA_real_)
})
}) %>%
mutate(Ativo = sapply(Ativo, limpar_nome_ativo))
• Estime também os betas das ações para todos os ativos durante todo o período e em cada janela de 1 ano no período de 2005 a 2025, mas agora com dados dos últimos 5 anos. Ou seja, para o último dia de cada janela de 1 ano, observe os dados dos 3 anos anteriores para estimar o beta.
library(tidyverse)
library(htmltools)
options(scipen = 999)
# 1. Definir colunas ignoradas e ativos (com o nome correto do CDI)
colunas_excluir <- c("Data", "Ano", "IBOV_Fechamento", "Ret_IBOV", "CDI_diario", "CDI_Diario", "Em moeda orig","CDI_Dia")
colunas_ativos_geral <- setdiff(names(df_retornos), colunas_excluir)
# Garantir ordenação por Ano
df_retornos <- df_retornos %>% arrange(Ano)
# Função robusta para cálculo de Beta e p-valor (retornando um tibble limpo)
calcular_beta_pvalor <- function(dados, acao) {
subset_dados <- dados %>% select(all_of(c(acao, "Ret_IBOV"))) %>% drop_na()
if(nrow(subset_dados) > 10) {
modelo <- tryCatch({
lm(subset_dados[[acao]] ~ subset_dados$Ret_IBOV)
}, error = function(e) { NULL })
if(!is.null(modelo)) {
resumo <- summary(modelo)$coefficients
if(nrow(resumo) >= 2) {
return(tibble(
Beta = resumo[2, "Estimate"],
P_Valor = resumo[2, "Pr(>|t|)"]
))
}
}
}
tibble(Beta = NA_real_, P_Valor = NA_real_)
}
# ==============================================================================
# 2. CÁLCULO MÓVEL DE 5 ANOS (Janela de 5 anos anteriores para cada ano de 2005 a 2025)
# ==============================================================================
anos_alvo <- 2005:2025
betas_5anos_longo <- map_dfr(anos_alvo, function(ano_fim) {
ano_inicio <- ano_fim - 4 # Janela móvel exata de 5 anos (ex: 2001-2005, ..., 2021-2025)
dados_janela <- df_retornos %>% filter(Ano >= ano_inicio & Ano <= ano_fim)
map_dfr(colunas_ativos_geral, function(acao) {
res <- calcular_beta_pvalor(dados_janela, acao)
tibble(
Periodo = paste0(ano_inicio, "-", ano_fim),
Ativo = limpar_nome_ativo(acao),
Beta = round(res$Beta, 4),
P_Valor = round(res$P_Valor, 4)
)
})
})
# Preparação para o JSON
df_completo_5anos <- betas_5anos_longo %>%
mutate(Ativo = trimws(Ativo)) %>%
drop_na(Beta)
lista_ativos_5anos <- sort(unique(df_completo_5anos$Ativo))
json_dados_5anos <- jsonlite::toJSON(df_completo_5anos, dataframe = "rows")
# ==============================================================================
# 3. DASHBOARD PROFISSIONAL E COLORIDO
# ==============================================================================
browsable(
tagList(
tags$style(HTML("
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; color: #333; margin: 30px; }
.dashboard-card { background: #ffffff; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); max-width: 900px; margin: auto; }
h2 { color: #2c3e50; text-align: center; margin-bottom: 25px; font-weight: 600; }
.select-container { text-align: center; margin-bottom: 30px; }
select { padding: 12px 20px; font-size: 16px; border-radius: 8px; border: 1px solid #cbd5e1; background-color: #fff; width: 60%; cursor: pointer; transition: all 0.3s; }
select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); }
table { width: 100%; border-collapse: collapse; margin-top: 15px; background: white; border-radius: 8px; overflow: hidden; }
th, td { padding: 12px 18px; text-align: center; border-bottom: 1px solid #e2e8f0; font-size: 14px; }
th { background-color: #1e293b; color: white; font-weight: 600; text-transform: uppercase; font-size: 12px; letter-spacing: 0.5px; }
tr:hover { background-color: #f1f5f9; }
.badge-sig { background-color: #dcfce7; color: #166534; padding: 4px 8px; border-radius: 4px; font-weight: 600; }
.badge-nao-sig { background-color: #fee2e2; color: #991b1b; padding: 4px 8px; border-radius: 4px; }
.placeholder-text { text-align: center; color: #94a3b8; font-style: italic; padding: 40px; font-size: 16px; }
")),
tags$div(
class = "dashboard-card",
tags$h2("Painel de Betas - Janela Móvel de 5 Anos"),
tags$div(
class = "select-container",
tags$select(
id = "ativoSelect5Anos",
onchange = "filtrarDados5Anos()",
tags$option(value = "", "Selecione uma ação...", selected = TRUE, disabled = TRUE),
lapply(lista_ativos_5anos, function(a) tags$option(value = a, a))
)
),
tags$div(
id = "tabelaContainer5Anos",
tags$div(class = "placeholder-text", "Nenhuma ação selecionada. Escolha um ativo acima para carregar o histórico de 5 anos.")
)
),
tags$script(HTML(sprintf("
const dados5Anos = %s;
function filtrarDados5Anos() {
const select = document.getElementById('ativoSelect5Anos');
const ativoSelecionado = select.value;
const container = document.getElementById('tabelaContainer5Anos');
if (!ativoSelecionado) {
container.innerHTML = '<div class=\"placeholder-text\">Nenhuma ação selecionada. Escolha um ativo acima para carregar o histórico de 5 anos.</div>';
return;
}
const filtrados = dados5Anos.filter(d => d.Ativo === ativoSelecionado);
if (filtrados.length === 0) {
container.innerHTML = '<div class=\"placeholder-text\">Nenhum registro encontrado para este ativo.</div>';
return;
}
let html = '<table>';
html += '<thead><tr><th>Janela Móvel (5 Anos)</th><th>Ativo</th><th>Beta</th><th>p-valor</th></tr></thead>';
html += '<tbody>';
filtrados.forEach(row => {
let pValFormatado = row.P_Valor !== null ? row.P_Valor.toFixed(4) : 'NA';
let betaFormatado = row.Beta !== null ? row.Beta.toFixed(4) : 'NA';
let pValClass = '';
if (row.P_Valor !== null) {
pValClass = row.P_Valor < 0.05 ? 'badge-sig' : 'badge-nao-sig';
}
html += `<tr>
<td><b>${row.Periodo}</b></td>
<td>${row.Ativo}</td>
<td>${betaFormatado}</td>
<td><span class=\"${pValClass}\">${pValFormatado}</span></td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
", json_dados_5anos)))
)
)
• Apresente os 10 maiores e menores retornos acumulados e os 10 maiores e menores desvios padrão para o período de 2005 a 2025 e também as médias das 21 janelas desses indicadores. • Compare os betas de 1 ano, 3 anos e o de 5 anos dessas ações
library(tidyverse)
library(htmltools)
options(scipen = 999)
# 1. Preparação das colunas
colunas_excluir <- c("Data", "Ano", "IBOV_Fechamento", "Ret_IBOV", "CDI_Dia", "CDI_Diario", "Em moeda orig","CDI_Dia")
colunas_ativos <- setdiff(names(df_retornos), colunas_excluir)
# Garantir limpeza e ordenação
df_retornos <- df_retornos %>% arrange(Ano)
colnames(df_retornos) <- sapply(colnames(df_retornos), limpar_nome_ativo)
colunas_ativos_limpos <- setdiff(names(df_retornos), colunas_excluir)
# 2. Cálculos Globais e Médias das 21 Janelas (2005 a 2025)
df_2005_2025 <- df_retornos %>% filter(Ano >= 2005 & Ano <= 2025)
retorno_total <- df_2005_2025 %>%
summarise(across(all_of(colunas_ativos_limpos), ~ prod(1 + .x, na.rm = TRUE) - 1)) %>%
pivot_longer(everything(), names_to = "Ativo", values_to = "Retorno_Acumulado_Total")
retorno_anual_medio <- df_2005_2025 %>%
group_by(Ano) %>%
summarise(across(all_of(colunas_ativos_limpos), ~ prod(1 + .x, na.rm = TRUE) - 1), .groups = "drop") %>%
summarise(across(-Ano, ~ mean(.x, na.rm = TRUE))) %>%
pivot_longer(everything(), names_to = "Ativo", values_to = "Retorno_Anual_Medio_21Anos")
desvio_total <- df_2005_2025 %>%
summarise(across(all_of(colunas_ativos_limpos), ~ sd(.x, na.rm = TRUE))) %>%
pivot_longer(everything(), names_to = "Ativo", values_to = "Desvio_Padrao_Total")
desvio_anual_medio <- df_2005_2025 %>%
group_by(Ano) %>%
summarise(across(all_of(colunas_ativos_limpos), ~ sd(.x, na.rm = TRUE)), .groups = "drop") %>%
summarise(across(-Ano, ~ mean(.x, na.rm = TRUE))) %>%
pivot_longer(everything(), names_to = "Ativo", values_to = "Desvio_Padrao_Medio_21Anos")
calcular_beta_pvalor_janela <- function(dados, acao, anos_janela) {
sub_dados <- dados %>% filter(Ano %in% anos_janela) %>% select(all_of(c(acao, "Ret_IBOV"))) %>% drop_na()
if(nrow(sub_dados) > 5) {
mod <- tryCatch(lm(sub_dados[[acao]] ~ sub_dados$Ret_IBOV), error = function(e) NULL)
if(!is.null(mod)) {
res <- summary(mod)$coefficients
if(nrow(res) >= 2) {
return(tibble(Beta = res[2, "Estimate"], P_Valor = res[2, "Pr(>|t|)"]))
}
}
}
return(tibble(Beta = NA_real_, P_Valor = NA_real_))
}
betas_comparativo <- map_dfr(colunas_ativos_limpos, function(acao) {
b1 <- calcular_beta_pvalor_janela(df_retornos, acao, 2025)
b3_list <- map_dfr(2023:2025, function(a) calcular_beta_pvalor_janela(df_retornos, acao, (a-2):a))
b5_list <- map_dfr(2021:2025, function(a) calcular_beta_pvalor_janela(df_retornos, acao, (a-4):a))
tibble(
Ativo = acao,
Beta_1Ano = round(b1$Beta, 4), P_Valor_1Ano = round(b1$P_Valor, 4),
Beta_Media_3Anos = round(mean(b3_list$Beta, na.rm = TRUE), 4), P_Valor_Media_3Anos = round(mean(b3_list$P_Valor, na.rm = TRUE), 4),
Beta_Media_5Anos = round(mean(b5_list$Beta, na.rm = TRUE), 4), P_Valor_Media_5Anos = round(mean(b5_list$P_Valor, na.rm = TRUE), 4)
)
})
# 3. Consolidar Base Geral
df_final_consolidado <- retorno_total %>%
left_join(retorno_anual_medio, by = "Ativo") %>%
left_join(desvio_total, by = "Ativo") %>%
left_join(desvio_anual_medio, by = "Ativo") %>%
left_join(betas_comparativo, by = "Ativo")
# 4. Criar as 4 listas solicitadas (Top 10 Maiores e Menores)
top10_maiores_medias <- df_final_consolidado %>% arrange(desc(Retorno_Anual_Medio_21Anos)) %>% head(10)
top10_menores_medias <- df_final_consolidado %>% arrange(Retorno_Anual_Medio_21Anos) %>% head(10)
top10_maiores_desvios <- df_final_consolidado %>% arrange(desc(Desvio_Padrao_Medio_21Anos)) %>% head(10)
top10_menores_desvios <- df_final_consolidado %>% arrange(Desvio_Padrao_Medio_21Anos) %>% head(10)
# Estrutura agrupada para o JSON do Dashboard
dados_painel_rankings <- list(
maiores_medias = top10_maiores_medias,
menores_medias = top10_menores_medias,
maiores_desvios = top10_maiores_desvios,
menores_desvios = top10_menores_desvios
)
json_rankings <- jsonlite::toJSON(dados_painel_rankings, dataframe = "rows")
# 5. Dashboard Profissional Interativo com as 4 opções específicas
browsable(
tagList(
tags$style(HTML("
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; color: #333; margin: 30px; }
.dashboard-card { background: #ffffff; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); max-width: 1200px; margin: auto; }
h2 { color: #2c3e50; text-align: center; margin-bottom: 25px; font-weight: 600; }
.select-container { text-align: center; margin-bottom: 30px; }
select { padding: 12px 20px; font-size: 16px; border-radius: 8px; border: 1px solid #cbd5e1; background-color: #fff; width: 60%; cursor: pointer; }
table { width: 100%; border-collapse: collapse; margin-top: 15px; background: white; border-radius: 8px; overflow: hidden; }
th, td { padding: 12px 10px; text-align: center; border-bottom: 1px solid #e2e8f0; font-size: 12px; }
th { background-color: #1e293b; color: white; font-weight: 600; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; }
tr:hover { background-color: #f1f5f9; }
.placeholder-text { text-align: center; color: #94a3b8; font-style: italic; padding: 40px; font-size: 16px; }
.highlight { font-weight: bold; color: #0284c7; }
.badge-sig { background-color: #dcfce7; color: #166534; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
.badge-nao-sig { background-color: #fee2e2; color: #991b1b; padding: 2px 6px; border-radius: 4px; }
")),
tags$div(
class = "dashboard-card",
tags$h2("Painel Executivo - Rankings de Médias e Desvios Padrões (2005-2025)"),
tags$div(
class = "select-container",
tags$select(
id = "rankingSelect",
onchange = "renderizarRanking()",
tags$option(value = "", "Selecione uma categoria de ranking...", selected = TRUE, disabled = TRUE),
tags$option(value = "maiores_medias", "10 Maiores Médias (Retorno)"),
tags$option(value = "menores_medias", "10 Menores Médias (Retorno)"),
tags$option(value = "maiores_desvios", "10 Maiores Desvios Padrões (Risco)"),
tags$option(value = "menores_desvios", "10 Menores Desvios Padrões (Risco)")
)
),
tags$div(
id = "rankingContainer",
tags$div(class = "placeholder-text", "Nenhuma categoria selecionada. Escolha uma opção acima para exibir o ranking.")
)
),
tags$script(HTML(sprintf("
const dadosRankings = %s;
function renderizarRanking() {
const select = document.getElementById('rankingSelect');
const categoria = select.value;
const container = document.getElementById('rankingContainer');
if (!categoria) {
container.innerHTML = '<div class=\"placeholder-text\">Nenhuma categoria selecionada. Escolha uma opção acima para exibir o ranking.</div>';
return;
}
const lista = dadosRankings[categoria];
if (!lista || lista.length === 0) {
container.innerHTML = '<div class=\"placeholder-text\">Nenhum registro encontrado para esta categoria.</div>';
return;
}
function pClass(val) {
if (val === null || isNaN(val)) return '';
return val < 0.05 ? 'badge-sig' : 'badge-nao-sig';
}
let html = '<table>';
html += '<thead><tr>';
html += '<th>#</th><th>Ativo</th><th>Ret. Acum. Total</th><th>Média Ret. (21A)</th><th>D.P. Total</th><th>Média D.P. (21A)</th>';
html += '<th>Beta (1A)</th><th>p-val (1A)</th><th>Beta (3A)</th><th>p-val (3A)</th><th>Beta (5A)</th><th>p-val (5A)</th>';
html += '</tr></thead><tbody>';
lista.forEach((d, index) => {
html += `<tr>`;
html += `<td><b>${index + 1}º</b></td>`;
html += `<td class=\"highlight\">${d.Ativo}</td>`;
html += `<td>${(d.Retorno_Acumulado_Total * 100).toFixed(2)}%%</td>`;
html += `<td>${(d.Retorno_Anual_Medio_21Anos * 100).toFixed(2)}%%</td>`;
html += `<td>${d.Desvio_Padrao_Total.toFixed(4)}</td>`;
html += `<td>${d.Desvio_Padrao_Medio_21Anos.toFixed(4)}</td>`;
html += `<td>${d.Beta_1Ano !== null ? d.Beta_1Ano.toFixed(4) : 'NA'}</td>`;
html += `<td><span class=\"${pClass(d.P_Valor_1Ano)}\">${d.P_Valor_1Ano !== null ? d.P_Valor_1Ano.toFixed(4) : 'NA'}</span></td>`;
html += `<td>${d.Beta_Media_3Anos !== null ? d.Beta_Media_3Anos.toFixed(4) : 'NA'}</td>`;
html += `<td><span class=\"${pClass(d.P_Valor_Media_3Anos)}\">${d.P_Valor_Media_3Anos !== null ? d.P_Valor_Media_3Anos.toFixed(4) : 'NA'}</span></td>`;
html += `<td>${d.Beta_Media_5Anos !== null ? d.Beta_Media_5Anos.toFixed(4) : 'NA'}</td>`;
html += `<td><span class=\"${pClass(d.P_Valor_Media_5Anos)}\">${d.P_Valor_Media_5Anos !== null ? d.P_Valor_Media_5Anos.toFixed(4) : 'NA'}</span></td>`;
html += `</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
", json_rankings)))
)
)
O Índice de Treynor (ou Treynor Ratio) é uma métrica de desempenho ajustada ao risco desenvolvida por Jack L. Treynor (1965). Ele mede o retorno excedente por unidade de risco sistemático (risco de mercado, medido pelo \(\beta\)), diferentemente do Índice de Sharpe, que utiliza o risco total (desvio-padrão \(\sigma\)).
O Índice de Treynor (\(T_i\)) para
um ativo ou carteira \(i\) é definido
pela fórmula: \[T_i = \frac{\bar{R}_i -
\bar{R}_f}{\beta_i}\] O Índice de Treynor e o Modelo CAPM
Fórmula do Índice de Treynor:\[T_i =
\frac{\bar{R}_i - \bar{R}_f}{\beta_i}\]
Componentes da
Equação:
\(\bar{R}_i\): Retorno
médio do ativo \(i\) na janela
observada.
\(\bar{R}_f\): Retorno
médio do ativo livre de risco (Risk-Free Rate), representado no script R
pelo CDI médio (\(\text{CDI\_Medio}\)).
\(\beta_i\): Coeficiente Beta do
ativo \(i\) em relação ao benchmark de
mercado (\(M\)).
\(\bar{R}_i - \bar{R}_f\): Prêmio de risco do
ativo.
O Cálculo do Beta (\(\beta\)) via Regressão Linear
O \(\beta_i\) mede a sensibilidade do ativo
frente às oscilações do índice de mercado Ibovespa, derivado diretamente
do CAPM (Capital Asset Pricing Model):\[R_{i,t} = \alpha_i + \beta_i R_{M,t} +
\varepsilon_{i,t}\]
Estimativa por Mínimos Quadrados
Ordinários (MQO):
\[\beta_i =
\frac{\text{Cov}(R_i, R_M)}{\text{Var}(R_M)}\]
Analisando o código R fornecido, o processo de seleção das carteiras segue o seguinte fluxo metodológico:Janela Móvel de Formação de 5 Anos (Rolling Window): Para cada ano de formação (de 2005 a 2025), o algoritmo define um período histórico de 5 anos:
\[\text{Janela} = [\text{Ano\_Formacao} - 4, \; \text{Ano\_Formacao}]\]Exemplo: Para Ano_Formacao = 2009, a janela de dados vai de 01/01/2005 a 31/12/2009. Filtro de Liquidez/Amostragem Mínima: São mantidos apenas os ativos com no mínimo \(30\) observações diárias válidas na janela (\(N \ge 30\)). Estimativa dos Parâmetros
:\(\bar{R}_i = \text{mean}(\text{Retorno\_Ativo})\)\(\bar{R}_f = \text{mean}(\text{CDI\_Diario})\)\(\beta_i = \text{coeficiente da regressão com Ibovespa}\)
O indicador só é calculado se o ativo apresentar Beta estritamente positivo e finito (\(\beta_i > 0\)).
Ativos com \(\beta \le 0\) são desconsiderados (\(\text{Treynor} = \text{NA}\)).
Seleção e Ponderação Equi-ponderada (Equal-Weighted):Em cada ano, os ativos são ordenados de forma decrescente pelo valor do Índice de Treynor.A função slice_head(n = 10) seleciona as 10 ações com maior Índice de Treynor na janela histórica de 5 anos.
A cada ação selecionada atribui-se um peso inicial idêntico de \(10\%\) (\(\text{Peso\_Inicial} = 0.10\)).A carteira é montada para ser avaliada/testada fora da amostra no ano seguinte (Ano_Avaliacao = Ano_Formacao + 1).
Para fundamentar trabalhos acadêmicos ou relatórios técnicos baseados neste código, :
Referência Principal (Seminal)
*Referências Complementares de Finanças e CAPMSHARPE, William F. (1964). Capital Asset Prices: A Theory of Market Equilibrium under Conditions of Risk. Journal of Finance, vol. 19, no. 3, pp. 425-442.Nota:
Fundamentação do modelo CAPM e do conceito de Beta.
Nota: Livro-texto clássico que detalha e compara os índices de Sharpe, Treynor e Alfa de Jensen no capítulo de Avaliação de Desempenho de Carteiras (Portfolio Performance Evaluation).Formato de Citação (Exemplo ABNT)TREYNOR, J. L. How to Rate Management of Investment Funds. Harvard Business Review, v. 43, n. 1, p. 63-75, 1965.
library(dplyr)
library(purrr)
library(lubridate)
library(tibble)
library(DT)
# ============================================================
# 1. FUNCAO PARA LIMPAR O NOME DO ATIVO
# ============================================================
limpar_nome_ativo <- function(nome) {
partes <- unlist(strsplit(nome, "[\r\n]+"))
return(trimws(tail(partes, 1)))
}
# ============================================================
# 2. CORRECAO DE CODIFICACAO UTF-8
# ============================================================
corrigir_utf8 <- function(x) {
if (is.factor(x)) {
x <- as.character(x)
}
if (!is.character(x)) {
return(x)
}
resultado <- iconv(
x,
from = "",
to = "UTF-8",
sub = ""
)
resultado[is.na(resultado)] <- ""
Encoding(resultado) <- "UTF-8"
resultado
}
limpar_base_dt <- function(base) {
base <- as.data.frame(base)
names(base) <- corrigir_utf8(names(base))
nomes_vazios <- which(
is.na(names(base)) | names(base) == ""
)
if (length(nomes_vazios) > 0) {
names(base)[nomes_vazios] <- paste0(
"Coluna_",
nomes_vazios
)
}
names(base) <- make.unique(names(base))
for (j in seq_along(base)) {
if (is.character(base[[j]]) || is.factor(base[[j]])) {
base[[j]] <- corrigir_utf8(base[[j]])
}
}
base
}
# ============================================================
# 3. PREPARAR df_retornos (COM CALCULO DO RETORNO DO IBOV)
# ============================================================
if (!exists("df_retornos")) {
stop("O objeto df_retornos nao esta carregado.")
}
df_retornos <- as.data.frame(df_retornos)
if (!"Data" %in% names(df_retornos)) {
stop(
"A coluna Data nao foi encontrada em df_retornos. ",
"Confira names(df_retornos)."
)
}
if (!"IBOV_Fechamento" %in% names(df_retornos)) {
stop("A coluna IBOV_Fechamento e obrigatoria para calcular o Beta e o Treynor.")
}
# Converter a coluna de data
if (inherits(df_retornos$Data, "Date")) {
# Nenhuma conversao necessaria
} else if (inherits(df_retornos$Data, "POSIXt")) {
df_retornos$Data <- as.Date(df_retornos$Data)
} else {
df_retornos$Data <- as.Date(
as.character(df_retornos$Data)
)
}
## NULL
if (anyNA(df_retornos$Data)) {
stop(
"Existem datas invalidas na coluna Data. ",
"Verifique o formato original."
)
}
# Ordenar, criar ano e calcular o Retorno Diário do Ibovespa mantendo a base intacta
df_retornos <- df_retornos %>%
arrange(Data) %>%
mutate(
Ano = lubridate::year(Data),
Retorno_IBOV = IBOV_Fechamento / lag(IBOV_Fechamento) - 1
)
df_retornos1 <- df_retornos %>%
select(-IBOV_Fechamento)
# ============================================================
# 4. IDENTIFICAR A COLUNA DO CDI
# ============================================================
coluna_cdi <- "CDI_Diario"
if (!coluna_cdi %in% names(df_retornos1)) {
stop(
paste0(
"Coluna do CDI nao encontrada: ", coluna_cdi,
"\nColunas disponiveis:\n",
paste(names(df_retornos1), collapse = ", "),
"\nAltere a variavel coluna_cdi para o nome correto."
)
)
}
if (!is.numeric(df_retornos[[coluna_cdi]])) {
stop("A coluna do CDI precisa ser numerica.")
}
# ============================================================
# 5. IDENTIFICAR AS COLUNAS DAS ACOES
# ============================================================
colunas_acoes <- setdiff(
names(df_retornos1),
c("Data", "Ano", coluna_cdi, "Retorno_IBOV")
)
colunas_acoes <- colunas_acoes[
vapply(
df_retornos[colunas_acoes],
is.numeric,
logical(1)
)
]
if (length(colunas_acoes) < 10) {
stop(
"Foram identificadas menos de dez colunas numericas de acoes."
)
}
# Mapa entre nome original e ticker limpo
mapa_ativos <- tibble(
Coluna_Original = colunas_acoes,
Ativo = vapply(
colunas_acoes,
limpar_nome_ativo,
character(1)
)
)
if (anyDuplicated(mapa_ativos$Ativo)) {
duplicados <- mapa_ativos %>%
count(Ativo) %>%
filter(n > 1)
print(duplicados)
stop(
"Ha tickers duplicados depois da limpeza. ",
"Confira mapa_ativos antes de continuar."
)
}
cat(
"Observacoes na base:", nrow(df_retornos1), "\n",
"Acoes identificadas:", length(colunas_acoes), "\n"
)
## Observacoes na base: 4359
## Acoes identificadas: 67
# ============================================================
# 6. DEFINIR OS ANOS DE FORMACAO (2005 A 2025)
# ============================================================
anos_formacao <- 2005:2025
# ============================================================
# 7. FUNCAO DE CALCULO DO INDICE DE TREYNOR (Baseado no Beta)
# ============================================================
calcular_treynor <- function(base, ano_formacao) {
ano_inicio <- ano_formacao - 4
data_inicio <- as.Date(
paste0(ano_inicio, "-01-01")
)
data_fim <- as.Date(
paste0(ano_formacao, "-12-31")
)
janela <- base %>%
filter(
Data >= data_inicio,
Data <= data_fim
)
if (nrow(janela) == 0) {
warning(
paste("Janela sem observacoes:", ano_formacao)
)
return(tibble())
}
retorno_cdi <- janela[[coluna_cdi]]
retorno_ibov <- janela[["Retorno_IBOV"]]
resultados <- purrr::map_dfr(
colunas_acoes,
function(acao) {
retorno_acao <- janela[[acao]]
dados <- tibble(
Retorno_Ativo = retorno_acao,
Retorno_CDI = retorno_cdi,
Retorno_IBOV = retorno_ibov
) %>%
filter(
is.finite(Retorno_Ativo),
is.finite(Retorno_CDI),
is.finite(Retorno_IBOV)
)
n <- nrow(dados)
# Exigir pelo menos 30 observacoes validas para estabilidade do Beta
if (n < 30) {
return(
tibble(
Coluna_Original = acao,
Ativo = limpar_nome_ativo(acao),
Observacoes = n,
Retorno_Medio = NA_real_,
CDI_Medio = NA_real_,
Beta = NA_real_,
Treynor = NA_real_
)
)
}
media_acao <- mean(dados$Retorno_Ativo)
media_cdi <- mean(dados$Retorno_CDI)
# Estimar o Beta via Regressao Linear (lm)
modelo <- tryCatch({
lm(Retorno_Ativo ~ Retorno_IBOV, data = dados)
}, error = function(e) {
NULL
})
beta_ativo <- if (!is.null(modelo)) {
coef(modelo)[2]
} else {
NA_real_
}
# O Treynor exige Beta finito e estritamente positivo
valor_treynor <- if (
is.finite(beta_ativo) &&
beta_ativo > 0
) {
(media_acao - media_cdi) / beta_ativo
} else {
NA_real_
}
tibble(
Coluna_Original = acao,
Ativo = limpar_nome_ativo(acao),
Observacoes = n,
Retorno_Medio = media_acao,
CDI_Medio = media_cdi,
Beta = beta_ativo,
Treynor = valor_treynor
)
}
)
resultados %>%
mutate(
Ano_Formacao = as.integer(ano_formacao),
Ano_Inicio_Janela = as.integer(ano_inicio),
Ano_Fim_Janela = as.integer(ano_formacao),
Ano_Avaliacao = as.integer(ano_formacao + 1)
) %>%
group_by(Ano_Formacao) %>%
mutate(
Ranking = if_else(
is.na(Treynor),
NA_integer_,
min_rank(desc(Treynor))
)
) %>%
ungroup() %>%
select(
Ano_Formacao,
Ano_Inicio_Janela,
Ano_Fim_Janela,
Ano_Avaliacao,
Ranking,
Ativo,
Coluna_Original,
Observacoes,
Retorno_Medio,
CDI_Medio,
Beta,
Treynor
) %>%
arrange(Ano_Formacao, Ranking, Ativo)
}
# ============================================================
# 8. CALCULAR OS RANKINGS PARA TODOS OS ANOS (2005-2025)
# ============================================================
rankings <- purrr::map_dfr(
anos_formacao,
function(ano) {
calcular_treynor(
base = df_retornos,
ano_formacao = ano
)
}
)
if (nrow(rankings) == 0) {
stop("Nenhum ranking foi calculado.")
}
# ============================================================
# 9. SELECIONAR AS DEZ MELHORES ACOES POR ANO (TREYNOR)
# ============================================================
carteiras_20 <- rankings %>%
filter(!is.na(Treynor)) %>%
group_by(Ano_Formacao) %>%
arrange(
desc(Treynor),
Ativo,
.by_group = TRUE
) %>%
slice_head(n = 10) %>%
mutate(
Peso_Inicial = 0.10
) %>%
ungroup() %>%
arrange(Ano_Formacao, Ranking)
# ============================================================
# 10. RESUMO DA COMPOSICAO DAS CARTEIRAS
# ============================================================
resumo_carteiras <- carteiras_20 %>%
group_by(
Ano_Formacao,
Ano_Avaliacao
) %>%
summarise(
Numero_Ativos = n_distinct(Ativo),
Ativos_Selecionados = paste(
Ativo,
collapse = ", "
),
.groups = "drop"
) %>%
arrange(Ano_Formacao)
cat(
"Numero de carteiras formadas:",
n_distinct(carteiras_20$Ano_Formacao),
"\n"
)
## Numero de carteiras formadas: 21
cat(
"Numero de acoes disponiveis:",
length(colunas_acoes),
"\n"
)
## Numero de acoes disponiveis: 67
carteiras_incompletas <- resumo_carteiras %>%
filter(Numero_Ativos != 10)
if (nrow(carteiras_incompletas) > 0) {
warning(
"Existem carteiras com menos de dez ativos. ",
"Confira a disponibilidade de dados em cada janela."
)
}
# ============================================================
# 11. PREPARAR OBJETOS PARA O DT (CONVERTENDO ANOS PARA TEXTO)
# ============================================================
rankings_dt <- limpar_base_dt(rankings)
carteiras_dt <- limpar_base_dt(carteiras_20)
resumo_dt <- limpar_base_dt(resumo_carteiras)
# Converter colunas de ano para caractere para evitar vírgulas no DT
colunas_anos_rankings <- intersect(c("Ano_Formacao", "Ano_Inicio_Janela", "Ano_Fim_Janela", "Ano_Avaliacao"), names(rankings_dt))
rankings_dt[colunas_anos_rankings] <- lapply(rankings_dt[colunas_anos_rankings], as.character)
colunas_anos_carteiras <- intersect(c("Ano_Formacao", "Ano_Inicio_Janela", "Ano_Fim_Janela", "Ano_Avaliacao"), names(carteiras_dt))
carteiras_dt[colunas_anos_carteiras] <- lapply(carteiras_dt[colunas_anos_carteiras], as.character)
colunas_anos_resumo <- intersect(c("Ano_Formacao", "Ano_Avaliacao"), names(resumo_dt))
resumo_dt[colunas_anos_resumo] <- lapply(resumo_dt[colunas_anos_resumo], as.character)
verificar_utf8 <- function(base, nome) {
nomes_validos <- iconv(
names(base),
from = "UTF-8",
to = "UTF-8",
sub = NA
)
if (anyNA(nomes_validos)) {
stop(
paste(
"Existem nomes de colunas invalidos em",
nome
)
)
}
for (j in seq_along(base)) {
if (is.character(base[[j]])) {
x <- base[[j]]
resultado <- iconv(
x,
from = "UTF-8",
to = "UTF-8",
sub = NA
)
if (anyNA(resultado)) {
stop(
paste(
"Texto UTF-8 invalido em",
nome,
"- coluna:",
names(base)[j]
)
)
}
}
}
cat("UTF-8 validado:", nome, "\n")
}
verificar_utf8(rankings_dt, "rankings")
## UTF-8 validado: rankings
verificar_utf8(carteiras_dt, "carteiras_20")
## UTF-8 validado: carteiras_20
verificar_utf8(resumo_dt, "resumo_carteiras")
## UTF-8 validado: resumo_carteiras
# ============================================================
# 12. TABELA INTERATIVA DO RANKING COMPLETO (TREYNOR)
# ============================================================
tabela_ranking <- DT::datatable(
rankings_dt,
caption = "Ranking do Indice de Treynor por janela de formacao (2005-2025)",
rownames = FALSE,
filter = "top",
escape = TRUE,
extensions = "Buttons",
options = list(
pageLength = 15,
lengthMenu = c(10, 15, 25, 50, 100),
scrollX = TRUE,
dom = "Bfrtip",
buttons = c("copy", "csv", "excel"),
order = list(
list(0, "asc"),
list(4, "asc")
)
)
) %>%
DT::formatRound(
columns = c(
"Retorno_Medio",
CDI_Medio = "CDI_Medio",
"Beta",
"Treynor"
),
digits = 6
)
As carteiras formadas anualmente pelo Índice de Treynor entre os períodos analisados evidenciam uma nítida transição nos perfis de risco e retorno das empresas de destaque na bolsa brasileira, combinando ativos defensivos, utilities resilientes e empresas cíclicas com excelente relação de prêmio por unidade de risco sistêmico (\(\beta\)).
Nos primeiros anos da série histórica (como nas carteiras com início em 2005-2006 e 2006-2007), observa-se a forte presença de papéis de setores tradicionais, industriais e de consumo — a exemplo de empresas como ALPA4, PSSA3, TASA4 e GOLL4. Essas escolhas refletem o ambiente macroeconômico da época, marcado por expansão de crédito e forte apetite a risco em ativos de crescimento e industriais, onde o índice selecionava companhias capazes de entregar retornos médios acima da taxa livre de risco (CDI) com sensibilidade moderada frente ao Ibovespa.
Conforme a base temporal avança para o final da década de 2000 e início dos anos 2010 (períodos de 2007-2008 até 2012-2013), as carteiras incorporam de forma massiva empresas do setor elétrico, saneamento e telecomunicações legadas (como TELB4, COCE5, SAPR4, EGIE3 e TRPL4). A presença recorrente dessas companhias demonstra que, em momentos de maior volatilidade sistêmica ou ajustes macroeconômicos, o Índice de Treynor tendeu a privilegiar ativos com betas mais controlados e fluxos de caixa altamente previsíveis, garantindo um prêmio de risco positivo frente aos juros elevados da economia brasileira.Já nos blocos seguintes, cobrindo o período de 2013-2014 até 2019-2020, nota-se uma rotação interessante de portfólio. Empresas ligadas ao setor de bens de capital, papel e celulose (KLBN4, LEVE3, WEGE3) e petroquímico/metalúrgico (UNIP6, BRKM5) ganham protagonismo expressivo.
A consistência de empresas como WEGE3 e UNIP6 em várias janelas evidencia alta resiliência operacional e ganhos expressivos de eficiência, permitindo que elas se mantivessem no topo do ranking de Treynor mesmo diante de ciclos econômicos desafiadores.Por fim, nas janelas mais recentes que se estendem até o ciclo de 2023-2024 e 2024-2025, as carteiras consolidadas na Tabela 4 destacam um misto robusto entre empresas do setor elétrico e energético (CPLE3, CPLE6, CGAS5, PETR3, PETR4) e ativos industriais tradicionais (INEP4, TASA4, ETER3, CLSC4). Essa configuração final comprova a adaptabilidade da estratégia baseada no Índice de Treynor: ao filtrar ativos pelo excedente de retorno em relação ao CDI dividido pelo Beta (\(\beta\)), o modelo seleciona de forma dinâmica aquelas companhias que melhor compensam o investidor pelo risco de mercado assumido em cada janela temporal específica.
# ============================================================
# 13. TABELA INTERATIVA DAS CARTEIRAS (TREYNOR)
# ============================================================
tabela_carteiras <- DT::datatable(
carteiras_dt,
caption = "Composicao das carteiras pelo Indice de Treynor (Anos selecionados)",
rownames = FALSE,
filter = "top",
escape = TRUE,
extensions = "Buttons",
options = list(
pageLength = 10,
lengthMenu = c(10, 20, 50, 100),
scrollX = TRUE,
dom = "Bfrtip",
buttons = c("copy", "csv", "excel"),
order = list(
list(0, "asc"),
list(4, "asc")
)
)
) %>%
DT::formatRound(
columns = c(
"Retorno_Medio",
"CDI_Medio",
"Beta",
"Treynor",
"Peso_Inicial"
),
digits = 6
)
# ============================================================
# 14. TABELA INTERATIVA DO RESUMO ANUAL
# ============================================================
tabela_resumo <- DT::datatable(
resumo_dt,
caption = "Resumo das carteiras anuais (Anos selecionados)",
rownames = FALSE,
filter = "top",
escape = TRUE,
extensions = "Buttons",
options = list(
pageLength = 20,
scrollX = TRUE,
dom = "Bfrtip",
buttons = c("copy", "csv", "excel")
)
)
tabela_resumo
# ============================================================
# 15. EXPORTAR OS RESULTADOS
# ============================================================
write.csv(
rankings_dt,
"ranking_treynor_especifico.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
write.csv(
carteiras_dt,
"carteiras_especifico_treynor.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
write.csv(
resumo_dt,
"resumo_carteiras_especifico_treynor.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
# ============================================================
# ANÁLISE COMPLETA DAS CARTEIRAS ANUAIS - ÍNDICE DE TREYNOR
# Retorno acumulado, média, risco, beta, Sharpe,
# Treynor e alfa de Jensen
# ============================================================
library(dplyr)
library(purrr)
library(tibble)
library(DT)
# ------------------------------------------------------------
# 1. FUNÇÃO PARA CORRIGIR CODIFICAÇÃO UTF-8
# ------------------------------------------------------------
corrigir_utf8 <- function(x) {
if (is.factor(x)) {
x <- as.character(x)
}
if (is.character(x)) {
x <- iconv(
x,
from = "",
to = "UTF-8",
sub = ""
)
}
x
}
limpar_base_dt <- function(dados) {
dados <- as.data.frame(
dados,
stringsAsFactors = FALSE
)
names(dados) <- corrigir_utf8(names(dados))
dados[] <- lapply(dados, corrigir_utf8)
dados
}
# ------------------------------------------------------------
# 2. PREPARAR A BASE DE RETORNOS
# ------------------------------------------------------------
df_base <- df_retornos %>%
mutate(
Data = as.Date(Data)
) %>%
arrange(Data)
# Verificar variáveis necessárias
necessarias <- c(
"Data",
"CDI_Diario",
"IBOV_Fechamento"
)
ausentes <- setdiff(
necessarias,
names(df_base)
)
if (length(ausentes) > 0) {
stop(
paste(
"Variáveis ausentes em df_retornos:",
paste(ausentes, collapse = ", ")
)
)
}
if (anyDuplicated(df_base$Data)) {
stop(
"Há datas repetidas em df_retornos. ",
"Corrija as duplicações antes de continuar."
)
}
# Retorno diário do Ibovespa calculado a partir do índice
df_base <- df_base %>%
mutate(
Ano = as.integer(format(Data, "%Y")),
Retorno_IBOV = IBOV_Fechamento / lag(IBOV_Fechamento) - 1
)
# ------------------------------------------------------------
# 3. VALIDAR A ESTRUTURA DAS CARTEIRAS
# ------------------------------------------------------------
colunas_obrigatorias <- c(
"Ano_Formacao",
"Ativo",
"Coluna_Original"
)
ausentes <- setdiff(
colunas_obrigatorias,
names(carteiras_20)
)
if (length(ausentes) > 0) {
stop(
paste(
"Colunas ausentes em carteiras_20:",
paste(ausentes, collapse = ", ")
)
)
}
if (anyNA(carteiras_20$Coluna_Original)) {
stop("Existem nomes de colunas de ativos ausentes.")
}
# Verificar se as colunas selecionadas existem na base
ativos_ausentes <- setdiff(
unique(carteiras_20$Coluna_Original),
names(df_base)
)
if (length(ativos_ausentes) > 0) {
stop(
paste(
"Ativos não encontrados em df_retornos:",
paste(ativos_ausentes, collapse = ", ")
)
)
}
# Garantir dez ações distintas por carteira
conferencia <- carteiras_20 %>%
group_by(Ano_Formacao) %>%
summarise(
Numero_Ativos = n_distinct(Coluna_Original),
.groups = "drop"
)
if (any(conferencia$Numero_Ativos != 10)) {
print(conferencia)
stop("Cada carteira precisa conter dez ações distintas.")
}
# ------------------------------------------------------------
# 4. FUNÇÃO DE AVALIAÇÃO DE CADA CARTEIRA
# ------------------------------------------------------------
avaliar_carteira <- function(
ano_formacao,
dados_carteira,
base_dados) {
ano_avaliacao <- ano_formacao + 1
# Ações selecionadas no ano de formação
ativos <- dados_carteira %>%
filter(Ano_Formacao == ano_formacao)
colunas_ativos <- ativos$Coluna_Original
# Pesos iguais: dez ações com 10% cada
pesos <- rep(
1 / length(colunas_ativos),
length(colunas_ativos)
)
# Dados do ano seguinte
dados_ano <- base_dados %>%
filter(Ano == ano_avaliacao) %>%
select(
Data,
CDI_Diario,
Retorno_IBOV,
all_of(colunas_ativos)
)
# Remover observações incompletas
dados_ano <- dados_ano %>%
filter(
is.finite(CDI_Diario),
is.finite(Retorno_IBOV),
if_all(
all_of(colunas_ativos),
~ is.finite(.x)
)
)
# Resultado ausente se não houver observações suficientes
if (nrow(dados_ano) < 2) {
return(
tibble(
Ano_Formacao = ano_formacao,
Ano_Avaliacao = ano_avaliacao,
Numero_Dias = nrow(dados_ano),
Retorno_Acumulado = NA_real_,
Retorno_Medio_Diario = NA_real_,
Desvio_Padrao_Diario = NA_real_,
Beta = NA_real_,
Sharpe_Diario = NA_real_,
Treynor_Diario = NA_real_,
Alfa_Jensen_Diario = NA_real_
)
)
}
# ----------------------------------------------------------
# 5. RETORNO DIÁRIO DA CARTEIRA
# ----------------------------------------------------------
matriz_retornos <- as.matrix(
dados_ano[, colunas_ativos, drop = FALSE]
)
storage.mode(matriz_retornos) <- "double"
retorno_carteira <- as.vector(
matriz_retornos %*% pesos
)
retorno_cdi <- dados_ano$CDI_Diario
retorno_mercado <- dados_ano$Retorno_IBOV
# ----------------------------------------------------------
# 6. RETORNO ACUMULADO, MÉDIA E DESVIO-PADRÃO
# ----------------------------------------------------------
retorno_acumulado <- prod(
1 + retorno_carteira
) - 1
retorno_medio <- mean(retorno_carteira)
desvio_padrao <- sd(retorno_carteira)
# ----------------------------------------------------------
# 7. BETA EM RELAÇÃO AO IBOVESPA
# ----------------------------------------------------------
variancia_mercado <- var(retorno_mercado)
beta <- if (
is.finite(variancia_mercado) &&
variancia_mercado > 0
) {
cov(retorno_carteira, retorno_mercado) /
variancia_mercado
} else {
NA_real_
}
# ----------------------------------------------------------
# 8. ÍNDICE DE SHARPE
# ----------------------------------------------------------
sharpe <- if (
is.finite(desvio_padrao) &&
desvio_padrao > 0
) {
mean(retorno_carteira - retorno_cdi) /
desvio_padrao
} else {
NA_real_
}
# ----------------------------------------------------------
# 9. ÍNDICE DE TREYNOR
# ----------------------------------------------------------
treynor <- if (
is.finite(beta) &&
abs(beta) > 1e-10
) {
mean(retorno_carteira - retorno_cdi) /
beta
} else {
NA_real_
}
# ----------------------------------------------------------
# 10. ALFA DE JENSEN
# ----------------------------------------------------------
alfa_jensen <- if (is.finite(beta)) {
excesso_carteira <- mean(
retorno_carteira - retorno_cdi
)
excesso_mercado <- mean(
retorno_mercado - retorno_cdi
)
excesso_carteira -
beta * excesso_mercado
} else {
NA_real_
}
# ----------------------------------------------------------
# 11. RESULTADOS DA CARTEIRA
# ----------------------------------------------------------
tibble(
Ano_Formacao = ano_formacao,
Ano_Avaliacao = ano_avaliacao,
Numero_Dias = length(retorno_carteira),
Retorno_Acumulado = retorno_acumulado,
Retorno_Medio_Diario = retorno_medio,
Desvio_Padrao_Diario = desvio_padrao,
Beta = beta,
Sharpe_Diario = sharpe,
Treynor_Diario = treynor,
Alfa_Jensen_Diario = alfa_jensen
)
}
# ------------------------------------------------------------
# 12. CALCULAR AS CARTEIRAS ANUAIS
# ------------------------------------------------------------
anos_formacao <- sort(
unique(carteiras_20$Ano_Formacao)
)
resultados_carteiras <- map_dfr(
anos_formacao,
~ avaliar_carteira(
ano_formacao = .x,
dados_carteira = carteiras_20,
base_dados = df_base
)
)
# ------------------------------------------------------------
# 13. FORMATAR OS RESULTADOS (Convertendo anos para caractere para evitar vírgulas e multiplicando por 100)
# ------------------------------------------------------------
resultados_formatados <- resultados_carteiras %>%
mutate(
Ano_Formacao = as.character(Ano_Formacao),
Ano_Avaliacao = as.character(Ano_Avaliacao),
Retorno_Acumulado = Retorno_Acumulado * 100,
Retorno_Medio_Diario = Retorno_Medio_Diario * 100,
Desvio_Padrao_Diario = Desvio_Padrao_Diario * 100,
Alfa_Jensen_Diario = Alfa_Jensen_Diario * 100
) %>%
rename(
`Ano de formacao` = Ano_Formacao,
`Ano de avaliacao` = Ano_Avaliacao,
`Dias observados` = Numero_Dias,
`Retorno acumulado (%)` = Retorno_Acumulado,
`Retorno medio diario (%)` = Retorno_Medio_Diario,
`Desvio-padrao diario (%)` = Desvio_Padrao_Diario,
`Beta da carteira` = Beta,
`Indice de Sharpe diario` = Sharpe_Diario,
`Indice de Treynor diario` = Treynor_Diario,
`Alfa de Jensen diario (%)` = Alfa_Jensen_Diario
)
# ------------------------------------------------------------
# 14. CORRIGIR UTF-8 ANTES DO DT
# ------------------------------------------------------------
resultados_dt <- limpar_base_dt(
resultados_formatados
)
A análise detalhada dos resultados out-of-sample consolidados na Tabela 5 revela o comportamento dinâmico e a eficácia das carteiras anuais formadas com base no Índice de Treynor ao longo de duas décadas, abrangendo o período de 2005 a 2025. A estratégia demonstra uma notável capacidade de capturar ciclos de forte expansão no mercado acionário brasileiro, ao mesmo tempo em que evidencia mecanismos de atenuação de risco sistêmico em janelas de estresse macroeconômico. O desempenho global destaca a relevância de selecionar ativos com base no prêmio de risco excedente em relação ao CDI, ponderado pela sensibilidade do beta de mercado, resultando em portfólios que alternam rentabilidades expressivas com resiliência defensiva estrutural.
Nos primeiros anos da série histórica, os portfólios registraram retornos acumulados extraordinários, impulsionados pelo ambiente favorável de commodities e expansão econômica da época. Destacam-se as carteiras formadas com avaliação em 2005-2006, que entregaram um retorno acumulado impressionante de 208,76%, acompanhado por um alfa de Jensen diário positivo e beta controlado de 0,82. O ciclo subsequente (2006-2007) manteve o ímpeto com um ganho acumulado de 138,68% e um expressivo alfa diário de 0,31%. No entanto, esse cenário de alta foi abruptamente testado pela crise financeira global de 2007-2008, período em que a carteira sofreu uma retração acumulada de -48,80%. Apesar da forte queda decorrente do choque sistêmico, o portfólio manteve um beta defensivo de 0,77 e, na janela imediata de recuperação de 2008-2009, protagonizou um rebote espetacular de 100,73% de retorno acumulado, sustentado pelo menor beta da série (0,50) e pelo maior índice de Sharpe diário de todo o período analisado (0,23), evidenciando a rápida adaptação do modelo após eventos extremos.
Ao longo da década seguinte, o desempenho passou a refletir o ambiente de maior volatilidade e menor crescimento do mercado brasileiro, intercalando anos de moderação e contrações brandas com janelas de recuperação pontual. Períodos como 2011-2012 e 2012-2013 ilustram perfeitamente o perfil defensivo da estratégia, onde os betas das carteiras recuaram para patamares marcadamente baixos de 0,27 e 0,40, respetivamente, amortecendo os impactos das oscilações do Ibovespa. Essa característica defensiva pavimentou o caminho para o ciclo de alta subsequente iniciado em 2016-2017, quando a estratégia voltou a entregar um ganho acumulado de 57,60%, seguido por performances igualmente sólidas em 2018-2019 (47,35%) e 2019-2020 (43,65%), sempre amparadas por alfas diários consistentemente positivos e betas moderados entre 0,63 e 0,70.
Nas janelas mais recentes que encerram a série, compreendendo os ciclos pós-pandemia e o aperto monetário prolongado até 2024-2025, as carteiras refletiram as pressões macroeconômicas globais e locais, registrando perdas moderadas em períodos desafiadores — como o recuo de -7,71% em 2021-2022 e a retração de -19,22% em 2023-2024 —, seguidas por recuperações consistentes, a exemplo dos ganhos de 8,26% e 7,81% nos anos seguintes. Em suma, os dados da Tabela 5 comprovam que o Índice de Treynor atua como um critério robusto de alocação dinâmica, filtrando ativos capazes de remunerar adequadamente o investidor pelo risco sistêmico assumido e assegurando a geração de valor excedente no longo prazo.
tabela_carteiras <- DT::datatable(
resultados_dt,
rownames = FALSE,
filter = "top",
escape = TRUE,
options = list(
pageLength = 20,
scrollX = TRUE,
autoWidth = TRUE
),
caption = "Tabela 5: Desempenho das carteiras anuais baseadas no Índice de Treynor"
) %>%
DT::formatRound(
columns = c(
"Retorno acumulado (%)",
"Retorno medio diario (%)",
"Desvio-padrao diario (%)",
"Beta da carteira",
"Indice de Sharpe diario",
"Indice de Treynor diario",
"Alfa de Jensen diario (%)"
),
digits = 4
)
tabela_carteiras
# ============================================================
# ANALISE DE CARTEIRAS DE INVESTIMENTO - INDICE DE TREYNOR
# Fronteira eficiente | CML | SML/CAPM | Pesos | Comparacao
# Anos de avaliacao: 2005, 2010, 2015, 2020, 2025
# Painel HTML interativo com guias por ano, grafico e tabela
# ============================================================
# ============================================================
# 0. PACOTES
# ============================================================
pacotes <- c(
"dplyr",
"purrr",
"tibble",
"plotly",
"quadprog",
"htmlwidgets",
"htmltools",
"Matrix",
"DT"
)
faltantes <- pacotes[
!vapply(pacotes, requireNamespace, logical(1), quietly = TRUE)
]
if (length(faltantes) > 0) {
install.packages(faltantes)
}
invisible(lapply(pacotes, library, character.only = TRUE))
# ============================================================
# 1. FUNCOES DE TEXTO E DATA
# ============================================================
limpar_nome_ativo <- function(nome) {
partes <- unlist(strsplit(nome, "[\r\n]+"))
return(trimws(tail(partes, 1)))
}
corrigir_texto <- function(x) {
x <- as.character(x)
resultado <- suppressWarnings(
iconv(x, from = "", to = "UTF-8", sub = "")
)
falhas <- is.na(resultado) & !is.na(x)
if (any(falhas)) {
resultado[falhas] <- suppressWarnings(
iconv(x[falhas], from = "UTF-8", to = "UTF-8", sub = "")
)
}
resultado[is.na(resultado)] <- ""
resultado
}
ticker_limpo <- function(x) {
x <- corrigir_texto(x)
x <- gsub("[\r\n\t]+", " ", x)
x <- trimws(x)
partes <- strsplit(x, "[[:space:]]+")
resultado <- vapply(partes, function(p) {
if (length(p) == 0) return("")
tail(p, 1)
}, character(1))
resultado <- iconv(
resultado,
from = "UTF-8",
to = "ASCII//TRANSLIT",
sub = ""
)
resultado <- gsub("[^A-Za-z0-9._-]", "", resultado)
resultado
}
chave_nome <- function(x) {
x <- corrigir_texto(x)
x <- gsub("[\r\n\t]+", " ", x)
x <- gsub("[[:space:]]+", " ", x)
trimws(x)
}
converter_data <- function(x) {
if (inherits(x, "Date")) {
return(x)
}
if (inherits(x, "POSIXt")) {
return(as.Date(x))
}
if (is.numeric(x)) {
return(as.Date(x, origin = "1899-12-30"))
}
x <- corrigir_texto(x)
x <- trimws(x)
resultado <- suppressWarnings(as.Date(x))
formatos <- c(
"%d/%m/%Y",
"%d-%m-%Y",
"%Y/%m/%d",
"%d/%m/%y",
"%Y%m%d"
)
for (formato in formatos) {
pendentes <- is.na(resultado) & nzchar(x)
if (!any(pendentes)) break
resultado[pendentes] <- suppressWarnings(
as.Date(x[pendentes], format = formato)
)
}
resultado
}
corrigir_df <- function(df) {
df <- as.data.frame(df)
names(df) <- corrigir_texto(names(df))
for (j in seq_along(df)) {
if (is.character(df[[j]])) {
df[[j]] <- corrigir_texto(df[[j]])
}
}
df
}
# ============================================================
# 2. PREPARAR AS BASES E CALCULAR RETORNO DO IBOV
# ============================================================
if (!exists("df_retornos")) {
stop("O objeto df_retornos nao foi encontrado no ambiente.")
}
nomes_originais <- corrigir_texto(names(df_retornos))
base <- as.data.frame(df_retornos)
names(base) <- make.unique(nomes_originais)
if (!all(c("Data", "CDI_Diario", "IBOV_Fechamento") %in% names(base))) {
stop("df_retornos precisa conter Data, CDI_Diario e IBOV_Fechamento.")
}
base$Data <- converter_data(base$Data)
base <- base %>%
filter(!is.na(Data)) %>%
arrange(Data)
base$Ano <- as.integer(format(base$Data, "%Y"))
base$CDI_Diario <- suppressWarnings(
as.numeric(base$CDI_Diario)
)
base$IBOV_Fechamento <- suppressWarnings(
as.numeric(base$IBOV_Fechamento)
)
base$Retorno_IBOV <- base$IBOV_Fechamento /
dplyr::lag(base$IBOV_Fechamento) - 1
coluna_cdi <- "CDI_Diario"
colunas_acoes <- setdiff(
names(base),
c("Data", "Ano", coluna_cdi, "IBOV_Fechamento", "Retorno_IBOV")
)
colunas_acoes <- colunas_acoes[
vapply(base[colunas_acoes], is.numeric, logical(1))
]
if (length(colunas_acoes) < 10) {
stop("Foram identificadas menos de dez colunas numericas de acoes.")
}
# Anos de formação correspondentes aos anos de avaliação (2005, 2010, 2015, 2020, 2025)
anos_formacao <- c(2005, 2010, 2015, 2020, 2024)
anos_rotulo <- c(2005, 2010, 2015, 2020, 2025)
# ============================================================
# 3. MOTOR DE SELECAO PELO INDICE DE TREYNOR (JANELA DE 5 ANOS)
# ============================================================
calcular_treynor <- function(base, ano_formacao) {
ano_inicio <- ano_formacao - 4
data_inicio <- as.Date(paste0(ano_inicio, "-01-01"))
data_fim <- as.Date(paste0(ano_formacao, "-12-31"))
janela <- base %>%
filter(Data >= data_inicio, Data <= data_fim)
if (nrow(janela) == 0) return(tibble())
retorno_cdi <- janela[[coluna_cdi]]
retorno_ibov <- janela[["Retorno_IBOV"]]
resultados <- purrr::map_dfr(colunas_acoes, function(acao) {
retorno_acao <- janela[[acao]]
dados <- tibble(
Retorno_Ativo = retorno_acao,
Retorno_CDI = retorno_cdi,
Retorno_IBOV = retorno_ibov
) %>%
filter(is.finite(Retorno_Ativo), is.finite(Retorno_CDI), is.finite(Retorno_IBOV))
n <- nrow(dados)
if (n < 30) {
return(tibble(
Coluna_Original = acao, Ativo = limpar_nome_ativo(acao), Observacoes = n,
Retorno_Medio = NA_real_, CDI_Medio = NA_real_, Beta = NA_real_, Treynor = NA_real_
))
}
media_acao <- mean(dados$Retorno_Ativo)
media_cdi <- mean(dados$Retorno_CDI)
modelo <- tryCatch(lm(Retorno_Ativo ~ Retorno_IBOV, data = dados), error = function(e) NULL)
beta_ativo <- if (!is.null(modelo)) coef(modelo)[2] else NA_real_
valor_treynor <- if (is.finite(beta_ativo) && beta_ativo > 0) {
(media_acao - media_cdi) / beta_ativo
} else {
NA_real_
}
tibble(
Coluna_Original = acao, Ativo = limpar_nome_ativo(acao), Observacoes = n,
Retorno_Medio = media_acao, CDI_Medio = media_cdi, Beta = beta_ativo, Treynor = valor_treynor
)
})
resultados %>%
mutate(
Ano_Formacao = as.integer(ano_formacao),
Ano_Inicio_Janela = as.integer(ano_inicio),
Ano_Fim_Janela = as.integer(ano_formacao)
) %>%
group_by(Ano_Formacao) %>%
mutate(Ranking = if_else(is.na(Treynor), NA_integer_, min_rank(desc(Treynor)))) %>%
ungroup() %>%
select(Ano_Formacao, Ano_Inicio_Janela, Ano_Fim_Janela, Ranking, Ativo, Coluna_Original, Observacoes, Retorno_Medio, CDI_Medio, Beta, Treynor) %>%
arrange(Ano_Formacao, Ranking, Ativo)
}
rankings <- purrr::map_dfr(anos_formacao, function(ano) calcular_treynor(base = base, ano_formacao = ano))
if (nrow(rankings) == 0) stop("Nenhum ranking calculado.")
carteiras_20 <- rankings %>%
filter(!is.na(Treynor)) %>%
group_by(Ano_Formacao) %>%
arrange(desc(Treynor), Ativo, .by_group = TRUE) %>%
slice_head(n = 10) %>%
mutate(Peso_Inicial = 0.10) %>%
ungroup() %>%
arrange(Ano_Formacao, Ranking)
# ============================================================
# 4. OTIMIZACAO DE MINIMA VARIANCIA
# ============================================================
min_variancia <- function(Sigma, mu, alvo = NULL) {
n <- length(mu)
D <- 2 * Sigma + diag(1e-8, n)
d <- rep(0, n)
if (is.null(alvo)) {
A <- cbind(rep(1, n), diag(n))
b <- c(1, rep(0, n))
meq <- 1
} else {
A <- cbind(rep(1, n), mu, diag(n))
b <- c(1, alvo, rep(0, n))
meq <- 2
}
sol <- quadprog::solve.QP(
Dmat = D,
dvec = d,
Amat = A,
bvec = b,
meq = meq
)
w <- as.numeric(sol$solution)
w[abs(w) < 1e-8] <- 0
w / sum(w)
}
# ============================================================
# 5. ANALISAR UMA CARTEIRA
# ============================================================
analisar <- function(ano_formacao, ano_rotulo) {
selecao <- carteiras_20 %>%
filter(Ano_Formacao == ano_formacao)
ativos_informados <- unique(
as.character(selecao$Coluna_Original)
)
if (length(ativos_informados) != 10) {
stop(
"A carteira formada em ", ano_formacao,
" possui ", length(ativos_informados),
" ativos distintos, em vez de dez."
)
}
indices <- match(
chave_nome(ativos_informados),
chave_nome(nomes_originais)
)
if (anyNA(indices)) {
stop(
"Colunas nao encontradas na base para a carteira de ",
ano_formacao, ": ",
paste(ativos_informados[is.na(indices)], collapse = ", ")
)
}
ativos <- names(base)[indices]
dados <- base %>%
filter(
Ano >= ano_formacao - 4,
Ano <= ano_formacao
) %>%
select(
Data,
CDI_Diario,
Retorno_IBOV,
all_of(ativos)
)
for (a in ativos) {
dados[[a]] <- suppressWarnings(as.numeric(dados[[a]]))
}
X <- as.matrix(dados[, ativos, drop = FALSE])
storage.mode(X) <- "double"
mu <- vapply(seq_along(ativos), function(j) {
mean(X[is.finite(X[, j]), j])
}, numeric(1))
Sigma_original <- cov(
X,
use = "pairwise.complete.obs"
)
Sigma_original[!is.finite(Sigma_original)] <- 0
Sigma <- as.matrix(
Matrix::nearPD(
Sigma_original,
corr = FALSE,
keepDiag = TRUE
)$mat
)
cdi <- dados$CDI_Diario
cdi <- cdi[is.finite(cdi)]
mercado <- dados$Retorno_IBOV
mercado <- mercado[is.finite(mercado)]
rf <- mean(cdi)
media_mercado <- mean(mercado)
n <- length(ativos)
w_igual <- rep(1 / n, n)
ret_igual <- sum(w_igual * mu)
risco_igual <- sqrt(
drop(t(w_igual) %*% Sigma %*% w_igual)
)
sharpe_igual <- (ret_igual - rf) / risco_igual
w_gmv <- min_variancia(Sigma, mu)
ret_gmv <- sum(w_gmv * mu)
alvos <- seq(
ret_gmv,
max(mu),
length.out = 120
)
fronteira_lista <- lapply(alvos, function(alvo) {
w <- tryCatch(
min_variancia(Sigma, mu, alvo),
error = function(e) NULL
)
if (is.null(w)) return(NULL)
ret <- sum(w * mu)
risco <- sqrt(
drop(t(w) %*% Sigma %*% w)
)
if (!is.finite(risco) || risco <= 0) {
return(NULL)
}
data.frame(
Retorno = ret,
Risco = risco,
Sharpe = (ret - rf) / risco,
Pesos = I(list(w))
)
})
fronteira <- bind_rows(fronteira_lista)
indice_tangencia <- which.max(fronteira$Sharpe)
w_tan <- fronteira$Pesos[[indice_tangencia]]
ret_tan <- sum(w_tan * mu)
risco_tan <- sqrt(
drop(t(w_tan) %*% Sigma %*% w_tan)
)
sharpe_tan <- (ret_tan - rf) / risco_tan
dias <- 252
fronteira_plot <- fronteira %>%
mutate(
Retorno_Anual = Retorno * dias,
Risco_Anual = Risco * sqrt(dias)
) %>%
arrange(Risco_Anual)
rf_a <- rf * dias
ret_mercado_a <- media_mercado * dias
ret_tan_a <- ret_tan * dias
risco_tan_a <- risco_tan * sqrt(dias)
ret_igual_a <- ret_igual * dias
risco_igual_a <- risco_igual * sqrt(dias)
sharpe_tan_a <- (ret_tan_a - rf_a) / risco_tan_a
sharpe_igual_a <- (ret_igual_a - rf_a) / risco_igual_a
cml <- data.frame(
Risco = seq(
0,
max(fronteira_plot$Risco_Anual, risco_tan_a) * 1.05,
length.out = 100
)
)
cml$Retorno <- rf_a + sharpe_tan_a * cml$Risco
beta <- vapply(seq_along(ativos), function(j) {
ok <- is.finite(X[, j]) &
is.finite(dados$Retorno_IBOV)
if (sum(ok) < 20) return(NA_real_)
var_m <- var(dados$Retorno_IBOV[ok])
if (!is.finite(var_m) || var_m <= 0) {
return(NA_real_)
}
cov(
X[ok, j],
dados$Retorno_IBOV[ok]
) / var_m
}, numeric(1))
beta_igual <- if (all(is.finite(beta))) {
sum(w_igual * beta)
} else {
NA_real_
}
beta_tan <- if (all(is.finite(beta))) {
sum(w_tan * beta)
} else {
NA_real_
}
nomes_ativos <- ticker_limpo(ativos_informados)
pontos_ativos <- data.frame(
Ativo = nomes_ativos,
Risco = apply(X, 2, sd, na.rm = TRUE) * sqrt(dias),
Retorno = mu * dias,
Beta = beta,
stringsAsFactors = FALSE
)
# ==========================================================
# 6. GRAFICO DA FRONTEIRA EFICIENTE E CML
# ==========================================================
grafico_fronteira <- plotly::plot_ly() %>%
add_lines(
data = fronteira_plot,
x = ~Risco_Anual,
y = ~Retorno_Anual,
name = "Fronteira eficiente",
line = list(width = 3),
hovertemplate = paste(
"Risco: %{x:.2%}",
"<br>Retorno: %{y:.2%}<extra></extra>"
)
) %>%
add_lines(
data = cml,
x = ~Risco,
y = ~Retorno,
name = "CML",
line = list(dash = "dash", width = 2),
hovertemplate = paste(
"Risco: %{x:.2%}",
"<br>Retorno: %{y:.2%}<extra></extra>"
)
) %>%
add_markers(
data = pontos_ativos,
x = ~Risco,
y = ~Retorno,
text = ~Ativo,
name = "Acoes individuais",
marker = list(size = 9),
hovertemplate = paste(
"<b>%{text}</b>",
"<br>Risco: %{x:.2%}",
"<br>Retorno: %{y:.2%}<extra></extra>"
)
) %>%
add_markers(
x = risco_igual_a,
y = ret_igual_a,
text = "Carteira original",
name = "Carteira original",
marker = list(symbol = "diamond", size = 12),
hovertemplate = paste(
"%{text}",
"<br>Risco: %{x:.2%}",
"<br>Retorno: %{y:.2%}<extra></extra>"
)
) %>%
add_markers(
x = risco_tan_a,
y = ret_tan_a,
text = "Carteira de tangencia",
name = "Tangencia",
marker = list(symbol = "star", size = 14),
hovertemplate = paste(
"%{text}",
"<br>Risco: %{x:.2%}",
"<br>Retorno: %{y:.2%}<extra></extra>"
)
) %>%
add_markers(
x = 0,
y = rf_a,
text = "CDI",
name = "CDI",
marker = list(symbol = "square", size = 10),
hovertemplate = paste(
"%{text}",
"<br>Retorno: %{y:.2%}<extra></extra>"
)
) %>%
layout(
title = list(
text = paste0(
"Carteira ", ano_rotulo,
" - Fronteira eficiente e CML"
),
x = 0.03
),
autosize = TRUE,
height = 540,
xaxis = list(
title = "Risco anualizado",
tickformat = ".1%",
gridcolor = "#E8ECF1",
zeroline = FALSE
),
yaxis = list(
title = "Retorno anualizado",
tickformat = ".1%",
gridcolor = "#E8ECF1",
zeroline = FALSE
),
legend = list(
orientation = "h",
x = 0,
y = -0.2
),
hovermode = "closest",
margin = list(t = 75, b = 100, l = 70, r = 25),
paper_bgcolor = "white",
plot_bgcolor = "white"
) %>%
config(
responsive = TRUE,
displaylogo = FALSE,
tools = FALSE,
toImageButtonOptions = list(
format = "png",
filename = paste0("fronteira_CML_", ano_rotulo),
scale = 2
)
)
# ==========================================================
# 7. GRAFICO SML / CAPM
# ==========================================================
betas_validos <- beta[is.finite(beta)]
beta_min <- min(c(betas_validos, 0), na.rm = TRUE)
beta_max <- max(c(betas_validos, 1), na.rm = TRUE)
linha_sml <- data.frame(
Beta = seq(
beta_min - 0.1,
beta_max + 0.1,
length.out = 100
)
) %>%
mutate(
Retorno_CAPM = rf_a +
Beta * (ret_mercado_a - rf_a)
)
pontos_sml <- data.frame(
Ativo = c(
nomes_ativos,
"Carteira original",
"Carteira de tangencia",
"CDI",
"Ibovespa"
),
Beta = c(
beta,
beta_igual,
beta_tan,
0,
1
),
Retorno = c(
mu * dias,
ret_igual_a,
ret_tan_a,
rf_a,
ret_mercado_a
),
stringsAsFactors = FALSE
) %>%
filter(is.finite(Beta), is.finite(Retorno))
grafico_sml <- plotly::plot_ly() %>%
add_lines(
data = linha_sml,
x = ~Beta,
y = ~Retorno_CAPM,
name = "SML - CAPM",
line = list(width = 3),
hovertemplate = paste(
"Beta: %{x:.3f}",
"<br>Retorno CAPM: %{y:.2%}<extra></extra>"
)
) %>%
add_markers(
data = pontos_sml,
x = ~Beta,
y = ~Retorno,
text = ~Ativo,
name = "Acoes e carteiras",
marker = list(size = 9),
hovertemplate = paste(
"<b>%{text}</b>",
"<br>Beta: %{x:.3f}",
"<br>Retorno: %{y:.2%}<extra></extra>"
)
) %>%
layout(
title = list(
text = paste0("Carteira ", ano_rotulo, " - SML / CAPM"),
x = 0.03
),
autosize = TRUE,
height = 540,
xaxis = list(
title = "Beta em relacao ao Ibovespa",
gridcolor = "#E8ECF1",
zeroline = FALSE
),
yaxis = list(
title = "Retorno anualizado",
tickformat = ".1%",
gridcolor = "#E8ECF1",
zeroline = FALSE
),
legend = list(
orientation = "h",
x = 0,
y = -0.2
),
hovermode = "closest",
margin = list(t = 75, b = 100, l = 70, r = 25),
paper_bgcolor = "white",
plot_bgcolor = "white"
) %>%
config(
responsive = TRUE,
displaylogo = FALSE,
tools = FALSE,
toImageButtonOptions = list(
format = "png",
filename = paste0("SML_", ano_rotulo),
scale = 2
)
)
# ==========================================================
# 8. TABELAS DE RESULTADOS E PESOS (Anos como caractere p/ evitar vírgulas)
# ==========================================================
composicao <- data.frame(
Ano_Avaliacao = as.character(ano_rotulo),
Ano_Formacao = as.character(ano_formacao),
Ativo = nomes_ativos,
Peso_Original = w_igual,
Peso_Tangencia = w_tan,
Beta_Ibovespa = beta,
Retorno_Medio_Anualizado = mu * dias,
stringsAsFactors = FALSE
)
resumo <- data.frame(
Ano_Avaliacao = as.character(ano_rotulo),
Ano_Formacao = as.character(ano_formacao),
Inicio_Janela = as.character(ano_formacao - 4),
Fim_Janela = as.character(ano_formacao),
Observacoes_Base = nrow(dados),
CDI_Anualizado = rf_a,
Retorno_Original = ret_igual_a,
Risco_Original = risco_igual_a,
Sharpe_Original_Anualizado = sharpe_igual_a,
Retorno_Tangencia = ret_tan_a,
Risco_Tangencia = risco_tan_a,
Sharpe_Tangencia_Anualizado = sharpe_tan_a,
Beta_Tangencia = beta_tan,
stringsAsFactors = FALSE
)
list(
resumo = resumo,
composicao = composicao,
fronteira = fronteira_plot,
cml = cml,
pontos_ativos = pontos_ativos,
pontos_sml = pontos_sml,
grafico_fronteira = grafico_fronteira,
grafico_sml = grafico_sml
)
}
# ============================================================
# 9. EXECUTAR AS CARTEIRAS (2005, 2010, 2015, 2020, 2025)
# ============================================================
analises <- purrr::map2(
anos_formacao,
anos_rotulo,
analisar
)
names(analises) <- as.character(anos_rotulo)
resumo_carteiras <- purrr::map_dfr(
analises,
"resumo"
)
composicao_carteiras <- purrr::map_dfr(
analises,
"composicao"
)
fronteiras_carteiras <- purrr::imap_dfr(
analises,
function(x, ano) {
x$fronteira %>%
mutate(
Ano_Avaliacao = as.integer(ano),
.before = 1
)
}
)
# ============================================================
# 10. COMPARAR A COMPOSICAO DAS CARTEIRAS
# ============================================================
comparacao_carteiras <- purrr::map_dfr(
seq_along(anos_rotulo),
function(i) {
ano <- anos_rotulo[i]
atuais <- composicao_carteiras %>%
filter(Ano_Avaliacao == as.character(ano)) %>%
pull(Ativo)
if (i == 1) {
return(data.frame(
Ano_Avaliacao = as.character(ano),
Ativos_Mantidos = NA_integer_,
Ativos_Substituidos = NA_integer_,
stringsAsFactors = FALSE
))
}
anteriores <- composicao_carteiras %>%
filter(Ano_Avaliacao == as.character(anos_rotulo[i - 1])) %>%
pull(Ativo)
mantidos <- length(intersect(atuais, anteriores))
data.frame(
Ano_Avaliacao = as.character(ano),
Ativos_Mantidos = mantidos,
Ativos_Substituidos = 10 - mantidos,
stringsAsFactors = FALSE
)
}
)
resumo_carteiras <- resumo_carteiras %>%
left_join(
comparacao_carteiras,
by = "Ano_Avaliacao"
)
# ============================================================
# 11. FORMATAR TABELAS INTERATIVAS
# ============================================================
criar_tabela <- function(dados, rotulos, titulo) {
dados <- corrigir_df(dados)
if (length(rotulos) != ncol(dados)) {
stop(
"O numero de rotulos da tabela nao corresponde ao numero de colunas: ",
titulo
)
}
rotulos <- corrigir_texto(rotulos)
colunas_numericas <- which(
vapply(dados, is.numeric, logical(1))
)
tabela <- DT::datatable(
dados,
rownames = FALSE,
colnames = rotulos,
filter = "top",
caption = htmltools::tags$caption(
style = paste(
"caption-side: top;",
"text-align: left;",
"font-weight: 600;",
"padding: 12px 4px;",
"font-size: 16px;"
),
corrigir_texto(titulo)
),
extensions = "Buttons",
options = list(
pageLength = 10,
lengthMenu = c(5, 10, 20, 50),
scrollX = TRUE,
autoWidth = TRUE,
dom = "Bfrtip",
buttons = c("copy", "csv", "excel"),
language = list(
search = "Pesquisar:",
lengthMenu = "Mostrar _MENU_ registros",
info = "Exibindo _START_ a _END_ de _TOTAL_ registros",
infoEmpty = "Nenhum registro disponivel",
zeroRecords = "Nenhum resultado encontrado",
emptyTable = "Nao ha dados disponiveis",
paginate = list(
previous = "Anterior",
`next` = "Proxima",
first = "Primeira",
last = "Ultima"
)
)
)
)
if (length(colunas_numericas) > 0) {
tabela <- DT::formatRound(
tabela,
columns = colunas_numericas,
digits = 4
)
}
tabela
}
tabela_resumo <- criar_tabela(
resumo_carteiras,
c(
"Ano de avaliacao",
"Ano de formacao",
"Inicio da janela",
"Fim da janela",
"Observacoes",
"CDI anualizado",
"Retorno original",
"Risco original",
"Sharpe original anualizado",
"Retorno de tangencia",
"Risco de tangencia",
"Sharpe de tangencia anualizado",
"Beta de tangencia",
"Ativos mantidos",
"Ativos substituidos"
),
"Resumo comparativo das carteiras (Treynor)"
)
tabela_composicao <- criar_tabela(
composicao_carteiras,
c(
"Ano de avaliacao",
"Ano de formacao",
"Ativo",
"Peso original",
"Peso de tangencia",
"Beta do Ibovespa",
"Retorno medio anualizado"
),
"Composicao e pesos das carteiras (Treynor)"
)
tabela_comparacao <- criar_tabela(
comparacao_carteiras,
c(
"Ano de avaliacao",
"Ativos mantidos",
"Ativos substituidos"
),
"Mudancas na composicao entre carteiras (Treynor)"
)
# ============================================================
# 12. GUIAS DE NAVEGACAO DOS GRAFICOS
# ============================================================
criar_painel_ano <- function(ano) {
id_fronteira <- paste0("grafico_fronteira_", ano)
id_sml <- paste0("grafico_sml_", ano)
htmltools::tagList(
htmltools::tags$div(
class = "grafico-tabs",
htmltools::tags$button(
class = "tab-grafico active",
`data-target` = id_fronteira,
"Fronteira eficiente + CML"
),
htmltools::tags$button(
class = "tab-grafico",
`data-target` = id_sml,
"SML / CAPM"
)
),
htmltools::tags$div(
id = id_fronteira,
class = "grafico-pane",
analises[[as.character(ano)]]$grafico_fronteira
),
htmltools::tags$div(
id = id_sml,
class = "grafico-pane",
style = "display:none;",
analises[[as.character(ano)]]$grafico_sml
)
)
}
botoes_anos <- htmltools::tagList(
lapply(seq_along(anos_rotulo), function(i) {
ano <- anos_rotulo[i]
htmltools::tags$button(
class = if (i == 1) "tab-ano active" else "tab-ano",
`data-target` = paste0("painel_ano_", ano),
paste("Carteira", ano)
)
})
)
paineis_anos <- htmltools::tagList(
lapply(seq_along(anos_rotulo), function(i) {
ano <- anos_rotulo[i]
formacao <- anos_formacao[i]
htmltools::tags$div(
id = paste0("painel_ano_", ano),
class = "ano-pane",
style = if (i == 1) "" else "display:none;",
htmltools::tags$h3(
paste("Carteira de avaliacao", ano)
),
htmltools::tags$p(
class = "subtitulo",
paste0(
"Janela de formacao: ",
formacao - 4, " a ", formacao,
" | Selecionadas pelo Indice de Treynor"
)
),
criar_painel_ano(ano)
)
})
)
# ============================================================
# 13. GUIAS DAS TABELAS
# ============================================================
botoes_tabelas <- htmltools::tagList(
htmltools::tags$button(
class = "tab-tabela active",
`data-target` = "painel_tabela_resumo",
"Resumo das carteiras"
),
htmltools::tags$button(
class = "tab-tabela",
`data-target` = "painel_tabela_composicao",
"Composicao e pesos"
),
htmltools::tags$button(
class = "tab-tabela",
`data-target` = "painel_tabela_comparacao",
"Comparacao entre carteiras"
)
)
paineis_tabelas <- htmltools::tagList(
htmltools::tags$div(
id = "painel_tabela_resumo",
class = "tabela-pane",
tabela_resumo
),
htmltools::tags$div(
id = "painel_tabela_composicao",
class = "tabela-pane",
style = "display:none;",
tabela_composicao
),
htmltools::tags$div(
id = "painel_tabela_comparacao",
class = "tabela-pane",
style = "display:none;",
tabela_comparacao
)
)
# ============================================================
# 14. ESTILO VISUAL DO PAINEL
# ============================================================
estilo_css <- "
body {
margin: 0;
background: #f4f6fa;
color: #253247;
font-family: Arial, Helvetica, sans-serif;
}
.painel {
max-width: 1450px;
margin: 0 auto;
padding: 26px;
}
.cabecalho {
background: linear-gradient(120deg, #172b4d, #245b83);
color: white;
padding: 30px 32px;
border-radius: 15px;
margin-bottom: 24px;
}
.cabecalho h1 {
margin: 0 0 10px 0;
font-size: 27px;
line-height: 1.3;
}
.cabecalho p {
margin: 0;
color: #e5edf6;
line-height: 1.6;
}
.bloco {
background: white;
padding: 22px;
border-radius: 13px;
margin-bottom: 24px;
box-shadow: 0 3px 14px rgba(31, 48, 72, 0.07);
}
h2 {
color: #172b4d;
font-size: 21px;
margin-top: 0;
}
h3 {
color: #245b83;
font-size: 19px;
}
.subtitulo {
color: #627187;
margin-top: -5px;
margin-bottom: 20px;
line-height: 1.6;
}
.nav-tabs-custom, .grafico-tabs, .tabela-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.tab-ano, .tab-grafico, .tab-tabela {
border: 1px solid #d4dce7;
background: #f7f9fc;
color: #31435a;
padding: 11px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: background 0.2s ease;
}
.tab-ano:hover, .tab-grafico:hover, .tab-tabela:hover {
background: #e8eff8;
}
.tab-ano.active, .tab-grafico.active, .tab-tabela.active {
color: white;
background: #245b83;
border-color: #245b83;
}
.grafico-pane {
width: 100%;
min-height: 540px;
}
.tabela-pane {
width: 100%;
overflow-x: auto;
}
.dataTables_wrapper {
width: 100%;
overflow-x: auto;
}
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #ccd5e1;
border-radius: 6px;
padding: 6px;
}
.dataTables_wrapper table.dataTable thead th {
background: #edf2f8;
color: #24364d;
border-bottom: 2px solid #cbd6e4;
}
.dataTables_wrapper table.dataTable tbody tr:hover {
background: #f1f6fc;
}
.rodape {
color: #65758b;
font-size: 12px;
text-align: center;
padding: 16px;
line-height: 1.6;
}
@media (max-width: 700px) {
.painel {
padding: 12px;
}
.cabecalho {
padding: 22px;
}
.cabecalho h1 {
font-size: 22px;
}
.bloco {
padding: 14px;
}
.tab-ano, .tab-grafico, .tab-tabela {
padding: 9px 11px;
font-size: 12px;
}
}
"
# ============================================================
# 15. SCRIPT DE NAVEGACAO
# ============================================================
script_guias <- "
document.addEventListener('DOMContentLoaded', function() {
function redimensionarGraficos(painel) {
if (!painel) return;
window.setTimeout(function() {
painel.querySelectorAll('.plotly.html-widget').forEach(
function(grafico) {
if (window.Plotly) {
window.Plotly.Plots.resize(grafico);
}
}
);
}, 150);
}
// Navegacao dos anos
document.querySelectorAll('.tab-ano').forEach(function(botao) {
botao.addEventListener('click', function() {
var botoes = Array.from(document.querySelectorAll('.tab-ano'));
var paineis = Array.from(document.querySelectorAll('.ano-pane'));
botoes.forEach(function(b) {
b.classList.toggle('active', b === botao);
});
paineis.forEach(function(painel) {
var ativo = painel.id === botao.getAttribute('data-target');
painel.style.display = ativo ? '' : 'none';
if (ativo) redimensionarGraficos(painel);
});
});
});
// Navegacao dos graficos internos de cada ano
document.querySelectorAll('.tab-grafico').forEach(function(botao) {
botao.addEventListener('click', function() {
var grupo = botao.closest('.ano-pane');
var botoes = Array.from(grupo.querySelectorAll('.tab-grafico'));
var paineis = Array.from(grupo.querySelectorAll('.grafico-pane'));
botoes.forEach(function(b) {
b.classList.toggle('active', b === botao);
});
var alvo = botao.getAttribute('data-target');
paineis.forEach(function(painel) {
var ativo = painel.id === alvo;
painel.style.display = ativo ? '' : 'none';
if (ativo) redimensionarGraficos(painel);
});
});
});
// Navegacao das abas de tabelas
document.querySelectorAll('.tab-tabela').forEach(function(botao) {
botao.addEventListener('click', function() {
var botoes = Array.from(document.querySelectorAll('.tab-tabela'));
var paineis = Array.from(document.querySelectorAll('.tabela-pane'));
botoes.forEach(function(b) {
b.classList.toggle('active', b === botao);
});
var alvo = botao.getAttribute('data-target');
paineis.forEach(function(painel) {
painel.style.display = painel.id === alvo ? '' : 'none';
});
});
});
});
"
# ============================================================
# 16. MONTAR O PAINEL HTML COMPLETO
# ============================================================
painel_carteiras <- htmltools::tagList(
htmltools::tags$head(
htmltools::tags$meta(charset = "UTF-8"),
htmltools::tags$meta(
name = "viewport",
content = "width=device-width, initial-scale=1"
),
htmltools::tags$title(
"Analise de Carteiras - Indice de Treynor"
),
htmltools::tags$style(
htmltools::HTML(estilo_css)
)
),
htmltools::tags$div(
class = "painel",
htmltools::tags$div(
class = "cabecalho",
htmltools::tags$h1(
"Analise de Carteiras de Investimento (Indice de Treynor)"
),
htmltools::tags$p(
paste(
"Fronteira eficiente, Capital Market Line (CML),",
"Security Market Line (SML), composicao e comparacao",
"de carteiras selecionadas pelo Indice de Treynor."
)
)
),
# Bloco 1: Gráficos Interativos
htmltools::tags$div(
class = "bloco",
htmltools::tags$h2(
"1. Graficos interativos"
),
htmltools::tags$p(
class = "subtitulo",
paste(
"Selecione o ano da carteira e depois escolha",
"entre a fronteira eficiente com CML e a SML/CAPM."
)
),
htmltools::tags$div(
class = "nav-tabs-custom",
botoes_anos
),
paineis_anos
),
htmltools::tags$br(),
htmltools::tags$br(),
# Bloco 2: Tabelas Interativas
htmltools::tags$div(
class = "bloco",
htmltools::tags$h2(
"2. Tabelas interativas"
),
htmltools::tags$p(
class = "subtitulo",
"Explore o resumo, a composicao e as mudancas estruturais das carteiras."
),
htmltools::tags$div(
class = "tabela-tabs",
botoes_tabelas
),
paineis_tabelas
),
htmltools::tags$div(
class = "rodape",
paste(
"Fonte: df_retornos e selecao por Treynor.",
"Retornos anualizados por 252 pregoes;",
"risco anualizado pela raiz de 252."
)
)
),
htmltools::tags$script(
htmltools::HTML(script_guias)
)
)
# ============================================================
# 17. EXPORTAR OS RESULTADOS E O ARQUIVO HTML
# ============================================================
write.csv(
resumo_carteiras,
"resumo_treynor_tangencia.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
write.csv(
composicao_carteiras,
"composicao_pesos_treynor.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
write.csv(
comparacao_carteiras,
"comparacao_composicao_treynor.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
write.csv(
fronteiras_carteiras,
"pontos_fronteiras_treynor.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
painel_carteiras
Fronteira eficiente, Capital Market Line (CML), Security Market Line (SML), composicao e comparacao de carteiras selecionadas pelo Indice de Treynor.
Selecione o ano da carteira e depois escolha entre a fronteira eficiente com CML e a SML/CAPM.
Janela de formacao: 2001 a 2005 | Selecionadas pelo Indice de Treynor
Explore o resumo, a composicao e as mudancas estruturais das carteiras.
Carteira 2005 (Análise Detalhada): O gráfico referente ao ano de 2005 exibe um cenário de altíssima volatilidade e retornos nominais extraordinários, característico de períodos inflacionários ou de recuperação acentuada pós-crises na bolsa brasileira. A curva da Fronteira Eficiente inicia-se de maneira robusta, projetando-se agressivamente rumo ao canto superior direito, onde atinge patamares de retorno anualizado superiores a \(700\%\) para um risco anualizado próximo de \(105\%\). As ações individuais (pontos verdes) revelam uma dispersão ampla: há ativos ineficientes isolados na extrema direita que acumulam volatilidades próximas a \(120\%\) entregando retornos muito modestos (cerca de \(200\%\)), o que evidencia o grave risco de investir sem diversificação. Em contrapartida, a Carteira Original (losango vermelho) e o Ponto de Tangência (estrela roxa) posicionam-se com maestria na porção inicial e mais inclinada da curva, situando-se em um patamar de risco contido em torno de \(25\%\) e retornos expressivos próximos a \(300\%\). A Capital Market Line (linha laranja tracejada), partindo do CDI quase zerado na origem, tangencia a fronteira exatamente na estrela roxa, provando que a seleção baseada no modelo evitou armadilhas de volatilidade e capturou com precisão cirúrgica a máxima eficiência que o mercado oferecia naquele ano.
Carteira 2010 (Análise Detalhada): Na janela de 2010, a dinâmica visual é dominada por um perfil de dispersão singular, onde a Fronteira Eficiente se estende de maneira linear e esticada até uma ação individual isolada no canto superior direito, registrando o impressionante patamar de quase \(350\%\) de risco anualizado e mais de \(300\%\) de retorno. O restante do conjunto acionário (pontos verdes) agrupa-se densamente em uma faixa de risco muito mais comportada, situada entre \(40\%\) e \(60\%\), com retornos na casa dos \(50\%\) a \(80\%\). É fascinante notar que a Carteira Original (losango vermelho) e o Ponto de Tangência (estrela roxa) ignoram a tentação do ativo hipervolátil e se ancoram exatamente no “joelho” de inflexão da curva, com um risco próximo a \(30\%–35\%\) e retornos estáveis em torno de \(75\%\). A inclinação da CML é fortemente sustentada por essa tangência, demonstrando que o algoritmo de seleção soube blindar o portfólio contra o risco sistêmico e específico excessivo, priorizando a estabilidade e o ganho proporcional ajustado ao CDI, que na época já compunha uma taxa livre de risco mais elevada.
Carteira 2015 (Análise Detalhada): O gráfico de 2015 retrata um ambiente macroeconômico completamente diferente, marcado por taxas de juros elevadas e um mercado acionário deprimido ou lateralizado, o que se reflete diretamente nas escalas reduzidas dos eixos: o retorno anualizado varia modestamente de \(10\%\) a \(40\%\). O CDI (quadrado marrom) eleva-se claramente para o patamar de \(10\%\), tornando-se um concorrente fortíssimo para a renda variável. As ações individuais espalham-se de forma dispersa à direita da Fronteira Eficiente, com volatilidades variando de \(23\%\) a quase \(40\%\), mas entregando retornos desfavoráveis que muitas vezes mal cobrem a taxa do CDI. É neste cenário adverso que a eficiência se prova crucial: a Carteira Original (losango vermelho) e o Ponto de Tangência (estrela roxa) posicionam-se com um risco controlado entre \(15\%\) e \(18\%\), garantindo retornos de \(23\%\) a \(28\%\). A CML tangencia a curva de forma sutil, evidenciando que, em ciclos de vacas magras na bolsa, a seleção rigorosa de ativos é a única barreira que impede o investidor de perder para a renda fixa tradicional.
Carteira 2020 (Análise Detalhada): A janela de 2020 captura os impactos e as reviravoltas da crise global e da recuperação em V impulsionada pelos estímulos de liquidez, gerando uma Fronteira Eficiente que desafia os limites gráficos ao alcançar retornos de até \(120\%\) para um risco de \(165\%\) em um ponto isolado de ação individual na extrema direita. O CDI permanece comprimido próximo a zero devido aos cortes drásticos da taxa Selic naquele período, o que empurra a CML para uma inclinação acentuada e altamente vantajosa para quem toma risco com inteligência. Observa-se que a grande maioria das ações verdes concentra-se em faixas de risco entre \(35\%\) e \(50\%\), mas a Carteira Original (losango vermelho) e o Ponto de Tangência (estrela roxa) destacam-se por ocuparem a zona de menor volatilidade da fronteira (ao redor de \(25\%\) a \(30\%\) de risco), entregando retornos formidáveis na faixa de \(70\%\). Essa convergência perfeita entre o losango e a estrela valida a robustez da estratégia, mostrando que a carteira otimizada conseguiu extrair o ápice do upside do mercado sem se expor à insanidade volátil dos ativos periféricos.
Carteira 2025 (Análise Detalhada): O último gráfico, referente a 2025, apresenta uma Fronteira Eficiente madura, de formato parabólico clássico e bem comportado, com o topo da curva estabilizando-se em torno de \(40\%\) de retorno anualizado para um risco de \(45\%\), enquanto as ações individuais formam uma nuvem difusa que se estende até volatilidades de \(90\%\) com retornos menores (\(28\%\)). O CDI (quadrado marrom) retoma um patamar expressivo de aproximadamente \(8\%\), servindo de alicerce sólido para a projeção da CML. O Ponto de Tangência (estrela roxa) e a Carteira Original (losango vermelho) encontram-se praticamente sobrepostos no “joelho” ideal da curva, exigindo um risco de \(26\%\) a \(31\%\) para entregar retornos consistentes de \(32\%\) a \(33\%\). Essa simbiose perfeita no final do horizonte temporal comprova a eficácia duradoura da metodologia quantitativa: em um mercado moderno e competitivo, a seleção baseada em métricas ajustadas ao risco continua sendo implacable para filtrar o ruído, rejeitar ações ineficientes e entregar um portfólio que domina a fronteira de eficiência teórica.