--- title: "Volatility-Robust Alternatives to radf()" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Volatility-Robust Alternatives to radf()} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, warning = FALSE, message = FALSE, comment = "#>" ) ``` ```{r load} library(exuber) ``` ## The shared problem Plain `radf()` assumes a constant innovation variance. Real series rarely have one, and when volatility varies over time the standard critical values no longer control the size of the test. exuber offers several fixes, and each takes a structurally different approach. Four of the five covered below (`radf_sign()`, `radf_sign_dm()`, `radf_kp()` and `radf_sbz()`) keep the `radf_obj` class and support `summary()`, `datestamp()`, `tidy()` and `autoplot()` in the same way as plain `radf()`. `vignette("naming-and-analysis")` shows how each plugs into the pipeline. `radf_sbz_union()` is the exception. It bundles the statistic of `radf_sbz()` and the classic `supDF` into one union-of-rejections call and has its own class, not `radf_obj`, so only its own `print()` and `autoplot()` apply. The time-deformation approach of `radf_tt()` is described in the second part of this page, and the first part covers the rest. All of these functions target non-stationary volatility, meaning a permanent shift or trend in the unconditional innovation variance. They do not target stationary GARCH-type conditional heteroskedasticity, whose variance profile is asymptotically flat and leaves plain `radf()` with the correct size. The running example is therefore the `sim_psy1()` bubble driven by `sim_vol_break()` innovations, whose standard deviation triples half-way through the sample. This is the case in which plain `radf()` over-rejects the most: ```{r dgp} y <- sim_psy1(n = 200, seed = 1, e = sim_vol_break(199)) ``` | Function | Paper | Approach | |---|---|---| | `radf_sign()` / `radf_sign_dm()` | Harvey, Leybourne & Zu (2020) | Transforms the series to the cumulated sign of its first differences, which is exactly invariant to any heteroskedasticity pattern and needs no bootstrap. The `_dm` version demeans first, which makes it robust to level shifts (Harvey, Leybourne, Tatlow & Zu 2025). | | `radf_kp()` | Harvey, Leybourne, Taylor & Zu (2024) | Purges volatility. It divides each first difference by a kernel spot-volatility estimate, cumulates the result and runs the ordinary PSY test on the purged series, whose null distribution is identical to the standard homoskedastic one. | | `radf_sbz()` / `radf_sbz_cv()` | Harvey, Leybourne & Zu (2019) | A WLS-weighted recursive Dickey-Fuller statistic (`supBZ`). It weights the observations and does not purge or transform the series. It uses the same kernel spot-volatility estimator as `radf_kp()`, but as regression weights. | | `radf_sbz_union()` | Harvey, Leybourne & Zu (2019) | Combines `supDF` (the classic statistic) and `supBZ` (the statistic of `radf_sbz()`) as a union of rejections, using a wild bootstrap sized jointly for both. It catches whichever of the two has power on a given series. | ## Sign-based: `radf_sign()` `radf_sign()` has full pipeline support. `radf_sign_cv()` computes the time-varying `badf_cv` and `bsadf_cv` boundary and not only the scalar critical values that `summary()` needs (see `vignette("naming-and-analysis")`): ```{r sign} res <- radf_sign(y, minw = 20) cv <- radf_sign_cv(n = 200, minw = 20) summary(res, cv = cv) datestamp(res, cv = cv) ``` ## Kernel-purged: `radf_kp()` `radf_kp()` purges volatility and then calls `radf()` unmodified, so it has full pipeline support in the simplest possible way. It needs no new critical-value code, and `radf_mc_cv()` applies as it is: ```{r kp} res_kp <- radf_kp(y, minw = 20) cv_kp <- radf_mc_cv(n = attr(res_kp, "n"), minw = 20) summary(res_kp, cv = cv_kp) ``` ## WLS + kernel volatility: `radf_sbz()` `radf_sbz()` is a statistic function of its own, separate from the union test. `radf_sbz_cv()` computes the time-varying `badf_cv` and `bsadf_cv` boundary in the same way as `radf_tt_cv()` and `radf_sign_cv()` (see `vignette("naming-and-analysis")`), so it has full pipeline support: ```{r sbz} res_sbz <- radf_sbz(y, minw = 20) cv_sbz <- radf_sbz_cv(y, minw = 20, nboot = 200, seed = 1) summary(res_sbz, cv = cv_sbz) ``` The kernel-volatility weighting that makes `supBZ` robust to heteroskedasticity costs some power relative to the other tests. The default bubble of `sim_psy1()` is mild (30 periods at `rho = 1 + 200^-0.6`, then a collapse). It does not clear the 95% critical value of `supBZ` here, although every other test above rejects on the same series. A stronger bubble that does not collapse (`rho = 1.03` from `t = 120` to the end of the sample, on the same volatility break) does clear it: ```{r sbz-strong} y_strong <- sim_psy1(n = 200, te = 120, tf = 200, c = 0.03, alpha = 0, seed = 1, e = sim_vol_break(199)) res_sbz2 <- radf_sbz(y_strong, minw = 20) cv_sbz2 <- radf_sbz_cv(y_strong, minw = 20, nboot = 200, seed = 1) summary(res_sbz2, cv = cv_sbz2) datestamp(res_sbz2, cv = cv_sbz2) ``` This is the same trade-off that `radf_sbz_union()`, below, hedges against by combining `supBZ` with the classic `supDF`. ## Union-of-rejections: `radf_sbz_union()` ```{r sbz-union} radf_sbz_union(y, nboot = 200, seed = 1) ``` `supDF` is the classic PWY statistic, and `supBZ` is the WLS-weighted version that `radf_sbz()` also returns on its own. `U` is their union. Each statistic has its own bootstrap p-value, so one can flag a series without the other. Here `supDF` rejects, `supBZ` does not, and the union `U` follows `supDF`. The value of `U` is defined with a bootstrap-derived scaling ratio between `supDF` and `supBZ`, and its size guarantee requires that the `supDF` and `supBZ` bootstrap draws come from the same resampled series in each replicate. For both reasons `radf_sbz_union()` cannot be reconstructed by calling `radf_sbz_cv()` and plain `radf_wb_cv()` separately. It stays a single bundled call with its own class and is not a `radf_obj`. ## Which to reach for - If you want exact invariance to any heteroskedasticity pattern with no bootstrap, use `radf_sign()`. Use `radf_sign_dm()` if a level shift, and not only volatility, is a concern. Both have full pipeline support. - If you want to stay closest to plain `radf()` with no new critical-value code, use `radf_kp()`. - If you want the efficiency gain from WLS together with full `datestamp()` and `autoplot()` support, use `radf_sbz()` with `radf_sbz_cv()`. - If you want to hedge between the classic and the WLS-weighted statistics on the same series and do not need `datestamp()` or `autoplot()`, use `radf_sbz_union()`. It is the only function in this group without pipeline support, because `U` bundles both statistics and their scalar joint critical value in one call and does not return a `radf_obj`. - If volatility is the main concern and you prefer a time-deformation approach without a bootstrap, use `radf_tt()` (see the time-transformed test section below). - If the volatility is unknown or complex and a bootstrap is acceptable, `radf_wb_cv()` remains the general-purpose choice.