quant_rv 1.0.1: cleanup and stats

We progress. There was a lot of borrowed code in 1.0.0 (nearly 100%) and there are a number of stylistic and code readability issues that warrant a cleanup release, so here it is.

  1. Variable names: type_underscore_descriptor. I’m not a stickler for details everywhere, but I do appreciate well-named variables. My current preference in R is combo names where the front part loosely describes the type of variable and the backend describes how it differs from other of the same type. One-off variables don’t need this breakdown. So here’s how it looks in v1.0.1:
    • “spy_data” from 1.0.0 becomes “data_spy” in 1.0.1. This isn’t too important now (there is but one OHLC “data” var currently) but WILL become important when we add more ETFs to the strategy.
    • “price_data” is a vector of one price series (not OHLC), hence was confusingly named. Now it becomes prices_signal, prices_benchmark, and prices_trade for the three current usages of these price vectors.
    • several other places, some commented in code some not. one obvious candidate (“lookback_period”) we’ll ignore for now because it’s a one-off.
  2. Handling ROC sensibly. ROC (rate of change) calculates a vector of one-day returns (n=1) and will be used more than once in future releases of quant_rv. To avoid repeatedly calculating ROC, we do it one time here, and refer to the roc_ vectors when we calculate strategy/benchmark returns later in the code, and to calculate volatility from roc_signal.
  3. Our volatility signal and its lag. 1.0.0 used (and I commented on) the Open instead of the Closing price for both trading and the volatility signal. This was a wrong move, and the prices_signal is now separate from the prices_trade, and prices_signal is set to use the (Cl)osing price series. This is because we want to use the day’s end prices to develop our signals and to use the next day Open for our trading. Using the Open price series may be a valid approach, but lagging it properly would require some fiddling, and done improperly would introduce future-peeking in the strategy backtest, or “magical thinking” in using the next day Open price to calculate our signal, and then trade at that same Open price. No thanks, not for quant_rv.
  4. New stats. Rather than type “table.AnnualizedReturns(comparison)” into the R Studio console, I’ve added a new “stats_rv” variable that gets the default returns table plus a couple more statistics (max drawdown, average recovery (days to make a new high).
  5. Our benchmark has changed. In the original, the S&P 500 index was used for benchmarking and trading. We fixed the trading part of that by moving to SPY instead, but in 1.0.0 we used the Close for calculating benchmark returns, to better match the original. We really should be using the Adjusted Close (Ad) for our benchmarking, because this includes the very real impact of dividends compounding in the buy-and-hold benchmark account. This results in a significant improvement in the benchmark returns and Sharpe ratio, and sets the bar appropriately higher for our quant_rv strategy to reach for. This will be fun!

Here’s the comparison for returns and Sharpe:

Strategy Old Benchmark New Benchmark
Annualized Return 0.0398 0.0556 0.7530
Annualized Std Dev 0.0922 0.1968 0.1964
Annualized Sharpe (Rf=0%) 0.4315 0.2825 0.3835

Here’s the v1.0.1 code:

### quant_rv v1.0.1 by babbage9010 and friends
# cleanup and stats release
### released under MIT License

# Step 1: Load necessary libraries and data
library(quantmod)
library(PerformanceAnalytics)

#bab: sensible var names, type_description
date_start <- as.Date("2000-01-01")
date_end <- as.Date("2021-12-31")
symbol <- "SPY"  # SPY ETF symbol 

#bab: reorient this so the variable name is in front
data_spy <- getSymbols(symbol, src = "yahoo", from = date_start, to = date_end, auto.assign = FALSE)
#bab: rename these, separate trade from signal from benchmark
prices_benchmark <- Ad(data_spy) #SPY ETF, Adjusted(Ad) for the benchmark
prices_signal <- Cl(data_spy) #SPY ETF, Close(Cl) for the signal (realized vol)
prices_trade <- Op(data_spy) #SPY data, Open(Op) for our trading

# Step 2: Calculate volatility as a risk indicator
lookback_period <- 20
#bab: rename, separate ROC components
roc_signal <-    ROC(prices_signal, n = 1, type = "discrete")
roc_benchmark <- ROC(prices_benchmark, n = 1, type = "discrete")
roc_trade <-     ROC(prices_trade, n = 1, type = "discrete")
#bab: vol formula now easier to read - SD of the daily ROC, annualized
volatility <- runSD(roc_signal, n = lookback_period) * sqrt(252)

# Step 3: Develop the trading strategy
threshold <- 0.15
signal <- ifelse(volatility < threshold, 1, 0)
signal[is.na(signal)] <- 0

# Step 4: Backtest the strategy
returns_strategy <- roc_trade * Lag(signal, 2)
returns_strategy <- na.omit(returns_strategy)

# Calculate benchmark returns
returns_benchmark <- roc_benchmark 
returns_benchmark <- Lag(returns_benchmark, 2)
returns_benchmark <- na.omit(returns_benchmark)

# Step 5: Evaluate performance and risk metrics
#switch the order to switch colors
comparison <- cbind(returns_strategy, returns_benchmark)
colnames(comparison) <- c("Strategy", "Benchmark")
#bab: new line for basic statistics in a table
stats_rv <- rbind(table.AnnualizedReturns(comparison), maxDrawdown(comparison), AverageRecovery(comparison))

charts.PerformanceSummary(comparison, main = "Long/Flat Strategy vs S&P 500 Benchmark")

And, of course a plot showing the new steeper benchmark:

And finally, the new stats_rv table:

                          Strategy  Benchmark
Annualized Return         0.0352000 0.0753000
Annualized Std Dev        0.0841000 0.1964000
Annualized Sharpe (Rf=0%) 0.4182000 0.3835000
Worst Drawdown            0.1841412 0.5518948
Average Recovery          23.4      13.3
 

Comments always welcome.



Leave a comment

Blog at WordPress.com.

Design a site like this with WordPress.com
Get started