--- title: "Post-Shock Forecasting Workflow" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Post-Shock Forecasting Workflow} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup, message=FALSE, warning=FALSE,include=FALSE} library(postshock) ``` # Overview The donor-adjustment framework builds on the post-shock forecasting approach of Lin and Eck (2021). Large structural shocks can cause forecasting models fitted only to pre-shock data to perform poorly in the post-shock period. The **postshock** package provides a donor-based forecasting framework that uses information from historical shock episodes to adjust forecasts following a new shock to the target series. The package supports forecasting of both conditional mean and volatility dynamics. Its main components are: - `dbw()`, which constructs donor balancing weights using pre-shock features; - `SynthPrediction()`, which produces donor-adjusted post-shock mean forecasts; - the MultiPool extension of `SynthPrediction()`, which organizes donors into interpretable shock groups; - `auto_garchx()`, which selects GARCH-X orders using an information criterion; - `SynthVolForecast()`, which produces donor-adjusted post-shock conditional variance forecasts using GARCH-X models. The general workflow is: 1. define a target episode and a collection of historical donor episodes; 2. align the target and donors relative to their shock times; 3. compare their pre-shock characteristics and estimate donor weights; 4. estimate donor-specific post-shock effects; and 5. add the weighted donor effect to a baseline forecast for the target. This vignette demonstrates the main package workflows using processed data objects distributed with **postshock**. We first illustrate post-shock mean forecasting using the ConocoPhillips example, then introduce the structured donor-pool extension, and finally demonstrate post-shock volatility forecasting using the IYG example. # Package data structure The forecasting functions in **postshock** operate on collections of target and donor series aligned relative to their respective shock times. At the function interface, the target and donor series are supplied through `Y_series_list`, with the target stored in the first position and the historical donors stored in the remaining positions. Each series has its own shock-timing information: - `shock_time_vec` records the last pre-shock observation for each series; - `shock_length_vec` defines the post-shock window length associated with each series; for donor series, this window is used to estimate the corresponding shock effect. The covariate structure differs between the two forecasting workflows: - `SynthPrediction()` uses `covariates_series_list`, whose elements are aligned with the target and donor series in `Y_series_list`; - `SynthVolForecast()` uses `covariates_series_list` for donor covariates only, while target covariates are supplied separately through `target_covariates`. The ordering of the series, covariate inputs, shock times, and shock lengths must remain consistent within the relevant workflow. Entries representing the same target or donor episode must be correctly aligned. The `cop_oil_postshock` object follows the mean-forecasting structure. Its target and donor series are stored in `Y`, while their corresponding covariate matrices are stored in `X`. The list names do not need to be identical across `Y` and `X`, but they should clearly identify the corresponding episodes. For example, `Y_2008_03_17` and `X_2008_03_17` contain the series and covariate matrix associated with the same historical shock episode. This vignette uses two processed data objects distributed with the package: - `cop_oil_postshock`, for post-shock mean forecasting; and - `iyg_onestep_input`, for post-shock variance forecasting. Additional processed objects for the Apple control-shock examples are also available in the package. We begin with the ConocoPhillips data object. ```{r load-cop-data} data("cop_oil_postshock", package = "postshock") names(cop_oil_postshock) ``` The target and donor series are stored in `Y`, while their corresponding covariate matrices are stored in `X`. ```{r inspect-cop-data} cop_structure <- data.frame( Position = seq_along(cop_oil_postshock$Y), Series = names(cop_oil_postshock$Y), Covariates = names(cop_oil_postshock$X), Shock_time = cop_oil_postshock$shock_time_vec, Shock_length = cop_oil_postshock$shock_length_vec ) knitr::kable( cop_structure, caption = "Alignment of the target and donor inputs in `cop_oil_postshock`." ) ``` The first element represents the target episode, and the remaining elements represent historical donor episodes. For every series, `shock_time_vec` identifies the last observation in the pre-shock period, so the shock effect begins at the following observation. # Donor balancing weights with `dbw()` Historical donor episodes are not necessarily equally informative for a new target shock. The function `dbw()` constructs donor balancing weights using characteristics observed before the shock. The function compares the target's pre-shock feature vector with weighted combinations of the donor feature vectors. Donors that provide a closer pre-shock match can therefore receive larger weights. A commonly used specification constrains the donor weights to be nonnegative and sum to one. Under this specification, the weights can be interpreted as the contribution of each donor to a synthetic donor combination. For the ConocoPhillips example, we use all available covariate columns and an L2-regularized balancing specification. ```{r fit-cop-dbw, results='hide', message=FALSE, warning=FALSE} cop_dbw <- dbw( X = cop_oil_postshock$X, dbw_indices = seq_len(ncol(cop_oil_postshock$X[[1]])), shock_time_vec = cop_oil_postshock$shock_time_vec, center = TRUE, scale = TRUE, sum_to_1 = TRUE, bounded_below_by = 0, bounded_above_by = 1, normchoice = "l2", penalty_normchoice = "l2", penalty_lambda = 1e-2 ) ``` The returned object contains the estimated donor weights, the final balancing loss, and the optimizer convergence result. ```{r inspect-cop-dbw} names(cop_dbw) cop_dbw$convergence cop_dbw$loss sum(cop_dbw$opt_params) ``` The convergence result indicates whether the numerical optimization completed successfully. The loss measures the remaining discrepancy between the target features and the weighted donor features under the selected scaling and regularization specification. It is primarily useful for comparing alternative DBW specifications fitted to the same data. The donor weights are stored in `opt_params`. ```{r display-cop-dbw} cop_weight_table <- data.frame( Donor = sub( "^Y_", "", names(cop_oil_postshock$Y)[-1] ), Weight = as.numeric(cop_dbw$opt_params) ) knitr::kable( cop_weight_table, digits = 3, caption = "Donor balancing weights for the ConocoPhillips example." ) ``` The estimated weights sum to one, up to numerical precision. In this example, most of the weight is assigned to the 2014-11-28 and 2008-09-29 episodes, while the remaining donor episodes receive weights close to zero. Under the selected pre-shock covariates and balancing specification, these two episodes therefore provide the main contribution to the synthetic donor combination. These weights determine how donor-specific shock effects are combined. They should not be interpreted as probabilities that particular historical events will recur. In most applications, users do not need to call `dbw()` separately. `SynthPrediction()` and `SynthVolForecast()` can call it internally when donor-based weighting is requested. A direct call is nevertheless useful for examining the pre-shock match and understanding which donors contribute to the synthetic adjustment. # Mean forecasting with `SynthPrediction()` `SynthPrediction()` produces post-shock mean forecasts by combining a baseline forecast for the target series with estimated post-shock effects from historical donor episodes. The procedure has four main steps. First, an ARIMA or ARIMAX model containing a post-shock indicator is fitted to each donor series. The coefficient of this indicator represents the estimated post-shock shift for that donor. Second, the donor-specific shifts are combined using either equal weights or donor balancing weights. Third, a baseline model is fitted to the target using only its pre-shock observations. Finally, the combined donor shift is added to the target baseline forecast. The function returns three forecast versions: - `unadjusted`, the target baseline forecast without a donor adjustment; - `arithmetic_mean`, the baseline forecast adjusted by the simple average of the donor-specific post-shock effects; and - `adjusted`, the baseline forecast adjusted by the DBW-weighted donor effect. ## ConocoPhillips example The target episode is the large decline in the ConocoPhillips (`COP`) share price in March 2020. Five earlier crisis and oil-market episodes are used as donors. The aligned series, covariates, and shock timing information are stored in the `cop_oil_postshock` object loaded in the previous section. ## Fit a one-step post-shock forecast We set `k = 1` to forecast the first observation after the target's pre-shock period. The call below uses the same feature columns, centering and scaling choices, and L2 penalty as the direct `dbw()` example above. The covariate matrices are used to construct donor weights. Because `covariate_indices = NULL`, they are not included as regressors in the ARIMA models. In that case, each donor model contains the post-shock indicator, while the target baseline is fitted without covariate regressors. ```{r fit-cop-synthprediction, results='hide', message=FALSE, warning=FALSE} cop_fit <- SynthPrediction( Y_series_list = cop_oil_postshock$Y, covariates_series_list = cop_oil_postshock$X, shock_time_vec = cop_oil_postshock$shock_time_vec, shock_length_vec = cop_oil_postshock$shock_length_vec, k = 1L, dbw_indices = seq_len(ncol(cop_oil_postshock$X[[1]])), dbw_center = TRUE, dbw_scale = TRUE, use_dbw = TRUE, covariate_indices = NULL, seasonal = TRUE, penalty_lambda = 1e-2, penalty_normchoice = "l2" ) ``` Setting `seasonal = TRUE` allows the function to consider a seasonal specification when a meaningful seasonal frequency is detected. It does not require every fitted model to contain a seasonal component. ## Inspect forecasts The returned object has three main components. `predictions` contains the forecast versions, `linear_combinations` contains the donor weights, and `meta` contains donor-specific effects and model information. ```{r inspect-cop-fit} names(cop_fit) names(cop_fit$predictions) names(cop_fit$meta) ``` ## Compare the forecasts The first post-shock observation occurs at the index immediately following the last pre-shock observation of the target. ```{r cop-forecast-comparison} forecast_index <- cop_oil_postshock$shock_time_vec[1] + 1L observed_value <- as.numeric( cop_oil_postshock$Y[[1]][forecast_index] ) forecast_values <- c( as.numeric(cop_fit$predictions$unadjusted[1]), as.numeric(cop_fit$predictions$arithmetic_mean[1]), as.numeric(cop_fit$predictions$adjusted[1]) ) cop_forecast_table <- data.frame( Method = c( "Observed value", "Unadjusted", "Arithmetic mean", "DBW adjusted" ), Value = c( observed_value, forecast_values ), Absolute_error = c( NA_real_, abs(forecast_values - observed_value) ) ) knitr::kable( cop_forecast_table, digits = 3, col.names = c("Method", "Value", "Absolute error"), caption = paste( "One-step forecast comparison for the March 2020", "ConocoPhillips shock episode." ) ) ``` The observed value is included only for retrospective evaluation. It is not supplied to `SynthPrediction()` when the target model or donor weights are estimated. As shown in the table, both donor-adjusted forecasts reduce the absolute error relative to the unadjusted baseline. The DBW-adjusted forecast also has a smaller error than the arithmetic-mean adjustment in this example. The improvement over the unadjusted baseline is substantial, whereas the additional gain from DBW weighting over equal weighting is more modest. Because the comparison is based on a single retrospective one-step forecast, it should be interpreted as an illustration of the workflow rather than general evidence that DBW always performs better. ## Inspect donor weights and the combined adjustment The donor weights used by `SynthPrediction()` are stored in `linear_combinations`. The estimated donor-specific post-shock effects are stored in `meta$omega_vec`. ```{r inspect-cop-donor-results} cop_donor_summary <- data.frame( Donor = sub( "^Y_", "", names(cop_oil_postshock$Y)[-1] ), Weight = as.numeric(cop_fit$linear_combinations), Estimated_effect = as.numeric(cop_fit$meta$omega_vec) ) knitr::kable( cop_donor_summary, digits = 3, col.names = c( "Donor", "DBW weight", "Estimated post-shock effect" ), caption = "Donor weights and estimated effects for the COP example." ) ``` Donors with weights close to zero make little contribution to the final adjustment, even when their estimated post-shock effects are relatively large, whereas donors with larger weights have greater influence. ```{r inspect-cop-adjustment} data.frame( Unadjusted_forecast = as.numeric(cop_fit$predictions$unadjusted[1]), Combined_donor_effect = as.numeric(cop_fit$meta$combined_omega), Adjusted_forecast = as.numeric(cop_fit$predictions$adjusted[1]) ) |> knitr::kable( digits = 3, col.names = c( "Unadjusted forecast", "Combined donor effect", "DBW-adjusted forecast" ), caption = "Construction of the DBW-adjusted COP forecast." ) ``` The sign of the combined donor effect determines the direction of the adjustment. In this example, the combined effect is negative, so it lowers the unadjusted forecast and moves it closer to the observed post-shock value. # Structured donor pools A single-pool analysis treats all historical donor episodes as members of one common donor set. In some applications, however, donor shocks may represent different economic mechanisms. The MultiPool extension allows donors to be organized into interpretable groups before their estimated adjustments are aggregated. For the ConocoPhillips example, we separate the donor episodes into two channels: - a `supply` pool containing the November 2014 oil-price shock; and - a `demand` pool containing the crisis-related episodes from 2008. These classifications are specified by the analyst and should reflect the economic mechanisms underlying the historical events. Numerical pre-shock characteristics are used later to construct donor weights within each pool. ## Define the donor pools Pools are supplied as a named list. Each element contains donor names that must match the corresponding entries in `names(Y_series_list)`. ```{r define-cop-pools} cop_pools <- list( supply = "Y_2014_11_28", demand = c( "Y_2008_03_17", "Y_2008_09_09", "Y_2008_09_15", "Y_2008_09_29" ) ) cop_pool_table <- data.frame( Pool = names(cop_pools), Donors = unname( vapply( cop_pools, function(x) { paste( gsub("_", "-", sub("^Y_", "", x)), collapse = ", " ) }, character(1) ) ) ) knitr::kable( cop_pool_table, row.names = FALSE, col.names = c("Pool", "Donor episodes"), caption = "Structured donor pools for the ConocoPhillips example." ) ``` The first element of `Y_series_list` is always treated as the target. Only donor names should therefore be included in `pools`; the target is added automatically when each pool-specific model is fitted. ## Fit the MultiPool specification Providing the `pools` argument causes `SynthPrediction()` to use the MultiPool extension. The function retains a common unadjusted target forecast and estimates a separate donor adjustment within each pool. Here, `pool_use_dbw = TRUE` applies donor balancing within each pool. Because the supply pool contains only one donor, its within-pool weight is necessarily one. Donor balancing primarily affects the multi-donor demand pool. ```{r fit-cop-multipool, results='hide', message=FALSE, warning=FALSE} cop_multipool <- SynthPrediction( Y_series_list = cop_oil_postshock$Y, covariates_series_list = cop_oil_postshock$X, shock_time_vec = cop_oil_postshock$shock_time_vec, shock_length_vec = cop_oil_postshock$shock_length_vec, k = 1L, covariate_indices = NULL, seasonal = TRUE, dbw_indices = seq_len( ncol(cop_oil_postshock$X[[1]]) ), dbw_center = TRUE, dbw_scale = TRUE, penalty_lambda = 1e-2, penalty_normchoice = "l2", pools = cop_pools, pool_agg = "sum", base_use_dbw = FALSE, pool_use_dbw = TRUE ) ``` With `pool_agg = "sum"`, the pool-specific adjustments are treated as additive components of the total post-shock adjustment. This is a modeling choice rather than a property automatically inferred from the data. Users can instead select `pool_agg = "weighted"` and provide a named numeric vector through `pool_weights`. ## Inspect the pool-specific adjustments The estimated adjustment for each pool is stored in `meta$multi_pool$pool_effects`, while the aggregated adjustment is stored in `meta$multi_pool$aggregated_effect`. ```{r inspect-cop-pool-effects} cop_pool_effects <- data.frame( Pool = names( cop_multipool$meta$multi_pool$pool_effects ), Estimated_adjustment = as.numeric( cop_multipool$meta$multi_pool$pool_effects ) ) knitr::kable( cop_pool_effects, row.names = FALSE, digits = 3, col.names = c( "Pool", "Estimated pool adjustment" ), caption = "Pool-specific adjustments for the ConocoPhillips example." ) ``` The MultiPool-adjusted forecast is returned in `predictions$adjusted_multipool`. ```{r compare-cop-multipool} cop_multipool_values <- c( as.numeric( cop_multipool$predictions$unadjusted[1] ), as.numeric( cop_fit$predictions$adjusted[1] ), as.numeric( cop_multipool$predictions$adjusted_multipool[1] ) ) cop_multipool_table <- data.frame( Method = c( "Observed value", "Unadjusted", "Single-pool DBW", "MultiPool (sum)" ), Value = c( observed_value, cop_multipool_values ), Absolute_error = c( NA_real_, abs(cop_multipool_values - observed_value) ) ) knitr::kable( cop_multipool_table, digits = 3, col.names = c("Method", "Value", "Absolute error"), caption = paste( "Comparison of single-pool and MultiPool forecasts", "for the ConocoPhillips example." ) ) ``` In this example, both pool-level effects lower the unadjusted forecast. Their sum produces the MultiPool adjustment and moves the forecast closer to the observed post-shock value. The result should be interpreted as an illustration of how economically meaningful donor groups can be represented in the package. A closer forecast in this single retrospective example does not imply that splitting donors into multiple pools will always improve forecasting performance. # Volatility forecasting with `SynthVolForecast()` `SynthVolForecast()` extends the donor-adjustment framework to conditional variance forecasting. It combines a baseline variance forecast fitted to the pre-shock target series with post-shock variance adjustments estimated from historical donor episodes. For each donor, the function fits a GARCH-X model containing a post-shock indicator in the conditional variance equation. The estimated coefficient of this indicator represents the donor-specific variance adjustment. These adjustments are then combined using donor balancing weights and added to the target's baseline variance forecast. The function returns three forecast versions: - `unadjusted`, the baseline conditional variance forecast; - `arithmetic_mean`, the baseline forecast adjusted by the simple average of the donor-specific variance effects; and - `adjusted`, the baseline forecast adjusted by the DBW-weighted donor effect. Because conditional variance is latent in empirical financial data, forecast evaluation requires an observable proxy. In the example below, the next-period squared return is used as a realized-variance proxy. ## Load the IYG example The target series is IYG, a financial-sector exchange-traded fund, and the target shock is the 2016 U.S. presidential election. Historical election and political-uncertainty episodes serve as potential donors. The processed inputs are distributed with **postshock** in the `iyg_onestep_input` object. ```{r load-iyg-data} data("iyg_onestep_input", package = "postshock") names(iyg_onestep_input) names(iyg_onestep_input$inputs) ``` The object provides three analyst-defined donor-pool configurations: `all`, `restricted`, and `elections_only`. In this vignette, we use the `restricted` configuration to demonstrate the main workflow. The donor-pool choice is treated as part of the analysis design rather than selected using the realized forecast error. ```{r inspect-iyg-input} iyg_restricted <- iyg_onestep_input$inputs$restricted names(iyg_restricted) ``` The restricted input contains the target and donor return series, the donor covariate matrices, a separate target covariate matrix, and the shock-timing vectors required by `SynthVolForecast()`. ```{r summarize-iyg-input} iyg_restricted$target_event iyg_restricted$donor_events c( number_of_series = length(iyg_restricted$Y_series_list), number_of_donors = length(iyg_restricted$donor_events), donor_covariate_sets = length(iyg_restricted$covariates_series_list), shock_time_entries = length(iyg_restricted$shock_time_vec), shock_length_entries = length(iyg_restricted$shock_length_vec) ) ``` The restricted configuration contains one target series and three donor series. The three donor covariate matrices correspond to the three donor episodes, while the shock-time and shock-length vectors contain one entry for every target or donor series. ## Produce a variance forecast We use the `restricted` donor-pool configuration and produce a one-step-ahead conditional variance forecast. This configuration contains the 2012 Election, Brexit Poll Released, and Brexit donor episodes. The GARCH-X orders are selected automatically. With `garch_order = NULL`, `SynthVolForecast()` calls `auto_garchx()` separately for each donor and for the pre-shock target series. Candidate specifications are compared using BIC over GARCH orders up to `max.p = 3` and ARCH orders up to `max.q = 3`, including both symmetric and asymmetric specifications. The donor covariates are used for DBW matching. Because `covariate_indices = NULL`, the donor-side variance models contain the post-shock indicator but do not include additional covariates as GARCH-X regressors. ```{r fit-iyg-volatility, results='hide', message=FALSE, warning=FALSE} iyg_fit <- SynthVolForecast( Y_series_list = iyg_restricted$Y_series_list, covariates_series_list = iyg_restricted$covariates_series_list, target_covariates = iyg_restricted$target_covariates, shock_time_vec = iyg_restricted$shock_time_vec, shock_length_vec = iyg_restricted$shock_length_vec, k = 1L, dbw_indices = seq_len( ncol(iyg_restricted$target_covariates) ), dbw_center = TRUE, dbw_scale = TRUE, penalty_lambda = 1e-2, penalty_normchoice = "l2", covariate_indices = NULL, garch_order = NULL, max.p = 3L, max.q = 3L, vcov.type = "robust", return_fits = FALSE ) ``` ### Inspect the selected GARCH-X orders The automatically selected orders are stored separately for the donor models and the target model. The order is reported as `(q, p, o)`, where `q` is the ARCH order, `p` is the GARCH order, and `o` indicates whether an asymmetric term is included. ```{r inspect-iyg-orders} iyg_selected_orders <- do.call( rbind, c( iyg_fit$meta$donor_orders, list(iyg_fit$meta$target_order) ) ) iyg_order_table <- data.frame( Model = c( paste("Donor:", iyg_restricted$donor_events), paste("Target:", iyg_restricted$target_event) ), ARCH_q = iyg_selected_orders[, 1], GARCH_p = iyg_selected_orders[, 2], Asymmetry_o = iyg_selected_orders[, 3] ) knitr::kable( iyg_order_table, row.names = FALSE, col.names = c( "Model", "ARCH order (q)", "GARCH order (p)", "Asymmetry indicator (o)" ), caption = paste( "Orders selected automatically by BIC for the", "donor and target variance models." ) ) ``` An asymmetry indicator of one means that the selected model includes a GJR-type asymmetric term, allowing positive and negative return shocks to have different effects on conditional variance. A GARCH order of zero means that the specification does not include a lagged conditional-variance term, although it may still depend on lagged squared returns through its ARCH component. Because BIC selection is performed separately for each donor and the target, the selected orders need not be identical across series. ## Inspect donor weights and variance adjustments The donor balancing weights are stored in `linear_combinations`, while the estimated donor-specific adjustments in the conditional variance equation are stored in `meta$omega_vec`. ```{r inspect-iyg-donor-adjustments} iyg_donor_summary <- data.frame( Donor = as.character(iyg_restricted$donor_events), Weight = as.numeric( iyg_fit$linear_combinations ), Estimated_adjustment = as.numeric( iyg_fit$meta$omega_vec ) ) knitr::kable( iyg_donor_summary, row.names = FALSE, digits = 6, col.names = c( "Donor episode", "DBW weight", "Estimated variance adjustment" ), caption = paste( "Donor weights and estimated variance adjustments", "for the restricted IYG specification." ) ) ``` In this example, Brexit and Brexit Poll Released receive the largest DBW weights, while the 2012 Election receives a smaller but still positive weight. All three estimated variance adjustments are positive, so each donor contributes to raising the target's baseline variance forecast. The weighted donor adjustment is stored in `meta$combined_omega`. ```{r inspect-iyg-combined-adjustment} iyg_adjustment_table <- data.frame( Unadjusted_forecast = as.numeric( iyg_fit$predictions$unadjusted[1] ), Combined_adjustment = as.numeric( iyg_fit$meta$combined_omega ), Adjusted_forecast = as.numeric( iyg_fit$predictions$adjusted[1] ) ) knitr::kable( iyg_adjustment_table, row.names = FALSE, digits = 6, col.names = c( "Unadjusted variance forecast", "Combined donor adjustment", "DBW-adjusted variance forecast" ), caption = "Construction of the DBW-adjusted IYG variance forecast." ) ``` The sign of the combined donor adjustment determines the direction in which the baseline variance forecast is changed. In this example, the combined adjustment is positive, so it raises the unadjusted variance forecast. `SynthVolForecast()` enforces a positive adjusted variance forecast. This constraint is not binding in the present example because the combined adjustment is positive. ### Compare the variance forecasts Because the latent conditional variance is not directly observed, we use the squared target return at the first post-shock observation as a realized-variance proxy. ```{r compare-iyg-variance-forecasts} iyg_proxy_index <- iyg_restricted$shock_time_vec[1] + 1L iyg_observed_proxy <- as.numeric( iyg_restricted$Y_series_list[[1]][iyg_proxy_index] )^2 iyg_forecast_values <- c( as.numeric( iyg_fit$predictions$unadjusted[1] ), as.numeric( iyg_fit$predictions$arithmetic_mean[1] ), as.numeric( iyg_fit$predictions$adjusted[1] ) ) iyg_forecast_table <- data.frame( Method = c( "Observed squared-return proxy", "Unadjusted", "Arithmetic mean", "DBW adjusted" ), Variance = c( iyg_observed_proxy, iyg_forecast_values ), Absolute_error = c( NA_real_, abs( iyg_forecast_values - iyg_observed_proxy ) ) ) knitr::kable( iyg_forecast_table, row.names = FALSE, digits = 6, col.names = c( "Method", "Variance", "Absolute error" ), caption = paste( "One-step variance forecast comparison for IYG", "under the restricted donor-pool specification." ) ) ``` As shown in the table, the unadjusted variance forecast substantially underestimates the observed squared-return proxy. Both donor-based adjustments reduce the absolute error, and the DBW-adjusted forecast is the closest of the three forecasts to the observed proxy in this retrospective example. This result illustrates the workflow but does not imply that DBW weighting will outperform alternative forecasts in every application. # Interpretation and practical guidance The examples above illustrate a common workflow for post-shock mean and conditional variance forecasting. In practice, the quality of the resulting forecast depends on the definition of the target episode, the choice of donor episodes, and the pre-shock information used for donor balancing. Several practical points should be considered when using **postshock**. ## Align episodes consistently The target and donor series should be aligned relative to their own shock times. The entry in `shock_time_vec` identifies the final pre-shock observation, and the shock effect begins at the following observation. The ordering of the series, covariate matrices, shock times, and shock lengths must remain consistent. For MultiPool analyses, donor names must also match the names used in the `pools` specification. ## Separate donor matching from model regressors The arguments `dbw_indices` and `covariate_indices` have different roles. - `dbw_indices` selects the pre-shock covariates used to construct donor balancing weights. - `covariate_indices` selects covariates included directly as regressors in the donor forecasting models. Covariates may therefore be used for donor matching without being included in the ARIMA, ARIMAX, or GARCH-X equations. This distinction should be made explicit when defining an analysis. ## Choose donors using substantive information Donor episodes should be selected using information available independently of the realized target outcome. Economic interpretation, event type, market environment, and pre-shock comparability may all be relevant. The MultiPool extension can be useful when donors represent distinct mechanisms. However, the pool definitions and aggregation rule are modeling choices. In particular, `pool_agg = "sum"` assumes that the pool-specific adjustments can be treated as additive components. ## Inspect weights and convergence Large DBW weights indicate donors that contribute more strongly to the synthetic adjustment under the selected pre-shock features and constraints. They should not be interpreted as probabilities that historical events will recur. Users should also inspect the DBW convergence information and consider whether the resulting weights are concentrated on only a small number of donors. Alternative feature sets, scaling choices, penalties, or pool definitions may produce different weights. ## Interpret empirical variance forecasts carefully `SynthVolForecast()` returns forecasts on the conditional variance scale. Taking the square root converts a variance forecast to the corresponding standard-deviation scale. In empirical applications, the latent conditional variance is not directly observed. A squared return can be used as a realized-variance proxy, but it is itself noisy. Forecast comparisons based on a single squared return should therefore be interpreted cautiously. When `garch_order = NULL`, the GARCH-X specification is selected automatically for each donor and the target. Users who need additional diagnostic information can set `return_fits = TRUE` and inspect the fitted model objects. ## Avoid post-shock information leakage Donor weights and the target baseline model should be estimated without using the target's post-shock observations. Historical donor post-shock outcomes are used by design to estimate donor-specific shock effects. Realized post-shock target values may be used afterward for retrospective evaluation, but they should not influence donor selection, tuning choices, or model specification. Thus, the no-leakage restriction concerns information from the target's post-shock period, not the already observed post-shock histories of the donor episodes. See the help pages for `dbw()`, `SynthPrediction()`, and `SynthVolForecast()` for complete argument and return-value documentation. # References Lin, J., and Eck, D. J. (2021). Minimizing post-shock forecasting error through aggregation of outside information. *International Journal of Forecasting*, 37(4), 1710–1727.