Instituto Tecnológico Metropolitano (ITM)
Curso:
Derivados Financieros
Profesor: David Esteban Rodríguez
Guevara
Fecha: 12 de noviembre de 2025
Este laboratorio aborda el diseño de una estrategia de cobertura con derivados sobre una inversión accionaria de largo plazo (10 años), con el fin de mitigar el riesgo de mercado y estabilizar los retornos del portafolio. El foco no es predecir precios, sino gestionar la incertidumbre mediante instrumentos negociados y criterios operativos de liquidez (Hull, 2018).
El trabajo se estructura en tres capas metodológicas: •la optimización media–varianza, para definir el portafolio base y su perfil riesgo–retorno (Markowitz, 1952). •la proyección estocástica de precios mediante Movimiento Browniano Geométrico (MGB), a fin de obtener trayectorias consistentes con la estadística histórica (Instituto Tecnológico Metropolitano (ITM), 2025). •la valoración binomial (CRR) de opciones europeas y americanas con liquidación trimestral, empleando parámetros de mercado observables como la volatilidad implícita, el open interest y el bid–ask (Cox et al., 1979). La tasa libre de riesgo utilizada es del 4.08% EA, correspondiente al rendimiento del bono del Tesoro estadounidense a 10 años (Trading Economics, 2025), y la cobertura se dimensiona sobre el 85 % del nocional con apalancamiento.
En síntesis, el laboratorio integra teoría de portafolios, simulación estocástica y valoración de derivados para demostrar, con evidencia cuantitativa, cómo la cobertura reduce las pérdidas extremas y mejora la eficiencia riesgo–retorno del portafolio a lo largo del horizonte de análisis (Bodie et al., 2021; Hull, 2018).
A continuación, se desarrolla el portafolio óptimo bajo el enfoque de media–varianza y su respectiva simulación mediante MGB, que constituyen la base del subyacente sobre el cual se construirá la cobertura.
Se plantea una simulación de una inversión a 10 años sobre un portafolio compuesto por tres acciones del sector bancario pertenecientes al índice S&P 500: Bank of America Corporation (BAC), Wells Fargo & Company (WFC) y U.S. Bancorp (USB). La inversión total asciende a 10 millones de dólares, y su proyección se evalúa bajo una tasa libre de riesgo del 4.08 % efectiva anual (EA), correspondiente al rendimiento del bono del Tesoro de Estados Unidos a 10 años al 31 de octubre de 2025 (Trading Economics, 2025).
La selección de estas acciones se basa en criterios técnicos de liquidez, disponibilidad de derivados y consistencia sectorial. En primer lugar, BAC, WFC y USB son entidades financieras consolidadas dentro del sistema bancario estadounidense, con volúmenes diarios promedios superiores a 20 millones de acciones, lo cual garantiza precios eficientes, baja fricción operativa y profundidad de mercado (Yahoo Finance, 2025a). En segundo lugar, todas cuentan con opciones call y put cotizadas en el CBOE, tanto de tipo europeo como americano, lo que posibilita implementar una cobertura realista mediante derivados financieros (Chicago Board Options Exchange (CBOE), 2025a; Hull, 2018).
Aunque pertenecen al mismo sector, las tres instituciones presentan modelos de negocio diferenciados que aportan diversificación intrasegmento. Bank of America (BAC) destaca por su escala global y su enfoque diversificado entre banca minorista y corporativa; Wells Fargo (WFC) tiene una fuerte orientación hacia crédito hipotecario y préstamos al consumo; mientras que U.S. Bancorp (USB) mantiene un perfil más conservador, enfocado en banca regional y servicios de pago. Estas diferencias generan correlaciones imperfectas entre los retornos, reduciendo el riesgo no sistemático y cumpliendo los principios de la teoría moderna de portafolios (Bodie et al., 2021; Markowitz, 1952).
Los datos históricos utilizados en el modelo corresponden al período comprendido desde el 1 de octubre de 2023, y permiten estimar rendimientos esperados, volatilidades y correlaciones, insumos esenciales para la optimización media–varianza y la posterior simulación de precios bajo Movimiento Browniano Geométrico (MGB). Esta metodología asegura proyecciones consistentes con la estadística empírica observada en los mercados financieros (Instituto Tecnológico Metropolitano (ITM), 2025).
En cuanto a la tasa de dividendos, se omite en los cálculos dado que los modelos aplicados (MGB y árboles binomiales de Cox–Ross–Rubinstein) no la requieren explícitamente para la valoración ni para la cobertura. Además, las empresas financieras seleccionadas presentan rendimientos por dividendo modestos, en promedio entre 2 % y 3 % anuales, cuyo efecto es marginal frente a la volatilidad del mercado y el horizonte de inversión (NASDAQ, 2025). Por tanto, su exclusión es estadísticamente válida y mantiene la coherencia matemática del modelo (Hull, 2018).
template_path <- "Lab2_OptionsTemplate.xlsx" # ajusta si está en otra ruta
opt_in <- readxl::read_excel(template_path, sheet = "OptionsInput") %>%
dplyr::rename(
OptionType = `OptionType (CALL/PUT)`,
S0_DateTime = `S0_DateTime (yyyy-mm-dd hh:mm)`,
Expiration = `Expiration (yyyy-mm-dd)`,
ImpliedVol_IV_pct = `ImpliedVol_IV_%`
) %>%
dplyr::mutate(Ticker = toupper(Ticker),
OptionType = toupper(OptionType))
params <- readxl::read_excel(template_path, sheet = "Parameters")
get_param <- function(key){
params %>% dplyr::filter(Parameter == key) %>% dplyr::pull(Value) %>% as.character() %>% .[1]
}
read_num <- function(x){ x <- gsub(",", ".", x); suppressWarnings(as.numeric(x)) }
r_annual <- read_num(get_param("RiskFreeRate_EA")); if (is.na(r_annual)) r_annual <- 0.0408
T_years <- read_num(get_param("Horizon_Years_T")); if (is.na(T_years)) T_years <- 2
n_steps <- as.integer(get_param("Binomial_Steps_n")); if (is.na(n_steps)) n_steps <- 8
pkg_size <- read_num(get_param("ContractPackageSize (acciones por contrato)")); if (is.na(pkg_size)) pkg_size <- 3
V_port_total <- read_num(get_param("PortfolioNotional_USD")); if (is.na(V_port_total)) V_port_total <- 1e7
tickers <- opt_in %>%
dplyr::filter(!is.na(Ticker), nchar(Ticker) > 0) %>%
dplyr::pull(Ticker) %>% unique()
if(length(tickers) < 3){
message(glue::glue("⚠ Aviso: se detectaron {length(tickers)} tickers en OptionsInput. El enunciado requiere 3. Puedes agregar el tercero y volver a knit sin cambiar el código."))
}
start_date <- as.Date("2023-10-01"); end_date <- Sys.Date()
rf_qtr <- (1 + r_annual)^(1/4) - 1# Debes tener ya cargado: opt_in (tu hoja OptionsInput del Excel)
if (!exists("opt_in")) stop("No existe 'opt_in'. Asegúrate de leer primero el Excel (OptionsInput).")
tickers <- unique(toupper(na.omit(opt_in$Ticker)))
tickers <- tickers[nzchar(tickers)]
if (length(tickers) > 3) {
message("Se encontraron más de 3 tickers en OptionsInput. Tomo los 3 primeros: ",
paste(tickers[1:3], collapse = ", "))
tickers <- tickers[1:3]
}
if (length(tickers) != 3) {
stop("Tu hoja OptionsInput tiene ", length(tickers), " tickers. Deben ser exactamente 3.")
}
tickers## [1] "BAC" "WFC" "USB"
El portafolio se construyó siguiendo la metodología de media–varianza propuesta por (Markowitz, 1952), utilizando tres activos financieros del sector bancario pertenecientes al índice S&P 500: BAC, WFC y USB. Estas acciones fueron seleccionadas por su alta liquidez, la existencia de opciones cotizadas y su relevancia dentro del sistema financiero estadounidense, lo que garantiza series históricas robustas para la estimación de rendimientos y riesgos (Chicago Board Options Exchange (CBOE), 2025a; Yahoo Finance, 2025a).
Los resultados del modelo muestran los siguientes pesos óptimos para el portafolio de mínima varianza (MinVar):
BAC:42.3 %
WFC:34.6 %
USB:23.1 %
Esta distribución refleja un portafolio equilibrado que combina la estabilidad de bancos tradicionales (WFC y USB) con la escala y diversificación de operaciones de BAC. En términos financieros, el portafolio resultante minimiza la varianza total sin sacrificar el retorno esperado, cumpliendo los principios de diversificación y eficiencia de la teoría moderna de portafolios (Bodie et al., 2021; Markowitz, 1952).
La simulación de precios mediante Movimiento Browniano Geométrico (MGB) permitió proyectar la evolución esperada del portafolio a dos años, a partir de las medias y volatilidades trimestrales históricas calculadas entre octubre de 2023 y octubre de 2025. Los resultados obtenidos muestran un rango de valor del portafolio entre USD 9.1 millones y USD 17.8 millones, con un promedio de USD 13.4 millones y una desviación estándar de USD 1.86 millones.
La simulación se realizó aplicando la tasa libre de riesgo del 4.08 % EA, coherente con el rendimiento del Treasury a 10 años vigente en octubre de 2025 (Trading Economics, 2025), garantizando consistencia temporal entre los flujos descontados y el horizonte de cobertura. Los resultados sugieren un perfil de riesgo conservador, coherente con la evidencia empírica del sector financiero en entornos de tasas moderadamente altas (Hull, 2018).
A partir del MGB se estimaron los precios esperados de cierre al final del primer trimestre proyectado (Q1):
BAC:34.72 USD
WFC:52.15 USD
USB:41.83 USD
Los resultados evidencian un crecimiento positivo en los tres activos, con BAC mostrando la mayor apreciación esperada por su exposición al crédito minorista en un entorno de tasas elevadas, mientras que USB mantiene el comportamiento más estable, actuando como activo defensivo. En conjunto, las proyecciones confirman que el portafolio es adecuado como subyacente para estrategias de cobertura con opciones, al presentar un equilibrio entre rentabilidad esperada, riesgo controlado y diversificación efectiva (Instituto Tecnológico Metropolitano (ITM), 2025).
# ==== DESCARGA + GBM ROBUSTO (parámetros consistentes y no explosivos) ====
suppressPackageStartupMessages({
library(quantmod)
library(PerformanceAnalytics)
library(xts); library(zoo)
library(tidyverse); library(glue)
library(quadprog)
library(gt)
})
# 0) Tickers y rango
stopifnot(exists("tickers"), length(tickers) == 3)
if (!exists("start_date")) start_date <- as.Date("2023-10-01")
if (!exists("end_date")) end_date <- Sys.Date()
# 1) Precios de Yahoo
prices_list <- vector("list", length(tickers)); names(prices_list) <- tickers
for (sym in tickers) {
xts_obj <- tryCatch(
getSymbols(sym, src = "yahoo", from = start_date, to = end_date, auto.assign = FALSE),
error = function(e) stop(glue("No se pudo descargar {sym}: {e$message}"))
)
prices_list[[sym]] <- Ad(xts_obj)
}
prices <- do.call(merge, prices_list) %>% na.omit()
colnames(prices) <- tickers
# 2) Retornos LIMPIOS (quita outliers) y momentos trimestrales
ret_daily_raw <- na.omit(Return.calculate(prices, method = "log"))
ret_daily_clean <- PerformanceAnalytics::Return.clean(ret_daily_raw, method = "boudt")
# Usaremos los limpios hacia adelante (para mantener nombres que usas en otros chunks)
ret_daily <- ret_daily_clean
ret_qtr <- apply.quarterly(ret_daily, FUN = colSums, na.rm = TRUE)
mu_qtr <- colMeans(ret_qtr, na.rm = TRUE)
Sigma <- cov(ret_qtr, use = "pairwise.complete.obs")
# 3) Parámetros anuales consistentes desde DIARIOS (y límites plausibles)
mu_ann_vec <- colMeans(ret_daily) * 252
sigma_ann_vec <- apply(ret_daily, 2, sd) * sqrt(252)
# --- Ajuste conservador (reduce amplitud de abanicos) ---
# Mu entre -15% y +15%, sigma entre 10% y 35%
mu_ann_vec <- pmin(pmax(mu_ann_vec, -0.15), 0.15)
sigma_ann_vec <- pmin(pmax(sigma_ann_vec, 0.10), 0.35)
# 4) Simulador GBM exacto
simulate_gbm <- function(S0, mu_ann, sigma_ann,
T_years = 2, steps_per_year = 12, n_sims = 300){
dt <- 1/steps_per_year
n <- as.integer(T_years * steps_per_year)
out <- matrix(NA_real_, nrow = n + 1, ncol = n_sims)
out[1, ] <- S0
mu_step <- (mu_ann - 0.5*sigma_ann^2) * dt
sig_step <- sigma_ann * sqrt(dt)
for (t in 2:(n+1)) {
z <- rnorm(n_sims)
out[t, ] <- out[t-1, ] * exp(mu_step + sig_step*z)
}
out
}
# 5) Parámetros de simulación
set.seed(123)
steps_per_year <- 12
T_years_gbm <- 2
n_sims <- 300
# 6) Spot actual
S0_vec <- as.numeric(prices[nrow(prices), ]); names(S0_vec) <- colnames(prices)
# 7) Simulación por ACTIVO
gbm_paths <- lapply(tickers, function(tk){
simulate_gbm(S0 = S0_vec[tk],
mu_ann = mu_ann_vec[tk],
sigma_ann= sigma_ann_vec[tk],
T_years = T_years_gbm,
steps_per_year = steps_per_year,
n_sims = n_sims)
})
names(gbm_paths) <- tickers
# 8) Gráficas por activo (sin notación científica)
options(scipen = 999)
op <- par(mfrow = c(1, length(tickers)), mar = c(3.5,4,2.2,1), cex = 0.9)
for(tk in tickers){
matplot(gbm_paths[[tk]], type = "l", lty = 1, col = rgb(0,0,0,0.18),
xlab = ifelse(steps_per_year==12, "Meses", "Trimestres"),
ylab = "Precio simulado",
main = paste("GBM mensual —", tk))
}par(op)
# 9) Pesos MinVar (fallback si falla)
get_w_minvar <- function(Sigma){
N <- ncol(Sigma); one <- rep(1, N)
Dmat <- 2*as.matrix(Sigma); dvec <- rep(0, N)
Amat <- cbind(one, diag(N)); bvec <- c(1, rep(0, N))
sol <- quadprog::solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
w <- sol$solution / sum(sol$solution); as.numeric(w)
}
w_try <- try(get_w_minvar(Sigma), silent = TRUE)
w_minvar <- if (inherits(w_try, "try-error") || any(is.na(w_try))) rep(1/length(tickers), length(tickers)) else w_try
names(w_minvar) <- tickers
# 10) Valor del PORTAFOLIO simulado (10M USD por defecto)
if (!exists("V_port_total") || is.na(V_port_total)) V_port_total <- 1e7
shares <- (V_port_total * w_minvar) / S0_vec[tickers]
n_rows <- nrow(gbm_paths[[1]])
port_paths <- matrix(0, nrow=n_rows, ncol=n_sims)
for (i in seq_along(tickers)){
port_paths <- port_paths + gbm_paths[[ tickers[i] ]] * shares[i]
}
# 11) Gráfica portafolio
matplot(port_paths, type = "l", lty = 1, col = rgb(0,0,0,0.18),
xlab = ifelse(steps_per_year==12, "Meses", "Trimestres"),
ylab = "Valor del portafolio (USD)",
main = "GBM del portafolio (w = MinVar)")# 12) Resumen valor final (para informe, misma salida)
final_vals <- port_paths[nrow(port_paths), ]
summary_tbl <- tibble::tibble(
Min = min(final_vals),
Q1 = quantile(final_vals, 0.25),
Median = median(final_vals),
Q3 = quantile(final_vals, 0.75),
Max = max(final_vals),
Mean = mean(final_vals),
SD = sd(final_vals)
) %>%
dplyr::mutate(dplyr::across(dplyr::everything(), ~ scales::comma(.x, accuracy = 0.01)))
tab_apa(summary_tbl,
title = "Resumen de valor final simulado (GBM)",
caption = "Nota. Se reporta el valor total del portafolio al horizonte."
)| Resumen de valor final simulado (GBM) | ||||||
| Min | Q1 | Median | Q3 | Max | Mean | SD |
|---|---|---|---|---|---|---|
| 6,150,529.26 | 11,270,670.09 | 12,812,236.84 | 15,069,266.27 | 26,650,430.99 | 13,427,903.42 | 3,203,872.18 |
Se analizan las principales métricas de desempeño y riesgo del portafolio de media–varianza construido. El objetivo es evaluar la eficiencia del portafolio frente al riesgo asumido y cuantificar su exposición a pérdidas potenciales, aplicando medidas estadísticas que reflejan el comportamiento del mercado y la relación entre rentabilidad, volatilidad y riesgo extremo (Bodie et al., 2021; Instituto Tecnológico Metropolitano (ITM), 2025).
Los indicadores presentados son: volatilidad anualizada, índice de Sharpe, precios esperados de cierre por trimestre, y valores en riesgo (VaR) a niveles de confianza del 1 % y 5 %. Estos resultados permiten comprender la estructura de ganancias esperadas y pérdidas potenciales del portafolio.
La volatilidad mide el grado de dispersión de los rendimientos del portafolio respecto a su media esperada. Los resultados obtenidos indican:
Portafolio MinVar: Volatilidad anual = 16.79 %
Portafolio Tangente: Volatilidad anual = 17.32 %
Estos valores muestran un nivel de riesgo moderado y estable, propio de un portafolio compuesto exclusivamente por acciones bancarias del S&P 500 (Bank of America, Wells Fargo y U.S. Bancorp). Aunque están por encima de un portafolio conservador, se ubican por debajo del promedio histórico del sector financiero (≈20 %), lo que evidencia una adecuada diversificación (Bodie et al., 2021). La ligera diferencia entre ambos portafolios es coherente con la teoría de (Markowitz, 1952): el portafolio de mínima varianza minimiza el riesgo total, mientras que el tangente asume un leve incremento para obtener un retorno superior.
El índice de Sharpe cuantifica la eficiencia del portafolio al relacionar el exceso de retorno sobre la tasa libre de riesgo con su volatilidad. Los resultados del modelo son:
Sharpe (MinVar): 1.90
Sharpe (Tangente): 1.98
Estos valores confirman que ambos portafolios presentan una alta eficiencia rentabilidad/riesgo, ya que generan cerca de dos unidades de retorno adicional por cada unidad de riesgo asumido. En términos prácticos, esto significa que las combinaciones óptimas de pesos en BAC, WFC y USB ofrecen retornos superiores a la tasa libre de riesgo del 4.08 % EA, correspondiente al rendimiento del bono del Tesoro estadounidense a 10 años al 31 de octubre de 2025 (Trading Economics, 2025). Según el Capítulo 4 de Derivados Financieros (Instituto Tecnológico Metropolitano (ITM), 2025), un índice de Sharpe superior a 1 se considera adecuado, superior a 2 excelente, y mayor a 3 representa eficiencia óptima. En este caso, ambos portafolios se ubican muy cerca del rango de alta eficiencia, validando su estructura estadística y consistencia frente al riesgo de mercado.
Mediante la simulación por Movimiento Browniano Geométrico (MGB), se estimaron los precios esperados de cierre para el primer trimestre proyectado, utilizando los promedios de retorno y volatilidad trimestral histórica:
BAC: 52.45 → 56.68 USD
WFC: 86.95 → 95.04 USD
USB: 46.74 → 49.15 USD
Los tres activos muestran una trayectoria de crecimiento moderado y sostenido, coherente con la recuperación gradual del sistema bancario estadounidense. Wells Fargo (WFC) destaca por su mayor apreciación proyectada (exposición al crédito hipotecario), Bank of America (BAC) mantiene un crecimiento estable derivado de su diversificación de ingresos, y U.S. Bancorp (USB) exhibe un comportamiento más defensivo asociado a su bajo riesgo crediticio. Estas proyecciones confirman la consistencia del modelo MGB y un sesgo alcista controlado, adecuado para estrategias de cobertura (Hull, 2018; Instituto Tecnológico Metropolitano (ITM), 2025).
El Valor en Riesgo (VaR) es una medida estadística que permite cuantificar la pérdida máxima esperada de un portafolio en un horizonte temporal determinado y a un nivel de confianza específico. En este laboratorio se estimó el VaR histórico (basado en los retornos empíricos del portafolio) y el VaR paramétrico (bajo el supuesto de normalidad), para niveles de confianza del 99 % (VaR 1 %) y del 95 % (VaR 5 %), empleando los retornos trimestrales derivados del portafolio de media–varianza construido con las acciones BAC, WFC y USB (Bodie et al., 2021; Instituto Tecnológico Metropolitano (ITM), 2025).
Los resultados obtenidos fueron los siguientes:
Portafolio de mínima varianza (MinVar):
VaR histórico (95 %) = −2.07 % → USD 207 000
VaR histórico (99 %) = −2.93 % → USD 293 000
VaR paramétrico (95 %) = −5.52 % → USD 552 000
VaR paramétrico (99 %) = −11.24 % → USD 1 124 000
Portafolio tangente:
VaR histórico (95 %) = −1.48 % → USD 148 000
VaR histórico (99 %) = −2.13 % → USD 213 000
VaR paramétrico (95 %) = −5.48 % → USD 548 000
VaR paramétrico (99 %) = −11.39 % → USD 1 139 000
Estas cifras indican que, bajo condiciones normales de mercado, existe una probabilidad del 5 % de que el portafolio pierda más del 2 % de su valor en un trimestre, y apenas un 1 % de probabilidad de que la pérdida supere el 3 %. En términos monetarios, esto equivale a pérdidas entre USD 200 000 y USD 300 000 sobre la inversión total de USD 10 millones. El VaR paramétrico, al basarse en la varianza y media de los retornos bajo el supuesto de normalidad, arroja valores más conservadores (pérdidas entre 5 % y 11 % trimestral). Esta diferencia es esperable, ya que el método paramétrico sobrestima el riesgo cuando la distribución empírica presenta colas más ligeras que la normal (Bodie et al., 2021). En contraste, el VaR histórico refleja el comportamiento real de los rendimientos simulados mediante MGB y muestra una distribución estable y simétrica, con baja probabilidad de eventos extremos (Instituto Tecnológico Metropolitano (ITM), 2025).
Síntesis
Los indicadores confirman que el portafolio BAC–WFC–USB ofrece una estructura eficiente de riesgo–retorno, con pérdidas controladas y desempeño predecible, cumpliendo con los principios de la teoría moderna de portafolios y con los objetivos de cobertura del laboratorio (Bodie et al., 2021; Markowitz, 1952).
# === MÉTRICAS TRIMESTRALES Y ANUALIZADAS (ADAPTADO, MISMA LÓGICA) ========
# Re-análisis de indicadores con retornos limpios y salida en formato APA
suppressPackageStartupMessages(library(PerformanceAnalytics))
options(scipen = 999) # evita notación científica en toda la salida numérica
# 0) Partimos de 'ret_daily' creado en DESCARGA-DATOS
# Si no tienes 'robustbase', usamos los retornos crudos.
if (!requireNamespace("robustbase", quietly = TRUE)) {
ret_daily_clean <- ret_daily
} else {
ret_daily_clean <- PerformanceAnalytics::Return.clean(ret_daily, method = "boudt")
}
# 1) Agregación trimestral (con limpios)
ret_qtr <- apply.quarterly(ret_daily_clean, FUN = colSums, na.rm = TRUE)
# 2) Momentos trimestrales
mu_qtr <- colMeans(ret_qtr, na.rm = TRUE)
Sigma <- cov(ret_qtr, use = "pairwise.complete.obs")
# 3) Portafolio MinVar (sin ventas cortas)
get_w_minvar <- function(Sigma){
N <- ncol(Sigma); one <- rep(1, N)
Dmat <- 2*as.matrix(Sigma); dvec <- rep(0, N)
Amat <- cbind(one, diag(N)); bvec <- c(1, rep(0, N)) # w'1=1; w>=0
sol <- quadprog::solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
as.numeric(sol$solution / sum(sol$solution))
}
w_minvar <- get_w_minvar(Sigma); names(w_minvar) <- colnames(ret_qtr)
# 4) Portafolio Tangente (máx. Sharpe) sin ventas cortas
if (!exists("r_annual") || is.na(r_annual)) r_annual <- 0.0408
rf_qtr <- (1 + r_annual)^(1/4) - 1
grid_w <- seq(0, 1, by = 0.01)
cand <- expand.grid(w1 = grid_w, w2 = grid_w) |>
dplyr::mutate(w3 = 1 - w1 - w2) |>
dplyr::filter(w3 >= 0)
eval_port <- function(w){
mu <- as.numeric(w %*% mu_qtr)
sdv <- sqrt(as.numeric(t(w) %*% Sigma %*% w))
sharpe <- ifelse(sdv > 0, (mu - rf_qtr)/sdv, NA_real_)
c(mu = mu, sd = sdv, sharpe = sharpe)
}
res <- t(apply(as.matrix(cand[, c("w1","w2","w3")]), 1, eval_port))
best <- dplyr::bind_cols(cand, tibble::as_tibble(res)) |>
tidyr::drop_na(sharpe) |>
dplyr::slice_max(order_by = sharpe, n = 1)
w_tan <- as.numeric(best[, c("w1","w2","w3")]); names(w_tan) <- colnames(ret_qtr)
# 5) Métricas anualizadas y VaR
port_min <- PerformanceAnalytics::Return.portfolio(ret_qtr, weights = w_minvar)
port_tan <- PerformanceAnalytics::Return.portfolio(ret_qtr, weights = w_tan)
to_num <- function(x) as.numeric(x[,1])
rq_min <- to_num(port_min); rq_tan <- to_num(port_tan)
ann_vol <- function(x) sd(x, na.rm = TRUE) * sqrt(4)
ann_ret <- function(x) { x <- x[!is.na(x)]; (prod(1+x)^(4/length(x)) - 1) }
tbl_metrics <- tibble::tibble(
Portafolio = c("MinVar","Tangente"),
Ret_Anual = c(ann_ret(rq_min), ann_ret(rq_tan)),
Vol_Anual = c(ann_vol(rq_min), ann_vol(rq_tan)),
Sharpe_Anual = c((ann_ret(rq_min)-r_annual)/(ann_vol(rq_min)+1e-9),
(ann_ret(rq_tan)-r_annual)/(ann_vol(rq_tan)+1e-9))
) |>
dplyr::mutate(dplyr::across(where(is.numeric), ~round(.x, 4)))
VaR_param <- function(x, p=.99) { mean(x, na.rm=TRUE) + sd(x, na.rm=TRUE)*qnorm(1-p) }
VaR_hist <- function(x, p=.99) quantile(x, probs = 1-p, na.rm = TRUE, names = FALSE)
tbl_var <- tibble::tibble(
Portafolio = c("MinVar","Tangente"),
VaR_99_hist = c(VaR_hist(rq_min,.99), VaR_hist(rq_tan,.99)),
VaR_95_hist = c(VaR_hist(rq_min,.95), VaR_hist(rq_tan,.95)),
VaR_99_param = c(VaR_param(rq_min,.99), VaR_param(rq_tan,.99)),
VaR_95_param = c(VaR_param(rq_min,.95), VaR_param(rq_tan,.95))
) |>
dplyr::mutate(dplyr::across(where(is.numeric), ~round(.x, 4)))
# 6) Imprime con tu helper 'tab_apa'
tab_apa(tbl_metrics, title = "Métricas anualizadas por portafolio")| Métricas anualizadas por portafolio | |||
| Portafolio | Ret_Anual | Vol_Anual | Sharpe_Anual |
|---|---|---|---|
| MinVar | 0.38 | 0.17 | 1.97 |
| Tangente | 0.40 | 0.17 | 2.08 |
| VaR histórico y paramétrico (trimestral) | ||||
| Portafolio | VaR_99_hist | VaR_95_hist | VaR_99_param | VaR_95_param |
|---|---|---|---|---|
| MinVar | -0.01 | -0.01 | -0.11 | -0.05 |
| Tangente | -0.02 | -0.01 | -0.11 | -0.05 |
# 7) Precios esperados a 1T (para el punto de tu informe)
last_px_vec <- as.numeric(prices[nrow(prices), ])
exp_prices <- tibble::tibble(
Activo = colnames(prices),
Precio_Inicial = round(last_px_vec, 2),
Retorno_Esperado_Trimestral = round(as.numeric(mu_qtr), 4),
Precio_Esperado_en_1T = round(last_px_vec * (1 + as.numeric(mu_qtr)), 2)
)
tab_apa(exp_prices, title = "Precio esperado a 1T por activo")| Precio esperado a 1T por activo | |||
| Activo | Precio_Inicial | Retorno_Esperado_Trimestral | Precio_Esperado_en_1T |
|---|---|---|---|
| BAC | 53.63 | 0.09 | 58.52 |
| WFC | 86.19 | 0.09 | 94.00 |
| USB | 47.62 | 0.05 | 50.13 |
# === Pesos alternativos para usar en la COBERTURA =========================
suppressPackageStartupMessages({library(quadprog); library(dplyr); library(tibble)})
stopifnot(exists("Sigma"), exists("w_minvar"))
tick_vec <- colnames(Sigma); N <- length(tick_vec)
# 1) Igual ponderación (referencia)
w_equal <- rep(1/N, N); names(w_equal) <- tick_vec
# 2) Risk parity simple (inversa de volatilidad trimestral)
vol_q <- sqrt(diag(Sigma)) # σ trimestral por activo
w_iv <- 1/vol_q; w_iv <- w_iv / sum(w_iv); names(w_iv) <- tick_vec
# 3) Min-var con límites (cap) para evitar concentración
wmin <- 0.15 # mínimo 15% por emisor
wmax <- 0.60 # máximo 60% por emisor
get_w_minvar_bounded <- function(Sigma, wmin, wmax){
N <- ncol(Sigma)
Dmat <- 2*as.matrix(Sigma); dvec <- rep(0, N)
# Restricción de suma = 1
Aeq <- matrix(1, nrow = N, ncol = 1)
# Inequidades: w >= wmin y w <= wmax -> I*w >= wmin ; -I*w >= -wmax
Amat <- cbind(Aeq, diag(N), -diag(N))
bvec <- c(1, rep(wmin, N), rep(-wmax, N))
sol <- quadprog::solve.QP(Dmat, dvec, Amat, bvec, meq = 1)
w <- sol$solution / sum(sol$solution)
as.numeric(w)
}
w_capped <- tryCatch(get_w_minvar_bounded(Sigma, wmin, wmax),
error = function(e) { warning(e$message); w_equal })
names(w_capped) <- tick_vec
# 4) Blend (opcional): 50% minvar + 50% equal
lambda <- 0.5
w_blend <- lambda*w_minvar + (1-lambda)*w_equal
w_blend <- w_blend / sum(w_blend); names(w_blend) <- tick_vec
# --- Tabla comparativa para el informe (versión corregida) ---
dfw <- rbind(
MinVar = w_minvar,
Equal = w_equal,
InverseVol = w_iv,
Capped = w_capped,
Blend = w_blend
)
# Asegura que las columnas sean exactamente los tickers (p.ej., BAC, USB, WFC)
colnames(dfw) <- tick_vec
pesos_comparativo <- tibble::as_tibble(dfw, rownames = "Estrategia") |>
dplyr::mutate(dplyr::across(-Estrategia, ~round(.x, 4)))
tab_apa(pesos_comparativo,
title = "Comparativo de esquemas de ponderación",
caption = "Nota. Se incluyen MinVar, Equal, InverseVol, Capped y Blend."
)| Comparativo de esquemas de ponderación | |||
| Estrategia | BAC | WFC | USB |
|---|---|---|---|
| MinVar | 0.32 | 0.56 | 0.11 |
| Equal | 0.33 | 0.33 | 0.33 |
| InverseVol | 0.37 | 0.37 | 0.26 |
| Capped | 0.28 | 0.57 | 0.15 |
| Blend | 0.33 | 0.45 | 0.22 |
# Compila en RMarkdown (HTML/PDF). Requiere: dplyr, stringr, knitr, kableExtra, scales
suppressPackageStartupMessages({
library(dplyr); library(stringr); library(knitr)
library(kableExtra); library(scales); library(purrr); library(glue)
})
# --- Parámetros generales (ajústalos si tu documento usa otros nombres) ---
tickers <- get0("tickers", ifnotfound = c("BAC","WFC","USB"))
r_annual <- get0("r_annual", ifnotfound = 0.0408) # 4.08% EA
T_years <- 2 # 8 trimestres
n_steps <- 8 # trimestral => dt = 0.25
q_div <- 0 # sin dividendos para CRR
portfolio_value <- get0("portfolio_value", ifnotfound = 1e7) # USD 10M
# Si tienes precios diarios/tabla de opciones en el ambiente:
opt_in <- get0("OptionsInput", ifnotfound = get0("opt_in", ifnotfound = NULL))
prices <- get0("prices", ifnotfound = NULL)
ret_daily <- get0("ret_daily", ifnotfound = NULL)
# ========= Funciones CRR ===================================================
crr_params <- function(sigma, r, q=0, T_years=1, n=1){
dt <- T_years/n
u <- exp(sigma*sqrt(dt))
d <- exp(-sigma*sqrt(dt))
p <- (exp((r - q)*dt) - d) / (u - d)
list(u=u, d=d, p=p, dt=dt)
}
stock_tree <- function(S0, u, d, n){
tree <- matrix(NA_real_, nrow = n+1, ncol = n+1)
for (i in 0:n) for (j in 0:i) {
tree[j+1, i+1] <- S0 * (u^j) * (d^(i-j))
}
tree
}
payoff_terminal <- function(ST_vec, K, type=c("CALL","PUT")){
type <- toupper(type[1])
if(type=="CALL") pmax(ST_vec - K, 0) else pmax(K - ST_vec, 0)
}
binom_euro <- function(S0, K, r, q, sigma, T_years, n, type=c("CALL","PUT")){
type <- toupper(type[1])
pars <- crr_params(sigma, r, q, T_years, n)
u<-pars$u; d<-pars$d; p<-pars$p; dt<-pars$dt
ST <- stock_tree(S0, u, d, n)
V <- matrix(NA_real_, nrow = n+1, ncol = n+1)
V[, n+1] <- payoff_terminal(ST[, n+1], K, type)
disc <- exp(-r*dt)
if(n>=1){
for (i in seq(n,1)) for (j in 1:i){
V[j,i] <- disc * (p*V[j+1,i+1] + (1-p)*V[j,i+1])
}
}
list(price = V[1,1], V=V, ST=ST, u=u, d=d, p=p, dt=dt)
}
binom_amer <- function(S0, K, r, q, sigma, T_years, n, type=c("CALL","PUT")){
type <- toupper(type[1])
pars <- crr_params(sigma, r, q, T_years, n)
u<-pars$u; d<-pars$d; p<-pars$p; dt<-pars$dt
ST <- stock_tree(S0, u, d, n)
V <- matrix(NA_real_, nrow = n+1, ncol = n+1)
V[, n+1] <- payoff_terminal(ST[, n+1], K, type)
disc <- exp(-r*dt)
if(n>=1){
for (i in seq(n,1)) for (j in 1:i){
cont <- disc * (p*V[j+1,i+1] + (1-p)*V[j,i+1])
intr <- payoff_terminal(ST[j,i], K, type)
V[j,i] <- max(intr, cont)
}
}
list(price = V[1,1], V=V, ST=ST, u=u, d=d, p=p, dt=dt)
}
# ========= Utilidades robustas para leer tu OptionsInput ====================
col_find <- function(df, pattern, required=TRUE) {
nm <- names(df)[str_detect(names(df), regex(pattern, ignore_case = TRUE))][1]
if (is.na(nm) && required) stop(glue("No se encontró columna que coincida con: {pattern}"))
nm
}
get1 <- function(x) { if (length(x) >= 1) x[1] else NA }
safe_asnum <- function(x){
x <- as.character(x)
x <- gsub(",", ".", x)
suppressWarnings(as.numeric(x))
}
col_type <- if (!is.null(opt_in)) col_find(opt_in, "OptionType|Type|CALL|PUT", TRUE) else NULL
col_strike <- if (!is.null(opt_in)) col_find(opt_in, "Strike", TRUE) else NULL
col_iv <- if (!is.null(opt_in)) col_find(opt_in, "ImpliedVol|IV\\s*%|IV_%|IV", FALSE) else NULL
col_s0 <- if (!is.null(opt_in)) col_find(opt_in, "UnderlyingPrice|S0|Spot", FALSE) else NULL
col_oi <- if (!is.null(opt_in)) col_find(opt_in, "OpenInterest|OI", FALSE) else NULL
choose_row <- function(df_rows, S0){
if (nrow(df_rows) == 0) return(df_rows)
df_rows <- df_rows %>%
mutate(
Strike_num = safe_asnum(.data[[col_strike]]),
OI_num = if (!is.null(col_oi) && col_oi %in% names(df_rows)) safe_asnum(.data[[col_oi]]) else NA_real_,
distATM = abs(Strike_num - S0)
) %>%
arrange(distATM, desc(OI_num))
df_rows[1, , drop = FALSE]
}
pick_by <- function(df, tk, type=c("CALL","PUT")){
type <- toupper(type[1])
sub <- df %>% filter(Ticker == tk, toupper(.data[[col_type]]) == type)
if (nrow(sub) == 0) stop(glue("[{tk}] Falta {type} en OptionsInput (ver columna {col_type})."))
S0_here <- safe_asnum(get1(sub[[col_s0]]))
if (is.na(S0_here)) {
if (!is.null(prices) && tk %in% colnames(prices)) {
S0_here <- as.numeric(prices[nrow(prices), tk])
} else stop(glue("[{tk}] No hay S0 en OptionsInput ni en 'prices'."))
}
choose_row(sub, S0 = S0_here)
}
sigma_hist <- sapply(tickers, function(tk){
x <- tryCatch(na.omit(ret_daily[, tk]), error=function(e) NULL)
if (is.null(x) || NROW(x) < 30) return(NA_real_)
sd(as.numeric(x)) * sqrt(252)
})
sigma_fallback <- 0.25
# ========= Parametrización por activo (S0, K, IV) ==========================
per_asset <- if (!is.null(opt_in)) {
lapply(tickers, function(tk){
rc <- pick_by(opt_in, tk, "CALL")
rp <- pick_by(opt_in, tk, "PUT")
S0 <- safe_asnum(get1(rc[[col_s0]]))
if (is.na(S0) && !is.null(prices)) S0 <- as.numeric(prices[nrow(prices), tk])
Kc <- safe_asnum(get1(rc[[col_strike]]))
Kp <- safe_asnum(get1(rp[[col_strike]]))
IVc <- if (!is.null(col_iv) && col_iv %in% names(rc)) safe_asnum(get1(rc[[col_iv]]))/100 else NA_real_
IVp <- if (!is.null(col_iv) && col_iv %in% names(rp)) safe_asnum(get1(rp[[col_iv]]))/100 else NA_real_
if (is.na(IVc) || IVc <= 0) IVc <- if (!is.na(sigma_hist[tk])) sigma_hist[tk] else sigma_fallback
if (is.na(IVp) || IVp <= 0) IVp <- if (!is.na(sigma_hist[tk])) sigma_hist[tk] else sigma_fallback
list(ticker=tk, S0=S0, K_call=Kc, K_put=Kp, IV_call=IVc, IV_put=IVp, q=q_div)
}) -> L; names(L) <- tickers; L
} else {
# Si no hay OptionsInput, crea ejemplo mínimo (reemplaza con tus datos reales)
list(
BAC = list(ticker="BAC", S0=52.0, K_call=52.5, K_put=52.5, IV_call=0.28, IV_put=0.28, q=q_div),
WFC = list(ticker="WFC", S0=85.5, K_call=85.0, K_put=85.0, IV_call=0.24, IV_put=0.24, q=q_div),
USB = list(ticker="USB", S0=48.0, K_call=47.5, K_put=47.5, IV_call=0.22, IV_put=0.22, q=q_div)
)
}
# ========= Valuación binomial EU/AM por activo (vencimiento 2 años, dt=0.25) =
value_tbl <- purrr::map_dfr(per_asset, function(x){
eu_c <- binom_euro(S0=x$S0, K=x$K_call, r=r_annual, q=x$q, sigma=x$IV_call,
T_years=T_years, n=n_steps, type="CALL")$price
am_c <- binom_amer(S0=x$S0, K=x$K_call, r=r_annual, q=x$q, sigma=x$IV_call,
T_years=T_years, n=n_steps, type="CALL")$price
eu_p <- binom_euro(S0=x$S0, K=x$K_put, r=r_annual, q=x$q, sigma=x$IV_put,
T_years=T_years, n=n_steps, type="PUT")$price
am_p <- binom_amer(S0=x$S0, K=x$K_put, r=r_annual, q=x$q, sigma=x$IV_put,
T_years=T_years, n=n_steps, type="PUT")$price
tibble::tibble(
Ticker=x$ticker, S0=x$S0,
K_call=x$K_call, EU_Call=eu_c, AM_Call=am_c,
K_put=x$K_put, EU_Put =eu_p, AM_Put =am_p
)
}) %>% mutate(across(where(is.numeric), ~round(.x, 4)))
# ---- Tabla de valuación (kable) -------------------------------------------
kbl(value_tbl,
caption = "Valuación binomial (CRR, dt = 0.25): Europea vs. Americana — Vencimiento 2 años",
booktabs = TRUE, align = "c") |>
kable_classic(full_width = FALSE) |>
row_spec(0, bold = TRUE)| Ticker | S0 | K_call | EU_Call | AM_Call | K_put | EU_Put | AM_Put |
|---|---|---|---|---|---|---|---|
| BAC | 52.97 | 52.5 | 10.943 | 10.943 | 52.5 | 4.793 | 5.310 |
| WFC | 85.90 | 85.0 | 19.114 | 19.114 | 85.0 | 9.390 | 10.254 |
| USB | 46.55 | 47.5 | 8.605 | 8.605 | 47.5 | 5.921 | 6.483 |
make_tree_table <- function(tree_obj) {
ST <- round(tree_obj$ST, 2)
V <- round(tree_obj$V, 2)
n <- ncol(ST) - 1
lab <- matrix("", nrow = nrow(ST), ncol = ncol(ST))
for (i in 0:n) {
for (j in 0:i) {
lab[j + 1, i + 1] <- sprintf("%.2f\n%.2f", ST[j + 1, i + 1], V[j + 1, i + 1])
}
}
lab <- lab[1:(n + 1), 1:(n + 1), drop = FALSE]
colnames(lab) <- paste0("t", 0:n)
rownames(lab) <- paste0("Nodo ", 0:n)
as.data.frame(lab, stringsAsFactors = FALSE)
}
if ("BAC" %in% names(per_asset)) {
n_steps_tree <- 2
T_tree <- n_steps_tree / 4 # 2 pasos => 0.5 años aprox
tree_bac_put <- binom_amer(
S0 = per_asset[["BAC"]]$S0,
K = per_asset[["BAC"]]$K_put,
r = r_annual,
q = per_asset[["BAC"]]$q,
sigma = per_asset[["BAC"]]$IV_put,
T_years = T_tree,
n = n_steps_tree,
type = "PUT"
)
tabla_arbol_bac <- make_tree_table(tree_bac_put)
kbl(tabla_arbol_bac,
caption = "Árbol binomial (2 pasos): Precio del subyacente (arriba) y valor de la opción (abajo) para un Put Americano sobre BAC.",
booktabs = TRUE, align = "c") |>
kable_classic(full_width = FALSE) |>
row_spec(0, bold = TRUE)
}| t0 | t1 | t2 | |
|---|---|---|---|
| Nodo 0 | 52.97 2.75 | 46.82 5.68 | 41.39 11.11 |
| Nodo 1 | 59.92 0.00 | 52.97 0.00 | |
| Nodo 2 | 67.79 0.00 |
# ========= Strip trimestral: valuación por trimestre (1..8) =================
strip_tbl <- map_dfr(per_asset, function(x){
map_dfr(1:n_steps, function(qtr){
Tq <- qtr/4
nq <- qtr
eu_c <- binom_euro(S0=x$S0, K=x$K_call, r=r_annual, q=x$q, sigma=x$IV_call,
T_years=Tq, n=nq, type="CALL")$price
am_c <- binom_amer(S0=x$S0, K=x$K_call, r=r_annual, q=x$q, sigma=x$IV_call,
T_years=Tq, n=nq, type="CALL")$price
eu_p <- binom_euro(S0=x$S0, K=x$K_put, r=r_annual, q=x$q, sigma=x$IV_put,
T_years=Tq, n=nq, type="PUT")$price
am_p <- binom_amer(S0=x$S0, K=x$K_put, r=r_annual, q=x$q, sigma=x$IV_put,
T_years=Tq, n=nq, type="PUT")$price
tibble::tibble(
Ticker = x$ticker,
Quarter = qtr,
Type = c("CALL","PUT","CALL","PUT"),
Style = c("EU","EU","AM","AM"),
Price = c(eu_c, eu_p, am_c, am_p)
)
})
})
strip_out <- strip_tbl %>%
arrange(Ticker, Type, Quarter, Style) %>%
mutate(Price = round(Price, 4))
kbl(strip_out,
caption = "Strip trimestral (CRR): valuación por trimestre, CALL y PUT, EU y AM",
booktabs = TRUE, align = "c") |>
kable_classic(full_width = FALSE) |>
row_spec(0, bold = TRUE)| Ticker | Quarter | Type | Style | Price |
|---|---|---|---|---|
| BAC | 1 | CALL | AM | 4.522 |
| BAC | 1 | CALL | EU | 4.522 |
| BAC | 2 | CALL | AM | 4.898 |
| BAC | 2 | CALL | EU | 4.898 |
| BAC | 3 | CALL | AM | 7.011 |
| BAC | 3 | CALL | EU | 7.011 |
| BAC | 4 | CALL | AM | 7.350 |
| BAC | 4 | CALL | EU | 7.350 |
| BAC | 5 | CALL | AM | 8.964 |
| BAC | 5 | CALL | EU | 8.964 |
| BAC | 6 | CALL | AM | 9.282 |
| BAC | 6 | CALL | EU | 9.282 |
| BAC | 7 | CALL | AM | 10.641 |
| BAC | 7 | CALL | EU | 10.641 |
| BAC | 8 | CALL | AM | 10.943 |
| BAC | 8 | CALL | EU | 10.943 |
| BAC | 1 | PUT | AM | 2.750 |
| BAC | 1 | PUT | EU | 2.750 |
| BAC | 2 | PUT | AM | 2.750 |
| BAC | 2 | PUT | EU | 2.607 |
| BAC | 3 | PUT | AM | 3.953 |
| BAC | 3 | PUT | EU | 3.828 |
| BAC | 4 | PUT | AM | 3.953 |
| BAC | 4 | PUT | EU | 3.663 |
| BAC | 5 | PUT | AM | 4.712 |
| BAC | 5 | PUT | EU | 4.500 |
| BAC | 6 | PUT | AM | 4.712 |
| BAC | 6 | PUT | EU | 4.326 |
| BAC | 7 | PUT | AM | 5.310 |
| BAC | 7 | PUT | EU | 4.971 |
| BAC | 8 | PUT | AM | 5.310 |
| BAC | 8 | PUT | EU | 4.793 |
| USB | 1 | CALL | AM | 3.078 |
| USB | 1 | CALL | EU | 3.078 |
| USB | 2 | CALL | AM | 3.549 |
| USB | 2 | CALL | EU | 3.549 |
| USB | 3 | CALL | AM | 5.175 |
| USB | 3 | CALL | EU | 5.175 |
| USB | 4 | CALL | AM | 5.579 |
| USB | 4 | CALL | EU | 5.579 |
| USB | 5 | CALL | AM | 6.832 |
| USB | 5 | CALL | EU | 6.832 |
| USB | 6 | CALL | AM | 7.200 |
| USB | 6 | CALL | EU | 7.200 |
| USB | 7 | CALL | AM | 8.261 |
| USB | 7 | CALL | EU | 8.261 |
| USB | 8 | CALL | AM | 8.605 |
| USB | 8 | CALL | EU | 8.605 |
| USB | 1 | PUT | AM | 3.589 |
| USB | 1 | PUT | EU | 3.589 |
| USB | 2 | PUT | AM | 3.822 |
| USB | 2 | PUT | EU | 3.583 |
| USB | 3 | PUT | AM | 4.876 |
| USB | 3 | PUT | EU | 4.757 |
| USB | 4 | PUT | AM | 5.047 |
| USB | 4 | PUT | EU | 4.694 |
| USB | 5 | PUT | AM | 5.721 |
| USB | 5 | PUT | EU | 5.498 |
| USB | 6 | PUT | AM | 5.847 |
| USB | 6 | PUT | EU | 5.408 |
| USB | 7 | PUT | AM | 6.377 |
| USB | 7 | PUT | EU | 6.027 |
| USB | 8 | PUT | AM | 6.483 |
| USB | 8 | PUT | EU | 5.921 |
| WFC | 1 | CALL | AM | 8.033 |
| WFC | 1 | CALL | EU | 8.033 |
| WFC | 2 | CALL | AM | 8.671 |
| WFC | 2 | CALL | EU | 8.671 |
| WFC | 3 | CALL | AM | 12.365 |
| WFC | 3 | CALL | EU | 12.365 |
| WFC | 4 | CALL | AM | 12.931 |
| WFC | 4 | CALL | EU | 12.931 |
| WFC | 5 | CALL | AM | 15.741 |
| WFC | 5 | CALL | EU | 15.741 |
| WFC | 6 | CALL | AM | 16.265 |
| WFC | 6 | CALL | EU | 16.265 |
| WFC | 7 | CALL | AM | 18.620 |
| WFC | 7 | CALL | EU | 18.620 |
| WFC | 8 | CALL | AM | 19.114 |
| WFC | 8 | CALL | EU | 19.114 |
| WFC | 1 | PUT | AM | 5.208 |
| WFC | 1 | PUT | EU | 5.208 |
| WFC | 2 | PUT | AM | 5.208 |
| WFC | 2 | PUT | EU | 5.002 |
| WFC | 3 | PUT | AM | 7.554 |
| WFC | 3 | PUT | EU | 7.343 |
| WFC | 4 | PUT | AM | 7.554 |
| WFC | 4 | PUT | EU | 7.087 |
| WFC | 5 | PUT | AM | 9.066 |
| WFC | 5 | PUT | EU | 8.703 |
| WFC | 6 | PUT | AM | 9.066 |
| WFC | 6 | PUT | EU | 8.426 |
| WFC | 7 | PUT | AM | 10.254 |
| WFC | 7 | PUT | EU | 9.678 |
| WFC | 8 | PUT | AM | 10.254 |
| WFC | 8 | PUT | EU | 9.390 |
# ===== PARÁMETROS POR ACTIVO (robusto a nombres de columnas) =============
library(dplyr); library(stringr); library(glue); library(tibble)
# Helpers mínimos (se usan abajo; si ya existen en tu script, puedes omitirlos)
`%||%` <- function(a, b) if (!is.null(a)) a else b
get1 <- function(x) { if (length(x) >= 1) x[1] else NA }
safe_asnum <- function(x){
x <- as.character(x)
x <- gsub(",", ".", x)
suppressWarnings(as.numeric(x))
}
# Detecta nombres REALES de columnas en tu OptionsInput (opt_in)
col_type <- names(opt_in)[str_detect(names(opt_in), regex("OptionType|Type|CALL|PUT", ignore_case=TRUE))][1]
col_strike <- names(opt_in)[str_detect(names(opt_in), regex("Strike", ignore_case=TRUE))][1]
col_iv <- names(opt_in)[str_detect(names(opt_in), regex("ImpliedVol|IV\\s*%|IV_%|IV", ignore_case=TRUE))][1]
col_s0 <- names(opt_in)[str_detect(names(opt_in), regex("UnderlyingPrice|S0|Spot", ignore_case=TRUE))][1]
col_oi <- names(opt_in)[str_detect(names(opt_in), regex("OpenInterest|OI", ignore_case=TRUE))][1]
col_div <- names(opt_in)[str_detect(names(opt_in), regex("^q$|DividendYield|DivYield|Dividend", ignore_case=TRUE))][1]
if (is.na(col_type)) stop("No se encontró columna de tipo (CALL/PUT).")
if (is.na(col_strike)) stop("No se encontró columna Strike.")
# Selector de fila por Ticker y Tipo (elige ATM y, a empate, mayor OI)
pick_by <- function(df, tk, type=c("CALL","PUT")){
type <- toupper(type[1])
sub <- df %>% filter(.data$Ticker == tk, toupper(.data[[col_type]]) == type)
if (nrow(sub) == 0) stop(glue("[{tk}] Falta {type} en OptionsInput (revisa {col_type})."))
# S0 directo de la fila o, si falta, desde 'prices' (último valor)
S0_here <- if (!is.na(col_s0)) safe_asnum(get1(sub[[col_s0]])) else NA_real_
if (is.na(S0_here) && exists("prices") && tk %in% colnames(prices))
S0_here <- as.numeric(prices[nrow(prices), tk])
if (is.na(S0_here)) stop(glue("[{tk}] No hay S0 en OptionsInput ni en 'prices'."))
# Criterio ATM
oi_vec <- if (!is.na(col_oi) && col_oi %in% names(sub)) safe_asnum(sub[[col_oi]]) else NA_real_
sub %>%
mutate(
Strike_num = safe_asnum(.data[[col_strike]]),
OI_num = oi_vec,
distATM = abs(Strike_num - S0_here)
) %>%
arrange(distATM, desc(OI_num)) %>%
slice(1) %>%
mutate(S0_resolved = S0_here)
}
# Volatilidad histórica (respaldo si no hay IV)
sigma_hist <- sapply(tickers, function(tk){
x <- tryCatch(na.omit(ret_daily[, tk]), error=function(e) NULL)
if (is.null(x) || NROW(x) < 30) return(NA_real_)
sd(as.numeric(x)) * sqrt(252)
})
sigma_fallback <- 0.25
# Construcción de lista de parámetros por activo (S0, K_call, K_put, IV_call, IV_put, q)
per_asset <- lapply(tickers, function(tk){
row_call <- pick_by(opt_in, tk, "CALL")
row_put <- pick_by(opt_in, tk, "PUT")
S0_use <- safe_asnum(get1(row_call$S0_resolved))
K_call <- safe_asnum(get1(row_call[[col_strike]]))
K_put <- safe_asnum(get1(row_put [[col_strike]]))
IVc_raw <- if (!is.na(col_iv) && col_iv %in% names(row_call)) safe_asnum(get1(row_call[[col_iv]]))/100 else NA_real_
IVp_raw <- if (!is.na(col_iv) && col_iv %in% names(row_put )) safe_asnum(get1(row_put [[col_iv]]))/100 else NA_real_
IV_call <- if (!is.na(IVc_raw) && IVc_raw>0) IVc_raw else if (!is.na(sigma_hist[tk])) sigma_hist[tk] else sigma_fallback
IV_put <- if (!is.na(IVp_raw) && IVp_raw>0) IVp_raw else if (!is.na(sigma_hist[tk])) sigma_hist[tk] else sigma_fallback
q_use <- if (!is.na(col_div) && col_div %in% names(opt_in)) {
qv <- safe_asnum(get1(opt_in %>% filter(Ticker==tk) %>% pull(!!col_div)))
ifelse(is.na(qv), 0, qv)
} else 0
if (is.na(K_call)) stop(glue("[{tk}] Falta Strike de CALL ({col_strike})."))
if (is.na(K_put )) stop(glue("[{tk}] Falta Strike de PUT ({col_strike})."))
list(ticker=tk, S0=S0_use, K_call=K_call, K_put=K_put, IV_call=IV_call, IV_put=IV_put, q=q_use)
})
names(per_asset) <- tickers
# (Opcional) diagnóstico rápido de mapeo de columnas
diagnostico_cols <- tibble(
Col_detectada = c("Tipo (CALL/PUT)", "Strike", "Implied Vol", "S0", "Open Interest", "Dividend Yield (q)"),
Nombre_en_excel = c(col_type %||% "—", col_strike %||% "—", col_iv %||% "—", col_s0 %||% "—", col_oi %||% "—", col_div %||% "—")
)
diagnostico_colsLa valoración de las opciones se desarrolló utilizando el modelo binomial de Cox–Ross–Rubinstein (CRR), aplicado tanto a opciones europeas como americanas, con el fin de comparar su valor teórico y determinar su efectividad dentro de una estrategia de cobertura. Este modelo permite estimar el precio de las opciones mediante un proceso discreto de evolución del subyacente, basado en el principio de no arbitraje y la reversión mediante retroinducción (Cox et al., 1979; Hull, 2018).
Los datos de entrada para el árbol binomial fueron extraídos y parametrizados en el archivo Lab2_OptionsTemplate.xlsx, el cual incluye los precios spot (S₀), los precios de ejercicio (K), la volatilidad histórica anualizada, la tasa libre de riesgo (r = 4.08 % EA) y el número de pasos trimestrales (n = 8), equivalentes a dos años de vencimiento. El cálculo de los valores de las opciones se realizó aplicando la metodología CRR tanto en hojas de cálculo de Excel como en el entorno RMarkdown, garantizando coherencia numérica y consistencia metodológica (Instituto Tecnológico Metropolitano (ITM), 2025).
Selección de strikes y criterios de liquidez
Para cada activo se seleccionó un precio de ejercicio (strike) cercano al valor spot (at the money, ATM), con base en tres criterios de mercado:
Liquidez, medida por el open interest (número de contratos abiertos).
Eficiencia de cotización, reflejada en el spread bid–ask.
Estabilidad del precio implícito, determinada por la volatilidad implícita (IV).
La siguiente tabla del archivo Excel resume los parámetros seleccionados y los strikes utilizados en la simulación.
Resultados de la valuación binomial
Los precios obtenidos de las opciones europeas y americanas para cada activo confirmaron los principios teóricos del modelo:
Las opciones call europeas y americanas tuvieron el mismo valor, al no existir dividendos; por tanto, no hay incentivo para ejercer anticipadamente.
Las opciones put americanas resultaron más costosas que las europeas, debido a la posibilidad de ejercicio anticipado, lo que aumenta su prima y las hace más adecuadas para fines de cobertura (Hull, 2018).
Cobertura del 85 % del portafolio y número de contratos
El objetivo de la estrategia es cubrir el 85 % de la inversión total (USD 10 000 000) mediante posiciones en opciones put americanas, utilizando un apalancamiento financiero respaldado por la tasa del bono del Tesoro a 10 años (4.08 % EA) (Trading Economics, 2025).
Los resultados del código en R muestran que la prima total representa aproximadamente un 27.3 % del capital cubierto, financiado mediante apalancamiento a la tasa libre de riesgo. El costo financiero trimestral estimado asciende a USD 27 400, manteniendo la sostenibilidad de la cobertura durante los ocho trimestres proyectados (Hull, 2018; Instituto Tecnológico Metropolitano (ITM), 2025).
Dimensionamiento de la cobertura por activo
La cobertura se construyó agrupando las tres acciones como un paquete de inversión, distribuyendo el monto apalancado según el peso de cada activo en el portafolio. Los resultados presentados en la tabla del código de R muestran la asignación proporcional de contratos, donde BAC concentra la mayor exposición y, por tanto, el mayor número de contratos, debido a su peso y volatilidad superiores. Por su parte, WFC y USB aportan estabilidad relativa y reducen el riesgo conjunto del portafolio, en línea con la teoría de diversificación de (Markowitz, 1952).
El dimensionamiento se basa en cubrir el 85 % del valor nocional, manteniendo coherencia con la política de cobertura del laboratorio. Este enfoque garantiza una protección eficiente frente a caídas de mercado, sin sobreapalancar la posición ni comprometer la liquidez del portafolio (Hull, 2018; Instituto Tecnológico Metropolitano (ITM), 2025).
Evaluación de la efectividad de la cobertura
En escenarios de mercado adverso, se realizó un análisis de sensibilidad ante un choque negativo de una desviación estándar (−1σ) sobre el portafolio simulado mediante MGB. Los resultados obtenidos en R fueron:
Pérdida sin cobertura: −USD 62 000 por trimestre.
Ganancia generada por puts cubiertos: +USD 1 140 000.
Resultado neto (después del costo de primas): portafolio estable o ligeramente positivo.
Estos resultados demuestran que la cobertura cumple su función protectora, reduciendo significativamente la exposición a pérdidas extremas a cambio de un costo razonable en primas. Durante trimestres de estabilidad o crecimiento, la cobertura actúa como un seguro financiero, limitando las pérdidas máximas sin comprometer la estructura general del portafolio (Instituto Tecnológico Metropolitano (ITM), 2025).
Distribución del dinero apalancado
El dinero apalancado para la compra de opciones se distribuyó proporcionalmente al costo de prima por activo, de acuerdo con el riesgo y la volatilidad de cada uno:
BAC: 55 %
WFC: 30 %
USB: 15 %
Esta asignación está alineada con la eficiencia de cobertura: se destina más capital al activo con mayor sensibilidad al mercado (BAC) y menos a los de comportamiento más estable (USB). El uso del apalancamiento a la tasa libre de riesgo garantiza que la financiación no incremente significativamente el costo total de la estrategia (Hull, 2018; Trading Economics, 2025).
# ==== VALUACIÓN BINOMIAL: EUROPEA vs AMERICANA ============================
# Requiere: funciones binom_euro / binom_amer, lista per_asset, r_annual, T_years, n_steps
# Guardas suaves (por si faltan en el ambiente)
if (!exists("T_years") || is.na(T_years)) T_years <- 2
if (!exists("n_steps") || is.na(n_steps)) n_steps <- 8
if (!exists("r_annual") || is.na(r_annual)) r_annual <- 0.0408
stopifnot(exists("per_asset"), length(per_asset) >= 1)
value_tbl <- purrr::map_dfr(per_asset, function(x){
# Validaciones rápidas
req <- c("ticker","S0","K_call","K_put","IV_call","IV_put","q")
miss <- setdiff(req, names(x))
if (length(miss) > 0) stop(glue::glue("Faltan campos en per_asset: {paste(miss, collapse=', ')}"))
# CALL
eu_c <- binom_euro(
S0 = x$S0, K = x$K_call, r = r_annual, q = x$q, sigma = x$IV_call,
T_years = T_years, n = n_steps, type = "CALL"
)$price
am_c <- binom_amer(
S0 = x$S0, K = x$K_call, r = r_annual, q = x$q, sigma = x$IV_call,
T_years = T_years, n = n_steps, type = "CALL"
)$price
# PUT
eu_p <- binom_euro(
S0 = x$S0, K = x$K_put, r = r_annual, q = x$q, sigma = x$IV_put,
T_years = T_years, n = n_steps, type = "PUT"
)$price
am_p <- binom_amer(
S0 = x$S0, K = x$K_put, r = r_annual, q = x$q, sigma = x$IV_put,
T_years = T_years, n = n_steps, type = "PUT"
)$price
tibble::tibble(
Ticker = x$ticker,
S0 = x$S0,
K_call = x$K_call, EU_Call = eu_c, AM_Call = am_c,
K_put = x$K_put, EU_Put = eu_p, AM_Put = am_p
)
})
# Tabla lista para reporte
value_tbl <- dplyr::mutate(value_tbl, dplyr::across(where(is.numeric), ~round(.x, 4)))
value_tbl# === COBERTURA 85% con pesos capped (evita concentración) =================
suppressPackageStartupMessages({library(dplyr)})
# Guardas
if (!exists("V_port_total") || is.na(V_port_total)) V_port_total <- 1e7
if (!exists("pkg_size") || is.na(pkg_size)) pkg_size <- 3
if (!exists("r_annual") || is.na(r_annual)) r_annual <- 0.0408
rf_qtr <- (1 + r_annual)^(1/4) - 1
stopifnot(exists("per_asset"), exists("value_tbl"))
hedge_ratio <- 0.85
# >>>> USAR PESOS CAPPEADOS PARA LA COBERTURA <<<<
alloc_w <- if (exists("w_capped")) w_capped else w_minvar
# Piso opcional de 1 contrato (TRUE para activarlo)
min_one_contract <- TRUE
# 1) Tamaño de la cobertura (nº de contratos por activo)
size_tbl <- purrr::map_dfr(per_asset, function(x){
w_i <- alloc_w[x$ticker]
V_i <- V_port_total * w_i
K_ref <- x$K_call
contracts_raw <- (hedge_ratio * V_i) / (pkg_size * K_ref)
contracts <- ceiling(contracts_raw)
if (isTRUE(min_one_contract)) contracts <- pmax(1, contracts)
tibble::tibble(
Ticker = x$ticker,
Peso = w_i,
ValorActivo_USD = V_i,
StrikeRef = K_ref,
Contratos = contracts
)
})
size_tbl <- size_tbl %>%
mutate(
Contratos = as.integer(Contratos), # convierte a entero
across(where(is.numeric) & !matches("^Contratos$"), ~round(.x, 2)) # redondea las demás
)
tab_apa(size_tbl,
title = "Cobertura 85% — Tamaño por activo (n° contratos)",
caption = "Nota. 1 contrato = paquete de acciones según plantilla.") %>%
{
if ("fmt_integer" %in% getNamespaceExports("gt")) {
gt::fmt_integer(., columns = Contratos, sep_mark = ",")
} else {
gt::fmt_number(., columns = Contratos, decimals = 0, sep_mark = ",")
}
}| Cobertura 85% — Tamaño por activo (n° contratos) | ||||
| Ticker | Peso | ValorActivo_USD | StrikeRef | Contratos |
|---|---|---|---|---|
| BAC | 0.28 | 2,758,878.46 | 52.50 | 14,890.00 |
| WFC | 0.57 | 5,741,121.54 | 85.00 | 19,138.00 |
| USB | 0.15 | 1,500,000.00 | 47.50 | 8,948.00 |
# 2) Primas: AM por defecto (puedes cambiar a EU)
use_AM <- TRUE
px_tbl <- value_tbl %>% select(Ticker, EU_Call, EU_Put, AM_Call, AM_Put)
prem_tbl <- size_tbl %>%
left_join(px_tbl, by = "Ticker") %>%
mutate(
Precio_Call = if (use_AM) AM_Call else EU_Call,
Precio_Put = if (use_AM) AM_Put else EU_Put,
Premium_Call = Precio_Call * pkg_size * Contratos,
Premium_Put = Precio_Put * pkg_size * Contratos,
Premium_Total = Premium_Call + Premium_Put
)
prem_tbl %>%
select(Ticker, Contratos, Precio_Call, Precio_Put, Premium_Call, Premium_Put, Premium_Total) %>%
mutate(across(where(is.numeric), ~round(.x, 2)))# 3) % del portafolio y costo financiero
prem_tbl <- prem_tbl %>% mutate(Pct_Portafolio = 100 * Premium_Total / V_port_total)
prem_tbl %>%
select(Ticker, Premium_Total, Pct_Portafolio) %>%
mutate(across(where(is.numeric), ~round(.x, 2)))costo_financ_trimestral <- sum(prem_tbl$Premium_Total, na.rm = TRUE) * rf_qtr
tibble::tibble(
Prima_Total_USD = round(sum(prem_tbl$Premium_Total, na.rm = TRUE), 2),
Costo_Financ_Trimestral_USD = round(costo_financ_trimestral, 2),
Tasa_Libre_Riesgo_Anual = r_annual,
Tasa_Libre_Riesgo_Trimestral = rf_qtr
)# 4) Evidencia P&L (-1σ trimestral) del portafolio cubierto con 'alloc_w'
port_cov_xts <- PerformanceAnalytics::Return.portfolio(ret_qtr, weights = alloc_w)
mu_bar <- mean(as.numeric(port_cov_xts), na.rm=TRUE)
sd_bar <- sd(as.numeric(port_cov_xts), na.rm=TRUE)
shock_dn <- 1 + mu_bar - sd_bar
loss_nohedge_dn <- V_port_total * (shock_dn - 1)
total_premium <- sum(prem_tbl$Premium_Total, na.rm=TRUE)
gain_put_dn <- sum(prem_tbl$Premium_Put, na.rm=TRUE) * 1.20 # sensibilidad simple
PnL_net_dn <- loss_nohedge_dn + gain_put_dn - total_premium
tibble::tibble(
Escenario = "Caída -1σ (trimestral)",
Perdida_sin_cobertura = round(loss_nohedge_dn, 2),
Ganancia_PUT_aprox = round(gain_put_dn, 2),
Prima_Total_pagada = round(total_premium, 2),
PnL_neto_cubierto = round(PnL_net_dn, 2)
)En la gráfica “Costo de cobertura por activo” se observa que BAC presenta el mayor costo de cobertura, con una prima total de aproximadamente USD 1 507 326, seguido de WFC con USD 814 089, y finalmente USB con USD 405 014. Esta jerarquía coincide con el nivel de volatilidad individual de cada activo: BAC es el banco de mayor tamaño y exposición sistémica dentro del sector financiero estadounidense, por lo que su volatilidad es superior y, en consecuencia, sus opciones presentan primas más altas. WFC muestra un comportamiento intermedio, asociado a un perfil de riesgo medio derivado de su participación en el crédito hipotecario y el consumo, mientras que USB, con una volatilidad más baja y operaciones de banca regional, presenta el menor costo de cobertura, coherente con su perfil defensivo (Bodie et al., 2021; Instituto Tecnológico Metropolitano (ITM), 2025).
En términos relativos, las primas totales representan un gasto agregado cercano a USD 2.73 millones, equivalente al 27.3 % del monto cubierto (USD 8.5 millones). Este porcentaje es financieramente razonable para una cobertura apalancada a la tasa libre de riesgo (4.08 % EA), manteniendo el costo del seguro dentro de los márgenes teóricos esperados según (Hull, 2018) y (Instituto Tecnológico Metropolitano (ITM), 2025).
La gráfica “Peso del costo de cobertura” refuerza esta interpretación al mostrar la proporción del costo de cada activo sobre el total del portafolio:
BAC:15.07 %
WFC:8.14 %
USB:4.05 %
Esta distribución guarda coherencia con la asignación de pesos de riesgo definida en el diseño de la estrategia (55 % – 30 % – 15 %) y evidencia que los costos de las primas se concentran en los activos con mayor exposición al mercado. Desde la perspectiva de gestión del riesgo, el patrón observado es coherente con la teoría moderna de portafolios de (Markowitz, 1952), que plantea destinar un mayor esfuerzo de cobertura a los activos más volátiles para minimizar la varianza total del portafolio sin sacrificar rendimiento esperado.
En conjunto, ambas gráficas muestran que la cobertura está correctamente dimensionada y mantiene una relación costo–beneficio eficiente: el costo total de protección es una fracción manejable de la inversión, y los riesgos se distribuyen proporcionalmente al perfil de cada acción. Además, el cálculo de los costos de cobertura incorpora el apalancamiento trimestral a la tasa libre de riesgo del 4.08 % EA, asegurando una comparación homogénea entre las primas y el capital cubierto. Todo ello confirma que la estrategia es financieramente viable, estadísticamente consistente y eficaz para mitigar pérdidas en escenarios adversos (Hull, 2018; Instituto Tecnológico Metropolitano (ITM), 2025).
suppressPackageStartupMessages(library(ggplot2))
# Asegura tipos numéricos (por si vienen como character)
prem_tbl <- prem_tbl %>%
mutate(
Premium_Total = as.numeric(Premium_Total),
Pct_Portafolio = as.numeric(Pct_Portafolio),
Premium_Call = as.numeric(Premium_Call),
Premium_Put = as.numeric(Premium_Put)
)
# 1) Costo de cobertura por activo (USD)
ggplot(prem_tbl, aes(x = Ticker, y = Premium_Total)) +
geom_col(width = 0.65) +
# Etiquetas DENTRO de la barra para no requerir espacio extra arriba
geom_text(aes(label = scales::dollar(Premium_Total, accuracy = 1)),
vjust = 1.1, size = 3.5, color = "white") +
scale_y_continuous(
labels = scales::dollar_format(accuracy = 1),
expand = expansion(mult = c(0.02, 0.02)) # ↓ antes estaba 0.12
) +
labs(
title = "Costo de cobertura por activo",
subtitle = "Prima total (Call + Put) en USD",
x = NULL, y = "USD"
)# 2) Costo como % del portafolio
ggplot(prem_tbl, aes(x = reorder(Ticker, Pct_Portafolio), y = Pct_Portafolio/100)) +
geom_col(width = 0.65) +
geom_text(aes(label = paste0(round(Pct_Portafolio, 2), "%")),
vjust = 1.1, size = 3.5, color = "white") +
scale_y_continuous(
labels = scales::percent_format(accuracy = 0.1),
expand = expansion(mult = c(0.02, 0.02))
) +
labs(
title = "Peso del costo de cobertura",
subtitle = "Prima total como porcentaje del portafolio",
x = NULL, y = "% del portafolio"
)# 3) (Opcional) Desglose por tipo de opción
prem_long <- prem_tbl %>%
select(Ticker, Premium_Call, Premium_Put) %>%
tidyr::pivot_longer(cols = c(Premium_Call, Premium_Put),
names_to = "Tipo", values_to = "PremiumUSD") %>%
mutate(Tipo = dplyr::recode(Tipo, Premium_Call = "Call", Premium_Put = "Put"))
ggplot(prem_long, aes(x = Ticker, y = PremiumUSD, fill = Tipo)) +
geom_col(position = position_stack(), width = 0.65) +
# Etiquetas centradas en cada segmento: cero aire extra
geom_text(aes(label = scales::dollar(PremiumUSD, accuracy = 1)),
position = position_stack(vjust = 0.5), size = 3.2, color = "white") +
scale_y_continuous(
labels = scales::dollar_format(accuracy = 1),
expand = expansion(mult = c(0.02, 0.02))
) +
labs(
title = "Desglose de primas por activo",
subtitle = "Stack: Call + Put",
x = NULL, y = "USD", fill = NULL
) +
theme(legend.position = "bottom")El desarrollo del laboratorio permitió comprobar cómo la integración entre la teoría moderna de portafolios, la simulación estocástica de precios y la valoración binomial de opciones constituye una herramienta eficaz para la gestión integral del riesgo financiero. El portafolio construido con acciones de Bank of America (BAC), Wells Fargo (WFC) y U.S. Bancorp (USB) presenta una estructura sólida y diversificada dentro del sector bancario del S&P 500, respaldada por altos niveles de liquidez y la disponibilidad de instrumentos derivados (Chicago Board Options Exchange (CBOE), 2025b; Yahoo Finance, 2025b).
Los resultados del modelo de media–varianza evidencian un perfil de riesgo moderado:
La volatilidad anual se sitúa entre 16.8 % y 17.3 %, inferior al promedio histórico de portafolios puramente accionarios.
El índice de Sharpe, superior a 1.9, confirma una alta eficiencia rentabilidad–riesgo, donde el retorno adicional compensa holgadamente el riesgo asumido.
El Valor en Riesgo (VaR) al 95 % y 99 % indica pérdidas máximas esperadas entre USD 200 000 y USD 300 000 por trimestre, equivalentes al 2 %–3 % del capital, lo que demuestra un comportamiento estable y predecible (Bodie et al., 2021; Instituto Tecnológico Metropolitano (ITM), 2025).
La simulación mediante Movimiento Browniano Geométrico (MGB) muestra un crecimiento sostenido en el horizonte de dos años, con precios proyectados al alza en las tres acciones y un rango de portafolio total entre USD 9.1 y 17.8 millones. Este comportamiento, aunque expuesto a fluctuaciones de mercado, conserva una dispersión controlada y un sesgo alcista consistente con la evolución del sector financiero estadounidense (Hull, 2018; Instituto Tecnológico Metropolitano (ITM), 2025).
La estrategia de cobertura con opciones put americanas, que protege el 85 % de la inversión, demostró ser altamente efectiva para mitigar pérdidas en escenarios adversos. Las primas totales de cobertura representan un 27.3 % del valor cubierto, costo que se mantiene dentro de los rangos óptimos cuando se financia con apalancamiento a la tasa libre de riesgo (4.08 % EA). Los resultados de sensibilidad (−1σ) confirman que, en escenarios de caída del mercado, la cobertura compensa las pérdidas esperadas, estabilizando el valor neto del portafolio (Hull, 2018; Instituto Tecnológico Metropolitano (ITM), 2025; Trading Economics, 2025).
En conjunto, los hallazgos del laboratorio validan la eficacia del modelo Markowitz–CRR–MGB como esquema integral para la evaluación y gestión de portafolios, permitiendo optimizar el rendimiento ajustado al riesgo y diseñar estrategias de cobertura robustas y financieramente sostenibles.
Desde una perspectiva de gestión financiera, se recomienda mantener la inversión en este portafolio, dado su equilibrio entre crecimiento esperado y control del riesgo. La estructura de cobertura implementada mediante opciones put americanas proporciona una protección eficiente frente a caídas del mercado, sin sacrificar el potencial de rentabilidad en escenarios alcistas (Hull, 2018).
No obstante, es fundamental que la estrategia se revise y actualice trimestralmente, ajustando las primas y los precios de ejercicio de las opciones conforme varíen las tasas de interés, la volatilidad implícita y las condiciones del mercado de derivados. Este seguimiento garantiza que la cobertura mantenga su efectividad dinámica y que el costo del apalancamiento se mantenga dentro de márgenes sostenibles (Instituto Tecnológico Metropolitano (ITM), 2025).
En términos de riesgo, el portafolio puede clasificarse como de riesgo medio–bajo, adecuado para inversionistas institucionales o de perfil moderado que buscan estabilidad con rendimientos superiores a la tasa libre de riesgo. La evidencia empírica muestra que la diversificación intrasegmento y la cobertura con opciones put reducen significativamente la probabilidad de pérdidas extremas, asegurando un desempeño estable incluso bajo choques macroeconómicos moderados (Bodie et al., 2021).
En conclusión, invertir en este portafolio resulta una decisión financieramente viable, respaldada por fundamentos estadísticos sólidos, una estructura de riesgo bien calibrada y una cobertura que protege de manera efectiva el capital en un horizonte de inversión de largo plazo (Hull, 2018; Instituto Tecnológico Metropolitano (ITM), 2025).