Skip to content

Commit c94198e

Browse files
authored
Merge pull request #750 from bashtage/fixed-result-bugs
BUG: Fix bugs in fixed result
2 parents 47e796f + 779bf28 commit c94198e

File tree

5 files changed

+180
-32
lines changed

5 files changed

+180
-32
lines changed

arch/tests/univariate/test_mean.py

+145-5
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,14 @@
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)
85+
86+
87+
def close_plots():
88+
import matplotlib.pyplot as plt
89+
90+
plt.close("all")
7691

7792

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

85100

101+
simple_mean_models = [
102+
ARX(SP500, lags=1),
103+
HARX(SP500, lags=[1, 5]),
104+
ConstantMean(SP500),
105+
ZeroMean(SP500),
106+
]
107+
108+
mean_models = [
109+
ARX(SP500, x=X, lags=1),
110+
HARX(SP500, x=X, lags=[1, 5]),
111+
LS(SP500, X),
112+
] + simple_mean_models
113+
114+
analytic_volatility_processes = [
115+
ARCH(3),
116+
FIGARCH(1, 1),
117+
GARCH(1, 1, 1),
118+
HARCH([1, 5, 22]),
119+
ConstantVariance(),
120+
EWMAVariance(0.94),
121+
FixedVariance(np.full_like(SP500, SP500.var())),
122+
MIDASHyperbolic(),
123+
RiskMetrics2006(),
124+
]
125+
126+
other_volatility_processes = [
127+
APARCH(1, 1, 1, 1.5),
128+
EGARCH(1, 1, 1),
129+
]
130+
131+
volatility_processes = analytic_volatility_processes + other_volatility_processes
132+
133+
134+
@pytest.fixture(
135+
scope="module",
136+
params=list(itertools.product(simple_mean_models, analytic_volatility_processes)),
137+
ids=[
138+
f"{a.__class__.__name__}-{b}"
139+
for a, b in itertools.product(simple_mean_models, analytic_volatility_processes)
140+
],
141+
)
142+
def forecastable_model(request):
143+
mod: ARCHModel
144+
vol: VolatilityProcess
145+
mod, vol = request.param
146+
mod.volatility = vol
147+
res = mod.fit()
148+
return res, mod.fix(res.params)
149+
150+
151+
@pytest.fixture(
152+
scope="module",
153+
params=list(itertools.product(mean_models, volatility_processes)),
154+
ids=[
155+
f"{a.__class__.__name__}-{b}"
156+
for a, b in itertools.product(mean_models, volatility_processes)
157+
],
158+
)
159+
def fit_fixed_models(request):
160+
mod: ARCHModel
161+
vol: VolatilityProcess
162+
mod, vol = request.param
163+
mod.volatility = vol
164+
res = mod.fit()
165+
return res, mod.fix(res.params)
166+
167+
86168
class TestMeanModel:
87169
@classmethod
88170
def setup_class(cls):
@@ -480,9 +562,7 @@ def test_ar_plot(self):
480562
with pytest.raises(ValueError):
481563
res.plot(annualize="unknown")
482564

483-
import matplotlib.pyplot as plt
484-
485-
plt.close("all")
565+
close_plots()
486566

487567
res.plot(scale=360)
488568
res.hedgehog_plot(start=500)
@@ -491,7 +571,7 @@ def test_ar_plot(self):
491571
res.hedgehog_plot(start=500, method="simulation", simulations=100)
492572
res.hedgehog_plot(plot_type="volatility", method="bootstrap")
493573

494-
plt.close("all")
574+
close_plots()
495575

496576
def test_arch_arx(self):
497577
self.rng.seed(12345)
@@ -1370,3 +1450,63 @@ def test_non_contiguous_input(use_numpy):
13701450
mod = arch_model(y, mean="Zero")
13711451
res = mod.fit()
13721452
assert res.params.shape[0] == 3
1453+
1454+
1455+
def test_fixed_equivalence(fit_fixed_models):
1456+
res, res_fixed = fit_fixed_models
1457+
1458+
assert_allclose(res.aic, res_fixed.aic)
1459+
assert_allclose(res.bic, res_fixed.bic)
1460+
assert_allclose(res.loglikelihood, res_fixed.loglikelihood)
1461+
assert res.nobs == res_fixed.nobs
1462+
assert res.num_params == res_fixed.num_params
1463+
assert_allclose(res.params, res_fixed.params)
1464+
assert_allclose(res.conditional_volatility, res_fixed.conditional_volatility)
1465+
assert_allclose(res.std_resid, res_fixed.std_resid)
1466+
assert_allclose(res.resid, res_fixed.resid)
1467+
assert_allclose(res.arch_lm_test(5).stat, res_fixed.arch_lm_test(5).stat)
1468+
assert res.model.__class__ is res_fixed.model.__class__
1469+
assert res.model.volatility.__class__ is res_fixed.model.volatility.__class__
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+
1475+
1476+
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not installed")
1477+
def test_fixed_equivalence_plots(fit_fixed_models):
1478+
res, res_fixed = fit_fixed_models
1479+
1480+
fig = res.plot()
1481+
fixed_fig = res_fixed.plot()
1482+
assert isinstance(fig, type(fixed_fig))
1483+
1484+
close_plots()
1485+
1486+
1487+
@pytest.mark.slow
1488+
@pytest.mark.parametrize("simulations", [1, 100])
1489+
def test_fixed_equivalence_forecastable(forecastable_model, simulations):
1490+
res, res_fixed = forecastable_model
1491+
f1 = res.forecast(horizon=5)
1492+
f2 = res_fixed.forecast(horizon=5)
1493+
assert isinstance(f1, type(f2))
1494+
assert_allclose(f1.mean, f2.mean)
1495+
assert_allclose(f1.variance, f2.variance)
1496+
1497+
f1 = res.forecast(horizon=5, method="simulation", simulations=simulations)
1498+
f2 = res_fixed.forecast(horizon=5, method="simulation", simulations=simulations)
1499+
assert isinstance(f1, type(f2))
1500+
f1 = res.forecast(horizon=5, method="bootstrap", simulations=simulations)
1501+
f2 = res_fixed.forecast(horizon=5, method="bootstrap", simulations=simulations)
1502+
assert isinstance(f1, type(f2))
1503+
1504+
1505+
@pytest.mark.slow
1506+
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not installed")
1507+
def test_fixed_equivalence_forecastable_plots(forecastable_model):
1508+
res, res_fixed = forecastable_model
1509+
fig1 = res.hedgehog_plot(start=SP500.shape[0] - 25)
1510+
fig2 = res_fixed.hedgehog_plot(start=SP500.shape[0] - 25)
1511+
assert isinstance(fig1, type(fig2))
1512+
close_plots()

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

ci/azure/azure_template_posix.yml

+12-12
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,12 @@ jobs:
2828
python_311_copy_on_write:
2929
python.version: '3.11'
3030
ARCH_TEST_COPY_ON_WRITE: 1
31-
python_39_coverage:
32-
python.version: '3.9'
33-
ARCH_CYTHON_COVERAGE: true
34-
PYTEST_PATTERN: "(not slow)"
35-
python_39_statsmodels_main:
31+
python_minimums:
3632
python.version: '3.9'
37-
STATSMODELS_MAIN: true
38-
coverage: false
33+
NUMPY: 1.23.0
34+
SCIPY: 1.9.0
35+
MATPLOTLIB: 3.4.0
36+
PANDAS: 1.4.0
3937
python_39_conda_numba:
4038
python.version: '3.9'
4139
use.conda: 'true'
@@ -48,6 +46,10 @@ jobs:
4846
NUMPY: 1.22.3
4947
SCIPY: 1.8.0
5048
PANDAS: 1.4.0
49+
python_311_cython_coverage:
50+
python.version: '3.11'
51+
ARCH_CYTHON_COVERAGE: true
52+
PYTEST_PATTERN: "(not slow)"
5153
python_311_no_binary:
5254
python.version: '3.11'
5355
ARCH_NO_BINARY: true
@@ -72,12 +74,10 @@ jobs:
7274
NUMPY: 1.24.0
7375
USE_NUMBA: false
7476
PYTEST_PATTERN: "(slow or not slow)"
75-
python_minimums:
77+
python_312_statsmodels_main:
7678
python.version: '3.9'
77-
NUMPY: 1.23.0
78-
SCIPY: 1.9.0
79-
MATPLOTLIB: 3.4.0
80-
PANDAS: 1.4.0
79+
STATSMODELS_MAIN: true
80+
coverage: false
8181
python312_pre:
8282
python.version: '3.12'
8383
pip.pre: true

0 commit comments

Comments
 (0)