import numpy as np
import pandas as pd
from scipy import stats
from arch import arch_model
[docs]class Garch:
"""
Parametric bootstrapping with GARCH models.
"""
def __init__(self, data, vol='Garch', p=1, q=1, dist='normal', update_freq=0, disp='off', horizon=1, start=None, reindex=False):
"""
Initializing the GARCH model.
Parameters:
data: pandas.Series, np.array, DataFrame
Time series data
vol: str, optional
Name of the volatility model. Default is 'Garch'. Others are 'ARCH', 'EGARCH', 'FIGARCH', 'APARCH', and 'HARCH'
p: int, optional
Lag order of the symmetric innovation. Default is 1.
q: int
Lag order of the lagged conditional variance. Default is 1.
dist: str, optional
Name of the distribution assumption for the errors. Options are:
* Normal: 'normal', 'gaussian' (default)
* Students's t: 't', 'studentst'
* Skewed Student's t: 'skewstudent', 'skewt'
* Generalized Error Distribution: 'ged', 'generalized error"
update_freq: int, optional
Frequency of iteration updates to generate model output. Default is 0 (no updating).
disp: str or bool optional
Display option for the model estimation. Either 'final' to print optimization result or 'off' (default) to display
nothing. If using a boolean, False is "off" and True is "final"
horizon: int, optional
Forecast horizon. Default is 1.
start: int or str or datetime or Timestamp, optional
Starting index or date for forecasting. Default is None.
reindex: bool, optional
Reindex the forecasted series to match the original data. Default is False.
"""
if vol not in ['Garch', 'ARCH', 'EGARCH', 'FIGARCH', 'APARCH', 'HARCH']:
raise ValueError("Invalid value for 'vol' parameter. Allowed values are 'Garch', 'ARCH', 'EGARCH', 'FIGARCH', 'APARCH', and 'HARCH'.")
if not isinstance(p, int) or p < 1:
raise ValueError("The 'p' parameter must be an integer greater than or equal to 1.")
if not isinstance(q, int) or q < 1:
raise ValueError("The 'q' parameter must be an integer greater than or equal to 1.")
allowed_dists = ['normal', 'gaussian', 't', 'studentst', 'skewstudent', 'skewt', 'ged', 'generalized error']
if dist not in allowed_dists:
raise ValueError("Invalid value for 'dist' parameter. Allowed values are: " + ", ".join(allowed_dists) + ".")
if not isinstance(update_freq, int) or update_freq < 0:
raise ValueError("The 'update_freq' parameter must be a non-negative integer.")
if isinstance(disp, bool):
if disp:
disp = 'final'
else:
disp = 'off'
elif not isinstance(disp, str):
raise ValueError("The 'disp' parameter must be a string or a boolean.")
if not isinstance(horizon, int) or horizon < 1:
raise ValueError("The 'horizon' parameter must be an integer greater than or equal to 1.")
self.vol = vol
self.p = p
self.q = q
self.dist = dist
self.horizon = horizon
try:
self.model = arch_model(data, vol=self.vol, p=self.p, q=self.q, dist=self.dist)
self.result = self.model.fit(disp=disp)
self.prediction = self.result.forecast(horizon=self.horizon, start=start, reindex=reindex)
except Exception as e:
raise ValueError("An error occurred while fitting the GARCH model. Error message: " + str(e))
@property
def summary(self):
"""
Get the summary of the fitted model.
Returns:
arch.univariate.base.ARCHModelResultSummary: Summary of the fitted model.
"""
if self.result is None:
raise ValueError("No model result available. Please fit the GARCH model first.")
return self.result.summary()
@property
def conditional_volatility(self):
"""
Get the conditional volatility of the fitted model.
Returns:
pandas.Series: Conditional volatility series.
"""
if self.result is None:
raise ValueError("No model result available. Please fit the GARCH model first.")
return self.result.conditional_volatility
@property
def standardised_residuals(self):
"""
Get the standardized residuals of the fitted model.
Returns:
pandas.Series: Standardized residuals series.
"""
if self.result is None:
raise ValueError("No model result available. Please fit the GARCH model first.")
return self.result.std_resid
@property
def forecast_mean(self):
"""
Get the forecasted conditional mean of the model.
Returns:
pandas.DataFrame: Forecasted conditional mean series.
"""
if self.prediction is None:
raise ValueError("No forecast available. Please fit the GARCH model and generate forecast first.")
return self.prediction.mean
@property
def forecast_variance(self):
"""
Get the forecasted conditional variance of the model.
Returns:
pandas.DataFrame: Forecasted conditional variance series.
"""
if self.prediction is None:
raise ValueError("No forecast available. Please fit the GARCH model and generate forecast first.")
return self.prediction.variance
@property
def forecast_residual_variance(self):
"""
Get the forecasted conditional variance of the residuals of the model.
Returns:
pandas.DataFrame: Forecasted conditional residual variance series.
"""
if self.prediction is None:
raise ValueError("No forecast available. Please fit the GARCH model and generate forecast first.")
return self.prediction.residual_variance
#-------------------------------------------------------------------------------------------
[docs] def bootstrap(self, num_iterations=1000):
"""
Perform parametric bootstrapping to estimate the forecast distribution.
Parameters:
num_iterations: int, optional
Number of bootstrap iterations. Default is 1000.
Returns:
bool: True if the bootstrap is successful.
"""
if num_iterations < 1 or not isinstance(num_iterations, int):
raise ValueError("The 'num_iterations' parameter must be an integer greater than or equal to 1.")
if self.standardised_residuals is None:
raise ValueError("No standardised residuals available. Please fit the GARCH model first.")
std_resid = self.standardised_residuals
bootstrap_samples = []
try:
for _ in range(num_iterations):
bootstrap_residuals = std_resid.sample(n=len(std_resid), replace=True)
bootstrap_model = arch_model(
bootstrap_residuals,
vol=self.vol,
p=self.p,
q=self.q,
dist=self.dist
)
bootstrap_result = bootstrap_model.fit(disp='off')
forecasted_mean = bootstrap_result.forecast(horizon=self.horizon, start=None, reindex=False).mean
forecasted_volatility = bootstrap_result.forecast(horizon=self.horizon, start=None, reindex=False).variance
bootstrap_samples.append((forecasted_mean, forecasted_volatility))
self._bootstrap_samples = bootstrap_samples
self.bootstrap_result = bootstrap_result
return True
except Exception as e:
raise ValueError("An error occurred during bootstrapping. Error message: " + str(e))
@property
def bootstrap_summary(self):
"""
Get the summary of the bootstrapped model.
Returns:
arch.univariate.base.ARCHModelResultSummary: Summary of the bootstrapped model.
"""
if self.bootstrap_result is None:
raise ValueError("No bootstrap result available. Please run the 'bootstrap' method first.")
return self.bootstrap_result.summary()
@property
def bootstrap_samples(self):
"""
Get the forecasted mean and volatility list from the bootstrapped model.
Returns:
list: List of tuples containing forecasted mean and volatility for each bootstrap iteration.
"""
if self._bootstrap_samples is None:
raise ValueError("No bootstrap samples available. Please run the 'bootstrap' method to generate bootstrap samples.")
return self._bootstrap_samples
#-------------------------------------------------------------------------------------------
[docs] def estimate_risk(self, confidence_level=0.95, q='empirical'):
"""
Estimate risk measures: volatility and Value-at-Risk (VaR) using the bootstrapped model.
Parameters:
confidence_level: float, optional
Confidence level for calculating VaR and volatility. Default is 0.95.
q: str, optional
Quantile calculation method. 'empirical' for empirical quantile, 'parametric' for parametric quantile.
Default is 'empirical'.
Returns:
dict: Dictionary containing risk estimates including mean volatility, volatility confidence interval,
mean VaR, and VaR confidence interval.
"""
if not (0 < confidence_level < 1):
raise ValueError("The 'confidence_level' parameter must be a float between 0 and 1 (exclusive).")
if q not in ['empirical', 'parametric']:
raise ValueError("Invalid value for 'q' parameter. Allowed values are 'empirical' and 'parametric'.")
if self.bootstrap_samples is None:
raise ValueError("No bootstrap samples available. Please run the 'bootstrap' method to generate bootstrap samples.")
volatility_estimates = np.sqrt([vol for _, vol in self.bootstrap_samples])
volatility_ci = np.percentile(volatility_estimates, [(1 - confidence_level) / 2 * 100, (1 + confidence_level) / 2 * 100])
var_estimates = []
for forecasted_mean, forecasted_volatility in self.bootstrap_samples:
if q == 'empirical':
if self.result.std_resid is None:
raise ValueError("No standardized residuals available. Please fit the GARCH model first.")
quantile = self.result.std_resid.quantile(1 - confidence_level)
elif q == 'parametric':
quantile = stats.norm.ppf(1 - confidence_level, loc=forecasted_mean, scale=np.sqrt(forecasted_volatility))
var_estimate = forecasted_mean + np.sqrt(forecasted_volatility) * quantile
var_estimates.append(var_estimate)
mean_var = np.mean(var_estimates)
var_ci = np.percentile(var_estimates, [(1 - confidence_level) / 2 * 100, (1 + confidence_level) / 2 * 100])
return {
'Mean Volatility': np.mean(volatility_estimates),
'Volatility Confidence Interval': volatility_ci,
'Mean VaR': mean_var,
'VaR Confidence Interval': var_ci
}