GARCH Volatility Model
Implement univariate GARCH, EGARCH, and GJR-GARCH volatility forecasting models to capture the well-documented volatility clustering phenomenon, asymmetric leverage effects, and conditional time-varying heteroskedasticity in financial asset return series.
Statistical Analysis: GARCH Volatility Forecasting
This notebook delves into Generalized Autoregressive Conditional Heteroskedasticity (GARCH) models, a fundamental tool in financial econometrics for modeling and forecasting volatility in financial time series. Volatility, a measure of the dispersion of returns for a given security or market index, is crucial for risk management, asset pricing, portfolio optimization, and derivatives trading.
Traditional financial models often assume constant volatility, which is rarely observed in real-world markets. GARCH models address this limitation by allowing conditional variance to be dependent on past squared errors and past conditional variances, capturing key stylized facts of financial returns such as volatility clustering and leptokurtosis.
Key Concepts Covered:
| Concept | Description | Importance in GARCH |
|---|---|---|
| Volatility Clustering | Periods of high volatility tend to be followed by periods of high volatility, and vice versa. | GARCH models are specifically designed to capture this phenomenon. |
| Leptokurtosis | Financial return distributions often have fatter tails and a higher peak than a normal distribution. | GARCH models can implicitly account for this through time-varying variance. |
| Conditional Heteroskedasticity | The variance of the error term depends on the size of previous error terms. | The core assumption that GARCH models are built upon. |
| ARCH Model | Autoregressive Conditional Heteroskedasticity; a predecessor to GARCH, where current variance depends on past squared innovations. | GARCH is an extension of ARCH, adding a moving average component to the variance. |
| GARCH Model | Generalized ARCH; current conditional variance is a function of past squared residuals and past conditional variances. | Provides a more parsimonious and flexible way to model volatility clustering. |
| Maximum Likelihood Estimation (MLE) | A method for estimating the parameters of a statistical model by maximizing a likelihood function. | Commonly used to estimate GARCH model parameters. |
| Volatility Forecasting | Predicting future levels of market volatility. | A primary application of GARCH models in finance. |
| Model Evaluation | Assessing the performance and adequacy of a GARCH model. | Involves checking standardized residuals and comparing forecasts to realized volatility. |
Dependency Installation
This section installs all necessary Python libraries required for this notebook. The primary library for GARCH modeling is arch.
# Install necessary libraries
!pip install pandas numpy matplotlib seaborn arch yfinance --quiet[?25l [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m0.0/981.3 kB[0m [31m?[0m eta [36m-:--:--[0m [2K [91m━━━━━━━━━━━[0m[91m╸[0m[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m286.7/981.3 kB[0m [31m9.2 MB/s[0m eta [36m0:00:01[0m [2K [91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m[91m╸[0m [32m972.8/981.3 kB[0m [31m17.3 MB/s[0m eta [36m0:00:01[0m [2K [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m981.3/981.3 kB[0m [31m10.4 MB/s[0m eta [36m0:00:00[0m [?25h
Library Imports
This section imports all standard and third-party libraries used throughout the notebook. Organized for clarity with standard libraries first, followed by specialized third-party packages.
# Standard library imports
import logging
import collections
import datetime
import time
import random
# Third-party library imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import yfinance as yf
from arch import arch_model
from sklearn.metrics import mean_squared_error
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['figure.dpi'] = 100Core Functions
This section defines the core functions used for GARCH modeling, including data fetching, preprocessing, model fitting, forecasting, and evaluation. Each function is encapsulated in its own code block with detailed documentation, type hints, and logging.
Function Name: create_garch_state
This function initializes the state dictionary for the GARCH analysis. It sets up default parameters and empty containers that will be populated by subsequent functions.
Parameters:
initial_params(dict, optional): A dictionary of initial parameters to override defaults.
Returns:
- (dict): An initialized state dictionary for GARCH analysis.
def create_garch_state(initial_params: dict = None) -> dict:
"""
Initializes the state dictionary for GARCH analysis.
Parameters
----------
initial_params : dict, optional
A dictionary of initial parameters to override defaults.
Returns
-------
dict
An initialized state dictionary for GARCH analysis.
"""
state = {
'symbol': 'SPY', # Default stock symbol
'start_date': (datetime.date.today() - datetime.timedelta(days=5*365)).strftime('%Y-%m-%d'), # 5 years ago
'end_date': datetime.date.today().strftime('%Y-%m-%d'),
'data': pd.DataFrame(),
'returns': pd.Series(),
'model_fit': None,
'forecast_horizon': 5, # Days to forecast
'forecasts': pd.DataFrame(),
'eval_metrics': {},
'log_messages': collections.deque(maxlen=100) # For logging important events
}
if initial_params:
state.update(initial_params)
logger.info(f"GARCH state initialized for symbol: {state['symbol']} from {state['start_date']} to {state['end_date']}")
state['log_messages'].append(f"GARCH state initialized for symbol: {state['symbol']}")
return stateFunction Name: fetch_financial_data
This function fetches historical financial data for a given stock symbol within a specified date range using the yfinance library. It includes retry logic with exponential backoff to handle potential API transient errors.
Parameters:
state(dict): The current state dictionary.symbol(str): The stock ticker symbol (e.g., 'SPY', 'AAPL').start_date(str): The start date for data fetching in 'YYYY-MM-DD' format.end_date(str): The end date for data fetching in 'YYYY-MM-DD' format.
Returns:
- (dict): The updated state dictionary with the fetched financial data stored under the 'data' key.
def fetch_financial_data(state: dict, symbol: str, start_date: str, end_date: str) -> dict:
"""
Fetches historical financial data for a given symbol and date range.
Parameters
----------
state : dict
The current state dictionary.
symbol : str
The stock ticker symbol (e.g., 'SPY', 'AAPL').
start_date : str
The start date for data fetching in 'YYYY-MM-DD' format.
end_date : str
The end date for data fetching in 'YYYY-MM-DD' format.
Returns
-------
dict
The updated state dictionary with the fetched financial data stored under the 'data' key.
"""
max_retries = 5
base_delay = 1.0 # seconds
for attempt in range(max_retries):
try:
logger.info(f"Attempting to fetch data for {symbol} from {start_date} to {end_date} (Attempt {attempt + 1})")
data = yf.download(symbol, start=start_date, end=end_date)
if data.empty:
logger.warning(f"No data fetched for {symbol} in the specified range. Attempt {attempt + 1}.")
raise ValueError("Empty DataFrame received.")
state['data'] = data
state['symbol'] = symbol
state['start_date'] = start_date
state['end_date'] = end_date
logger.info(f"Successfully fetched {len(data)} rows for {symbol}.")
state['log_messages'].append(f"Fetched {len(data)} rows for {symbol}.")
return state
except Exception as e:
delay = (base_delay * (2 ** attempt)) + (random.random() * 0.5) # Exponential backoff with jitter
logger.error(f"Failed to fetch data for {symbol}: {e}. Retrying in {delay:.2f} seconds...")
state['log_messages'].append(f"Failed to fetch data for {symbol}: {e}. Retrying.")
time.sleep(delay)
logger.critical(f"Failed to fetch data for {symbol} after {max_retries} attempts.")
state['log_messages'].append(f"Critical: Failed to fetch data for {symbol} after {max_retries} attempts.")
return state # Return state even if data fetching failedFunction Name: preprocess_data
This function preprocesses the fetched financial data by calculating daily log returns. It handles potential missing values by dropping them and ensures the returns are ready for GARCH modeling.
Parameters:
state(dict): The current state dictionary containing the raw 'data'.
Returns:
- (dict): The updated state dictionary with 'returns' Series calculated and any NaN values handled.
def preprocess_data(state: dict) -> dict:
"""
Calculates daily log returns from the 'Adj Close' price and handles missing values.
Parameters
----------
state : dict
The current state dictionary containing the raw 'data' DataFrame.
Returns
-------
dict
The updated state dictionary with 'returns' Series calculated and any NaN values handled.
"""
if state['data'].empty:
logger.warning("Data DataFrame is empty, cannot preprocess. Skipping.")
state['log_messages'].append("Warning: Data DataFrame is empty, cannot preprocess.")
return state
logger.info("Calculating log returns and handling missing values...")
# Access the 'Close' column using the symbol from the MultiIndex
returns = 100 * state['data'][('Close', state['symbol'])].pct_change().dropna()
state['returns'] = returns
if returns.empty:
logger.warning("Returns Series is empty after calculation and dropping NaNs.")
state['log_messages'].append("Warning: Returns Series is empty after preprocessing.")
else:
logger.info(f"Successfully calculated {len(returns)} log returns. First 5: {returns.head().tolist()}")
state['log_messages'].append(f"Calculated {len(returns)} returns.")
return stateFunction Name: fit_garch_model
This function fits a GARCH(p,q) model to the preprocessed returns data. It uses the arch library, allowing for specification of the model order (p for ARCH terms, q for GARCH terms) and distribution.
Parameters:
state(dict): The current state dictionary containing the 'returns' Series.p(int, optional): The order of the ARCH component (defaults to 1).q(int, optional): The order of the GARCH component (defaults to 1).dist(str, optional): The error distribution for the model (e.g., 'normal', 't', 'skewt'; defaults to 'normal').
Returns:
- (dict): The updated state dictionary with the fitted GARCH model stored under the 'model_fit' key.
Examples:
# Assuming 'state' contains preprocessed returns
# state = fit_garch_model(state, p=1, q=1)
# state = fit_garch_model(state, p=2, q=1, dist='t')
def fit_garch_model(state: dict, p: int = 1, q: int = 1, dist: str = 'normal') -> dict:
"""
Fits a GARCH(p,q) model to the returns data.
Parameters
----------
state : dict
The current state dictionary containing the 'returns' Series.
p : int, optional
The order of the ARCH component, defaults to 1.
q : int, optional
The order of the GARCH component, defaults to 1.
dist : str, optional
The error distribution for the model (e.g., 'normal', 't', 'skewt'), defaults to 'normal'.
Returns
-------
dict
The updated state dictionary with the fitted GARCH model stored under the 'model_fit' key.
"""
if state['returns'].empty:
logger.warning("Returns Series is empty, cannot fit GARCH model. Skipping.")
state['log_messages'].append("Warning: Returns Series empty, cannot fit GARCH.")
return state
logger.info(f"Attempting to fit GARCH({p},{q}) model with {dist} distribution...")
try:
# Ensure returns are not all zero or constant, which can cause convergence issues
if state['returns'].std() < 1e-6:
logger.warning("Returns are constant or near zero, GARCH model might not converge.")
state['log_messages'].append("Warning: Returns constant, GARCH might fail.")
model = arch_model(state['returns'], vol='Garch', p=p, q=q, dist=dist)
model_fit = model.fit(disp='off') # disp='off' suppresses verbose output
state['model_fit'] = model_fit
logger.info(f"GARCH({p},{q}) model fitted successfully. AIC: {model_fit.aic:.2f}")
state['log_messages'].append(f"GARCH({p},{q}) model fitted. AIC: {model_fit.aic:.2f}")
except Exception as e:
logger.error(f"Error fitting GARCH({p},{q}) model: {e}")
state['log_messages'].append(f"Error fitting GARCH({p},{q}) model: {e}")
state['model_fit'] = None # Ensure model_fit is None if error occurs
return stateFunction Name: forecast_volatility
This function generates future volatility forecasts using a previously fitted GARCH model. It also calculates annualized volatility from the conditional standard deviation.
Parameters:
state(dict): The current state dictionary containing the 'model_fit' and 'forecast_horizon'.
Returns:
- (dict): The updated state dictionary with 'forecasts' DataFrame containing forecasted conditional variances and annualized volatilities.
Examples:
# Assuming 'state' contains a fitted model and forecast horizon
# state = forecast_volatility(state)
def forecast_volatility(state: dict) -> dict:
"""
Generates future volatility forecasts from a fitted GARCH model.
Parameters
----------
state : dict
The current state dictionary containing the 'model_fit' and 'forecast_horizon'.
Returns
-------
dict
The updated state dictionary with 'forecasts' DataFrame containing forecasted conditional variances and annualized volatilities.
"""
if state['model_fit'] is None:
logger.warning("No fitted GARCH model found in state, cannot forecast. Skipping.")
state['log_messages'].append("Warning: No fitted model, cannot forecast.")
return state
if not state['returns'].empty:
last_date = state['returns'].index[-1]
else:
last_date = pd.to_datetime(state['end_date']) # Fallback if returns empty
logger.info(f"Forecasting volatility for {state['forecast_horizon']} steps...")
try:
forecast_result = state['model_fit'].forecast(horizon=state['forecast_horizon'], start=last_date)
mean_forecast = forecast_result.mean.iloc[-1]
variance_forecast = forecast_result.variance.iloc[-1]
h_vol_forecast = np.sqrt(variance_forecast)
# Create a date range for the forecast
forecast_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=state['forecast_horizon'], freq='B') # Business days
if len(forecast_dates) > len(h_vol_forecast):
forecast_dates = forecast_dates[:len(h_vol_forecast)]
forecast_df = pd.DataFrame({
'mean_forecast': mean_forecast.values,
'variance_forecast': variance_forecast.values,
'h_vol_forecast': h_vol_forecast.values,
'annualized_volatility': h_vol_forecast.values * np.sqrt(252) # Annualize daily volatility
}, index=forecast_dates)
state['forecasts'] = forecast_df
logger.info(f"Successfully generated {state['forecast_horizon']} volatility forecasts.")
state['log_messages'].append(f"Generated {state['forecast_horizon']} volatility forecasts.")
except Exception as e:
logger.error(f"Error generating volatility forecast: {e}")
state['log_messages'].append(f"Error during forecasting: {e}")
state['forecasts'] = pd.DataFrame() # Ensure empty if error
return stateFunction Name: evaluate_forecast
This function evaluates the performance of the GARCH volatility forecasts against actual (realized) volatility. It uses the Mean Squared Error (MSE) to quantify the accuracy.
Parameters:
state(dict): The current state dictionary containing 'returns', 'model_fit', and 'forecasts'.
Returns:
- (dict): The updated state dictionary with 'eval_metrics' containing the MSE.
Examples:
# Assuming 'state' contains returns, a fitted model and forecasts
# state = evaluate_forecast(state)
def evaluate_forecast(state: dict) -> dict:
"""
Evaluates the performance of the GARCH volatility forecasts.
Parameters
----------
state : dict
The current state dictionary containing 'returns', 'model_fit', and 'forecasts'.
Returns
-------
dict
The updated state dictionary with 'eval_metrics' containing the MSE.
"""
if state['forecasts'].empty or state['model_fit'] is None or state['returns'].empty:
logger.warning("Cannot evaluate forecast: missing forecasts, model, or returns.")
state['log_messages'].append("Warning: Cannot evaluate forecast due to missing data.")
state['eval_metrics'] = {'mse': np.nan}
return state
logger.info("Evaluating GARCH model forecast...")
try:
# Get the actual (realized) squared returns for comparison
# We need to align the dates of forecasts and actuals
actual_volatility = state['returns'][-len(state['forecasts']):].values**2 # Squared returns as proxy for realized variance
predicted_variance = state['forecasts']['variance_forecast'].values
if len(actual_volatility) == len(predicted_variance):
mse = mean_squared_error(actual_volatility, predicted_variance)
state['eval_metrics']['mse'] = mse
logger.info(f"Forecast evaluation successful. MSE: {mse:.4f}")
state['log_messages'].append(f"Forecast evaluated. MSE: {mse:.4f}")
else:
logger.warning("Mismatch in length between actual and predicted volatilities, cannot calculate MSE.")
state['log_messages'].append("Warning: Length mismatch for forecast evaluation.")
state['eval_metrics']['mse'] = np.nan
except Exception as e:
logger.error(f"Error during forecast evaluation: {e}")
state['log_messages'].append(f"Error during forecast evaluation: {e}")
state['eval_metrics']['mse'] = np.nan
return stateDemonstration and Visualization
This section demonstrates the complete workflow of GARCH volatility forecasting using the defined functions. It covers data fetching, preprocessing, model fitting, forecasting, and visualization of the results.
We will use SPY (SPDR S&P 500 ETF Trust) as our example financial instrument for a 5-year period.
Step 1: Initialize State and Fetch Data
First, we'll create the initial state and then fetch historical SPY data.
# Initialize the GARCH state
initial_state = create_garch_state({
'symbol': 'SPY',
'start_date': (datetime.date.today() - datetime.timedelta(days=5*365)).strftime('%Y-%m-%d'),
'end_date': datetime.date.today().strftime('%Y-%m-%d'),
'forecast_horizon': 20 # Forecast 20 business days into the future
})
# Fetch financial data
state = fetch_financial_data(initial_state, initial_state['symbol'], initial_state['start_date'], initial_state['end_date'])
print("\n--- Raw Data Head ---")
display(state['data'].head())
print("\n--- Raw Data Tail ---")
display(state['data'].tail())/tmp/ipykernel_5831/3602311733.py:27: FutureWarning: YF.download() has changed argument auto_adjust default to True data = yf.download(symbol, start=start_date, end=end_date) [*********************100%***********************] 1 of 1 completed
--- Raw Data Head ---
| Price | Close | High | Low | Open | Volume |
|---|---|---|---|---|---|
| Ticker | SPY | SPY | SPY | SPY | SPY |
| Date | |||||
| 2021-06-10 | 395.644531 | 396.597212 | 393.720529 | 395.037448 | 51020100 |
| 2021-06-11 | 396.298340 | 396.410413 | 394.906714 | 396.195615 | 45570800 |
| 2021-06-14 | 397.185547 | 397.288271 | 395.168141 | 396.410325 | 42358500 |
| 2021-06-15 | 396.457092 | 397.372377 | 395.579146 | 397.335039 | 51508500 |
| 2021-06-16 | 394.243591 | 396.821394 | 392.198195 | 396.597247 | 80386100 |
--- Raw Data Tail ---
| Price | Close | High | Low | Open | Volume |
|---|---|---|---|---|---|
| Ticker | SPY | SPY | SPY | SPY | SPY |
| Date | |||||
| 2026-06-02 | 759.570007 | 760.400024 | 756.750000 | 757.030029 | 31581900 |
| 2026-06-03 | 754.239990 | 758.799988 | 753.570007 | 758.150024 | 51402500 |
| 2026-06-04 | 757.090027 | 758.309998 | 751.469971 | 752.099976 | 49923000 |
| 2026-06-05 | 737.549988 | 752.820007 | 735.530029 | 752.309998 | 93989400 |
| 2026-06-08 | 739.219971 | 745.340027 | 738.190002 | 743.359985 | 49213400 |
Step 2: Preprocess Data (Calculate Returns)
Next, we calculate the daily log returns from the fetched adjusted close prices. These returns are the input for our GARCH model.
# Preprocess the data to get returns
state = preprocess_data(state)
print("\n--- Returns Series Head ---")
display(state['returns'].head())
print("\n--- Returns Series Info ---")
state['returns'].info()
# Plot the returns series
fig, ax = plt.subplots(figsize=(12, 6))
state['returns'].plot(ax=ax, title=f'{state["symbol"]} Daily Log Returns')
ax.set_xlabel('Date')
ax.set_ylabel('Log Returns (%)')
plt.tight_layout()
plt.show()--- Returns Series Head ---
| Close | |
|---|---|
| SPY | |
| Date | |
| 2021-06-11 | 0.165252 |
| 2021-06-14 | 0.223874 |
| 2021-06-15 | -0.183404 |
| 2021-06-16 | -0.558320 |
| 2021-06-17 | -0.033169 |
--- Returns Series Info ---
<class 'pandas.core.series.Series'>
DatetimeIndex: 1253 entries, 2021-06-11 to 2026-06-08
Series name: ('Close', 'SPY')
Non-Null Count Dtype
-------------- -----
1253 non-null float64
dtypes: float64(1)
memory usage: 19.6 KB
Step 3: Fit GARCH Model
We will now fit a GARCH(1,1) model to the calculated returns. A GARCH(1,1) model is often a good starting point for financial time series.
# Fit the GARCH model (GARCH(1,1) with normal distribution)
state = fit_garch_model(state, p=1, q=1, dist='normal')
if state['model_fit']:
print("\n--- GARCH Model Summary ---")
print(state['model_fit'].summary())
# Plot conditional volatility
fig, ax = plt.subplots(figsize=(12, 6))
state['model_fit'].conditional_volatility.plot(ax=ax, title=f'{state["symbol"]} Conditional Volatility (GARCH(1,1))')
ax.set_xlabel('Date')
ax.set_ylabel('Conditional Volatility (%)')
plt.tight_layout()
plt.show()
# Plot standardized residuals
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
state['model_fit'].resid.plot(ax=axes[0], title='Standardized Residuals')
axes[0].set_xlabel('Date')
axes[0].set_ylabel('Residual')
sns.histplot(state['model_fit'].std_resid, kde=True, ax=axes[1])
axes[1].set_title('Histogram of Standardized Residuals')
axes[1].set_xlabel('Standardized Residual')
axes[1].set_ylabel('Density')
plt.tight_layout()
plt.show()
else:
print("GARCH model could not be fitted.")
--- GARCH Model Summary ---
Constant Mean - GARCH Model Results
==============================================================================
Dep. Variable: ('Close', 'SPY') R-squared: 0.000
Mean Model: Constant Mean Adj. R-squared: 0.000
Vol Model: GARCH Log-Likelihood: -1712.97
Distribution: Normal AIC: 3433.93
Method: Maximum Likelihood BIC: 3454.47
No. Observations: 1253
Date: Tue, Jun 09 2026 Df Residuals: 1252
Time: 05:58:32 Df Model: 1
Mean Model
==========================================================================
coef std err t P>|t| 95.0% Conf. Int.
--------------------------------------------------------------------------
mu 0.0825 2.386e-02 3.458 5.442e-04 [3.574e-02, 0.129]
Volatility Model
============================================================================
coef std err t P>|t| 95.0% Conf. Int.
----------------------------------------------------------------------------
omega 0.0369 1.328e-02 2.777 5.483e-03 [1.086e-02,6.293e-02]
alpha[1] 0.1054 2.512e-02 4.196 2.715e-05 [5.618e-02, 0.155]
beta[1] 0.8612 2.926e-02 29.432 2.118e-190 [ 0.804, 0.919]
============================================================================
Covariance estimator: robust
Step 4: Forecast Volatility
Now we use the fitted GARCH model to forecast future volatility for the specified horizon.
# Forecast volatility
state = forecast_volatility(state)
if not state['forecasts'].empty:
print(f"\n--- Volatility Forecasts for next {state['forecast_horizon']} days ---")
display(state['forecasts'])
# Plot historical conditional volatility and future forecasts
fig, ax = plt.subplots(figsize=(12, 7))
# Historical conditional volatility
if state['model_fit']:
state['model_fit'].conditional_volatility.plot(ax=ax, label='Historical Conditional Volatility', color='blue')
# Forecasted conditional volatility
state['forecasts']['h_vol_forecast'].plot(ax=ax, label='Forecasted Volatility', color='red', linestyle='--')
ax.set_title(f'{state["symbol"]} Historical and Forecasted Conditional Volatility')
ax.set_xlabel('Date')
ax.set_ylabel('Volatility (%)')
ax.legend()
plt.tight_layout()
plt.show()
# Plot annualized volatility forecast separately
fig, ax = plt.subplots(figsize=(12, 6))
state['forecasts']['annualized_volatility'].plot(ax=ax, color='green', marker='o', linestyle='-')
ax.set_title(f'{state["symbol"]} Annualized Volatility Forecast (Next {state["forecast_horizon"]} Days)')
ax.set_xlabel('Date')
ax.set_ylabel('Annualized Volatility (%)')
ax.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()
else:
print("No volatility forecasts generated.")--- Volatility Forecasts for next 20 days ---
| mean_forecast | variance_forecast | h_vol_forecast | annualized_volatility | |
|---|---|---|---|---|
| 2026-06-09 | 0.082511 | 1.055747 | 1.027495 | 16.310982 |
| 2026-06-10 | 0.082511 | 1.057456 | 1.028327 | 16.324183 |
| 2026-06-11 | 0.082511 | 1.059109 | 1.029130 | 16.336933 |
| 2026-06-12 | 0.082511 | 1.060706 | 1.029906 | 16.349249 |
| 2026-06-15 | 0.082511 | 1.062250 | 1.030655 | 16.361146 |
| 2026-06-16 | 0.082511 | 1.063743 | 1.031379 | 16.372639 |
| 2026-06-17 | 0.082511 | 1.065186 | 1.032079 | 16.383740 |
| 2026-06-18 | 0.082511 | 1.066581 | 1.032754 | 16.394465 |
| 2026-06-19 | 0.082511 | 1.067930 | 1.033407 | 16.404826 |
| 2026-06-22 | 0.082511 | 1.069233 | 1.034037 | 16.414835 |
| 2026-06-23 | 0.082511 | 1.070493 | 1.034647 | 16.424504 |
| 2026-06-24 | 0.082511 | 1.071712 | 1.035235 | 16.433846 |
| 2026-06-25 | 0.082511 | 1.072889 | 1.035804 | 16.442872 |
| 2026-06-26 | 0.082511 | 1.074027 | 1.036353 | 16.451592 |
| 2026-06-29 | 0.082511 | 1.075128 | 1.036884 | 16.460018 |
| 2026-06-30 | 0.082511 | 1.076191 | 1.037396 | 16.468158 |
| 2026-07-01 | 0.082511 | 1.077220 | 1.037892 | 16.476023 |
| 2026-07-02 | 0.082511 | 1.078214 | 1.038371 | 16.483623 |
| 2026-07-03 | 0.082511 | 1.079174 | 1.038833 | 16.490966 |
| 2026-07-06 | 0.082511 | 1.080103 | 1.039280 | 16.498062 |
Step 5: Evaluate Forecasts
Finally, we evaluate the accuracy of our GARCH model's volatility forecasts. For simplicity, we compare forecasted variance with realized squared returns over the forecast horizon.
# Evaluate the forecast
state = evaluate_forecast(state)
if 'mse' in state['eval_metrics'] and not np.isnan(state['eval_metrics']['mse']):
print(f"\n--- Forecast Evaluation Metrics ---")
display(pd.DataFrame([state['eval_metrics']]))
else:
print("Forecast evaluation could not be performed or resulted in NaN.")
# Display recent log messages
print("\n--- Recent Log Messages ---")
for msg in state['log_messages']:
print(msg)--- Forecast Evaluation Metrics ---
| mse | |
|---|---|
| 0 | 2.254576 |
--- Recent Log Messages --- GARCH state initialized for symbol: SPY Fetched 1254 rows for SPY. Warning: Returns Series empty, cannot fit GARCH. Warning: No fitted model, cannot forecast. Warning: Cannot evaluate forecast due to missing data. Calculated 1253 returns. GARCH(1,1) model fitted. AIC: 3433.93 Generated 20 volatility forecasts. Forecast evaluated. MSE: 2.2546
Production Considerations
Implementing GARCH models in a production environment requires careful attention to several practical aspects beyond theoretical understanding. This section outlines best practices and considerations for robustness, performance, and monitoring.
| Consideration | Description |
|---|---|
| Data Quality & Latency | Ensure reliable, low-latency data feeds. Missing data or errors can severely impact model performance. Implement robust data validation and imputation strategies. |
| Model Re-estimation | Volatility dynamics change over time. GARCH models should be regularly re-estimated (e.g., daily, weekly, or monthly) with new data to capture evolving market conditions. Define a clear re-estimation schedule and process. |
| Out-of-Sample Performance | Always evaluate models on out-of-sample data. In-sample fit can be misleading. Implement a rolling window or expanding window approach for backtesting forecasts. |
| Computational Efficiency | Fitting GARCH models can be computationally intensive, especially for large datasets or complex models. Optimize code, use efficient libraries, and consider parallel processing or cloud computing for large-scale applications. |
| Robustness to Outliers | Financial data often contains extreme outliers. These can significantly distort GARCH parameter estimates. Consider robust estimation techniques or pre-processing steps like winsorization or alternative error distributions (e.g., Student's t-distribution, skewed Student's t). |
| Error Handling & Retries | Implement comprehensive try-except blocks with exponential backoff for API calls and model fitting procedures, as demonstrated in fetch_financial_data. This ensures resilience against transient errors and maintains system uptime. |
| Logging and Monitoring | Robust logging (as demonstrated with logger and collections.deque) is critical for debugging and monitoring model behavior in production. Monitor model parameters, forecast errors, and system health metrics. Set up alerts for anomalous behavior. |
| Model Selection & Diagnostics | Regularly perform model diagnostics (e.g., Ljung-Box test on standardized residuals, ARCH-LM test on standardized residuals squared) to ensure the model adequately captures conditional heteroskedasticity and that residuals are white noise. Consider alternative GARCH specifications (e.g., EGARCH, GJR-GARCH). |
| Backtesting & Stress Testing | Beyond simple evaluation metrics, conduct thorough backtesting of strategies built on GARCH forecasts. Perform stress tests to understand model behavior under extreme market conditions. |
| Scalability | If forecasting volatility for a large portfolio of assets, ensure the infrastructure and code can scale efficiently. This might involve containerization (Docker), orchestration (Kubernetes), and distributed computing frameworks. |
Conclusion
This notebook has provided a comprehensive walkthrough of GARCH volatility forecasting, from data acquisition and preprocessing to model fitting, forecasting, and evaluation. We've seen how GARCH models effectively capture the stylized facts of financial time series, such as volatility clustering.
The core functions developed (create_garch_state, fetch_financial_data, preprocess_data, fit_garch_model, forecast_volatility, evaluate_forecast) demonstrate a modular and robust approach to statistical modeling, incorporating best practices like type hints, detailed docstrings, logging, and retry mechanisms.
While GARCH models are powerful, it's crucial to consider the practical aspects of their deployment, including data quality, regular re-estimation, rigorous evaluation, and robust error handling, as highlighted in the production considerations. This framework serves as a solid foundation for further exploration into advanced volatility models and their applications in quantitative finance.