Laboratorio #2: Opciones y Árboles Binomiales

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."
)
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.

  1. Volatilidad: medición del riesgo total

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.

  1. Índice de Sharpe: eficiencia rentabilidad–riesgo

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.

  1. Precios esperados de cierre por trimestre

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).

  1. Valor en Riesgo (VaR) al 1 % y 5 %

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
tab_apa(tbl_var,     title = "VaR histórico y paramétrico (trimestral)")
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."
)
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)
Valuación binomial (CRR, dt = 0.25): Europea vs. Americana — Vencimiento 2 años
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)
}
Árbol binomial (2 pasos): Precio del subyacente (arriba) y valor de la opción (abajo) para un Put Americano sobre BAC.
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)
Strip trimestral (CRR): valuación por trimestre, CALL y PUT, EU y AM
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_cols

La 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 = ",")
    }
  }
Nota. 1 contrato = paquete de acciones según plantilla.
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).

REFERENCIAS

Bodie, Z., Kane, A., & Marcus, A. J. (2021). Investments (12th ed.). McGraw-Hill Education.
Chicago Board Options Exchange (CBOE). (2025a). Option chains and product specifications. https://www.cboe.com
Chicago Board Options Exchange (CBOE). (2025b). Option chains and product specifications. https://www.cboe.com
Cox, J. C., Ross, S. A., & Rubinstein, M. (1979). Option pricing: A simplified approach. Journal of Financial Economics, 7(3), 229–263. https://doi.org/10.1016/0304-405X(79)90015-1
Hull, J. C. (2018). Options, futures, and other derivatives (10th ed.). Pearson Education.
Instituto Tecnológico Metropolitano (ITM). (2025). Derivados financieros — apuntes de clase (cap. 4 y 5). Apuntes institucionales.
Markowitz, H. (1952). Portfolio selection. The Journal of Finance, 7(1), 77–91. https://doi.org/10.1111/j.1540-6261.1952.tb01525.x
NASDAQ. (2025). Dividend yields: BAC, WFC, USB. https://www.nasdaq.com
Trading Economics. (2025). US 10-year treasury bond yield. https://tradingeconomics.com/united-states/government-bond-yield
Yahoo Finance. (2025a). BAC, WFC, USB — historical data & volumes. https://finance.yahoo.com
Yahoo Finance. (2025b). BAC, WFC, USB — historical data & volumes. https://finance.yahoo.com