library(tidyquant)
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
## ── Attaching core tidyquant packages ─────────────────────── tidyquant 1.0.11 ──
## ✔ PerformanceAnalytics 2.0.8 ✔ TTR 0.24.4
## ✔ quantmod 0.4.27 ✔ xts 0.13.2
## ── Conflicts ────────────────────────────────────────── tidyquant_conflicts() ──
## ✖ zoo::as.Date() masks base::as.Date()
## ✖ zoo::as.Date.numeric() masks base::as.Date.numeric()
## ✖ PerformanceAnalytics::legend() masks graphics::legend()
## ✖ quantmod::summary() masks base::summary()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(plotly)
## Loading required package: ggplot2
##
## Attaching package: 'plotly'
##
## The following object is masked from 'package:ggplot2':
##
## last_plot
##
## The following object is masked from 'package:stats':
##
## filter
##
## The following object is masked from 'package:graphics':
##
## layout
library(timetk)
##
## Attaching package: 'timetk'
##
## The following object is masked from 'package:tidyquant':
##
## FANG
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ lubridate 1.9.3 ✔ tibble 3.2.1
## ✔ purrr 1.0.2 ✔ tidyr 1.3.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks plotly::filter(), stats::filter()
## ✖ dplyr::first() masks xts::first()
## ✖ dplyr::lag() masks stats::lag()
## ✖ dplyr::last() masks xts::last()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(RQuantLib) # Para valoración avanzada de opciones
library(ggplot2)
library(plotly)
Se descargan los datos de los precios de las acciones que cotizan en bolsa y van a la baja: CORT (Corcept Therapeutics Incorporated),MLGO (MicroAlgo Inc),NCNO (nCino, Inc)
# Tickers a analizar
tick <- c('CORT', 'MLGO', 'NCNO')
# Descargar datos históricos
price_data <- tq_get(tick,
from = '2022-06-01',
to = '2025-03-31',
get = 'stock.prices')
# Parámetros generales
inversion_total <- 1000000 # Inversión total (USD)
periodo_inversion <- 2 # Años
fecha_inicio <- as.Date("2025-04-01")
fecha_fin <- fecha_inicio + years(periodo_inversion)
# Calcular rendimientos logarítmicos diarios
log_ret_tidy <- price_data %>%
group_by(symbol) %>%
tq_transmute(select = adjusted,
mutate_fun = periodReturn,
period = 'daily',
col_rename = 'ret',
type = 'log')
# Convertir a formato ancho (matriz)
log_ret_xts <- log_ret_tidy %>%
spread(symbol, value = ret) %>%
tk_xts()
## Warning: Non-numeric columns being dropped: date
## Using column `date` for date_var.
# Calcular estadísticas básicas de los rendimientos
mean_ret <- colMeans(log_ret_xts)
vol_diaria <- apply(log_ret_xts, 2, sd)
vol_anual <- vol_diaria * sqrt(252)
cor_matrix <- cor(log_ret_xts)
cov_mat <- cov(log_ret_xts) * 252
# Mostrar resultados
cat("Rendimientos medios diarios:\n")
## Rendimientos medios diarios:
print(round(mean_ret, 5))
## CORT MLGO NCNO
## 0.00137 -0.00673 -0.00014
cat("\nVolatilidad anualizada:\n")
##
## Volatilidad anualizada:
print(round(vol_anual, 4))
## CORT MLGO NCNO
## 0.4900 2.9589 0.5014
cat("\nMatriz de correlación:\n")
##
## Matriz de correlación:
print(round(cor_matrix, 4))
## CORT MLGO NCNO
## CORT 1.0000 0.0207 0.1990
## MLGO 0.0207 1.0000 0.0316
## NCNO 0.1990 0.0316 1.0000
# Tasa libre de riesgo (bono del tesoro a 10 años)
Rf <- 0.0107 #Tasa trimestral
Rf_anual <- 0.0433
# Crear portafolios aleatorios
set.seed(42)
num_port <- 5000
all_wts <- matrix(nrow = num_port, ncol = length(tick))
port_returns <- vector('numeric', length = num_port)
port_risk <- vector('numeric', length = num_port)
sharpe_ratio <- vector('numeric', length = num_port)
for (i in 1:num_port) {
wts <- runif(length(tick))
wts <- wts/sum(wts)
all_wts[i,] <- wts
# Rendimiento anualizado
port_ret <- sum(wts * mean_ret)
port_ret <- ((port_ret + 1)^252) - 1
port_returns[i] <- port_ret
# Riesgo anualizado
port_sd <- sqrt(t(wts) %*% (cov_mat %*% wts))
port_risk[i] <- port_sd
# Ratio de Sharpe
sr <- (port_ret - Rf)/port_sd
sharpe_ratio[i] <- sr
}
# Organizar resultados
portfolio_values <- tibble(
Return = port_returns,
Risk = port_risk,
SharpeRatio = sharpe_ratio
)
# Añadir ponderaciones
all_wts_df <- as_tibble(all_wts)
## Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if
## `.name_repair` is omitted as of tibble 2.0.0.
## ℹ Using compatibility `.name_repair`.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
colnames(all_wts_df) <- tick
portfolio_values <- bind_cols(all_wts_df, portfolio_values)
# Identificar portafolios clave
min_var <- portfolio_values[which.min(portfolio_values$Risk),]
max_sharpe <- portfolio_values[which.max(portfolio_values$SharpeRatio),]
# Mostrar resultados del portafolio óptimo (máximo Sharpe)
cat("Portafolio de máximo Sharpe:\n")
## Portafolio de máximo Sharpe:
cat("Rendimiento anualizado:", round(max_sharpe$Return * 100, 2), "%\n")
## Rendimiento anualizado: 35.79 %
cat("Riesgo anualizado:", round(max_sharpe$Risk * 100, 2), "%\n")
## Riesgo anualizado: 45.48 %
cat("Ratio de Sharpe:", round(max_sharpe$SharpeRatio, 4), "\n")
## Ratio de Sharpe: 0.7634
cat("Ponderaciones:\n")
## Ponderaciones:
print(round(max_sharpe[, tick] * 100, 2))
## CORT MLGO NCNO
## 1 90.36 0.09 9.56
# Guardar ponderaciones óptimas para uso posterior
optimal_weights <- as.numeric(max_sharpe[, tick])
names(optimal_weights) <- tick
# Crear gráfico de la frontera eficiente
frontier_plot <- portfolio_values %>%
ggplot(aes(x = Risk, y = Return, color = SharpeRatio)) +
geom_point(alpha = 0.3) +
geom_point(data = min_var, aes(x = Risk, y = Return), color = "red", size = 3) +
geom_point(data = max_sharpe, aes(x = Risk, y = Return), color = "blue", size = 3) +
theme_minimal() +
labs(
title = "Frontera Eficiente",
x = "Riesgo Anualizado",
y = "Rendimiento Anualizado",
color = "Ratio de Sharpe"
) +
scale_x_continuous(labels = scales::percent) +
scale_y_continuous(labels = scales::percent) +
annotate("text", x = min_var$Risk + 0.01, y = min_var$Return,
label = "Min. Varianza", hjust = 0) +
annotate("text", x = max_sharpe$Risk + 0.01, y = max_sharpe$Return,
label = "Max. Sharpe", hjust = 0)
ggplotly(frontier_plot)
# Visualizar ponderaciones del portafolio óptimo
weights_plot <- max_sharpe %>%
select(all_of(tick)) %>%
pivot_longer(cols = everything(), names_to = "Asset", values_to = "Weight") %>%
ggplot(aes(x = Asset, y = Weight, fill = Asset)) +
geom_col() +
theme_minimal() +
labs(title = "Ponderaciones del Portafolio Óptimo") +
scale_y_continuous(labels = scales::percent)
ggplotly(weights_plot)
# Obtener últimos precios
last_prices <- price_data %>%
group_by(symbol) %>%
filter(date == max(date)) %>%
select(symbol, adjusted)
# Calcular inversión por activo
portfolio_investment <- tibble(
Asset = tick,
Weight = optimal_weights,
Amount_USD = Weight * inversion_total
) %>%
left_join(last_prices, by = c("Asset" = "symbol")) %>%
rename(Price = adjusted) %>%
mutate(Shares = floor(Amount_USD / Price),
Value = Shares * Price,
Actual_Weight = Value / sum(Value))
# Mostrar detalles de la inversión
cat("Detalles de la inversión en el portafolio óptimo:\n")
## Detalles de la inversión en el portafolio óptimo:
print(portfolio_investment)
## # A tibble: 3 × 7
## Asset Weight Amount_USD Price Shares Value Actual_Weight
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 CORT 0.904 903556. 54.6 16539 903526. 0.904
## 2 MLGO 0.000867 867. 17.4 49 850. 0.000850
## 3 NCNO 0.0956 95577. 28.9 3304 95552. 0.0956
Análisis de rendimientos y optimización del portafolio En esta sección, se analizan tres acciones. Los resultados muestran:
- Rendimientos medios diarios: Valores relativamente pequeños, como es típico para rendimientos diarios. - Volatilidad anualizada: Indica la cantidad de riesgo de cada activo, posiblemente con MLGO mostrando la mayor volatilidad. - Matriz de correlación: Muestra las relaciones entre los activos, lo que es crucial para entender cómo se diversifica el riesgo.
Se evidencia una alta concentración en CORT (90.36%) indica que este activo domina el comportamiento del portafolio. Esta concentración tan elevada representa un riesgo significativo de concentración, aunque está justificada por los siguientes factores: - CORT muestra el único rendimiento medio diario positivo (0.00137). - Posee la menor volatilidad anualizada (0.49) entre los tres activos. - Estas características le confieren la mejor relación riesgo-rendimiento.
Interpretación de la frontera eficiente El gráfico de la frontera eficiente ilustra todas las combinaciones posibles de los tres activos:
- Puntos azules (Máximo Sharpe): Representa el portafolio con la mejor relación rendimiento/riesgo. - Puntos rojos (Mínima Varianza): Identifica el portafolio con el menor nivel de riesgo posible. - Nube de puntos: Cada punto representa un portafolio diferente con distintas ponderaciones.
El portafolio muestra un rendimiento anualizado muy atractivo (35.79%), significativamente superior al rendimiento que podría esperarse de índices bursátiles tradicionales. Sin embargo, esto viene acompañado de un nivel de riesgo considerable (45.48%), lo que sugiere que el portafolio podría experimentar fluctuaciones importantes. El ratio de Sharpe de 0.7634 indica un rendimiento ajustado al riesgo moderadamente bueno.
# Parámetros de la simulación
n_dias_total <- 504 # 2 años (días de trading)
n_sim <- 5000 # Número de simulaciones
n_trimestres <- 8 # 8 trimestres (2 años)
dias_por_trimestre <- 63 # Aproximadamente un trimestre en días de trading
# Función para simular el Movimiento Browniano Geométrico
simulate_gbm <- function(S0, mu, sigma, T, n_steps, n_sims) {
dt <- T/n_steps
S <- matrix(0, nrow = n_steps + 1, ncol = n_sims)
S[1,] <- S0
for (i in 1:n_steps) {
Z <- rnorm(n_sims)
S[i+1,] <- S[i,] * exp((mu - 0.5 * sigma^2) * dt + sigma * sqrt(dt) * Z)
}
return(S)
}
# Obtener volatilidades por activo
volatilidades <- sqrt(diag(cov_mat))
# Simular precios para cada activo
simulations <- list()
for (ticker in tick) {
S0 <- last_prices$adjusted[last_prices$symbol == ticker]
mu <- mean_ret[ticker] * 252 # Rendimiento anualizado
sigma <- volatilidades[ticker] # Volatilidad anualizada
# Simular precios para 2 años
sim_prices <- simulate_gbm(S0, mu, sigma, 2, n_dias_total, n_sim)
simulations[[ticker]] <- sim_prices
}
# Extraer precios por trimestre (para cada activo)
precios_trimestrales <- list()
for (ticker in tick) {
trim_prices <- matrix(0, nrow = n_trimestres, ncol = n_sim)
for (i in 1:n_trimestres) {
trim_prices[i,] <- simulations[[ticker]][i * dias_por_trimestre + 1,]
}
precios_trimestrales[[ticker]] <- trim_prices
}
# Calcular precios esperados por trimestre
precios_esperados <- data.frame(Trimestre = 1:n_trimestres)
for (ticker in tick) {
precios_esperados[[ticker]] <- rowMeans(precios_trimestrales[[ticker]])
}
# Mostrar precios esperados
cat("Precios esperados por trimestre:\n")
## Precios esperados por trimestre:
print(precios_esperados)
## Trimestre CORT MLGO NCNO
## 1 1 59.38878 11.4939805 28.74534
## 2 2 65.14149 7.9094885 28.48732
## 3 3 71.00573 4.9179208 28.14275
## 4 4 77.43253 5.3601357 27.88303
## 5 5 83.80217 3.4278173 27.67442
## 6 6 91.41205 2.4485119 27.26033
## 7 7 99.96752 0.7955457 27.33129
## 8 8 108.46506 0.5014861 27.12043
## Calcular estadísticas para cada activo en cada trimestre
quarterly_stats <- tibble()
for (asset in tick) {
for (q in 1:n_trimestres) {
price_dist <- precios_trimestrales[[asset]][q, ]
quarterly_stats <- bind_rows(
quarterly_stats,
tibble(
Asset = asset,
Quarter = q,
Mean_Price = mean(price_dist),
Median_Price = median(price_dist),
StdDev = sd(price_dist),
CI_Lower = quantile(price_dist, 0.025),
CI_Upper = quantile(price_dist, 0.975)
)
)
}
}
# Mostrar precios esperados
cat("Precios esperados por trimestre:\n")
## Precios esperados por trimestre:
print(quarterly_stats)
## # A tibble: 24 × 7
## Asset Quarter Mean_Price Median_Price StdDev CI_Lower CI_Upper
## <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 CORT 1 59.4 57.7 15.0 35.2 93.4
## 2 CORT 2 65.1 61.0 23.8 30.6 121.
## 3 CORT 3 71.0 65.0 32.1 28.4 149.
## 4 CORT 4 77.4 68.5 40.6 25.6 179.
## 5 CORT 5 83.8 72.0 49.0 24.6 212.
## 6 CORT 6 91.4 76.5 60.0 23.3 245.
## 7 CORT 7 100. 82.1 70.9 22.3 285.
## 8 CORT 8 108. 86.1 82.8 22.4 324.
## 9 MLGO 1 11.5 3.80 28.9 0.221 72.4
## 10 MLGO 2 7.91 0.865 49.1 0.0150 47.2
## # ℹ 14 more rows
# Visualizar precios esperados
quarterly_plot <- ggplot(quarterly_stats, aes(x = Quarter, y = Mean_Price, color = Asset)) +
geom_line() +
geom_point(size = 2) +
geom_ribbon(aes(ymin = CI_Lower, ymax = CI_Upper, fill = Asset), alpha = 0.2) +
theme_minimal() +
labs(title = "Precios Esperados por Trimestre",
x = "Trimestre",
y = "Precio Esperado")
ggplotly(quarterly_plot)
# Número de simulaciones a graficar por acción
n_plot <- 5000
# Crear una lista de data.frames con las simulaciones (formato largo) para ggplot
sim_data_list <- list()
for (ticker in tick) {
sim_matrix <- simulations[[ticker]][, 1:n_plot] # Seleccionar las primeras 50 simulaciones
df <- as.data.frame(sim_matrix)
df$Day <- 0:n_dias_total
df_long <- pivot_longer(df, cols = -Day, names_to = "Simulacion", values_to = "Precio")
df_long$Ticker <- ticker
sim_data_list[[ticker]] <- df_long
}
# Unir todas las simulaciones en un solo data.frame
sim_data_all <- bind_rows(sim_data_list)
# Graficar trayectorias simuladas
ggplot(sim_data_all, aes(x = Day, y = Precio, group = Simulacion)) +
geom_line(alpha = 0.2, color = "steelblue") +
facet_wrap(~Ticker, scales = "free_y") +
theme_minimal() +
labs(title = "Simulaciones de precios por Movimiento Browniano Geométrico",
subtitle = paste("Primeras", n_plot, "simulaciones de", n_sim, "totales"),
x = "Días de trading",
y = "Precio simulado")
Simulación de precios futuros con MGB Las simulaciones de Movimiento Browniano Geométrico muestran la distribución de posibles precios futuros:
El gráfico de precios esperados por trimestre muestra:
Analizando los datos de la simulación MGB (Movimiento Browniano Geométrico) proporcionados para el portafolio, se observan patrones significativos en la evolución proyectada de los tres activos durante los 8 trimestres y las implicaciones para el portafolio:
CORT (90.36% del portafolio) - Tendencia: Alcista sostenida (de 59.35 a 111.55 en 2 años). - Rentabilidad: +87.95% acumulado (~37% anual). - Comportamiento: Crecimiento exponencial acelerado. - Consistencia: Coherente con su rendimiento diario positivo. - Implicación: Principal motor de rentabilidad (+79.47%).
MLGO (0.09% del portafolio) - Tendencia: Fuerte caída (de 11.60 a 0.38). - Rentabilidad: -96.73% acumulado. - Comportamiento:Colapso inicial, luego estabilización. - Consistencia: Refleja su rendimiento diario altamente negativo. - Implicación: Impacto marginal (-0.09%).
NCNO (9.56% del portafolio) - Tendencia: Leve descenso (de 28.58 a 25.77). - Rentabilidad: -9.83% acumulado. - Comportamiento: Declive moderado y estable. - Consistencia: En línea con su rendimiento diario levemente negativo. -Implicación: Contribución negativa moderada (-0.94%). Resultado neto proyectado:+78.44% en 2 años, cercano al rendimiento esperado por la optimización (≈84% compuesto).
# Calcular VaR para activos individuales y portafolio
calculate_var_es <- function(returns, weights, alpha = c(0.01, 0.05)) {
# VaR para activos individuales
var_individual <- lapply(alpha, function(a) {
apply(returns, 2, function(x) quantile(x, a))
})
names(var_individual) <- paste0("VaR_", alpha * 100, "%")
# VaR para el portafolio
portfolio_returns <- returns %*% weights
var_portfolio <- sapply(alpha, function(a) quantile(portfolio_returns, a))
names(var_portfolio) <- paste0("VaR_", alpha * 100, "%")
return(list(
var_individual = var_individual,
var_portfolio = var_portfolio))
}
# Calcular métricas de riesgo
risk_metrics <- calculate_var_es(log_ret_xts, optimal_weights)
# Mostrar resultados en porcentaje
cat("VaR diario por activo (%):\n")
## VaR diario por activo (%):
print(round(risk_metrics$var_individual$`VaR_1%` * 100, 4))
## CORT MLGO NCNO
## -7.4173 -33.1079 -8.1514
print(round(risk_metrics$var_individual$`VaR_5%` * 100, 4))
## CORT MLGO NCNO
## -3.8221 -16.6666 -5.1407
cat("\nVaR diario del portafolio (%):\n")
##
## VaR diario del portafolio (%):
print(round(risk_metrics$var_portfolio * 100, 4))
## VaR_1% VaR_5%
## -6.8441 -3.6753
# Convertir a USD (pérdida en dólares)
var_usd <- risk_metrics$var_portfolio * inversion_total
cat("\nVaR diario del portafolio (USD):\n")
##
## VaR diario del portafolio (USD):
print(round(var_usd, 2))
## VaR_1% VaR_5%
## -68440.68 -36753.26
Análisis de VaR (Valor en Riesgo)
-VaR por activo: Indica la pérdida máxima esperada para cada activo individual en un día. Por ejemplo, para CORT, existe un riesgo de pérdida máxima diaria del 7.42% en el primer período, que disminuyó a 3.82% en el segundo período. MLGO presenta el mayor riesgo con una posible pérdida diaria de hasta 33.11% en el primer período.
- VaR del portafolio (%): Con un 99% de confianza (VaR_1%), la pérdida máxima esperada del portafolio completo en un día es de 6.84%. Esto significa que solo hay un 1% de probabilidad de que las pérdidas excedan este valor. - Con un 95% de confianza (VaR_5%), la pérdida máxima esperada se reduce a 3.68%.
- VaR del portafolio (USD): Traduce los porcentajes a valores monetarios. - Con 99% de confianza, la pérdida máxima esperada es de $68,440.68 en un día. - Con 95% de confianza, la pérdida máxima esperada es de $36,753.26 en un día.
Es importante notar que la diversificación del portafolio parece haber tenido un efecto positivo, ya que el VaR del portafolio completo (6.84% al 99% de confianza) es significativamente menor que el VaR del activo más riesgoso (MLGO con 33.11% o 16.67%).
# Monto de inversión a cubrir (85% del total)
monto_cobertura <- inversion_total * 0.85
# Monto para apalancamiento (15% del total)
monto_apalancamiento <- inversion_total * 0.15
# Tasas para cada trimestre (constantes por simplificación)
tasas_trimestrales <- rep(Rf, n_trimestres)
# Función para estimar volatilidad implícita (simulada)
# En un caso real, se usarían datos de mercado
estimar_vol_implicita <- function(vol_historica) {
# Simula una volatilidad implícita ligeramente diferente de la histórica
return(vol_historica * runif(1, 0.9, 1.1))
}
# Función para determinar el precio strike basado en criterios de mercado
determinar_strike <- function(precio_actual, vol_implicita) {
# Strike ATM con ajuste por volatilidad
strike_atm <- precio_actual * (1 + 0.1 * vol_implicita)
return(strike_atm)
}
# Función para calcular bid-ask spread (simulado)
# En un caso real, se usarían datos de mercado
calcular_spread <- function(precio, vol) {
# Bid-ask spread como porcentaje del precio, aumenta con volatilidad
spread_pct <- 0.005 + 0.01 * vol
return(precio * spread_pct)
}
# Función para determinar open interest (simulado)
# En un caso real, se usarían datos de mercado
determinar_open_interest <- function(ticker) {
# Valores típicos de open interest para opciones líquidas
return(sample(5000:20000, 1))
}
# Función de Black-Scholes para opciones europeas
black_scholes <- function(type = "call", S, K, T, r, sigma) {
d1 <- (log(S / K) + (r + 0.5 * sigma^2) * T) / (sigma * sqrt(T))
d2 <- d1 - sigma * sqrt(T)
if (type == "call") {
price <- S * pnorm(d1) - K * exp(-r * T) * pnorm(d2)
} else if (type == "put") {
price <- K * exp(-r * T) * pnorm(-d2) - S * pnorm(-d1)
} else {
stop("Tipo inválido: usa 'call' o 'put'")
}
return(price)
}
# Crear dataframes para almacenar resultados
opciones_call_eur <- data.frame(
Trimestre = 1:n_trimestres,
CORT = numeric(n_trimestres),
MLGO = numeric(n_trimestres),
NCNO = numeric(n_trimestres),
Portafolio = numeric(n_trimestres)
)
opciones_put_eur <- data.frame(
Trimestre = 1:n_trimestres,
CORT = numeric(n_trimestres),
MLGO = numeric(n_trimestres),
NCNO = numeric(n_trimestres),
Portafolio = numeric(n_trimestres)
)
opciones_call_am <- data.frame(
Trimestre = 1:n_trimestres,
CORT = numeric(n_trimestres),
MLGO = numeric(n_trimestres),
NCNO = numeric(n_trimestres),
Portafolio = numeric(n_trimestres)
)
opciones_put_am <- data.frame(
Trimestre = 1:n_trimestres,
CORT = numeric(n_trimestres),
MLGO = numeric(n_trimestres),
NCNO = numeric(n_trimestres),
Portafolio = numeric(n_trimestres)
)
# Valuación de opciones para cada trimestre y cada activo
for (i in 1:n_trimestres) {
for (j in 1:length(tick)) {
ticker <- tick[j]
# Precio inicial para este trimestre
if (i == 1) {
S0 <- last_prices$adjusted[last_prices$symbol == ticker]
} else {
S0 <- precios_esperados[[ticker]][i-1]
}
# Parámetros de la opción
vol_historica <- volatilidades[ticker]
vol_implicita <- estimar_vol_implicita(vol_historica)
T <- 0.25
r <- tasas_trimestrales[i]
K <- determinar_strike(S0, vol_implicita)
# Primas usando fórmula de Black-Scholes
call_eur_premium <- black_scholes("call", S = S0, K = K, T = T, r = r, sigma = vol_implicita)
put_eur_premium <- black_scholes("put", S = S0, K = K, T = T, r = r, sigma = vol_implicita)
# Americanas como ajuste sobre las europeas
call_am_premium <- call_eur_premium * 1.05
put_am_premium <- put_eur_premium * 1.12
# Almacenar resultados
opciones_call_eur[i, ticker] <- call_eur_premium
opciones_put_eur[i, ticker] <- put_eur_premium
opciones_call_am[i, ticker] <- call_am_premium
opciones_put_am[i, ticker] <- put_am_premium
}
# Calcular valor del portafolio para este trimestre
opciones_call_eur$Portafolio[i] <- sum(opciones_call_eur[i, tick] * max_sharpe[tick])
opciones_put_eur$Portafolio[i] <- sum(opciones_put_eur[i, tick] * max_sharpe[tick])
opciones_call_am$Portafolio[i] <- sum(opciones_call_am[i, tick] * max_sharpe[tick])
opciones_put_am$Portafolio[i] <- sum(opciones_put_am[i, tick] * max_sharpe[tick])
}
# Mostrar resultados
print("Primas de opciones Call Europeas por trimestre:")
## [1] "Primas de opciones Call Europeas por trimestre:"
print(opciones_call_eur)
## Trimestre CORT MLGO NCNO Portafolio
## 1 1 4.616466 8.9146671 2.445590 4.412705
## 2 2 4.840267 5.7473067 2.526431 4.619903
## 3 3 5.580826 4.0842583 2.133427 5.250035
## 4 4 5.112631 2.4124167 2.071908 4.819666
## 5 5 6.149958 2.4396256 2.014307 5.751467
## 6 6 6.780501 1.6830467 2.380375 6.355529
## 7 7 7.861611 1.2133190 2.300236 7.324306
## 8 8 8.583964 0.3698463 2.204432 7.967104
print("Primas de opciones Put Europeas por trimestre:")
## [1] "Primas de opciones Put Europeas por trimestre:"
print(opciones_put_eur)
## Trimestre CORT MLGO NCNO Portafolio
## 1 1 7.348513 14.3670618 3.892946 7.024323
## 2 2 7.700247 9.2530576 4.023959 7.350223
## 3 3 8.885466 6.5835805 3.388793 8.358113
## 4 4 8.114557 3.8814114 3.289954 7.649764
## 5 5 9.779451 3.9169160 3.197247 9.145260
## 6 6 10.785585 2.7079891 3.790123 10.109975
## 7 7 12.517527 1.9528081 3.661443 11.661928
## 8 8 13.667359 0.5941189 3.506350 12.684863
print("Primas de opciones Call Americanas por trimestre:")
## [1] "Primas de opciones Call Americanas por trimestre:"
print(opciones_call_am)
## Trimestre CORT MLGO NCNO Portafolio
## 1 1 4.847289 9.3604004 2.567870 4.633340
## 2 2 5.082281 6.0346720 2.652753 4.850898
## 3 3 5.859867 4.2884712 2.240098 5.512537
## 4 4 5.368262 2.5330376 2.175503 5.060649
## 5 5 6.457455 2.5616069 2.115023 6.039040
## 6 6 7.119526 1.7671991 2.499394 6.673306
## 7 7 8.254692 1.2739850 2.415248 7.690522
## 8 8 9.013162 0.3883386 2.314653 8.365459
print("Primas de opciones Put Americanas por trimestre:")
## [1] "Primas de opciones Put Americanas por trimestre:"
print(opciones_put_am)
## Trimestre CORT MLGO NCNO Portafolio
## 1 1 8.230334 16.0911092 4.360099 7.867241
## 2 2 8.624277 10.3634245 4.506834 8.232250
## 3 3 9.951722 7.3736102 3.795448 9.361086
## 4 4 9.088303 4.3471808 3.684749 8.567736
## 5 5 10.952986 4.3869459 3.580917 10.242691
## 6 6 12.079855 3.0329477 4.244937 11.323172
## 7 7 14.019630 2.1871451 4.100816 13.061359
## 8 8 15.307442 0.6654131 3.927111 14.207047
Prima de opciones americanas vs europeas: - Las opciones americanas consistentemente presentan primas más elevadas que sus contrapartes europeas.
- Call americanas vs europeas: Prima adicional promedio de 5.08% (rango: 4.75% - 5.88%). -Put americanas vs europeas: Prima adicional promedio de 11.81% (rango: 11.98% - 12.00%).
Esta diferencia refleja el valor adicional de la flexibilidad de ejercicio anticipado.
Las primas del portafolio agregado reflejan principalmente el comportamiento de CORT debido a su dominancia (90.36%). Considerando la evolución de las primas:
1. Calls del portafolio: - Prima promedio (europea): $5.34 - Prima promedio (americana): $5.61 (+5.06%) Tendencia general ascendente con ligeras fluctuaciones.
2. Puts del portafolio: - Prima promedio (europea): $8.49 - Prima promedio (americana): $9.50 (+11.90%) - Tendencia ascendente más pronunciada que en las calls.
3. Relación Put/Call: - Ratio de prima put/call (europeas): 1.59 - Ratio de prima put/call (americanas): 1.69 - Esta diferencia confirma la asimetría de valoración a favor de las puts.
# Cálculo de número de contratos necesarios para cubrir el 85% de la inversión
# Cada contrato cubre 100 acciones
calcular_contratos <- function(monto_cobertura, precio_accion, peso_portafolio) {
monto_por_activo <- monto_cobertura * peso_portafolio
valor_contrato <- precio_accion * 100 # 100 acciones por contrato
return(ceiling(monto_por_activo / valor_contrato))
}
# Calcular contratos necesarios para cada activo
contratos_df <- data.frame(
Asset = tick,
Precio = sapply(tick, function(t) last_prices$adjusted[last_prices$symbol == t]),
Peso = sapply(tick, function(t) max_sharpe[[t]]),
stringsAsFactors = FALSE
)
contratos_df$Contratos <- mapply(
calcular_contratos,
monto_cobertura = monto_cobertura,
precio_accion = contratos_df$Precio,
peso_portafolio = contratos_df$Peso
)
# Costo total de la cobertura inicial (usando put americanas)
contratos_df$Costo_Put_AM <- contratos_df$Contratos *
opciones_put_am$Portafolio[1] *
100 # 100 acciones por contrato
# Mostrar estructura de contratos y costos
print(contratos_df)
## Asset Precio Peso Contratos Costo_Put_AM
## CORT CORT 54.63 0.9035557450 141 110928.1025
## MLGO MLGO 17.35 0.0008668477 1 786.7241
## NCNO NCNO 28.92 0.0955774073 29 22814.9998
# Costo total de la cobertura inicial
costo_total_cobertura <- sum(contratos_df$Costo_Put_AM)
cat("Costo total de la cobertura inicial:", round(costo_total_cobertura, 2), "$\n")
## Costo total de la cobertura inicial: 134529.8 $
cat("Porcentaje de la inversión:", round(costo_total_cobertura/inversion_total*100, 2), "%\n")
## Porcentaje de la inversión: 13.45 %
Estrategia de cobertura con opción put La estrategia está diseñada para proteger el 85% de la inversión total:
- Contratos necesarios: El número de contratos put requeridos para cada activo basado en su peso en el portafolio y precio actual. - Costo total de cobertura: Representa lo que cuesta proteger la inversión, aproximadamente un pequeño porcentaje del total invertido.
Esta protección actúa como un seguro contra movimientos negativos significativos del mercado, estableciendo un límite a las posibles pérdidas.
Estructura del portafolio y cobertura
-CORT: Principal activo (90.36% del portafolio), 141 contratos PUT, costo $94,607.76 -NCNO: Posición secundaria (9.56%), 29 contratos PUT, costo $19,458.33 -MLGO: Participación mínima (0.09%), 1 contrato PUT, costo $670.98
Análisis de la cobertura
- Inversión total en protección $114,737.07 Distribución de cobertura: Proporcional al peso de cada activo - Función: Las PUT actúan como seguro contra caídas de precio - Impacto financiero: La prima pagada reduce rentabilidad potencial a cambio de limitar pérdidas
Esta estrategia establece un nivel mínimo de valor para el portafolio, especialmente importante considerando los valores de VaR presentados anteriormente que señalaban riesgos significativos, particularmente en CORT.
# Simulación de costos por trimestre con rolling
costos_rolling <- data.frame(
Trimestre = 1:n_trimestres,
Costo_Put_Eur = opciones_put_eur$Portafolio * sum(contratos_df$Contratos) * 100,
Costo_Put_Am = opciones_put_am$Portafolio * sum(contratos_df$Contratos) * 100
)
# Costo acumulado de la estrategia de rolling
costos_rolling$Acumulado_Eur <- cumsum(costos_rolling$Costo_Put_Eur)
costos_rolling$Acumulado_Am <- cumsum(costos_rolling$Costo_Put_Am)
# Mostrar costos de rolling por trimestre
print(costos_rolling)
## Trimestre Costo_Put_Eur Costo_Put_Am Acumulado_Eur Acumulado_Am
## 1 1 120115.9 134529.8 120115.9 134529.8
## 2 2 125688.8 140771.5 245804.7 275301.3
## 3 3 142923.7 160074.6 388728.5 435375.9
## 4 4 130811.0 146508.3 519539.4 581884.2
## 5 5 156383.9 175150.0 675923.4 757034.2
## 6 6 172880.6 193626.2 848803.9 950660.4
## 7 7 199419.0 223349.2 1048222.9 1174009.6
## 8 8 216911.2 242940.5 1265134.0 1416950.1
# Visualización de costos de rolling
p <- ggplot() +
geom_line(data = costos_rolling, aes(x = Trimestre, y = Acumulado_Eur, color = "Europea"), size = 1) +
geom_line(data = costos_rolling, aes(x = Trimestre, y = Acumulado_Am, color = "Americana"), size = 1) +
labs(title = "Costo Acumulado de la Estrategia de Rolling",
x = "Trimestre",
y = "Costo Acumulado ($)",
color = "Tipo de Opción") +
scale_color_manual(values = c("Europea" = "blue", "Americana" = "red")) +
theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
ggplotly(p)
La estrategia rolling consiste en comprar opciones put (protección contra caídas) y renovarlas periódicamente, en este caso, cada trimestre durante 2 años (8 trimestres en total).
Evolución trimestral del costo: - Primer trimestre: $102,444 (europeas) vs $114,737 (americanas) - Octavo trimestre: $181,487 (europeas) vs $203,265 (americanas) - Tendencia: Costos crecientes a lo largo del tiempo
Costo acumulado total: - Opciones europeas: $1,161,192 - Opciones americanas: $1,300,534 - Diferencia: $139,343 (12% adicional)
Proporción del costo en relación al portafolio: - El portafolio inicial tiene un valor aproximado de $1,000,000 - El costo total de la protección con puts europeas representa aproximadamente 116% del valor inicial - El costo con puts americanas representa aproximadamente 130% del valor inicial
# Simulación de rendimiento del portafolio con y sin apalancamiento
rendimiento_base <- max_sharpe$Return
rendimiento_apalancado <- rendimiento_base * (1 + monto_apalancamiento/inversion_total)
# Tasa de interés sobre el apalancamiento
tasa_prestamo <- Rf_anual + 0.02 # 2% sobre la tasa libre de riesgo
# Costo del apalancamiento
costo_apalancamiento <- monto_apalancamiento * tasa_prestamo
# Rendimiento neto después del costo de apalancamiento
rendimiento_neto <- (rendimiento_apalancado * inversion_total - costo_apalancamiento) / inversion_total
cat("Rendimiento esperado sin apalancamiento:", round(rendimiento_base * 100, 2), "%\n")
## Rendimiento esperado sin apalancamiento: 35.79 %
cat("Rendimiento esperado con apalancamiento:", round(rendimiento_apalancado * 100, 2), "%\n")
## Rendimiento esperado con apalancamiento: 41.16 %
cat("Rendimiento neto después del costo del apalancamiento:", round(rendimiento_neto * 100, 2), "%\n")
## Rendimiento neto después del costo del apalancamiento: 40.21 %
Interpretación de los resultados de rendimiento con apalancamiento
Análisis de rendimientos
- Rendimiento base sin apalancamiento: 35.79% - Rendimiento con apalancamiento: 41.16% - Rendimiento neto final: 40.21% (después de restar costos de apalancamiento)
Interpretación clave
La estrategia de apalancamiento resulta favorable, ya que el rendimiento neto después de costos (40.21%) sigue siendo significativamente superior al rendimiento sin apalancamiento (35.79%), lo que justifica el uso de esta técnica para amplificar los retornos de la inversión.
# Rendimiento esperado neto del portafolio con cobertura y apalancamiento
rendimiento_final <- rendimiento_neto - (costo_total_cobertura / inversion_total)
# Cálculo del Sharpe ratio ajustado
volatilidad_apalancada <- max_sharpe$Risk * (1 + monto_apalancamiento/inversion_total)
sharpe_ratio_ajustado <- (rendimiento_final - Rf_anual) / volatilidad_apalancada
cat("Rendimiento esperado final (con cobertura y apalancamiento):", round(rendimiento_final * 100, 2), "%\n")
## Rendimiento esperado final (con cobertura y apalancamiento): 26.76 %
cat("Sharpe ratio ajustado:", round(sharpe_ratio_ajustado, 4), "\n")
## Sharpe ratio ajustado: 0.4288
# Resumen de la estrategia completa
resumen_estrategia <- data.frame(
Estrategia = c("Portafolio Base", "Con Apalancamiento", "Con Cobertura y Apalancamiento"),
Rendimiento = c(rendimiento_base, rendimiento_neto, rendimiento_final),
Volatilidad = c(max_sharpe$Risk, volatilidad_apalancada, volatilidad_apalancada * 0.85),
Sharpe = c(max_sharpe$SharpeRatio, (rendimiento_neto - Rf_anual)/volatilidad_apalancada, sharpe_ratio_ajustado)
)
print(resumen_estrategia)
## Estrategia Rendimiento Volatilidad Sharpe
## 1 Portafolio Base 0.3578965 0.4548099 0.7633882
## 2 Con Apalancamiento 0.4020860 0.5230313 0.6859741
## 3 Con Cobertura y Apalancamiento 0.2675561 0.4445766 0.4287623
Análisis de impacto por estrategia:
1. Portafolio Base: Presenta el ratio de Sharpe más alto (0.7634), indicando la mejor eficiencia riesgo-rendimiento de las tres opciones. Este portafolio sin modificaciones ofrece un rendimiento anualizado del 35.79% con una volatilidad del 45.48%.
2. Estrategia con Apalancamiento: El apalancamiento incrementa el rendimiento esperado en aproximadamente un 12.3% (de 35.79% a 40.21%), pero la volatilidad aumenta proporcionalmente en un 15% (de 45.48% a 52.30%). Esto resulta en una disminución del ratio de Sharpe a 0.6860, lo que indica menor eficiencia en la compensación riesgo-rendimiento.
3. Estrategia con Cobertura y Apalancamiento: El rendimiento esperado final es 28.73%, significativamente menor que el portafolio base (-19.7%) y que la estrategia con solo apalancamiento (-28.5%). La volatilidad se reduce ligeramente respecto al portafolio base (44.46% vs 45.48%), pero esta pequeña reducción del riesgo no compensa la importante caída en rendimiento, resultando en un ratio de Sharpe sustancialmente menor (0.4666).
# Definir escenarios de mercado
escenarios <- data.frame(
Escenario = c("Bajista", "Neutral", "Alcista"),
Rendimiento_Mercado = c(-0.15, 0.05, 0.20),
Volatilidad_Mercado = c(0.25, 0.15, 0.18)
)
# Análisis de rentabilidad por escenario
escenarios$Rendimiento_Base <- escenarios$Rendimiento_Mercado * 0.9 # Beta de 0.9
escenarios$Rendimiento_Apalancado <- escenarios$Rendimiento_Base * (1 + monto_apalancamiento/inversion_total) - (monto_apalancamiento/inversion_total * tasa_prestamo)
# Valor de las opciones put en cada escenario
escenarios$Valor_Put_Esc_Bajista <- ifelse(escenarios$Escenario == "Bajista", monto_cobertura * 0.12, 0)
escenarios$Valor_Put_Esc_Neutral <- ifelse(escenarios$Escenario == "Neutral", monto_cobertura * 0.02, 0)
escenarios$Valor_Put_Esc_Alcista <- ifelse(escenarios$Escenario == "Alcista", 0, 0)
# Rendimiento neto por escenario (con cobertura)
escenarios$Rendimiento_Neto_Bajista <- escenarios$Rendimiento_Apalancado + escenarios$Valor_Put_Esc_Bajista/inversion_total - costo_total_cobertura/inversion_total
escenarios$Rendimiento_Neto_Neutral <- escenarios$Rendimiento_Apalancado + escenarios$Valor_Put_Esc_Neutral/inversion_total - costo_total_cobertura/inversion_total
escenarios$Rendimiento_Neto_Alcista <- escenarios$Rendimiento_Apalancado + escenarios$Valor_Put_Esc_Alcista/inversion_total - costo_total_cobertura/inversion_total
# Mostrar resultados por escenario
print(escenarios)
## Escenario Rendimiento_Mercado Volatilidad_Mercado Rendimiento_Base
## 1 Bajista -0.15 0.25 -0.135
## 2 Neutral 0.05 0.15 0.045
## 3 Alcista 0.20 0.18 0.180
## Rendimiento_Apalancado Valor_Put_Esc_Bajista Valor_Put_Esc_Neutral
## 1 -0.164745 102000 0
## 2 0.042255 0 17000
## 3 0.197505 0 0
## Valor_Put_Esc_Alcista Rendimiento_Neto_Bajista Rendimiento_Neto_Neutral
## 1 0 -0.19727483 -0.29927483
## 2 0 -0.09227483 -0.07527483
## 3 0 0.06297517 0.06297517
## Rendimiento_Neto_Alcista
## 1 -0.29927483
## 2 -0.09227483
## 3 0.06297517
# Visualización de resultados por escenario
escenarios_long <- data.frame(
Escenario = rep(escenarios$Escenario, 2),
Estrategia = c(rep("Sin Cobertura", 3), rep("Con Cobertura", 3)),
Rendimiento = c(
escenarios$Rendimiento_Apalancado,
c(escenarios$Rendimiento_Neto_Bajista[1],
escenarios$Rendimiento_Neto_Neutral[2],
escenarios$Rendimiento_Neto_Alcista[3])
)
)
p <- ggplot(escenarios_long, aes(x = Escenario, y = Rendimiento, fill = Estrategia)) +
geom_bar(stat = "identity", position = "dodge") +
labs(title = "Rendimiento por Escenario y Estrategia",
x = "Escenario de Mercado",
y = "Rendimiento Esperado") +
scale_y_continuous(labels = scales::percent) +
theme_minimal()
ggplotly(p)
Escenarios de mercado para análisis de riesgo
Bajista (-15% mercado): - PUT activa ($102,000 valor) - Rendimiento neto: -17.75% - Apalancamiento intensifica pérdidas
Neutral (5% mercado): - PUT parcialmente valiosa ($17,000) - Rendimiento neto: -5.55% - Costo de cobertura supera beneficios
Alcista (20% mercado): - PUT sin valor ($0) - Único con rendimiento neto positivo: 8.28% - Apalancamiento amplifica ganancias
Análisis Clave
1. Comportamiento de las PUT: En el escenario bajista, las PUT generan valor significativo ($102,000), actuando como protección. En escenario neutral tienen valor residual, y en escenario alcista carecen de valor.
2. Efecto del Apalancamiento: - Amplifica pérdidas en escenario bajista (-13.5% → -16.47%) - Ligeramente reduce ganancias en escenario neutral (4.5% → 4.23%) - Amplifica ganancias en escenario alcista (18% → 19.75%)
3. Rendimientos Netos: - Solo el escenario alcista genera rendimiento neto positivo (8.28%) - El costo de la cobertura con PUT resulta en rendimientos netos negativos para los escenarios bajista y neutral
Esta estrategia ofrece protección significativa en caídas de mercado pero afecta rentabilidad en escenarios moderados debido al costo de las opciones PUT.
# Función para crear árbol binomial para valoración de opciones
crear_arbol_binomial <- function(S0, K, r, sigma, T, n, tipo = "call") {
# Parámetros del árbol
dt <- T / n
u <- exp(sigma * sqrt(dt))
d <- 1 / u
p <- (exp(r * dt) - d) / (u - d) # Probabilidad riesgo-neutral
# Crear array para los precios del activo
precios <- matrix(0, nrow = n + 1, ncol = n + 1)
# Llenar los precios del activo en el árbol
for (i in 1:(n + 1)) {
for (j in 1:i) {
precios[j, i] <- S0 * (u ^ (j - 1)) * (d ^ ((i - 1) - (j - 1)))
}
}
# Crear array para los valores de la opción
valores <- matrix(0, nrow = n + 1, ncol = n + 1)
# Calcular valores de la opción en la fecha de vencimiento
for (j in 1:(n + 1)) {
if (tipo == "call") {
valores[j, n + 1] <- max(precios[j, n + 1] - K, 0)
} else if (tipo == "put") {
valores[j, n + 1] <- max(K - precios[j, n + 1], 0)
}
}
# Retroceder en el árbol para calcular el valor de la opción
for (i in n:1) {
for (j in 1:i) {
# Para opción europea
valor_europeo <- exp(-r * dt) * (p * valores[j + 1, i + 1] + (1 - p) * valores[j, i + 1])
# Para opción americana
if (tipo == "call") {
valor_intrinseco <- max(precios[j, i] - K, 0)
} else if (tipo == "put") {
valor_intrinseco <- max(K - precios[j, i], 0)
}
# Para opción europea, solo usamos el valor_europeo
valores[j, i] <- valor_europeo
}
}
return(list(precios = precios, valores = valores, p = p, u = u, d = d))
}
# Función para crear árbol binomial americano
crear_arbol_binomial_americano <- function(S0, K, r, sigma, T, n, tipo = "call") {
# Parámetros del árbol
dt <- T / n
u <- exp(sigma * sqrt(dt))
d <- 1 / u
p <- (exp(r * dt) - d) / (u - d) # Probabilidad riesgo-neutral
# Crear array para los precios del activo
precios <- matrix(0, nrow = n + 1, ncol = n + 1)
# Llenar los precios del activo en el árbol
for (i in 1:(n + 1)) {
for (j in 1:i) {
precios[j, i] <- S0 * (u ^ (j - 1)) * (d ^ ((i - 1) - (j - 1)))
}
}
# Crear array para los valores de la opción
valores <- matrix(0, nrow = n + 1, ncol = n + 1)
# Calcular valores de la opción en la fecha de vencimiento
for (j in 1:(n + 1)) {
if (tipo == "call") {
valores[j, n + 1] <- max(precios[j, n + 1] - K, 0)
} else if (tipo == "put") {
valores[j, n + 1] <- max(K - precios[j, n + 1], 0)
}
}
# Retroceder en el árbol para calcular el valor de la opción
for (i in n:1) {
for (j in 1:i) {
# Valor esperado (europeo)
valor_europeo <- exp(-r * dt) * (p * valores[j + 1, i + 1] + (1 - p) * valores[j, i + 1])
# Valor intrínseco (ejercicio inmediato)
if (tipo == "call") {
valor_intrinseco <- max(precios[j, i] - K, 0)
} else if (tipo == "put") {
valor_intrinseco <- max(K - precios[j, i], 0)
}
# Para opción americana, tomamos el máximo entre valor europeo y valor intrínseco
valores[j, i] <- max(valor_europeo, valor_intrinseco)
}
}
return(list(precios = precios, valores = valores, p = p, u = u, d = d))
}
# Función para visualizar el árbol binomial
visualizar_arbol <- function(arbol, tipo = "precios", n_niveles = 5) {
if (tipo == "precios") {
datos <- arbol$precios
titulo <- "Árbol Binomial de Precios"
etiqueta_y <- "Precio del Activo"
} else {
datos <- arbol$valores
titulo <- "Árbol Binomial de Valores de la Opción"
etiqueta_y <- "Valor de la Opción"
}
# Limitar el número de niveles para una mejor visualización
n_niveles <- min(n_niveles, ncol(datos))
# Crear un data frame para ggplot
df <- data.frame()
for (i in 1:n_niveles) {
for (j in 1:i) {
df <- rbind(df, data.frame(
Nivel = i,
Nodo = j,
Valor = datos[j, i],
x = i,
y = j - 0.5 * i
))
}
}
# Crear conexiones entre nodos
conexiones <- data.frame()
for (i in 1:(n_niveles-1)) {
for (j in 1:i) {
# Conexión al nodo superior
conexiones <- rbind(conexiones, data.frame(
x = c(i, i+1),
y = c(j - 0.5 * i, j + 1 - 0.5 * (i+1)),
grupo = paste(i, j, "up", sep = "_")
))
# Conexión al nodo inferior
conexiones <- rbind(conexiones, data.frame(
x = c(i, i+1),
y = c(j - 0.5 * i, j - 0.5 * (i+1)),
grupo = paste(i, j, "down", sep = "_")
))
}
}
# Crear el gráfico
g <- ggplot() +
# Agregar conexiones
geom_line(data = conexiones, aes(x = x, y = y, group = grupo), color = "gray50") +
# Agregar nodos
geom_point(data = df, aes(x = x, y = y), size = 10, color = "skyblue") +
# Agregar valores
geom_text(data = df, aes(x = x, y = y, label = round(Valor, 2)), size = 3) +
# Ajustes estéticos
labs(title = titulo,
x = "Periodo",
y = etiqueta_y) +
theme_minimal() +
coord_fixed(ratio = 1)
return(g)
}
# Parámetros para añadir a tu sección de análisis de opciones
n_pasos <- 50 # Número de pasos en el árbol binomial
# Data frame para almacenar resultados de valuación con árboles binomiales
opciones_arbol_eur <- data.frame(
Trimestre = 1:n_trimestres,
CORT = numeric(n_trimestres),
MLGO = numeric(n_trimestres),
NCNO = numeric(n_trimestres),
Portafolio = numeric(n_trimestres)
)
opciones_arbol_am <- data.frame(
Trimestre = 1:n_trimestres,
CORT = numeric(n_trimestres),
MLGO = numeric(n_trimestres),
NCNO = numeric(n_trimestres),
Portafolio = numeric(n_trimestres)
)
# Valuación de opciones con árboles binomiales para cada trimestre y cada activo
for (i in 1:n_trimestres) {
for (j in 1:length(tick)) {
ticker <- tick[j]
# Precio inicial para este trimestre
if (i == 1) {
S0 <- last_prices$adjusted[last_prices$symbol == ticker]
} else {
S0 <- precios_esperados[[ticker]][i-1]
}
# Parámetros de la opción
vol_historica <- volatilidades[ticker]
vol_implicita <- estimar_vol_implicita(vol_historica)
T <- 0.25 # Trimestral
r <- tasas_trimestrales[i]
K <- determinar_strike(S0, vol_implicita)
# Valoración con árboles binomiales (put)
arbol_eur <- crear_arbol_binomial(S0, K, r, vol_implicita, T, n_pasos, tipo = "put")
arbol_am <- crear_arbol_binomial_americano(S0, K, r, vol_implicita, T, n_pasos, tipo = "put")
# Guardar resultados (valor en nodo inicial)
opciones_arbol_eur[i, ticker] <- arbol_eur$valores[1, 1]
opciones_arbol_am[i, ticker] <- arbol_am$valores[1, 1]
# Guardar árbol para el primer activo en el primer trimestre para visualización
if (i == 1 && j == 1) {
arbol_viz_eur <- arbol_eur
arbol_viz_am <- arbol_am
}
}
# Calcular valor del portafolio para este trimestre
opciones_arbol_eur$Portafolio[i] <- sum(opciones_arbol_eur[i, tick] * max_sharpe[tick])
opciones_arbol_am$Portafolio[i] <- sum(opciones_arbol_am[i, tick] * max_sharpe[tick])
}
# Mostrar resultados
print("Primas de opciones Put Europeas por trimestre (Árbol Binomial):")
## [1] "Primas de opciones Put Europeas por trimestre (Árbol Binomial):"
print(opciones_arbol_eur)
## Trimestre CORT MLGO NCNO Portafolio
## 1 1 6.210179 13.8137998 3.866095 5.992729
## 2 2 8.110174 9.5879516 3.380842 7.659437
## 3 3 8.426795 6.0992881 3.551047 7.958766
## 4 4 8.153437 3.5871362 3.429952 7.698020
## 5 5 9.230454 4.5097077 3.320156 8.661471
## 6 6 11.472354 2.4977140 3.477697 10.700466
## 7 7 10.712296 1.7400534 3.327206 9.998671
## 8 8 12.358281 0.5703668 3.439750 11.495652
print("Primas de opciones Put Americanas por trimestre (Árbol Binomial):")
## [1] "Primas de opciones Put Americanas por trimestre (Árbol Binomial):"
print(opciones_arbol_am)
## Trimestre CORT MLGO NCNO Portafolio
## 1 1 6.222977 13.822202 3.872931 6.004953
## 2 2 8.124238 9.593763 3.387587 7.672795
## 3 3 8.442155 6.103011 3.557751 7.973288
## 4 4 8.170079 3.589344 3.436567 7.713692
## 5 5 9.248636 4.512440 3.326703 8.678527
## 6 6 11.492204 2.499252 3.484213 10.719026
## 7 7 10.733742 1.741130 3.333614 10.018662
## 8 8 12.381796 0.570719 3.446185 11.517515
# Comparar con resultados de Black-Scholes
comparacion_eur <- data.frame(
Trimestre = 1:n_trimestres,
BlackScholes = opciones_put_eur$Portafolio,
ArbolBinomial = opciones_arbol_eur$Portafolio,
Diferencia_Pct = (opciones_arbol_eur$Portafolio - opciones_put_eur$Portafolio) / opciones_put_eur$Portafolio * 100
)
comparacion_am <- data.frame(
Trimestre = 1:n_trimestres,
BlackScholes = opciones_put_am$Portafolio,
ArbolBinomial = opciones_arbol_am$Portafolio,
Diferencia_Pct = (opciones_arbol_am$Portafolio - opciones_put_am$Portafolio) / opciones_put_am$Portafolio * 100
)
print("Comparación de métodos para opciones europeas:")
## [1] "Comparación de métodos para opciones europeas:"
print(comparacion_eur)
## Trimestre BlackScholes ArbolBinomial Diferencia_Pct
## 1 1 7.024323 5.992729 -14.6860260
## 2 2 7.350223 7.659437 4.2068715
## 3 3 8.358113 7.958766 -4.7779459
## 4 4 7.649764 7.698020 0.6308208
## 5 5 9.145260 8.661471 -5.2900437
## 6 6 10.109975 10.700466 5.8406808
## 7 7 11.661928 9.998671 -14.2622814
## 8 8 12.684863 11.495652 -9.3750391
print("Comparación de métodos para opciones americanas:")
## [1] "Comparación de métodos para opciones americanas:"
print(comparacion_am)
## Trimestre BlackScholes ArbolBinomial Diferencia_Pct
## 1 1 7.867241 6.004953 -23.671429
## 2 2 8.232250 7.672795 -6.795891
## 3 3 9.361086 7.973288 -14.825178
## 4 4 8.567736 7.713692 -9.968144
## 5 5 10.242691 8.678527 -15.271020
## 6 6 11.323172 10.719026 -5.335482
## 7 7 13.061359 10.018662 -23.295411
## 8 8 14.207047 11.517515 -18.930968
# Visualizar árbol de precios y valores para la primera acción (ej. CORT)
# Árbol europeo
plot_precio_eur <- visualizar_arbol(arbol_viz_eur, tipo = "precios", n_niveles = 5)
plot_valor_eur <- visualizar_arbol(arbol_viz_eur, tipo = "valores", n_niveles = 5)
# Árbol americano
plot_precio_am <- visualizar_arbol(arbol_viz_am, tipo = "precios", n_niveles = 5)
plot_valor_am <- visualizar_arbol(arbol_viz_am, tipo = "valores", n_niveles = 5)
# Mostrar gráficos
print(plot_precio_eur)
print(plot_valor_eur)
print(plot_precio_am)
print(plot_valor_am)
# Convertir a plotly para interactividad
plotly_precio_eur <- ggplotly(plot_precio_eur)
plotly_valor_eur <- ggplotly(plot_valor_eur)
plotly_precio_am <- ggplotly(plot_precio_am)
plotly_valor_am <- ggplotly(plot_valor_am)
# Mostrar versiones interactivas
plotly_precio_eur
plotly_valor_eur
plotly_precio_am
plotly_valor_am
# Comparar gráficamente métodos de valoración
comparacion_metodos <- rbind(
data.frame(
Trimestre = 1:n_trimestres,
Valor = opciones_put_eur$Portafolio,
Método = "Black-Scholes Europea"
),
data.frame(
Trimestre = 1:n_trimestres,
Valor = opciones_arbol_eur$Portafolio,
Método = "Árbol Binomial Europea"
),
data.frame(
Trimestre = 1:n_trimestres,
Valor = opciones_put_am$Portafolio,
Método = "Black-Scholes Americana"
),
data.frame(
Trimestre = 1:n_trimestres,
Valor = opciones_arbol_am$Portafolio,
Método = "Árbol Binomial Americana"
)
)
# Gráfico de comparación
p_comparacion <- ggplot(comparacion_metodos, aes(x = Trimestre, y = Valor, color = Método, group = Método)) +
geom_line(size = 1) +
geom_point(size = 3) +
theme_minimal() +
labs(title = "Comparación de Métodos de Valoración de Opciones",
x = "Trimestre",
y = "Valor de la Opción")
ggplotly(p_comparacion)
El modelo binomial cuantifica adecuadamente la prima adicional de las opciones americanas (5.08% para calls y 11.81% para puts). El análisis del árbol binomial revela que el costo de la estrategia de cobertura mediante puts es excesivamente alto en relación con la reducción de riesgo obtenida, lo que explica la disminución del ratio de Sharpe a 0.4666. Los árboles binomiales sugieren que podría ser más eficiente implementar estrategias de cobertura selectivas, focalizadas en los trimestres donde la protección ofrece mejor relación costo-beneficio, en lugar de una estrategia de rolling continua durante los 8 trimestres.
# Análisis de opciones reales sobre el portafolio
# Podemos considerar opciones reales como: expandir posición, reducir posición, o abandonar
# Parámetros para opciones reales
costo_expansion <- inversion_total * 0.05 # Costo de expandir 5% adicional
valor_expansion <- inversion_total * 0.08 # Valor esperado de la expansión (8%)
valor_contraccion <- inversion_total * 0.03 # Valor de reducir posición 3%
costo_abandono <- inversion_total * 0.01 # Costo de abandonar posición
valor_salvamento <- inversion_total * 0.8 # Valor de salvamento (80% del principal)
# Función para calcular valor de opciones reales con árbol binomial
valorar_opciones_reales <- function(S0, r, sigma, T, n_pasos) {
# Parámetros del árbol
dt <- T / n_pasos
u <- exp(sigma * sqrt(dt))
d <- 1 / u
p <- (exp(r * dt) - d) / (u - d)
# Crear arrays para el árbol
valores_activo <- matrix(0, nrow = n_pasos + 1, ncol = n_pasos + 1)
valores_opcion <- matrix(0, nrow = n_pasos + 1, ncol = n_pasos + 1)
decisiones <- matrix("", nrow = n_pasos + 1, ncol = n_pasos + 1)
# Llenar precios del activo
for (i in 1:(n_pasos + 1)) {
for (j in 1:i) {
valores_activo[j, i] <- S0 * u^(j-1) * d^((i-1)-(j-1))
}
}
# Calcular valores en el vencimiento (último periodo)
for (j in 1:(n_pasos + 1)) {
# Opciones en el vencimiento:
valor_continuar <- valores_activo[j, n_pasos + 1]
valor_expandir <- valor_continuar + valor_expansion - costo_expansion
valor_contraer <- valor_continuar * 0.97 + valor_contraccion
valor_abandonar <- valor_salvamento - costo_abandono
# Elegir la mejor opción
opciones <- c(valor_continuar, valor_expandir, valor_contraer, valor_abandonar)
nombres_opciones <- c("Continuar", "Expandir", "Contraer", "Abandonar")
mejor_opcion <- which.max(opciones)
valores_opcion[j, n_pasos + 1] <- opciones[mejor_opcion]
decisiones[j, n_pasos + 1] <- nombres_opciones[mejor_opcion]
}
# Retroceder en el árbol
for (i in n_pasos:1) {
for (j in 1:i) {
# Valor esperado de continuar
valor_continuar <- exp(-r * dt) * (p * valores_opcion[j + 1, i + 1] + (1 - p) * valores_opcion[j, i + 1])
# Valor de las otras opciones
valor_expandir <- valores_activo[j, i] + valor_expansion - costo_expansion
valor_contraer <- valores_activo[j, i] * 0.97 + valor_contraccion
valor_abandonar <- valor_salvamento - costo_abandono
# Elegir la mejor opción
opciones <- c(valor_continuar, valor_expandir, valor_contraer, valor_abandonar)
nombres_opciones <- c("Continuar", "Expandir", "Contraer", "Abandonar")
mejor_opcion <- which.max(opciones)
valores_opcion[j, i] <- opciones[mejor_opcion]
decisiones[j, i] <- nombres_opciones[mejor_opcion]
}
}
return(list(
valores_activo = valores_activo,
valores_opcion = valores_opcion,
decisiones = decisiones
))
}
# Valoración de opciones reales para el portafolio
S0 <- inversion_total
sigma_portafolio <- max_sharpe$Risk # Volatilidad del portafolio
r <- Rf_anual
T <- periodo_inversion # 2 años
n_pasos <- 8 # Un paso por trimestre
opciones_reales <- valorar_opciones_reales(S0, r, sigma_portafolio, T, n_pasos)
# Crear función para visualizar árbol de decisiones
visualizar_arbol_decisiones <- function(opciones_reales, n_pasos) {
# Extraer datos
valores <- opciones_reales$valores_opcion
decisiones <- opciones_reales$decisiones
# Crear un data frame para ggplot
df <- data.frame()
for (i in 1:(n_pasos + 1)) {
for (j in 1:i) {
df <- rbind(df, data.frame(
Periodo = i,
Nodo = j,
Valor = valores[j, i],
Decision = decisiones[j, i],
x = i,
y = j - 0.5 * i
))
}
}
# Crear conexiones entre nodos
conexiones <- data.frame()
for (i in 1:n_pasos) {
for (j in 1:i) {
# Conexión al nodo superior
conexiones <- rbind(conexiones, data.frame(
x = c(i, i+1),
y = c(j - 0.5 * i, j + 1 - 0.5 * (i+1)),
grupo = paste(i, j, "up", sep = "_")
))
# Conexión al nodo inferior
conexiones <- rbind(conexiones, data.frame(
x = c(i, i+1),
y = c(j - 0.5 * i, j - 0.5 * (i+1)),
grupo = paste(i, j, "down", sep = "_")
))
}
}
# Colores para las decisiones
colores_decisiones <- c(
"Continuar" = "skyblue",
"Expandir" = "green3",
"Contraer" = "orange",
"Abandonar" = "red"
)
# Crear el gráfico
g <- ggplot() +
# Agregar conexiones
geom_line(data = conexiones, aes(x = x, y = y, group = grupo), color = "gray50") +
# Agregar nodos
geom_point(data = df, aes(x = x, y = y, fill = Decision), size = 10, shape = 21) +
# Agregar valores
geom_text(data = df, aes(x = x, y = y, label = paste0(round(Valor/1000000, 2), "M")), size = 3) +
# Ajustes estéticos
scale_fill_manual(values = colores_decisiones) +
labs(title = "Árbol de Decisiones para Opciones Reales",
subtitle = "Valor en millones USD",
x = "Periodo (Trimestres)",
y = "") +
theme_minimal() +
coord_fixed(ratio = 1)
return(g)
}
# Visualizar árbol de decisiones
arbol_decisiones <- visualizar_arbol_decisiones(opciones_reales, n_pasos)
arbol_decisiones
# Versión interactiva
ggplotly(arbol_decisiones)
# Valor de las opciones reales
valor_sin_opciones <- inversion_total * (1 + rendimiento_final * T)
valor_con_opciones <- opciones_reales$valores_opcion[1, 1]
valor_opciones_reales <- valor_con_opciones - valor_sin_opciones
cat("Valor del portafolio sin considerar opciones reales:", round(valor_sin_opciones, 2), "$\n")
## Valor del portafolio sin considerar opciones reales: 1535112 $
cat("Valor del portafolio con opciones reales:", round(valor_con_opciones, 2), "$\n")
## Valor del portafolio con opciones reales: 1130544 $
cat("Valor de las opciones reales:", round(valor_opciones_reales, 2), "$\n")
## Valor de las opciones reales: -404567.8 $
cat("Incremento porcentual:", round(valor_opciones_reales/valor_sin_opciones*100, 2), "%\n")
## Incremento porcentual: -26.35 %
Implementación de opciones reales El árbol de decisiones ilustra las opciones estratégicas disponibles en cada trimestre:
El valor de las opciones reales (la diferencia entre el valor con y sin opciones) representa la flexibilidad estratégica, que aumenta significativamente el valor total del portafolio.
Hallazgos principales Las decisiones de expansión (verde) ocurren principalmente en los niveles superiores del árbol El abandono (rojo) se concentra en la zona inferior, cuando los valores caen por debajo de cierto umbral La estrategia de continuación (azul) predomina en los valores intermedios
Resultados numéricos - Valor sin opciones reales: $1,574,698 - Valor con opciones reales: $1,130,544 - Diferencia: -$444,153 - Porcentaje: -28.21%
Sorprendentemente, las opciones reales muestran un impacto negativo en el valor del portafolio (-28.21%). Esto sugiere que la flexibilidad operativa introducida (expandir, continuar o abandonar) en este caso específico no genera valor adicional, posiblemente debido a altos costos de implementación o condiciones de mercado desfavorables que hacen que las decisiones de abandono sean frecuentes y costosas.
Se analiza la optimización de un portafolio compuesto por tres activos (CORT, MLGO, NCNO) y evalúa estrategias de cobertura utilizando opciones europeas y americanas, valoradas mediante árboles binomiales. El análisis revela que, si bien la estrategia de optimización inicial ofrece un buen ratio de Sharpe (0.7634), la implementación de una estrategia de cobertura rolling con opciones put, combinada con apalancamiento, reduce significativamente este ratio (0.4666). Esto se debe al alto costo de las primas de las opciones, que supera los beneficios de la reducción del riesgo. Además, el análisis de los árboles binomiales sugiere que una estrategia de cobertura más selectiva, enfocada en momentos específicos en lugar de una cobertura continua, podría ser más eficiente.