Skip to content

Commit a8f78d2

Browse files
committed
add polymorphic compute_filtered_probs to VolatilityProcess and MSGARCH. store filtered_probs in ARCHModelResult
1 parent 6e50240 commit a8f78d2

File tree

3 files changed

+56
-8
lines changed

3 files changed

+56
-8
lines changed

arch/tests/univariate/test_volatility.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
assert_array_equal,
1111
assert_equal,
1212
)
13+
from sklearn.mixture import GaussianMixture
1314
import pytest
1415
from scipy.special import gamma, gammaln
1516
from arch import arch_model

arch/univariate/base.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -863,10 +863,7 @@ def fit(
863863
resids_final = np.full(self._y.shape, np.nan, dtype=float)
864864
resids_final[first_obs:last_obs] = resids
865865

866-
if isinstance(self.volatility, MSGARCH):
867-
filtered_probs = self.volatility.filtered_probs # n_regimes x n_obs
868-
else:
869-
filtered_probs = None
866+
filtered_probs = self.volatility.compute_filtered_probs(params, resids, sigma2, backcast, var_bounds)
870867

871868

872869
if isinstance(self.volatility, MSGARCH):
@@ -896,6 +893,7 @@ def fit(
896893
fit_start,
897894
fit_stop,
898895
model_copy,
896+
filtered_probs,
899897
)
900898

901899
@abstractmethod
@@ -1829,6 +1827,7 @@ def __init__(
18291827
fit_start: int,
18301828
fit_stop: int,
18311829
model: ARCHModel,
1830+
filtered_probs: Float64Array | None = None,
18321831
) -> None:
18331832
super().__init__(
18341833
params, resid, volatility, dep_var, names, loglikelihood, is_pandas, model
@@ -1839,6 +1838,7 @@ def __init__(
18391838
self._r2 = r2
18401839
self.cov_type: str = cov_type
18411840
self._optim_output = optim_output
1841+
self._filtered_probs = filtered_probs
18421842

18431843
@cached_property
18441844
def scale(self) -> float:
@@ -2089,6 +2089,10 @@ def optimization_result(self) -> OptimizeResult:
20892089
Result from numerical optimization of the log-likelihood.
20902090
"""
20912091
return self._optim_output
2092+
2093+
@property
2094+
def filtered_probs(self):
2095+
return self._filtered_probs
20922096

20932097

20942098
def _align_forecast(

arch/univariate/volatility.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,12 @@ def parameter_names(self) -> list[str]:
861861
"""
862862

863863

864+
def compute_filtered_probs(self, params, resids, sigma2, backcast, var_bounds):
865+
"""
866+
Default behavior for non MS models. Returns 1's for each time period (we are always in the single regime).
867+
"""
868+
return np.ones((1, len(resids)))
869+
864870
class ConstantVariance(VolatilityProcess, metaclass=AbstractDocStringInheritor):
865871
r"""
866872
Constant volatility process
@@ -4011,10 +4017,6 @@ def msgarch_loglikelihood(self, parameters, resids, sigma2, backcast, var_bounds
40114017
#mean params, volatility params per regime, distribution params
40124018
mp, vp, dp = self._parse_parameters(parameters)
40134019

4014-
# GARCH params per regime
4015-
garch_block = vp[:self.k*self.num_params_single]
4016-
garch_params = garch_block.reshape(self.k, self.num_params_single)
4017-
40184020
# extract the free parameters for the transition matrix
40194021
start = self.k * self.num_params_single
40204022
end = start + self.k # only one free param per row
@@ -4110,5 +4112,46 @@ def _parse_parameters(self, x: np.ndarray):
41104112
dp = x[km+kv:]
41114113

41124114
return mp, vp, dp
4115+
4116+
4117+
def compute_filtered_probs(self, parameters, resids, sigma2, backcast, var_bounds):
4118+
# parse parameters
4119+
mp, vp, dp = self._parse_parameters(parameters)
4120+
4121+
# transition matrix
4122+
start = self.k * self.num_params_single
4123+
end = start + self.k
4124+
free_P = vp[start:end]
4125+
transition_matrix = np.zeros((self.k, self.k))
4126+
transition_matrix[0, 0] = free_P[0]
4127+
transition_matrix[0, 1] = 1.0 - free_P[0]
4128+
transition_matrix[1, 0] = free_P[1]
4129+
transition_matrix[1, 1] = 1.0 - free_P[1]
4130+
4131+
# Initial probabilties
4132+
eigvals, eigvecs = np.linalg.eig(transition_matrix.T)
4133+
pi = np.real(eigvecs[:, np.isclose(eigvals, 1)].flatten())
4134+
pi /= pi.sum()
4135+
4136+
4137+
p = np.zeros((self.k, len(resids)))
4138+
p[:, 0] = pi
4139+
sigma2 = self.compute_variance(vp, resids, np.zeros((len(resids), self.k)), backcast, var_bounds)
4140+
4141+
# Hamilton filter
4142+
var_floor = 1e-4
4143+
sigma2 = np.maximum(sigma2, var_floor)
4144+
log_p = np.log(np.maximum(p, 1e-12))
4145+
4146+
for t in range(1, len(resids)):
4147+
sigma2_t = sigma2[t, :]
4148+
log_likes = -0.5 * (np.log(2 * np.pi * sigma2_t) + (resids[t] ** 2) / sigma2_t)
4149+
log_pred_probs = logsumexp(np.log(np.maximum(transition_matrix.T, 1e-12)) + log_p[:, t-1][:, None], axis=0)
4150+
log_mixture = logsumexp(log_pred_probs + log_likes)
4151+
log_p[:, t] = log_pred_probs + log_likes - log_mixture
4152+
4153+
# return filtered probs
4154+
return np.exp(log_p)
4155+
41134156

41144157

0 commit comments

Comments
 (0)