Backtesting Monthly Inverse-Volatility Weights with a 20-Day Window
Summary
The document shows how to backtest a portfolio of ETFs using inverse-volatility weights estimated from recent returns. A target-weight function obtains closing prices for the preceding 20 trading days, computes returns, estimates each asset’s volatility, and assigns weights inversely proportional to those estimates before normalizing them. It also shows an alternative using a portfolio optimization package to produce inverse-volatility weights.
The example uses a backtesting function configured to rebalance at each month end, with a burn-in period so the rolling window is available before trading begins. It demonstrates how to inspect portfolio performance, transactions, and the resulting equity series. The reported example uses SPY, EEM, and IUSB, substituting IUSB because the author could not retrieve the requested bond ticker. The response is a sketch tied to a particular R package and data format; it does not compare results against other strategies or discuss transaction costs and other implementation assumptions.
Key ideas
- Estimate inverse-volatility weights from returns over a rolling 20-trading-day window.
- Normalize reciprocal standard deviations so the weights sum to one.
- Use a month-end rebalance schedule to refresh portfolio allocations.
- Allow an initial burn-in period before the rolling volatility window is available.
- Review performance statistics and trade records to inspect the backtest.
Tags
Full text
# Backtest with rolling volatility in R
# Backtest with rolling volatility in R
So I'm very new in R. I want to backtest a strategy for 3 stocks SPY, EEM, AGG.
With library(RiskPortfolios), I can calculate
```
optimalPortfolio(Sigma = Sigma,
control = list(type = 'invvol',constraint = 'lo'))
```
which gives me the inverse volatility for the whole time period that the stock prices are available.
What if I want calculate inv vol weights based on volatility of the past 20 trading days?
I also want to rebalance the portfolio monthly with the new weights that are based on volatility of the past 20 trading days instead of keeping the same weighting for the entire time. I heard I can do it with package (PerformanceAnalytics) but I do not know how.
## Answer by Enrico Schumann (score 2, accepted)
https://quant.stackexchange.com/a/39765
Here is a sketch how such a backtest can be done with the `btest` function, which is in the PMwR package. The package is not on CRAN, but is available from https://github.com/enricoschumann/PMwR. (Disclosure: I am the package author.)
The main input for `btest` is a function that computes the target portfolio, either as an actual position or as weights. In your case, it may look as follows:
```
inv_vol <- function() {
## get prices for last 20 days
## and compute returns
R <- returns(Close(n = 20))
optimalPortfolio(Sigma = cov(R),
control = list(type = 'invvol',
constraint = 'lo'))
}
```
I should warn here that I have never used the `RiskPortfolios` package. If you really only want portfolio weights proportional to inverse vol, you may write the function as follows:
```
inv_vol2 <- function() {
## get prices for last 20 days
## and compute returns
R <- returns(Close(n = 20))
w <- 1/apply(R, 2, sd)
w/sum(w)
}
```
Here would be the complete example. I first prepare some data. I could not get price data for your ticker `AAG` from Yahoo, so I replaced it. Just plug in your prices: they should be stored as a zoo series `prices` and not have missing values.
```
library("PMwR")
library("RiskPortfolios")
library("tseries")
library("zoo")
start <- as.Date("2017-1-1")
ticker <- c("SPY", "EEM", "IUSB")
temp <- list()
for (t in ticker)
temp[[t]] <- get.hist.quote(t, start = start,
quote = "AdjClose")
prices <- do.call(merge, temp)
colnames(prices) <- ticker
head(prices)
## SPY EEM IUSB
## 2017-01-03 220.0632 34.74012 48.47526
## 2017-01-04 221.3724 35.00487 48.50420
## 2017-01-05 221.1965 35.38727 48.60073
## 2017-01-06 221.9879 35.24019 48.52351
## 2017-01-09 221.2552 35.21078 48.56213
## 2017-01-10 221.2552 35.41669 48.55247
```
The backtest is run by passing the data and the `inv_vol` function to `btest`.
```
res <- btest(list(coredata(prices)),
signal = inv_vol,
do.rebalance = "lastofmonth",
b = 20, ## burnin
convert.weights = TRUE,
initial.cash = 100,
include.data = TRUE,
timestamp = index(prices))
```
Access the results:
```
## summary stats
summary(as.NAVseries(res))
## ---------------------------------------------------------
## 31 Jan 2017 ==> 11 May 2018 (323 data points, 0 NAs)
## 100 108.488
## ---------------------------------------------------------
## High 111.86 (26 Jan 2018)
## Low 99.35 (09 Mar 2017)
## ---------------------------------------------------------
## Return (%) 6.6 (annualised)
## ---------------------------------------------------------
## Max. drawdown (%) 4.2
## _ peak 111.86 (26 Jan 2018)
## _ trough 107.11 (08 Feb 2018)
## _ underwater now (%) 3.0
## ---------------------------------------------------------
## Volatility (%) 3.5 (annualised)
## _ upside 3.2
## _ downside 2.1
## ---------------------------------------------------------
##
## Monthly returns ▂▁▂▃█▅▃
##
## Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec YTD
## 2017 0.0 0.8 1.0 1.1 0.3 1.6 0.9 -0.1 1.2 0.6 0.9 8.5
## 2018 2.1 -2.2 0.4 -0.9 0.6 -0.1
## trades
journal(res)
## instrument timestamp amount price
## 1 SPY 2017-02-28 0.158031858 231.03508
## 2 EEM 2017-02-28 0.464671930 37.25028
## 3 IUSB 2017-02-28 0.938666713 48.90283
## [....]
##
## 48 transactions
## raw equity series (zoo series)
as.zoo(as.NAVseries(res))
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.