Skip to content

Commit 52423a1

Browse files
committed
BUG: Fix bugs in fixed result
Systematically find an fix bugs that affect fixed results closes #749
1 parent 47e796f commit 52423a1

File tree

4 files changed

+155
-16
lines changed

4 files changed

+155
-16
lines changed

arch/tests/univariate/test_mean.py

+132-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from arch.compat.pandas import MONTH_END
22

33
from io import StringIO
4+
import itertools
45
from itertools import product
56
from string import ascii_lowercase
67
import struct
@@ -26,7 +27,12 @@
2627

2728
from arch.data import sp500
2829
from arch.typing import Literal
29-
from arch.univariate.base import ARCHModelForecast, ARCHModelResult, _align_forecast
30+
from arch.univariate.base import (
31+
ARCHModel,
32+
ARCHModelForecast,
33+
ARCHModelResult,
34+
_align_forecast,
35+
)
3036
from arch.univariate.distribution import (
3137
GeneralizedError,
3238
Normal,
@@ -46,6 +52,7 @@
4652
FixedVariance,
4753
MIDASHyperbolic,
4854
RiskMetrics2006,
55+
VolatilityProcess,
4956
)
5057
from arch.utility.exceptions import ConvergenceWarning, DataScaleWarning
5158

@@ -73,6 +80,8 @@
7380
DISPLAY: Literal["off", "final"] = "off"
7481
UPDATE_FREQ = 0 if DISPLAY == "off" else 3
7582
SP500 = 100 * sp500.load()["Adj Close"].pct_change().dropna()
83+
rs = np.random.RandomState(20241029)
84+
X = SP500 * 0.01 + SP500.std() * rs.standard_normal(SP500.shape)
7685

7786

7887
@pytest.fixture(scope="module", params=[True, False])
@@ -83,6 +92,73 @@ def simulated_data(request):
8392
return np.asarray(sim_data.data) if request.param else sim_data.data
8493

8594

95+
simple_mean_models = [
96+
ARX(SP500, lags=1),
97+
HARX(SP500, lags=[1, 5]),
98+
ConstantMean(SP500),
99+
ZeroMean(SP500),
100+
]
101+
102+
mean_models = [
103+
ARX(SP500, x=X, lags=1),
104+
HARX(SP500, x=X, lags=[1, 5]),
105+
LS(SP500, X),
106+
] + simple_mean_models
107+
108+
analytic_volatility_processes = [
109+
ARCH(3),
110+
FIGARCH(1, 1),
111+
GARCH(1, 1, 1),
112+
HARCH([1, 5, 22]),
113+
ConstantVariance(),
114+
EWMAVariance(0.94),
115+
FixedVariance(np.full_like(SP500, SP500.var())),
116+
MIDASHyperbolic(),
117+
RiskMetrics2006(),
118+
]
119+
120+
other_volatility_processes = [
121+
APARCH(1, 1, 1, 1.5),
122+
EGARCH(1, 1, 1),
123+
]
124+
125+
volatility_processes = analytic_volatility_processes + other_volatility_processes
126+
127+
128+
@pytest.fixture(
129+
scope="module",
130+
params=list(itertools.product(simple_mean_models, analytic_volatility_processes)),
131+
ids=[
132+
f"{a.__class__.__name__}-{b}"
133+
for a, b in itertools.product(simple_mean_models, analytic_volatility_processes)
134+
],
135+
)
136+
def forecastable_model(request):
137+
mod: ARCHModel
138+
vol: VolatilityProcess
139+
mod, vol = request.param
140+
mod.volatility = vol
141+
res = mod.fit()
142+
return res, mod.fix(res.params)
143+
144+
145+
@pytest.fixture(
146+
scope="module",
147+
params=list(itertools.product(mean_models, volatility_processes)),
148+
ids=[
149+
f"{a.__class__.__name__}-{b}"
150+
for a, b in itertools.product(mean_models, volatility_processes)
151+
],
152+
)
153+
def fit_fixed_models(request):
154+
mod: ARCHModel
155+
vol: VolatilityProcess
156+
mod, vol = request.param
157+
mod.volatility = vol
158+
res = mod.fit()
159+
return res, mod.fix(res.params)
160+
161+
86162
class TestMeanModel:
87163
@classmethod
88164
def setup_class(cls):
@@ -1370,3 +1446,58 @@ def test_non_contiguous_input(use_numpy):
13701446
mod = arch_model(y, mean="Zero")
13711447
res = mod.fit()
13721448
assert res.params.shape[0] == 3
1449+
1450+
1451+
def test_fixed_equivalence(fit_fixed_models):
1452+
res, res_fixed = fit_fixed_models
1453+
1454+
assert_allclose(res.aic, res_fixed.aic)
1455+
assert_allclose(res.bic, res_fixed.bic)
1456+
assert_allclose(res.loglikelihood, res_fixed.loglikelihood)
1457+
assert res.nobs == res_fixed.nobs
1458+
assert res.num_params == res_fixed.num_params
1459+
assert_allclose(res.params, res_fixed.params)
1460+
assert_allclose(res.conditional_volatility, res_fixed.conditional_volatility)
1461+
assert_allclose(res.std_resid, res_fixed.std_resid)
1462+
assert_allclose(res.resid, res_fixed.resid)
1463+
assert_allclose(res.arch_lm_test(5).stat, res_fixed.arch_lm_test(5).stat)
1464+
assert res.model.__class__ is res_fixed.model.__class__
1465+
assert res.model.volatility.__class__ is res_fixed.model.volatility.__class__
1466+
assert isinstance(res.summary(), type(res_fixed.summary()))
1467+
if res.num_params > 0:
1468+
assert "std err" in str(res.summary())
1469+
assert "std err" not in str(res_fixed.summary())
1470+
1471+
1472+
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not installed")
1473+
def test_fixed_equivalence_plots(fit_fixed_models):
1474+
res, res_fixed = fit_fixed_models
1475+
1476+
fig = res.plot()
1477+
fixed_fig = res_fixed.plot()
1478+
assert isinstance(fig, type(fixed_fig))
1479+
1480+
import matplotlib.pyplot as plt
1481+
1482+
plt.close("all")
1483+
1484+
1485+
@pytest.mark.parametrize("simulations", [1, 100])
1486+
def test_fixed_equivalence_forecastable(forecastable_model, simulations):
1487+
res, res_fixed = forecastable_model
1488+
f1 = res.forecast(horizon=5)
1489+
f2 = res_fixed.forecast(horizon=5)
1490+
assert isinstance(f1, type(f2))
1491+
assert_allclose(f1.mean, f2.mean)
1492+
assert_allclose(f1.variance, f2.variance)
1493+
fig1 = res.hedgehog_plot(start=SP500.shape[0] - 25)
1494+
fig2 = res_fixed.hedgehog_plot(start=SP500.shape[0] - 25)
1495+
assert isinstance(fig1, type(fig2))
1496+
plt.close("all")
1497+
1498+
f1 = res.forecast(horizon=5, method="simulation", simulations=simulations)
1499+
f2 = res_fixed.forecast(horizon=5, method="simulation", simulations=simulations)
1500+
assert isinstance(f1, type(f2))
1501+
f1 = res.forecast(horizon=5, method="bootstrap", simulations=simulations)
1502+
f2 = res_fixed.forecast(horizon=5, method="bootstrap", simulations=simulations)
1503+
assert isinstance(f1, type(f2))

arch/univariate/base.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,11 @@ def _fit_parameterless_model(
386386
vol_final[first_obs:last_obs] = vol
387387

388388
names = self._all_parameter_names()
389-
loglikelihood = self._static_gaussian_loglikelihood(y)
390389
r2 = self._r2(params)
391390
fit_start, fit_stop = self._fit_indices
391+
loglikelihood = -1.0 * self._loglikelihood(
392+
params, vol**2 * np.ones(fit_stop - fit_start), backcast, var_bounds
393+
)
392394

393395
assert isinstance(r2, float)
394396
return ARCHModelResult(
@@ -523,8 +525,7 @@ def fix(
523525
names = self._all_parameter_names()
524526
# Reshape resids and vol
525527
first_obs, last_obs = self._fit_indices
526-
resids_final = np.empty_like(self._y, dtype=np.double)
527-
resids_final.fill(np.nan)
528+
resids_final = np.full_like(self._y, np.nan, dtype=np.double)
528529
resids_final[first_obs:last_obs] = resids
529530
vol_final = np.empty_like(self._y, dtype=np.double)
530531
vol_final.fill(np.nan)
@@ -533,8 +534,8 @@ def fix(
533534
model_copy = deepcopy(self)
534535
return ARCHModelFixedResult(
535536
params,
536-
resids,
537-
vol,
537+
resids_final,
538+
vol_final,
538539
self._y_series,
539540
names,
540541
loglikelihood,

arch/univariate/mean.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,11 @@ def forecast(
10181018
for i in range(horizon):
10191019
_impulses = impulse[i::-1][:, None]
10201020
lrvp = variance_paths[:, :, : (i + 1)].dot(_impulses**2)
1021-
long_run_variance_paths[:, :, i] = np.squeeze(lrvp)
1021+
lrvp = np.squeeze(lrvp)
1022+
if lrvp.ndim < 2:
1023+
lrvp = np.atleast_1d(lrvp)
1024+
lrvp = lrvp[None, :]
1025+
long_run_variance_paths[:, :, i] = lrvp
10221026
t, m = self._y.shape[0], self._max_lags
10231027
mean_paths = np.empty(shocks.shape[:2] + (m + horizon,))
10241028
dynp_rev = dynp[::-1]

arch/univariate/volatility.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -930,8 +930,7 @@ def _analytic_forecast(
930930
forecasts = np.full((t - start, horizon), np.nan)
931931

932932
forecasts[:, :] = parameters[0]
933-
forecast_paths = None
934-
return VarianceForecast(forecasts, forecast_paths)
933+
return VarianceForecast(forecasts)
935934

936935
def _simulation_forecast(
937936
self,
@@ -2804,7 +2803,8 @@ class FixedVariance(VolatilityProcess, metaclass=AbstractDocStringInheritor):
28042803
----------
28052804
variance : {array, Series}
28062805
Array containing the variances to use. Should have the same shape as the
2807-
data used in the model.
2806+
data used in the model. This is not checked since the model is not
2807+
available when the FixedVariance process is created.
28082808
unit_scale : bool, optional
28092809
Flag whether to enforce a unit scale. If False, a scale parameter will be
28102810
estimated so that the model variance will be proportional to ``variance``.
@@ -2823,7 +2823,7 @@ def __init__(self, variance: Float64Array, unit_scale: bool = False) -> None:
28232823
self._name = "Fixed Variance"
28242824
self._name += " (Unit Scale)" if unit_scale else ""
28252825
self._variance_series = ensure1d(variance, "variance", True)
2826-
self._variance = np.asarray(variance)
2826+
self._variance = np.atleast_1d(variance)
28272827

28282828
def compute_variance(
28292829
self,
@@ -2841,6 +2841,10 @@ def compute_variance(
28412841
return sigma2
28422842

28432843
def starting_values(self, resids: Float64Array) -> Float64Array:
2844+
if self._variance.ndim != 1 or self._variance.shape[0] < self._stop:
2845+
raise ValueError(
2846+
f"variance must be a 1-d array with at least {self._stop} elements"
2847+
)
28442848
if not self._unit_scale:
28452849
_resids = resids / np.sqrt(self._variance[self._start : self._stop])
28462850
return np.array([_resids.var()])
@@ -2893,7 +2897,7 @@ def _analytic_forecast(
28932897
horizon: int,
28942898
) -> VarianceForecast:
28952899
t = resids.shape[0]
2896-
forecasts = np.full((t, horizon), np.nan)
2900+
forecasts = np.full((t - start, horizon), np.nan)
28972901

28982902
return VarianceForecast(forecasts)
28992903

@@ -2909,10 +2913,9 @@ def _simulation_forecast(
29092913
rng: RNGType,
29102914
) -> VarianceForecast:
29112915
t = resids.shape[0]
2912-
forecasts = np.full((t, horizon), np.nan)
2913-
forecast_paths = np.empty((t, simulations, horizon))
2914-
forecast_paths.fill(np.nan)
2915-
shocks = np.full((t, simulations, horizon), np.nan)
2916+
forecasts = np.full((t - start, horizon), np.nan)
2917+
forecast_paths = np.full((t - start, simulations, horizon), np.nan)
2918+
shocks = np.full((t - start, simulations, horizon), np.nan)
29162919

29172920
return VarianceForecast(forecasts, forecast_paths, shocks)
29182921

0 commit comments

Comments
 (0)