|
1 | 1 | import numpy as np |
| 2 | +from numpy.testing import assert_allclose |
2 | 3 | import pandas as pd |
| 4 | +from pandas.testing import assert_frame_equal |
3 | 5 | import pytest |
4 | 6 |
|
5 | 7 | from arch.data import sp500 |
6 | | -from arch.univariate import ARCHInMean, Normal |
| 8 | +from arch.univariate import ARX, ARCHInMean, Normal |
7 | 9 | from arch.univariate.recursions_python import ARCHInMeanRecursion |
8 | 10 | from arch.univariate.volatility import ( |
9 | 11 | ARCH, |
@@ -74,10 +76,148 @@ def test_smoke(form): |
74 | 76 | assert res.param_cov.shape == (5, 5) |
75 | 77 | assert isinstance(res.param_cov, pd.DataFrame) |
76 | 78 |
|
77 | | - with pytest.raises( |
78 | | - NotImplementedError, match=r"forecasts are not implemented for \(G\)ARCH" |
79 | | - ): |
80 | | - res.forecast(reindex=True) |
| 79 | + fc = res.forecast() |
| 80 | + assert fc.mean.shape == (1, 1) |
| 81 | + assert np.isfinite(fc.mean.values).all() |
| 82 | + assert np.isfinite(fc.variance.values).all() |
| 83 | + assert np.isfinite(fc.residual_variance.values).all() |
| 84 | + |
| 85 | + |
| 86 | +@pytest.mark.parametrize( |
| 87 | + ("form", "transform"), |
| 88 | + [("var", lambda v: v), ("vol", np.sqrt), ("log", np.log)], |
| 89 | +) |
| 90 | +def test_forecast_analytic_recursion(form, transform): |
| 91 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH(), form=form) |
| 92 | + res = gim.fit(disp="off") |
| 93 | + fc = res.forecast(horizon=3) |
| 94 | + y = np.asarray(gim._y) |
| 95 | + mp, _, _ = gim._parse_parameters(np.asarray(res.params)) |
| 96 | + arp = gim._har_to_ar(mp) |
| 97 | + const = arp[0] |
| 98 | + ar = arp[1:] |
| 99 | + kappa = mp[-1] |
| 100 | + rv = fc.residual_variance.values[0] |
| 101 | + expected = np.zeros(3) |
| 102 | + expected[0] = const + kappa * transform(rv[0]) + ar[0] * y[-1] + ar[1] * y[-2] |
| 103 | + expected[1] = const + kappa * transform(rv[1]) + ar[0] * expected[0] + ar[1] * y[-1] |
| 104 | + expected[2] = ( |
| 105 | + const + kappa * transform(rv[2]) + ar[0] * expected[1] + ar[1] * expected[0] |
| 106 | + ) |
| 107 | + assert_allclose(fc.mean.values[0], expected) |
| 108 | + |
| 109 | + |
| 110 | +def test_forecast_var_simulation_matches_analytic(): |
| 111 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH(), form="var") |
| 112 | + res = gim.fit(disp="off") |
| 113 | + fc = res.forecast(horizon=3) |
| 114 | + fc_sim = res.forecast(horizon=3, method="simulation", simulations=100000) |
| 115 | + sim_mean = fc_sim.simulations.values.mean(axis=1) |
| 116 | + assert_allclose(sim_mean, fc.mean.values, atol=0.05) |
| 117 | + |
| 118 | + |
| 119 | +def test_forecast_kappa_zero_matches_arx(): |
| 120 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH(), form="vol") |
| 121 | + res = gim.fit(disp="off") |
| 122 | + params = np.asarray(res.params) |
| 123 | + arx = ARX(SP500, lags=2, volatility=GARCH()) |
| 124 | + arx.fit(disp="off") |
| 125 | + kappa_zero = params.copy() |
| 126 | + kappa_zero[3] = 0.0 |
| 127 | + fc_gim = gim.forecast(kappa_zero, horizon=3, reindex=False) |
| 128 | + fc_arx = arx.forecast(np.delete(params, 3), horizon=3, reindex=False) |
| 129 | + assert_frame_equal(fc_gim.mean, fc_arx.mean) |
| 130 | + assert_frame_equal(fc_gim.variance, fc_arx.variance) |
| 131 | + assert_frame_equal(fc_gim.residual_variance, fc_arx.residual_variance) |
| 132 | + |
| 133 | + |
| 134 | +def test_forecast_bootstrap(): |
| 135 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH(), form="var") |
| 136 | + res = gim.fit(disp="off") |
| 137 | + fc = res.forecast( |
| 138 | + horizon=3, start=200, method="bootstrap", simulations=100, reindex=False |
| 139 | + ) |
| 140 | + assert fc.simulations.values.shape == (SP500.shape[0] - 200, 100, 3) |
| 141 | + assert np.isfinite(fc.simulations.values).all() |
| 142 | + assert np.isfinite(fc.mean.values).all() |
| 143 | + |
| 144 | + |
| 145 | +def test_forecast_exog(): |
| 146 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH(), form="var", x=X[0]) |
| 147 | + res = gim.fit(disp="off") |
| 148 | + fc = res.forecast(horizon=2, x=X[0].iloc[-2:]) |
| 149 | + y = np.asarray(gim._y) |
| 150 | + mp, _, _ = gim._parse_parameters(np.asarray(res.params)) |
| 151 | + arp = gim._har_to_ar(mp) |
| 152 | + const = arp[0] |
| 153 | + ar = arp[1:] |
| 154 | + kappa = mp[-1] |
| 155 | + exog_p = mp[-2] |
| 156 | + rv = fc.residual_variance.values[0] |
| 157 | + xv = np.asarray(X[0].iloc[-2:]) |
| 158 | + expected = np.zeros(2) |
| 159 | + expected[0] = const + kappa * rv[0] + ar[0] * y[-1] + ar[1] * y[-2] + exog_p * xv[0] |
| 160 | + expected[1] = ( |
| 161 | + const + kappa * rv[1] + ar[0] * expected[0] + ar[1] * y[-1] + exog_p * xv[1] |
| 162 | + ) |
| 163 | + assert_allclose(fc.mean.values[0], expected) |
| 164 | + |
| 165 | + |
| 166 | +def test_forecast_variance_one_step(): |
| 167 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH(), form="var") |
| 168 | + res = gim.fit(disp="off") |
| 169 | + fc = res.forecast(horizon=3) |
| 170 | + assert_allclose(fc.variance.values[:, 0], fc.residual_variance.values[:, 0]) |
| 171 | + |
| 172 | + |
| 173 | +def test_forecast_egarch_analytic_horizon(): |
| 174 | + gim = ARCHInMean(SP500, volatility=EGARCH(), form="log") |
| 175 | + res = gim.fit(disp="off") |
| 176 | + fc1 = res.forecast(horizon=1) |
| 177 | + assert fc1.mean.shape == (1, 1) |
| 178 | + with pytest.raises(ValueError, match=r"Analytic forecasts not available"): |
| 179 | + res.forecast(horizon=2) |
| 180 | + |
| 181 | + |
| 182 | +def test_forecast_errors(): |
| 183 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH()) |
| 184 | + res = gim.fit(disp="off") |
| 185 | + with pytest.raises(ValueError, match=r"horizon must be an integer"): |
| 186 | + gim.forecast(np.asarray(res.params), horizon=0) |
| 187 | + with pytest.raises(ValueError, match=r"Due to backcasting"): |
| 188 | + res.forecast(horizon=3, start=0) |
| 189 | + |
| 190 | + |
| 191 | +def test_forecast_padded_start(): |
| 192 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH()) |
| 193 | + res = gim.fit(disp="off") |
| 194 | + fc = res.forecast(horizon=3, start=1, reindex=False) |
| 195 | + assert fc.mean.shape == (SP500.shape[0] - 1, 3) |
| 196 | + assert np.isnan(fc.mean.values[0]).all() |
| 197 | + assert np.isfinite(fc.mean.values[1:]).all() |
| 198 | + fc_sim = res.forecast( |
| 199 | + horizon=3, start=1, method="simulation", simulations=100, reindex=False |
| 200 | + ) |
| 201 | + assert fc_sim.simulations.values.shape == (SP500.shape[0] - 1, 100, 3) |
| 202 | + assert np.isnan(fc_sim.simulations.values[0]).all() |
| 203 | + |
| 204 | + |
| 205 | +def test_forecast_simulation_rng(): |
| 206 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH()) |
| 207 | + res = gim.fit(disp="off") |
| 208 | + rng = np.random.RandomState(12345).standard_normal |
| 209 | + fc = res.forecast(horizon=2, method="simulation", simulations=100, rng=rng) |
| 210 | + assert np.isfinite(fc.simulations.values).all() |
| 211 | + |
| 212 | + |
| 213 | +def test_forecast_exog_simulation(): |
| 214 | + gim = ARCHInMean(SP500, lags=2, volatility=GARCH(), form="var", x=X[0]) |
| 215 | + res = gim.fit(disp="off") |
| 216 | + xf = np.zeros((1, 2)) |
| 217 | + fc = res.forecast( |
| 218 | + horizon=2, method="simulation", simulations=100, reindex=False, x=xf |
| 219 | + ) |
| 220 | + assert np.isfinite(fc.simulations.values).all() |
81 | 221 |
|
82 | 222 |
|
83 | 223 | def test_example_smoke(): |
|
0 commit comments