📊 KPIs

Row

Tickers Analizados

9

Observaciones (días limpios)

3455

Ticker Más Volátil

TSLA (3.6172%)

Mejor Sharpe Ratio

NVDA (0.0791)

Peor VaR 95%

TSLA (-5.1287%)

Row

📋 Tabla KPIs completa

📈 Sharpe vs Volatilidad

📉 Distribución & Colas

Row

Objetivo: Detectar asimetría, colas pesadas y comportamientos extremos en los retornos diarios.

Row

🎻 Violin + Boxplot — Dispersión completa

🌊 Ridgeline — Densidades por Ticker

Row

📐 Asimetría vs Curtosis — Forma de las colas

🔥 VaR y CVaR 95% — Riesgo de cola

🔍 Comparaciones

Row

Objetivo: Segmentar los activos por nivel de riesgo y comparar su comportamiento colectivo.

Row

📊 Clasificación por Volatilidad

📦 Boxplot comparativo por grupo

Row

📈 Serie de precios normalizados (base 100)

📊 Retorno acumulado por Ticker

🔗 Dependencia

Row

Objetivo: Identificar relaciones lineales y no lineales entre activos para diversificación de portafolio.

Row

🟦 Matriz de Correlación de Pearson

🌐 Heatmap interactivo de correlación

Row

📉 Dispersión: par más correlacionado

📊 Correlación promedio por Ticker (centralidad)

📁 Datos

Row

🗃️ Dataset limpio — Retornos diarios

Row

📖 Diccionario de Variables

---
title: "Dashboard Financiero — S&P500 Top 10"
author: "Jhonatan"
date: "2026-03-10"
output:
  flexdashboard::flex_dashboard:
    orientation: rows
    vertical_layout: scroll
    theme: flatly
    source_code: embed
    navbar:
      - { title: "Taller 1-2", icon: "fa-chart-line", align: right }
---

```{r setup, include=FALSE}
# ── Librerías ──────────────────────────────────────────────────────────────────
library(flexdashboard)
library(readr)
library(dplyr)
library(tidyr)
library(tibble)
library(ggplot2)
library(plotly)
library(DT)
library(scales)
library(moments)      # skewness / kurtosis
library(corrplot)     # correlación
library(ggridges)     # ridgeline plots

# ── Paleta corporativa ─────────────────────────────────────────────────────────
PALETTE <- c(
  "#2C3E50","#E74C3C","#3498DB","#2ECC71","#F39C12",
  "#9B59B6","#1ABC9C","#E67E22","#34495E","#E91E63"
)

# ── Carga de datos ─────────────────────────────────────────────────────────────
sp500_raw <- read_csv("sp500_top10_stocks_clean.csv", show_col_types = FALSE)

# ── Pipeline principal ─────────────────────────────────────────────────────────
sp500_reducido <- sp500_raw %>% select(Date, Ticker, Close)

sp500_ancho <- sp500_reducido %>%
  pivot_wider(names_from = Ticker, values_from = Close) %>%
  mutate(across(where(is.numeric), ~ round(., 2)))

sp500_imputado <- sp500_ancho %>% drop_na()

sp500_retornos <- sp500_imputado %>%
  pivot_longer(cols = -Date, names_to = "Ticker", values_to = "Precio") %>%
  arrange(Ticker, Date) %>%
  group_by(Ticker) %>%
  mutate(Retorno = (Precio - lag(Precio)) / lag(Precio) * 100) %>%
  filter(!is.na(Retorno)) %>%
  ungroup()

resumen <- sp500_retornos %>%
  group_by(Ticker) %>%
  summarise(
    Media    = round(mean(Retorno), 4),
    Mediana  = round(median(Retorno), 4),
    Desv_Std = round(sd(Retorno), 4),
    Asimetria= round(skewness(Retorno), 4),
    Curtosis = round(kurtosis(Retorno), 4),
    VaR_95   = round(quantile(Retorno, 0.05), 4),
    CVaR_95  = round(mean(Retorno[Retorno <= quantile(Retorno, 0.05)]), 4),
    Sharpe   = round(mean(Retorno) / sd(Retorno), 4),
    Min      = round(min(Retorno), 4),
    Max      = round(max(Retorno), 4),
    .groups  = "drop"
  ) %>%
  arrange(desc(Desv_Std))

tickers <- unique(sp500_retornos$Ticker)
n_obs   <- nrow(sp500_imputado)
n_dias  <- as.numeric(diff(range(sp500_imputado$Date)))
```

<!-- ═══════════════════════════════════════════════════════════════════
     PÁGINA 1 · KPIs CON INCERTIDUMBRE
═══════════════════════════════════════════════════════════════════ -->

# 📊 KPIs

## Row {data-height=130}

### Tickers Analizados
```{r}
valueBox(length(tickers), icon = "fa-th", color = "#2C3E50")
```

### Observaciones (días limpios)
```{r}
valueBox(n_obs, icon = "fa-calendar-check", color = "#3498DB")
```

### Ticker Más Volátil
```{r}
top_vol <- resumen %>% slice_max(Desv_Std, n = 1)
valueBox(
  paste0(top_vol$Ticker, " (", top_vol$Desv_Std, "%)"),
  icon  = "fa-bolt",
  color = "#E74C3C"
)
```

### Mejor Sharpe Ratio
```{r}
top_sharpe <- resumen %>% slice_max(Sharpe, n = 1)
valueBox(
  paste0(top_sharpe$Ticker, " (", top_sharpe$Sharpe, ")"),
  icon  = "fa-trophy",
  color = "#2ECC71"
)
```

### Peor VaR 95%
```{r}
worst_var <- resumen %>% slice_min(VaR_95, n = 1)
valueBox(
  paste0(worst_var$Ticker, " (", worst_var$VaR_95, "%)"),
  icon  = "fa-exclamation-triangle",
  color = "#E67E22"
)
```

## Row {data-height=420}

### 📋 Tabla KPIs completa {data-width=700}
```{r}
datatable(
  resumen %>% select(Ticker, Media, Desv_Std, Sharpe, VaR_95, CVaR_95, Asimetria, Curtosis),
  rownames  = FALSE,
  options   = list(pageLength = 10, dom = "frtip", scrollX = TRUE),
  caption   = "KPIs de Retornos Diarios (%) con métricas de riesgo"
) %>%
  formatStyle("Desv_Std",
    background = styleColorBar(resumen$Desv_Std, "#E74C3C33"),
    backgroundSize = "100% 90%", backgroundRepeat = "no-repeat",
    backgroundPosition = "center"
  ) %>%
  formatStyle("Sharpe",
    color = styleInterval(0, c("#E74C3C", "#2ECC71")),
    fontWeight = "bold"
  ) %>%
  formatStyle("VaR_95",
    color = "#E74C3C", fontWeight = "bold"
  )
```

### 📈 Sharpe vs Volatilidad {data-width=500}
```{r}
p <- ggplot(resumen, aes(x = Desv_Std, y = Sharpe, color = Ticker, size = abs(VaR_95))) +
  geom_point(alpha = 0.85) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
  geom_text(aes(label = Ticker), vjust = -1.1, size = 3, show.legend = FALSE) +
  scale_color_manual(values = PALETTE) +
  labs(
    title  = "Riesgo vs Retorno ajustado",
    x      = "Volatilidad (Desv. Std %)",
    y      = "Sharpe Ratio",
    size   = "|VaR 95%|",
    color  = "Ticker"
  ) +
  theme_minimal(base_size = 12)

ggplotly(p, tooltip = c("Ticker","Desv_Std","Sharpe","VaR_95")) %>%
  layout(legend = list(orientation = "h", y = -0.2))
```

<!-- ═══════════════════════════════════════════════════════════════════
     PÁGINA 2 · DISTRIBUCIÓN Y COLAS
═══════════════════════════════════════════════════════════════════ -->

# 📉 Distribución & Colas

## Row {data-height=50}
> **Objetivo:** Detectar asimetría, colas pesadas y comportamientos extremos en los retornos diarios.

## Row {data-height=420}

### 🎻 Violin + Boxplot — Dispersión completa
```{r}
p <- ggplot(sp500_retornos,
            aes(x = reorder(Ticker, Retorno, FUN = sd),
                y = Retorno, fill = Ticker)) +
  geom_violin(alpha = 0.55, trim = FALSE) +
  geom_boxplot(width = 0.18, outlier.color = "#E74C3C",
               outlier.size = 1.2, alpha = 0.8) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray40") +
  scale_fill_manual(values = PALETTE) +
  labs(title = "Distribución de Retornos Diarios por Ticker",
       subtitle = "Ordenado de menor a mayor volatilidad",
       x = "Ticker", y = "Retorno Diario (%)") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

ggplotly(p)
```

### 🌊 Ridgeline — Densidades por Ticker
```{r}
p <- ggplot(sp500_retornos,
            aes(x = Retorno, y = reorder(Ticker, Retorno, FUN = sd),
                fill = after_stat(x))) +
  geom_density_ridges_gradient(scale = 2.5, rel_min_height = 0.01,
                                quantile_lines = TRUE, quantiles = c(0.05, 0.95)) +
  scale_fill_gradient2(low = "#E74C3C", mid = "#ECF0F1", high = "#2ECC71",
                       midpoint = 0) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray40") +
  labs(title = "Densidad de Retornos por Ticker",
       subtitle = "Líneas verticales: percentiles 5% y 95%",
       x = "Retorno Diario (%)", y = "Ticker") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

ggplotly(p)
```

## Row {data-height=380}

### 📐 Asimetría vs Curtosis — Forma de las colas
```{r}
p <- ggplot(resumen, aes(x = Asimetria, y = Curtosis,
                          color = Ticker, size = Desv_Std)) +
  geom_point(alpha = 0.85) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "gray50") +
  geom_hline(yintercept = 3, linetype = "dashed", color = "#3498DB",
             linewidth = 0.8) +
  annotate("text", x = max(resumen$Asimetria)*0.85, y = 3.2,
           label = "Normal (curtosis=3)", color = "#3498DB", size = 3.5) +
  geom_text(aes(label = Ticker), vjust = -1, size = 3, show.legend = FALSE) +
  scale_color_manual(values = PALETTE) +
  labs(title = "Forma de la distribución de retornos",
       subtitle = "Curtosis > 3 = colas pesadas (leptocúrtica)",
       x = "Asimetría (Skewness)", y = "Curtosis",
       size = "Volatilidad", color = "Ticker") +
  theme_minimal(base_size = 12)

ggplotly(p, tooltip = c("Ticker","Asimetria","Curtosis","Desv_Std"))
```

### 🔥 VaR y CVaR 95% — Riesgo de cola
```{r}
riesgo_df <- resumen %>%
  select(Ticker, VaR_95, CVaR_95) %>%
  pivot_longer(cols = c(VaR_95, CVaR_95), names_to = "Metrica", values_to = "Valor")

p <- ggplot(riesgo_df, aes(x = reorder(Ticker, Valor, FUN = min),
                            y = Valor, fill = Metrica)) +
  geom_col(position = "dodge", alpha = 0.85) +
  scale_fill_manual(values = c("VaR_95" = "#E74C3C", "CVaR_95" = "#C0392B"),
                    labels = c("CVaR 95%", "VaR 95%")) +
  geom_hline(yintercept = 0, color = "gray40") +
  labs(title = "Riesgo de Cola: VaR y CVaR al 95%",
       subtitle = "Valores más negativos = mayor riesgo extremo",
       x = "Ticker", y = "Retorno (%)", fill = "Métrica") +
  theme_minimal(base_size = 12) +
  coord_flip()

ggplotly(p)
```

<!-- ═══════════════════════════════════════════════════════════════════
     PÁGINA 3 · COMPARACIONES POR GRUPOS
═══════════════════════════════════════════════════════════════════ -->

# 🔍 Comparaciones

## Row {data-height=50}
> **Objetivo:** Segmentar los activos por nivel de riesgo y comparar su comportamiento colectivo.

## Row {data-height=420}

### 📊 Clasificación por Volatilidad
```{r}
resumen_grupo <- resumen %>%
  mutate(
    Grupo = case_when(
      Desv_Std < quantile(Desv_Std, 0.33) ~ "🟢 Baja Volatilidad",
      Desv_Std < quantile(Desv_Std, 0.66) ~ "🟡 Media Volatilidad",
      TRUE                                 ~ "🔴 Alta Volatilidad"
    )
  )

p <- ggplot(resumen_grupo,
            aes(x = reorder(Ticker, Desv_Std), y = Desv_Std,
                fill = Grupo)) +
  geom_col(alpha = 0.88) +
  scale_fill_manual(values = c(
    "🟢 Baja Volatilidad"  = "#2ECC71",
    "🟡 Media Volatilidad" = "#F39C12",
    "🔴 Alta Volatilidad"  = "#E74C3C"
  )) +
  geom_text(aes(label = paste0(Desv_Std, "%")), hjust = -0.1, size = 3.2) +
  labs(title = "Clasificación de Tickers por Volatilidad",
       x = "Ticker", y = "Desv. Std (%)", fill = "Grupo") +
  theme_minimal(base_size = 12) +
  coord_flip() +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

ggplotly(p)
```

### 📦 Boxplot comparativo por grupo
```{r}
sp500_grupos <- sp500_retornos %>%
  left_join(resumen_grupo %>% select(Ticker, Grupo), by = "Ticker")

p <- ggplot(sp500_grupos, aes(x = Grupo, y = Retorno, fill = Grupo)) +
  geom_boxplot(outlier.color = "#E74C3C", outlier.size = 1, alpha = 0.8) +
  geom_jitter(width = 0.1, alpha = 0.05, color = "gray40") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray40") +
  scale_fill_manual(values = c(
    "🟢 Baja Volatilidad"  = "#2ECC71",
    "🟡 Media Volatilidad" = "#F39C12",
    "🔴 Alta Volatilidad"  = "#E74C3C"
  )) +
  labs(title = "Distribución de Retornos por Grupo de Riesgo",
       x = "Grupo", y = "Retorno Diario (%)") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

ggplotly(p)
```

## Row {data-height=380}

### 📈 Serie de precios normalizados (base 100)
```{r}
sp500_norm <- sp500_imputado %>%
  pivot_longer(cols = -Date, names_to = "Ticker", values_to = "Precio") %>%
  group_by(Ticker) %>%
  mutate(Precio_Norm = Precio / first(Precio) * 100) %>%
  ungroup()

p <- ggplot(sp500_norm, aes(x = Date, y = Precio_Norm,
                             color = Ticker, group = Ticker)) +
  geom_line(alpha = 0.85, linewidth = 0.7) +
  geom_hline(yintercept = 100, linetype = "dashed", color = "gray40") +
  scale_color_manual(values = PALETTE) +
  labs(title = "Evolución de Precios Normalizados (Base 100)",
       subtitle = "Permite comparar rendimiento relativo entre activos",
       x = "Fecha", y = "Precio Indexado (Base 100)", color = "Ticker") +
  theme_minimal(base_size = 12)

ggplotly(p) %>%
  layout(legend = list(orientation = "h", y = -0.2))
```

### 📊 Retorno acumulado por Ticker
```{r}
sp500_acum <- sp500_retornos %>%
  group_by(Ticker) %>%
  arrange(Date) %>%
  mutate(Retorno_Acum = cumprod(1 + Retorno / 100) - 1) %>%
  ungroup()

p <- ggplot(sp500_acum, aes(x = Date, y = Retorno_Acum * 100,
                             color = Ticker, group = Ticker)) +
  geom_line(alpha = 0.85, linewidth = 0.7) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray40") +
  scale_color_manual(values = PALETTE) +
  scale_y_continuous(labels = percent_format(scale = 1, suffix = "%")) +
  labs(title = "Retorno Acumulado por Ticker (%)",
       x = "Fecha", y = "Retorno Acumulado (%)", color = "Ticker") +
  theme_minimal(base_size = 12)

ggplotly(p) %>%
  layout(legend = list(orientation = "h", y = -0.2))
```

<!-- ═══════════════════════════════════════════════════════════════════
     PÁGINA 4 · DEPENDENCIA (CORRELACIONES)
═══════════════════════════════════════════════════════════════════ -->

# 🔗 Dependencia

## Row {data-height=50}
> **Objetivo:** Identificar relaciones lineales y no lineales entre activos para diversificación de portafolio.

## Row {data-height=500}

### 🟦 Matriz de Correlación de Pearson
```{r, fig.width=9, fig.height=7}
ret_matrix <- sp500_retornos %>%
  select(Date, Ticker, Retorno) %>%
  pivot_wider(names_from = Ticker, values_from = Retorno) %>%
  select(-Date)

cor_matrix <- cor(ret_matrix, use = "complete.obs", method = "pearson")

col_palette <- colorRampPalette(c("#E74C3C", "#FFFFFF", "#3498DB"))(200)

corrplot(
  cor_matrix,
  method      = "color",
  col         = col_palette,
  type        = "upper",
  order       = "hclust",
  addCoef.col = "black",
  number.cex  = 0.7,
  tl.col      = "#2C3E50",
  tl.srt      = 45,
  title       = "Correlación de Pearson — Retornos Diarios",
  mar         = c(0, 0, 2, 0)
)
```

### 🌐 Heatmap interactivo de correlación
```{r}
cor_df <- as.data.frame(cor_matrix) %>%
  rownames_to_column("Ticker_X") %>%
  pivot_longer(-Ticker_X, names_to = "Ticker_Y", values_to = "Correlacion")

p <- ggplot(cor_df, aes(x = Ticker_X, y = Ticker_Y,
                         fill = Correlacion, text = paste0(
                           Ticker_X, " vs ", Ticker_Y,
                           "\nCorrelación: ", round(Correlacion, 3)
                         ))) +
  geom_tile(color = "white", linewidth = 0.5) +
  scale_fill_gradient2(low = "#E74C3C", mid = "#FFFFFF", high = "#3498DB",
                       midpoint = 0, limits = c(-1, 1)) +
  geom_text(aes(label = round(Correlacion, 2)), size = 2.8, color = "gray20") +
  labs(title = "Heatmap de Correlaciones",
       x = NULL, y = NULL, fill = "Correlación") +
  theme_minimal(base_size = 11) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

ggplotly(p, tooltip = "text")
```

## Row {data-height=380}

### 📉 Dispersión: par más correlacionado
```{r}
# Par con mayor correlación (excluyendo diagonal)
diag(cor_matrix) <- NA
idx    <- which(cor_matrix == max(cor_matrix, na.rm = TRUE), arr.ind = TRUE)[1, ]
tick_a <- rownames(cor_matrix)[idx[1]]
tick_b <- colnames(cor_matrix)[idx[2]]
cor_val <- round(max(cor_matrix, na.rm = TRUE), 3)

par_df <- sp500_retornos %>%
  filter(Ticker %in% c(tick_a, tick_b)) %>%
  select(Date, Ticker, Retorno) %>%
  pivot_wider(names_from = Ticker, values_from = Retorno) %>%
  drop_na()

p <- ggplot(par_df, aes_string(x = tick_a, y = tick_b)) +
  geom_point(alpha = 0.45, color = "#3498DB") +
  geom_smooth(method = "lm", color = "#E74C3C", se = TRUE, linewidth = 1) +
  labs(
    title    = paste("Par más correlacionado:", tick_a, "vs", tick_b),
    subtitle = paste("Correlación de Pearson:", cor_val),
    x        = paste("Retorno", tick_a, "(%)"),
    y        = paste("Retorno", tick_b, "(%)")
  ) +
  theme_minimal(base_size = 12)

ggplotly(p)
```

### 📊 Correlación promedio por Ticker (centralidad)
```{r}
diag(cor_matrix) <- NA
cor_avg <- rowMeans(cor_matrix, na.rm = TRUE)

cor_avg_df <- data.frame(
  Ticker   = names(cor_avg),
  Cor_Prom = round(cor_avg, 4)
) %>% arrange(desc(Cor_Prom))

p <- ggplot(cor_avg_df, aes(x = reorder(Ticker, Cor_Prom),
                             y = Cor_Prom, fill = Cor_Prom)) +
  geom_col(alpha = 0.88) +
  scale_fill_gradient2(low = "#E74C3C", mid = "#ECF0F1", high = "#3498DB",
                       midpoint = median(cor_avg_df$Cor_Prom)) +
  geom_text(aes(label = Cor_Prom), hjust = -0.15, size = 3) +
  labs(title = "Correlación Promedio con el Resto de Tickers",
       subtitle = "Mayor valor = activo más 'sistémico'",
       x = "Ticker", y = "Correlación Promedio") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none") +
  coord_flip() +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

ggplotly(p)
```

<!-- ═══════════════════════════════════════════════════════════════════
     PÁGINA 5 · DATOS Y DICCIONARIO
═══════════════════════════════════════════════════════════════════ -->

# 📁 Datos

## Row {data-height=380}

### 🗃️ Dataset limpio — Retornos diarios
```{r}
datatable(
  sp500_retornos %>%
    mutate(Retorno = round(Retorno, 4),
           Precio  = round(Precio, 2)) %>%
    arrange(desc(Date)),
  rownames = FALSE,
  filter   = "top",
  options  = list(pageLength = 12, scrollX = TRUE, dom = "frtip")
) %>%
  formatStyle("Retorno",
    color = styleInterval(0, c("#E74C3C", "#2ECC71")),
    fontWeight = "bold"
  )
```

## Row {data-height=420}

### 📖 Diccionario de Variables
```{r}
diccionario <- data.frame(
  Sección = c(
    "Pipeline","Pipeline","Pipeline","Pipeline","Pipeline",
    "KPIs","KPIs","KPIs","KPIs","KPIs","KPIs",
    "Agrupación","Normalización"
  ),
  Variable = c(
    "sp500_raw","sp500_reducido","sp500_ancho","sp500_imputado","sp500_retornos",
    "Media","Desv_Std","Sharpe","VaR_95","CVaR_95","Curtosis",
    "Grupo","Precio_Norm"
  ),
  Tipo = c(
    "data.frame","data.frame","data.frame","data.frame","data.frame",
    "numeric","numeric","numeric","numeric","numeric","numeric",
    "character","numeric"
  ),
  Descripción = c(
    "Dataset original cargado desde CSV",
    "Columnas Date, Ticker, Close seleccionadas",
    "Formato ancho: una columna por Ticker",
    "Dataset sin filas con NA (drop_na)",
    "Retornos diarios % por Ticker en formato largo",
    "Promedio del retorno diario por Ticker (%)",
    "Volatilidad: desviación estándar de retornos (%)",
    "Retorno medio / desv. std (retorno ajustado por riesgo)",
    "Percentil 5%: pérdida máxima esperada al 95% confianza",
    "Media de retornos por debajo del VaR (pérdida esperada extrema)",
    "Curtosis: >3 indica colas pesadas (leptocúrtica)",
    "Baja / Media / Alta volatilidad según terciles de Desv_Std",
    "Precio indexado a 100 en el primer día disponible"
  ),
  stringsAsFactors = FALSE
)

datatable(
  diccionario,
  rownames = FALSE,
  options  = list(pageLength = 13, dom = "frtip", scrollX = TRUE)
) %>%
  formatStyle("Variable",
    fontFamily = "Courier New", color = "#C0392B", fontWeight = "bold"
  ) %>%
  formatStyle("Sección",
    fontWeight = "bold", color = "#1F4E79"
  ) %>%
  formatStyle("Tipo",
    fontStyle = "italic", color = "#27AE60"
  )
```