Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions arch/bootstrap/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ArrayLike2D,
BootstrapIndexT,
Float64Array,
Float64Array2D,
Int64Array,
Int64Array1D,
Literal,
Expand Down Expand Up @@ -201,7 +202,7 @@ def optimal_block_length(x: ArrayLike1D | ArrayLike2D) -> pd.DataFrame:
return pd.DataFrame(opt, index=idx, columns=["stationary", "circular"])


def _get_acceleration(jk_params: Float64Array) -> float:
def _get_acceleration(jk_params: Float64Array) -> Float64Array2D:
"""
Estimates the BCa acceleration parameter using jackknife estimates
of theta.
Expand Down Expand Up @@ -772,7 +773,7 @@ def conf_int(
)
a = self._bca_acceleration(func, extra_kwargs)
else:
a = 0.0
a = np.zeros((1, 1))
percentiles = stats.norm.cdf(
b + (b + norm_quantiles) / (1.0 - a * (b + norm_quantiles))
)
Expand Down Expand Up @@ -834,7 +835,7 @@ def _bca_bias(self) -> Float64Array:

def _bca_acceleration(
self, func: Callable[..., Float64Array], extra_kwags: dict[str, Any] | None
) -> float:
) -> Float64Array2D:
nobs = self._num_items
jk_params = _loo_jackknife(func, nobs, self._args, self._kwargs, extra_kwags)
return _get_acceleration(jk_params)
Expand Down
30 changes: 28 additions & 2 deletions arch/tests/univariate/test_volatility.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
)
import pytest
from scipy.special import gamma, gammaln

from arch import arch_model
from arch.univariate import ZeroMean
from arch.univariate.distribution import Normal, SkewStudent, StudentsT
import arch.univariate.recursions_python as recpy
Expand All @@ -28,6 +28,7 @@
FixedVariance,
MIDASHyperbolic,
RiskMetrics2006,
MSGARCH,
)
from arch.utility.exceptions import InitialValueWarning

Expand Down Expand Up @@ -1640,7 +1641,7 @@ def test_aparch(setup, initial_value):
assert_equal(aparch._delta, np.nan)
assert aparch.p == aparch.o == aparch.q == 1

parameters[1] = 0.9
parameters[1] = 0.20001
with pytest.warns(InitialValueWarning):
aparch.simulate(parameters, setup.t, rng.simulate([]))

Expand Down Expand Up @@ -1804,3 +1805,28 @@ def test_figarch_weights():
lam_cy = rec.figarch_weights(params, 1, 1, 1000)
assert_allclose(lam_py, lam_nb)
assert_allclose(lam_py, lam_cy)



def test_msgarch():
data = np.random.randn(100) # arbitary data series
model = arch_model(data, vol="MSGARCH",p=1,q=1,mean="zero") # 2 regimes
result = model.fit(disp="off")
forecast = result.forecast(horizon=1)
cond_var = result.conditional_volatility
probs = result.model.volatility.filtered_probs
ll = result.loglikelihood

assert hasattr(result, "params")
assert result is not None
assert result.params.shape[0] == 8 # 2*3 GARCH params + 2 transition matrix probabilities (this is valid whilst k=2)
assert np.all(np.isfinite(result.params))
assert result.model.volatility.k == 2
assert np.all(forecast.variance.values > 0)
assert forecast.variance.shape[1] == 1
assert np.allclose(probs[:,1:].sum(axis=0), 1.0, atol=1e-6) # excludes t=0 prob's as these have not been filtered
assert np.all((probs >= 0) & (probs <= 1))
assert cond_var.shape[0] == len(data)
assert np.all(cond_var > 0)
assert np.isfinite(ll)

2 changes: 2 additions & 0 deletions arch/univariate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
FixedVariance,
MIDASHyperbolic,
RiskMetrics2006,
MSGARCH,
)

recursions: types.ModuleType
Expand All @@ -55,6 +56,7 @@
"HARX",
"LS",
"MIDASHyperbolic",
"MSGARCH",
"Normal",
"RiskMetrics2006",
"SkewStudent",
Expand Down
55 changes: 42 additions & 13 deletions arch/univariate/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check notice

Code scanning / CodeQL

Cyclic import

Import of module [arch.univariate.volatility](1) begins an import cycle.
from arch.utility.array import ensure1d, to_array_1d
from arch.utility.exceptions import (
ConvergenceWarning,
Expand Down Expand Up @@ -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)
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)
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(),
Expand Down Expand Up @@ -790,12 +796,19 @@ 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
Copy link
Owner

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

The 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.
Sorry if im missing something obvious here, could you please provide some guidance on the best approach for this scenario?

args = (resids, sigma2, backcast, var_bounds)

from scipy.optimize import minimize
else:
func = self._loglikelihood # standard GARCH
Copy link
Owner

Choose a reason for hiding this comment

The 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)

# func = self.volatility.compute_loglikelihood()
# 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():
Expand Down Expand Up @@ -835,7 +848,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))

Expand All @@ -849,8 +862,17 @@ 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)
Expand All @@ -870,6 +892,7 @@ def fit(
fit_start,
fit_stop,
model_copy,
filtered_probs,
)

@abstractmethod
Expand Down Expand Up @@ -1803,6 +1826,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
Expand All @@ -1813,6 +1837,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:
Expand Down Expand Up @@ -2063,6 +2088,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(
Expand Down
22 changes: 13 additions & 9 deletions arch/univariate/mean.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
HARCH,
ConstantVariance,
VolatilityProcess,
MSGARCH
)
from arch.utility.array import (
AbstractDocStringInheritor,
Expand Down Expand Up @@ -188,7 +189,7 @@ class HARX(ARCHModel, metaclass=AbstractDocStringInheritor):
Flag indicating whether to automatically rescale data if the scale of the
data is likely to produce convergence issues when estimating model parameters.
If False, the model is estimated on the data without transformation. If True,
than y is rescaled and the new scale is reported in the estimation results.
then y is rescaled and the new scale is reported in the estimation results.

Examples
--------
Expand Down Expand Up @@ -1107,7 +1108,7 @@ class ConstantMean(HARX):
Flag indicating whether to automatically rescale data if the scale of the
data is likely to produce convergence issues when estimating model parameters.
If False, the model is estimated on the data without transformation. If True,
than y is rescaled and the new scale is reported in the estimation results.
then y is rescaled and the new scale is reported in the estimation results.

Examples
--------
Expand Down Expand Up @@ -1252,7 +1253,7 @@ class ZeroMean(HARX):
Flag indicating whether to automatically rescale data if the scale of the
data is likely to produce convergence issues when estimating model parameters.
If False, the model is estimated on the data without transformation. If True,
than y is rescaled and the new scale is reported in the estimation results.
then y is rescaled and the new scale is reported in the estimation results.

Examples
--------
Expand Down Expand Up @@ -1414,7 +1415,7 @@ class ARX(HARX):
Flag indicating whether to automatically rescale data if the scale of the
data is likely to produce convergence issues when estimating model parameters.
If False, the model is estimated on the data without transformation. If True,
than y is rescaled and the new scale is reported in the estimation results.
then y is rescaled and the new scale is reported in the estimation results.

Examples
--------
Expand Down Expand Up @@ -1540,7 +1541,7 @@ class LS(HARX):
Flag indicating whether to automatically rescale data if the scale of the
data is likely to produce convergence issues when estimating model parameters.
If False, the model is estimated on the data without transformation. If True,
than y is rescaled and the new scale is reported in the estimation results.
then y is rescaled and the new scale is reported in the estimation results.

Examples
--------
Expand Down Expand Up @@ -1615,7 +1616,7 @@ class ARCHInMean(ARX):
Flag indicating whether to automatically rescale data if the scale of the
data is likely to produce convergence issues when estimating model parameters.
If False, the model is estimated on the data without transformation. If True,
than y is rescaled and the new scale is reported in the estimation results.
then y is rescaled and the new scale is reported in the estimation results.
form : {"log", "vol", "var", int, float}
The form of the conditional variance that appears in the mean equation. The
string names use the log of the conditional variance ("log"), the square-root
Expand Down Expand Up @@ -1891,7 +1892,7 @@ def arch_model(
] = "Constant",
lags: int | list[int] | Int32Array | Int64Array | None = 0,
vol: Literal[
"GARCH", "ARCH", "EGARCH", "FIGARCH", "APARCH", "HARCH", "FIGARCH"
"GARCH", "ARCH", "EGARCH", "FIGARCH", "APARCH", "HARCH", "FIGARCH", "MSGARCH"
] = "GARCH",
p: int | list[int] = 1,
o: int = 0,
Expand Down Expand Up @@ -1953,7 +1954,7 @@ def arch_model(
Flag indicating whether to automatically rescale data if the scale
of the data is likely to produce convergence issues when estimating
model parameters. If False, the model is estimated on the data without
transformation. If True, than y is rescaled and the new scale is
transformation. If True, then y is rescaled and the new scale is
reported in the estimation results.

Returns
Expand Down Expand Up @@ -2000,6 +2001,7 @@ def arch_model(
"harch",
"constant",
"egarch",
"msgarch",
)
known_dist = (
"normal",
Expand Down Expand Up @@ -2060,8 +2062,10 @@ def arch_model(
elif vol_model == "aparch":
assert isinstance(p, int)
v = APARCH(p=p, o=o, q=q)
else: # vol == 'harch'
elif vol_model == 'harch':
v = HARCH(lags=p)
else: # vol_model == "msgarch":
v = MSGARCH()

if dist_name in ("skewstudent", "skewt"):
d: Distribution = SkewStudent()
Expand Down
Loading