Predicción de volatilidad: GARCH
INTRODUCCIÓN
El conjunto de datos “Repsol Stock Data - 20 Years”, disponible en Kaggle, ofrece información histórica de las acciones de Repsol durante un período de dos décadas. Contiene variables clave como precios de apertura, cierre, máximos, mínimos, volumen negociado y precios ajustados, lo que permite calcular retornos logarítmicos diarios, una métrica estándar para medir cambios relativos en series temporales financieras.
El uso de este dataset es especialmente relevante para modelar y predecir la volatilidad, dado que los mercados financieros, y en particular el sector energético, presentan patrones de heterocedasticidad condicional que modelos como GARCH pueden capturar de manera eficiente. Su capacidad para prever periodos de alta y baja volatilidad es crucial para comprender el comportamiento dinámico de los precios de los activos.
La predicción de la volatilidad financiera es un desafío crucial en finanzas cuantitativas. Los modelos GARCH (Generalized Autoregressive Conditional Heteroskedasticity) son tradicionales en este ámbito, ya que capturan la dinámica de la varianza condicional y permiten prever períodos de alta o baja volatilidad.
Un análisis del modelo GARCH permite identificar su fortaleza en escenarios donde la volatilidad presenta un comportamiento autoregresivo bien definido. Este modelo es especialmente útil en contextos financieros, como en el sector bursátil, donde los precios de las acciones son afectados por una interacción compleja de factores económicos y externos. La capacidad de GARCH para modelar la heterocedasticidad condicional permite predecir la volatilidad futura de manera más precisa, mejorando la toma de decisiones y la gestión de riesgos en mercados financieros.
Instalación de librerías
# Librerias para el desarrollo del proyecto
import math
import datetime
import warnings
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.figure_factory as ff
import matplotlib.pyplot as plt
import seaborn as sns
from arch import arch_model
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from ipywidgets import HBox, VBox
from tabulate import tabulate
from sklearn.metrics import mean_absolute_percentage_error
from sklearn.metrics import mean_squared_error
plt.style.use('fivethirtyeight')
# Ignorando algunas advertencias
warnings.filterwarnings('ignore')
Carga y Exploración del conjunto de datos
import pandas as pd
# Leer los datos como un DataFrame de pandas
repsol_stock_raw_data = pd.read_csv("RepsolStockData20Years.csv")
# Calcular el cambio porcentual por día
returns_repsol = 100 * repsol_stock_raw_data.close.pct_change().dropna()
# Eliminar 0 resultados, hay un error en el conjunto de datos y durante 74 días, el mercado de valores estuvo cerrado, por lo que el retorno es 0
returns_repsol = returns_repsol.drop(returns_repsol[returns_repsol == 0].index)
#Mostrar las 3 primeras filas de la Serie
returns_repsol.head(3)
## 1 -3.237407
## 2 1.486987
## 3 4.578755
## Name: close, dtype: float64
Visualización de rentabilidades y volatilidad
Se obtuvieron las rentabilidades diarias de las acciones de Repsol desde el 29 de octubre de 2002 hasta el 25 de octubre de 2022.
## np.float64(0.022709860241079974)
La media de las rentabilidades porcentuales diarias es 0,0223%, pero no utilizaremos esa información, ya que estudiaremos la volatilidad.
#Usando los precios de cierre brutos para representar gráficamente la evolución de las acciones
close_prices = pd.DataFrame(repsol_stock_raw_data["close"])
#Al dividir cada precio de cierre por el primer precio de nuestro conjunto de datos, calculamos la rentabilidad acumulada de cada día
cum_rets = close_prices / close_prices.iloc[0,:]
#Usando el módulo plotly.express podemos representar gráficamente nuestra nueva curva cum_rets
fig = px.line(cum_rets.iloc[:,:], width=1000, height=500)
#Añadiendo título
fig.update_layout(title_text='Rentabilidad acumulada de las acciones de Repsol (sin incluir dividendos) desde el 28/10/2002 hasta el 28/10/2022')
Medición de la volatilidad
#La volatilidad diaria es la desviación estándar de los retornos
daily_volatility = returns_repsol.std()
#La volatilidad mensual es el resultado de multiplicar el vol diario * la raíz cuadrada de 21, esto se debe a que hay 21 días de negociación en un mes
monthly_volatility = math.sqrt(21) * daily_volatility
#La volatilidad anual es el resultado de multiplicar el vol diario * la raíz cuadrada de 252, esto se debe a que hay 252 días de negociación en un año
annual_volatility = math.sqrt(252) * daily_volatility
#Usando el paquete tabulate podemos imprimir una bonita tabla
print(tabulate([['Repsol',daily_volatility,monthly_volatility,annual_volatility]],headers = ['Volatilidad Diaria %', 'Volatilidad Mensual %', 'Volatilidad Anual %'],tablefmt = 'fancy_grid',stralign='centro',numalign='centro',floatfmt=".2f"))
## ╒════════╤════════════════════════╤═════════════════════════╤═══════════════════════╕
## │ │ Volatilidad Diaria % │ Volatilidad Mensual % │ Volatilidad Anual % │
## ╞════════╪════════════════════════╪═════════════════════════╪═══════════════════════╡
## │ Repsol │ 1.97 │ 9.04 │ 31.32 │
## ╘════════╧════════════════════════╧═════════════════════════╧═══════════════════════╛
#Podemos representar gráficamente los retornos diarios de Repsol mediante un gráfico lineal usando .plot de pandas
returns_repsol.plot(figsize =(16,5), title = 'Retornos diarios de Repsol');
#Creamos un gráfico de distribución utilizando plotly.figure_factory, rediseñamos los datos para tenerlos en un vector
return_dist_plot = ff.create_distplot([returns_repsol.values.reshape(-1)], group_labels = [' '])
#Especificamos el diseño del gráfico
return_dist_plot.update_layout(showlegend=False, title_text='Distribución de los rendimientos diarios de Repsol', width=1000, height=500)
Este gráfico muestra la autocorrelación de los retornos al cuadrado, este resultado positivo nos permitirá predecir la volatilidad utilizando un modelo GARCH.
GARCH (4,4)
#Defina un modelo GARCH (4,4) que utilice una distribución GED
model = arch_model(returns_repsol,dist="ged", vol = 'GARCH', p=4, q=4)
#Ajuste del modelo
model_fit = model.fit(disp='off')
#Resumen del modelo
model_fit.summary()
Dep. Variable: | close | R-squared: | 0.000 |
---|---|---|---|
Mean Model: | Constant Mean | Adj. R-squared: | 0.000 |
Vol Model: | GARCH | Log-Likelihood: | -9576.84 |
Distribution: | Generalized Error Distribution | AIC: | 19175.7 |
Method: | Maximum Likelihood | BIC: | 19247.4 |
No. Observations: | 5018 | ||
Date: | sáb, Mar. 22 2025 | Df Residuals: | 5017 |
Time: | 23:06:12 | Df Model: | 1 |
coef | std err | t | P>|t| | 95.0% Conf. Int. | |
---|---|---|---|---|---|
mu | 0.0519 | 1.933e-02 | 2.686 | 7.233e-03 | [1.403e-02,8.980e-02] |
coef | std err | t | P>|t| | 95.0% Conf. Int. | |
---|---|---|---|---|---|
omega | 0.1136 | 3.056e-02 | 3.716 | 2.025e-04 | [5.367e-02, 0.173] |
alpha[1] | 0.0832 | 1.562e-02 | 5.325 | 1.009e-07 | [5.256e-02, 0.114] |
alpha[2] | 0.0544 | 1.985e-02 | 2.743 | 6.085e-03 | [1.554e-02,9.335e-02] |
alpha[3] | 0.0551 | 1.981e-02 | 2.783 | 5.385e-03 | [1.631e-02,9.396e-02] |
alpha[4] | 0.0579 | 1.711e-02 | 3.384 | 7.131e-04 | [2.437e-02,9.143e-02] |
beta[1] | 4.3433e-14 | 0.130 | 3.346e-13 | 1.000 | [ -0.254, 0.254] |
beta[2] | 5.8198e-03 | 0.108 | 5.382e-02 | 0.957 | [ -0.206, 0.218] |
beta[3] | 1.3490e-14 | 0.113 | 1.189e-13 | 1.000 | [ -0.222, 0.222] |
beta[4] | 0.7151 | 8.266e-02 | 8.651 | 5.087e-18 | [ 0.553, 0.877] |
coef | std err | t | P>|t| | 95.0% Conf. Int. | |
---|---|---|---|---|---|
nu | 1.3476 | 4.274e-02 | 31.528 | 3.550e-218 | [ 1.264, 1.431] |
Covariance estimator: robust
#Definir la serie completa como el modelo definido previamente
full_serie_garch = arch_model(returns_repsol,dist="ged", vol = 'GARCH', p=4, q=4)
#Ajustar el modelo para la serie completa
model_fit_full_serie = full_serie_garch.fit(disp='off')
#Gráficaremos contra la volatilidad móvil
rolling_vol = abs(returns_repsol.rolling(window=22, min_periods=22).std().dropna())
#Concatenar los valores verdaderos y los valores entrenados en nuestro modelo
garch_and_rolling_std = pd.concat([pd.DataFrame(model_fit_full_serie.conditional_volatility),rolling_vol.dropna()], axis=1).dropna()
#Gráficar
garch_and_rolling_std_plot = px.line(garch_and_rolling_std, title = 'GARCH vs. volatilidad continua de los retornos diarios TRAIN', width=1000, height=500)
#Impresión del gráfico
garch_and_rolling_std_plot.show()
#Utilizando un rango numérico de 251 para completar una lista de valores predichos, para cada día ajustamos un nuevo modelo con los mismos parámetros, pero agregando el último día.
test_size = 251
rolling_predictions = []
for i in range(test_size):
train = returns_repsol[:-(test_size-i)]
model = arch_model(train,dist="ged", vol = 'GARCH', p=4, q=4)
model_fit = model.fit(disp='off')
pred = model_fit.forecast(horizon=1, reindex = False)
rolling_predictions.append(np.sqrt(pred.variance.values[-1,:][0]))
#Transformándolo en una serie
rolling_predictions = pd.Series(rolling_predictions, index= returns_repsol.dropna().index[-test_size:])
#Establecer parámetros del gráfico
plt.figure(figsize=(10,4))
#Datos verdaderos
true, = plt.plot((rolling_vol)[-test_size:])
#Datos predichos
preds, = plt.plot(rolling_predictions)
#Gráfico de los datos
plt.title('Predicción de volatilidad para los próximos 251 días de negociación - Pronóstico continuo TEST con GARCH(4,4)', fontsize=10)
#Añadir leyenda
plt.legend(['Volatilidad verdadera', 'Volatilidad predicha'], fontsize=16)
RMSE_Serie = mean_squared_error(garch_and_rolling_std["close"],garch_and_rolling_std["cond_vol"],squared=False)
MAPE_Serie = mean_absolute_percentage_error(garch_and_rolling_std["close"], garch_and_rolling_std["cond_vol"])
print(f"El RMSE de nuestro modelo GARCH en los datos de la serie completa es {round(RMSE_Serie,4)}")
## El RMSE de nuestro modelo GARCH en los datos de la serie completa es 0.2397
print(f"El MAPE de nuestro modelo GARCH en los datos de la serie completa es {round(MAPE_Serie*100,2)}%")
## El MAPE de nuestro modelo GARCH en los datos de la serie completa es 12.07%
true_vol = rolling_vol[-test_size:]
pred_vol = rolling_predictions
RMSE = mean_squared_error(true_vol, pred_vol,squared=False)
MAPE = mean_absolute_percentage_error(true_vol, pred_vol)
print(f"El RMSE de nuestro modelo GARCH en los datos predichos es {round(RMSE,4)}")
## El RMSE de nuestro modelo GARCH en los datos predichos es 0.23
## El MAPE de nuestro modelo GARCH en los datos predichos es 8.38%
print(tabulate([['RMSE',round(RMSE_Serie,6),round(RMSE,6)]],
headers = ['GARCH ENTRENAMIENTO', 'GARCH PREDICCIONES'],tablefmt = 'fancy_grid',stralign='center',numalign='center',floatfmt=".2f"))
## ╒══════╤═══════════════════════╤══════════════════════╕
## │ │ GARCH ENTRENAMIENTO │ GARCH PREDICCIONES │
## ╞══════╪═══════════════════════╪══════════════════════╡
## │ RMSE │ 0.24 │ 0.23 │
## ╘══════╧═══════════════════════╧══════════════════════╛
print(tabulate([['MAPE',round(MAPE_Serie*100,2),round(MAPE*100,2)]],
headers = ['GARCH ENTRENAMIENTO %', 'GARCH PREDICCIONES %'],tablefmt = 'fancy_grid',stralign='center',numalign='center',floatfmt=".2f"))
## ╒══════╤═════════════════════════╤════════════════════════╕
## │ │ GARCH ENTRENAMIENTO % │ GARCH PREDICCIONES % │
## ╞══════╪═════════════════════════╪════════════════════════╡
## │ MAPE │ 12.07 │ 8.38 │
## ╘══════╧═════════════════════════╧════════════════════════╛
Nuestro estudio ha demostrado que el modelo GARCH puede predecir de manera efectiva la volatilidad de las acciones de Repsol. Este enfoque resalta la capacidad del modelo para capturar la naturaleza dinámica de la volatilidad a lo largo del tiempo, lo que lo convierte en una herramienta útil para la predicción en el ámbito financiero. Este proyecto subraya la importancia de tener en cuenta la variabilidad de la volatilidad en el análisis de los mercados financieros.
Universidad Nacional Agraria La Molina, 20180260@lamolina.edu.pe↩︎
Universidad Nacional Agraria La Molina, 20180272@lamolina.edu.pe↩︎
Universidad Nacional Agraria La Molina, 20200339@lamolina.edu.pe↩︎