-
Notifications
You must be signed in to change notification settings - Fork 270
Add Markov Switching GARCH volatility model #791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,7 +34,7 @@ | |
Literal, | ||
) | ||
from arch.univariate.distribution import Distribution, Normal | ||
from arch.univariate.volatility import ConstantVariance, VolatilityProcess | ||
from arch.univariate.volatility import ConstantVariance, VolatilityProcess, MSGARCH | ||
from arch.utility.array import ensure1d, to_array_1d | ||
from arch.utility.exceptions import ( | ||
ConvergenceWarning, | ||
|
@@ -714,15 +714,21 @@ def fit( | |
if total_params == 0: | ||
return self._fit_parameterless_model(cov_type=cov_type, backcast=backcast) | ||
|
||
sigma2 = np.zeros(resids.shape[0], dtype=float) | ||
self._backcast = backcast | ||
sv_volatility = v.starting_values(resids) | ||
n_regimes = v.k if isinstance(v, MSGARCH) else 1 | ||
sigma2 = np.zeros((resids.shape[0], n_regimes)) if n_regimes > 1 else np.zeros(resids.shape[0]) | ||
self._backcast = backcast | ||
sv_volatility = v.starting_values(resids) # initial guess for GARCH recursion | ||
self._var_bounds = var_bounds = v.variance_bounds(resids) | ||
v.compute_variance(sv_volatility, resids, sigma2, backcast, var_bounds) | ||
std_resids = resids / np.sqrt(sigma2) | ||
v.compute_variance(sv_volatility, resids, sigma2, backcast, var_bounds) # fills sigma 2 recursively using chosen vol model | ||
if n_regimes == 1: | ||
std_resids = resids / np.sqrt(sigma2) | ||
else: | ||
pi = self.volatility.pi # shape (k,) | ||
pi_weighted_sigma2 = sigma2 @ pi # (t,k) @ (k,) = (t,) | ||
std_resids = resids / np.sqrt(pi_weighted_sigma2) ## Using stationary distribution to weight regime variances. This is only an approximation (true weights should come from filtered probabilties), but we don't have these available at this stage | ||
|
||
# 2. Construct constraint matrices from all models and distribution | ||
constraints = ( | ||
constraints = ( | ||
self.constraints(), | ||
self.volatility.constraints(), | ||
self.distribution.constraints(), | ||
|
@@ -790,12 +796,16 @@ def fit( | |
_callback_info["display"] = update_freq | ||
disp_flag = True if disp == "final" else False | ||
|
||
func = self._loglikelihood | ||
args = (sigma2, backcast, var_bounds) | ||
ineq_constraints = constraint(a, b) | ||
if isinstance(self.volatility, MSGARCH): | ||
func = self.volatility.msgarch_loglikelihood # MS GARCH | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should overrigt the default likelihood in the MSGARCH subclass so that you don't need to use a pattern like this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the goal of avoiding isinstance checks for a cleaner design. The challenge is that the loglikelihood method resides in the ARCHModel class, while the new MSGARCH vol process is a subclass of VolatilityProcess in a separate module. This separation makes directly overriding the method difficult without significant architectural changes. To properly move the likelihood logic into the volatility classes would require refactoring much of the existing code, and will likely cause issues in other areas. |
||
args = (resids, sigma2, backcast, var_bounds) | ||
|
||
from scipy.optimize import minimize | ||
else: | ||
func = self._loglikelihood # standard GARCH | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't standard GARCH - it is every volatility process. |
||
args = (sigma2, backcast, var_bounds) | ||
|
||
ineq_constraints = constraint(a, b) | ||
from scipy.optimize import minimize | ||
options = {} if options is None else options | ||
options.setdefault("disp", disp_flag) | ||
with warnings.catch_warnings(): | ||
|
@@ -835,7 +845,7 @@ def fit( | |
mp, vp, dp = self._parse_parameters(params) | ||
|
||
resids = self.resids(mp) | ||
vol = np.zeros(resids.shape[0], dtype=float) | ||
vol = self.volatility._initialise_vol(resids, n_regimes) | ||
self.volatility.compute_variance(vp, resids, vol, backcast, var_bounds) | ||
vol = cast(Float64Array1D, np.sqrt(vol)) | ||
|
||
|
@@ -849,8 +859,18 @@ def fit( | |
first_obs, last_obs = self._fit_indices | ||
resids_final = np.full(self._y.shape, np.nan, dtype=float) | ||
resids_final[first_obs:last_obs] = resids | ||
vol_final = np.full(self._y.shape, np.nan, dtype=float) | ||
vol_final[first_obs:last_obs] = vol | ||
|
||
filtered_probs = self.volatility.compute_filtered_probs(params, resids, sigma2, backcast, var_bounds) | ||
|
||
|
||
if isinstance(self.volatility, MSGARCH): | ||
vol_final = np.full(self._y.shape, np.nan, dtype=float) | ||
weighted_sigma2 = (vol**2 * filtered_probs.T).sum(axis=1) | ||
vol_final[first_obs:last_obs] = np.sqrt(weighted_sigma2) | ||
|
||
else: | ||
vol_final = np.full(self._y.shape, np.nan, dtype=float) | ||
vol_final[first_obs:last_obs] = vol | ||
|
||
fit_start, fit_stop = self._fit_indices | ||
model_copy = deepcopy(self) | ||
|
@@ -870,6 +890,7 @@ def fit( | |
fit_start, | ||
fit_stop, | ||
model_copy, | ||
filtered_probs, | ||
) | ||
|
||
@abstractmethod | ||
|
@@ -1803,6 +1824,7 @@ def __init__( | |
fit_start: int, | ||
fit_stop: int, | ||
model: ARCHModel, | ||
filtered_probs: Float64Array | None = None, | ||
) -> None: | ||
super().__init__( | ||
params, resid, volatility, dep_var, names, loglikelihood, is_pandas, model | ||
|
@@ -1813,6 +1835,7 @@ def __init__( | |
self._r2 = r2 | ||
self.cov_type: str = cov_type | ||
self._optim_output = optim_output | ||
self._filtered_probs = filtered_probs | ||
|
||
@cached_property | ||
def scale(self) -> float: | ||
|
@@ -2063,6 +2086,10 @@ def optimization_result(self) -> OptimizeResult: | |
Result from numerical optimization of the log-likelihood. | ||
""" | ||
return self._optim_output | ||
|
||
@property | ||
def filtered_probs(self): | ||
return self._filtered_probs | ||
|
||
|
||
def _align_forecast( | ||
|
Check notice
Code scanning / CodeQL
Cyclic import