@@ -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+
864870class 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