---
title: "Replicating Quaedvlieg (2021)"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Replicating Quaedvlieg (2021)}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment  = "#>",
  fig.width  = 7,
  fig.height = 4
)
```

This article demonstrates the two multi-horizon superior predictive ability tests of Quaedvlieg (2021), implemented in **forecastdom** as `uspa_mh_test()` and `aspa_mh_test()`. Both compare a whole *path* of forecasts at horizons $h = 1, \dots, H$ rather than each horizon in isolation, and both use a moving-block bootstrap whose critical values absorb the serial dependence in the loss-differential path.

* **Uniform SPA** ($\mathrm{uSPA}$). The null is that the benchmark is at least as good as the competitor at *every* horizon. The test statistic is the minimum of the horizon-wise standardized loss differentials.
* **Average SPA** ($\mathrm{aSPA}$). The null is that the benchmark is at least as good as the competitor on a user-specified weighted average of horizons. The test statistic is the standardized weighted-average loss differential.

The loss differential is

$$d_{h,t} = L_{\text{bench},h,t} - L_{\text{comp},h,t},$$

so a positive entry means the benchmark has *higher* loss (is worse) at horizon $h$. Either null is rejected when the benchmark is worse uniformly (uSPA) or worse on average (aSPA).

```{r setup, message = FALSE}
library(forecastdom)
data(quaedvlieg2021)
str(quaedvlieg2021, max.level = 1)
```

The bundled `quaedvlieg2021` object holds the two loss-differential matrices distributed with the paper's replication archive: `$uspa` and `$aspa`, each $1000 \times 20$.

## Per-horizon picture

Before running the tests it is worth looking at the per-horizon mean loss differentials. The two example matrices are constructed precisely so that they tell the tests apart.

```{r mean-table}
H <- ncol(quaedvlieg2021$uspa)

means <- data.frame(
  h    = seq_len(H),
  uspa = colMeans(quaedvlieg2021$uspa),
  aspa = colMeans(quaedvlieg2021$aspa)
)

knitr::kable(
  means, digits = 3, row.names = FALSE,
  col.names = c("$h$", "uSPA dataset", "aSPA dataset"))
```

`$uspa` shows positive mean loss differentials at *every* horizon, so the benchmark is worse uniformly. `$aspa` shows positive means at most horizons but a near-zero mean at the shortest horizon, so the benchmark is worse on average but not uniformly.

```{r mean-plot, fig.height = 3.5, fig.alt = "Mean loss differential by horizon for the uspa and aspa example matrices"}
library(ggplot2)

df <- data.frame(
  horizon = rep(seq_len(H), 2),
  d_bar   = c(colMeans(quaedvlieg2021$uspa),
              colMeans(quaedvlieg2021$aspa)),
  dataset = rep(c("uspa", "aspa"), each = H)
)

ggplot(df, aes(horizon, d_bar, colour = dataset)) +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "grey60") +
  geom_line() + geom_point() +
  labs(x = "Horizon h", y = expression(bar(d)[h]),
       title = "Mean loss differential by horizon") +
  theme_minimal()
```

## Uniform multi-horizon SPA

```{r uspa, results = "hold"}
set.seed(1)
uspa_uspa <- uspa_mh_test(quaedvlieg2021$uspa, L = 3, B = 999)

set.seed(1)
uspa_aspa <- uspa_mh_test(quaedvlieg2021$aspa, L = 3, B = 999)

uspa_uspa
uspa_aspa
```

The uSPA test rejects on `$uspa` (every horizon shows the benchmark losing) but fails to reject on `$aspa` at the 5% level. The reason is that the shortest horizon's mean is essentially zero, and the `min` over standardized horizon-wise means is pulled down by that single horizon.

## Average multi-horizon SPA

With uniform weights $w_h = 1/H$ the aSPA statistic is the standardized unweighted average loss differential.

```{r aspa-uniform, results = "hold"}
w_unif <- rep(1 / H, H)

set.seed(1)
aspa_uspa <- aspa_mh_test(quaedvlieg2021$uspa, weights = w_unif, L = 3, B = 999)

set.seed(1)
aspa_aspa <- aspa_mh_test(quaedvlieg2021$aspa, weights = w_unif, L = 3, B = 999)

aspa_uspa
aspa_aspa
```

The aSPA test rejects on *both* datasets. The slightly negative $h = 1$ mean of `$aspa` is more than compensated by positive means at longer horizons, so the weighted average favours rejection. This is the power gain Quaedvlieg (2021) emphasises: by aggregating across the forecast path the average test detects model-level differences that horizon-by-horizon Diebold-Mariano comparisons miss under the multiple-testing burden.

### Custom weights

Down-weighting short horizons makes the aSPA test even more
decisive against `$aspa`:

```{r aspa-down, results = "hold"}
w_down <- c(rep(0, 4), rep(1, 16)) / 16 # zero weight on h = 1..4

set.seed(1)
aspa_mh_test(quaedvlieg2021$aspa, weights = w_down, L = 3, B = 999)
```

Up-weighting the shortest horizons pulls the statistic back toward
zero:

```{r aspa-up, results = "hold"}
w_up <- c(rep(4, 4), rep(0, 16)) / 16 # all weight on h = 1..4

set.seed(1)
aspa_mh_test(quaedvlieg2021$aspa, weights = w_up, L = 3, B = 999)
```

## Block-length sensitivity

The moving-block bootstrap depends on the block length $L$. The $p$-value is robust over a reasonable range: $L$ should be small enough to keep many blocks and large enough to capture the path's serial dependence.

```{r L-sensitivity}
Ls <- c(2, 3, 5, 8, 12)

sens <- do.call(rbind, lapply(Ls, function(L) {

  set.seed(1)
  u <- uspa_mh_test(quaedvlieg2021$uspa, L = L, B = 499)
  
  set.seed(1)
  a <- aspa_mh_test(quaedvlieg2021$uspa, weights = w_unif, L = L, B = 499)
  
  data.frame(L = L,
             uspa_stat = u$statistic, uspa_p = u$pvalue,
             aspa_stat = a$statistic, aspa_p = a$pvalue)

}))

knitr::kable(
  sens, digits = 3, row.names = FALSE,
  col.names = c("$L$",
                "uSPA stat", "uSPA $p$",
                "aSPA stat", "aSPA $p$"))
```

The statistics are independent of $L$ because they depend only on the QS HAC long-run variance, not on the bootstrap, and the $p$-values are stable across $L$ for this dataset.

## Takeaways

* `uspa_mh_test()` rejects only when the benchmark loses at *every* horizon. It has strong power against uniform underperformance and limited power against horizon-specific failures.
* `aspa_mh_test()` rejects when the *weighted-average* differential favours the competitor, even if performance at individual horizons is mixed. The choice of weights determines which part of the forecast path drives the decision.
* The moving-block bootstrap is essential. Forecast paths show natural serial dependence, especially when the same model is evaluated at successive horizons.

## References

- Andrews, D. W. K. (1991). Heteroskedasticity and autocorrelation
  consistent covariance matrix estimation. *Econometrica*, 59(3),
  817-858.
- Quaedvlieg, R. (2021). Multi-horizon forecast comparison.
  *Journal of Business & Economic Statistics*, 39(1), 40-53.
