Statistical Analysis·Time Series Methods·Intermediate

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.

machine-learningquant-analysistime-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:

ConceptDescriptionImportance in GARCH
Volatility ClusteringPeriods of high volatility tend to be followed by periods of high volatility, and vice versa.GARCH models are specifically designed to capture this phenomenon.
LeptokurtosisFinancial 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 HeteroskedasticityThe variance of the error term depends on the size of previous error terms.The core assumption that GARCH models are built upon.
ARCH ModelAutoregressive 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 ModelGeneralized 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 ForecastingPredicting future levels of market volatility.A primary application of GARCH models in finance.
Model EvaluationAssessing 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.

[1]
# Install necessary libraries
!pip install pandas numpy matplotlib seaborn arch yfinance --quiet
[?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/981.3 kB ? eta -:--:--
   ━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 286.7/981.3 kB 9.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 972.8/981.3 kB 17.3 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 981.3/981.3 kB 10.4 MB/s eta 0:00:00
[?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.

[2]
# 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'] = 100

Core 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.
[3]
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 state

Function 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.
[4]
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 failed

Function 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.
[14]
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 state

Function 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')
[6]
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 state

Function 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)
[7]
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 state

Function 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)
[8]
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 state

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

[19]
# 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.

[15]
# 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
cell output

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.

[16]
# 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
cell output
cell output

Step 4: Forecast Volatility

Now we use the fitted GARCH model to forecast future volatility for the specified horizon.

[17]
# 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
cell output
cell output

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.

[18]
# 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.

ConsiderationDescription
Data Quality & LatencyEnsure reliable, low-latency data feeds. Missing data or errors can severely impact model performance. Implement robust data validation and imputation strategies.
Model Re-estimationVolatility 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 PerformanceAlways 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 EfficiencyFitting 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 OutliersFinancial 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 & RetriesImplement 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 MonitoringRobust 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 & DiagnosticsRegularly 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 TestingBeyond simple evaluation metrics, conduct thorough backtesting of strategies built on GARCH forecasts. Perform stress tests to understand model behavior under extreme market conditions.
ScalabilityIf 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.