Skip to content

Commit ff7524e

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

File tree

4 files changed

+147
-16
lines changed

4 files changed

+147
-16
lines changed

arch/tests/univariate/test_mean.py

+124-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
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
78
import sys
89
import types
910
import warnings
1011

12+
import matplotlib.pyplot as plt
1113
import numpy as np
1214
from numpy.random import RandomState
1315
from numpy.testing import (
@@ -26,7 +28,12 @@
2628

2729
from arch.data import sp500
2830
from arch.typing import Literal
29-
from arch.univariate.base import ARCHModelForecast, ARCHModelResult, _align_forecast
31+
from arch.univariate.base import (
32+
ARCHModel,
33+
ARCHModelForecast,
34+
ARCHModelResult,
35+
_align_forecast,
36+
)
3037
from arch.univariate.distribution import (
3138
GeneralizedError,
3239
Normal,
@@ -46,6 +53,7 @@
4653
FixedVariance,
4754
MIDASHyperbolic,
4855
RiskMetrics2006,
56+
VolatilityProcess,
4957
)
5058
from arch.utility.exceptions import ConvergenceWarning, DataScaleWarning
5159

@@ -73,6 +81,8 @@
7381
DISPLAY: Literal["off", "final"] = "off"
7482
UPDATE_FREQ = 0 if DISPLAY == "off" else 3
7583
SP500 = 100 * sp500.load()["Adj Close"].pct_change().dropna()
84+
rs = np.random.RandomState(20241029)
85+
X = SP500 * 0.01 + SP500.std() * rs.standard_normal(SP500.shape)
7686

7787

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

8595

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