|
31 | 31 |
|
32 | 32 | SP500 = 100 * sp500.load()["Adj Close"].pct_change().dropna() |
33 | 33 |
|
| 34 | + |
| 35 | +def _recompute_ar_simulation_paths( |
| 36 | + y: np.ndarray, |
| 37 | + start_index: int, |
| 38 | + horizon: int, |
| 39 | + constant: float, |
| 40 | + dynp: np.ndarray, |
| 41 | + shocks: np.ndarray, |
| 42 | + variance_paths: np.ndarray, |
| 43 | + expected_x: np.ndarray, |
| 44 | + exog_p: np.ndarray, |
| 45 | + max_lags: int, |
| 46 | +) -> tuple[np.ndarray, np.ndarray]: |
| 47 | + """Recompute AR mean simulation paths (mirrors ``HARX.forecast``).""" |
| 48 | + impulse = _ar_to_impulse(horizon, dynp) |
| 49 | + long_run_variance_paths = variance_paths.copy() |
| 50 | + for i in range(horizon): |
| 51 | + impulses = impulse[i::-1][:, None] |
| 52 | + lrvp = variance_paths[:, :, : (i + 1)].dot(impulses**2) |
| 53 | + lrvp = lrvp[:, :, 0] |
| 54 | + long_run_variance_paths[:, :, i] = lrvp |
| 55 | + |
| 56 | + t = y.shape[0] |
| 57 | + m = max_lags |
| 58 | + mean_paths = np.empty(shocks.shape[:2] + (m + horizon,)) |
| 59 | + dynp_rev = dynp[::-1] |
| 60 | + for i in range(start_index, t): |
| 61 | + path_loc = i - start_index |
| 62 | + mean_paths[path_loc, :, :m] = y[i - m + 1 : i + 1] |
| 63 | + for j in range(horizon): |
| 64 | + mean_paths[path_loc, :, m + j] = ( |
| 65 | + constant |
| 66 | + + mean_paths[path_loc, :, j : m + j].dot(dynp_rev) |
| 67 | + + shocks[path_loc, :, j] |
| 68 | + ) |
| 69 | + if expected_x.shape[0] > 0: |
| 70 | + mean_paths[path_loc, :, m + j] += expected_x[:, path_loc, j].T @ exog_p |
| 71 | + mean_paths = mean_paths[:, :, m:] |
| 72 | + return mean_paths, long_run_variance_paths |
| 73 | + |
| 74 | + |
| 75 | +def _ar_mean_simulation_coefficients( |
| 76 | + model, params |
| 77 | +) -> tuple[float, np.ndarray, np.ndarray]: |
| 78 | + mp, _, _ = model._parse_parameters(np.asarray(params)) |
| 79 | + arp = model._har_to_ar(mp) |
| 80 | + nexog = 0 if model._x is None else model._x.shape[1] |
| 81 | + exog_p = np.empty(0) if model._x is None else mp[-nexog:] |
| 82 | + constant = arp[0] if model.constant else 0.0 |
| 83 | + dynp = arp[int(model.constant) :] |
| 84 | + return constant, dynp, exog_p |
| 85 | + |
| 86 | + |
34 | 87 | MEAN_MODELS = [ |
35 | 88 | HARX(SP500, lags=[1, 5]), |
36 | 89 | ARX(SP500, lags=2), |
@@ -539,6 +592,136 @@ def test_ar1_forecast_bootstrap(self): |
539 | 592 | assert_frame_equal(forecast.mean, repeat.mean) |
540 | 593 | assert_frame_equal(forecast.variance, repeat.variance) |
541 | 594 |
|
| 595 | + @pytest.mark.parametrize( |
| 596 | + ("method", "start", "simulations"), |
| 597 | + [ |
| 598 | + ("simulation", 50, 25), |
| 599 | + ("bootstrap", 200, 25), |
| 600 | + ], |
| 601 | + ) |
| 602 | + def test_ar1_garch_mean_forecast_simulation_paths( |
| 603 | + self, method: str, start: int, simulations: int |
| 604 | + ): |
| 605 | + am = arch_model(self.ar1, mean="AR", vol="GARCH", lags=[1]) |
| 606 | + res = am.fit(disp="off") |
| 607 | + horizon = 5 |
| 608 | + forecast_kwargs: dict = { |
| 609 | + "horizon": horizon, |
| 610 | + "start": start, |
| 611 | + "method": method, |
| 612 | + "simulations": simulations, |
| 613 | + } |
| 614 | + if method == "bootstrap": |
| 615 | + forecast_kwargs["random_state"] = RandomState(424242) |
| 616 | + forecast = res.forecast(**forecast_kwargs) |
| 617 | + sim = forecast.simulations |
| 618 | + model = res.model |
| 619 | + y = np.asarray(model._y) |
| 620 | + constant, dynp, exog_p = _ar_mean_simulation_coefficients(model, res.params) |
| 621 | + expected_mean, expected_lrv = _recompute_ar_simulation_paths( |
| 622 | + y, |
| 623 | + start, |
| 624 | + horizon, |
| 625 | + constant, |
| 626 | + dynp, |
| 627 | + sim.residuals, |
| 628 | + sim.residual_variances, |
| 629 | + np.empty(0), |
| 630 | + exog_p, |
| 631 | + model._max_lags, |
| 632 | + ) |
| 633 | + nobs = y.shape[0] - start |
| 634 | + assert sim.values.shape == (nobs, simulations, horizon) |
| 635 | + assert sim.residuals.shape == (nobs, simulations, horizon) |
| 636 | + assert sim.residual_variances.shape == (nobs, simulations, horizon) |
| 637 | + assert sim.variances.shape == (nobs, simulations, horizon) |
| 638 | + assert_allclose(sim.values, expected_mean) |
| 639 | + assert_allclose(sim.variances, expected_lrv) |
| 640 | + |
| 641 | + @pytest.mark.parametrize("method", ["simulation", "bootstrap"]) |
| 642 | + def test_ar2_garch_mean_forecast_simulation_paths(self, method: str): |
| 643 | + am = arch_model(self.ar2, mean="AR", vol="GARCH", lags=[1, 2], p=1, q=1) |
| 644 | + res = am.fit(disp="off") |
| 645 | + horizon = 4 |
| 646 | + start = 200 if method == "bootstrap" else 30 |
| 647 | + simulations = 20 |
| 648 | + forecast_kwargs: dict = { |
| 649 | + "horizon": horizon, |
| 650 | + "start": start, |
| 651 | + "method": method, |
| 652 | + "simulations": simulations, |
| 653 | + } |
| 654 | + if method == "bootstrap": |
| 655 | + forecast_kwargs["random_state"] = RandomState(535353) |
| 656 | + forecast = res.forecast(**forecast_kwargs) |
| 657 | + sim = forecast.simulations |
| 658 | + model = res.model |
| 659 | + y = np.asarray(model._y) |
| 660 | + constant, dynp, exog_p = _ar_mean_simulation_coefficients(model, res.params) |
| 661 | + expected_mean, expected_lrv = _recompute_ar_simulation_paths( |
| 662 | + y, |
| 663 | + start, |
| 664 | + horizon, |
| 665 | + constant, |
| 666 | + dynp, |
| 667 | + sim.residuals, |
| 668 | + sim.residual_variances, |
| 669 | + np.empty(0), |
| 670 | + exog_p, |
| 671 | + model._max_lags, |
| 672 | + ) |
| 673 | + assert_allclose(sim.values, expected_mean) |
| 674 | + assert_allclose(sim.variances, expected_lrv) |
| 675 | + |
| 676 | + def test_arx_garch_mean_forecast_simulation_paths(self): |
| 677 | + rng = RandomState(12345) |
| 678 | + am = arch_model(None, mean="ARX", lags=2) |
| 679 | + data = am.simulate([0.1, 1.2, -0.6, 0.1, 0.1, 0.8], nobs=1000) |
| 680 | + cols = ["x1", "x2"] |
| 681 | + x = pd.DataFrame( |
| 682 | + rng.standard_normal((data.data.shape[0], 2)), |
| 683 | + columns=cols, |
| 684 | + index=data.data.index, |
| 685 | + ) |
| 686 | + y = data.data + x @ np.array([0.25, 0.5]) |
| 687 | + y.name = "y" |
| 688 | + am = arch_model(y, x, mean="ARX", lags=2, p=1, q=1) |
| 689 | + res = am.fit(disp="off") |
| 690 | + horizon = 4 |
| 691 | + simulations = 20 |
| 692 | + start = y.shape[0] - 1 |
| 693 | + x_fcast = np.zeros((x.shape[1], 1, horizon)) |
| 694 | + for i in range(x_fcast.shape[0]): |
| 695 | + x_fcast[i] = np.arange(100 * i, 100 * i + horizon) |
| 696 | + |
| 697 | + forecast = res.forecast( |
| 698 | + horizon=horizon, |
| 699 | + start=start, |
| 700 | + method="simulation", |
| 701 | + simulations=simulations, |
| 702 | + x=x_fcast, |
| 703 | + ) |
| 704 | + sim = forecast.simulations |
| 705 | + model = res.model |
| 706 | + y_array = np.asarray(model._y) |
| 707 | + constant, dynp, exog_p = _ar_mean_simulation_coefficients(model, res.params) |
| 708 | + expected_x = model._reformat_forecast_x(x_fcast, horizon, start) |
| 709 | + expected_mean, expected_lrv = _recompute_ar_simulation_paths( |
| 710 | + y_array, |
| 711 | + start, |
| 712 | + horizon, |
| 713 | + constant, |
| 714 | + dynp, |
| 715 | + sim.residuals, |
| 716 | + sim.residual_variances, |
| 717 | + expected_x, |
| 718 | + exog_p, |
| 719 | + model._max_lags, |
| 720 | + ) |
| 721 | + assert sim.values.shape == (1, simulations, horizon) |
| 722 | + assert_allclose(sim.values, expected_mean) |
| 723 | + assert_allclose(sim.variances, expected_lrv) |
| 724 | + |
542 | 725 | def test_ar2_garch11(self): |
543 | 726 | pass |
544 | 727 |
|
|
0 commit comments