Backtesting basics in R
TLDR: Backtest trading strategies in R by downloading historical data, creating indicators, generating trading signals, and analyzing results - packages: quantmod
and PerformanceAnalytics
.
Backtesting
Backtesting is the process of evaluating a trading strategy using historical data to see how it would have performed and allows us to make data-driven decisions before risking real money.
Workflow
Get past price data.
Calculate technical indicators from this data.
Apply these indicators to the price series.
Test the strategy’s performance using the historical data.
Download and plot price data
The following code takes in a ticker, downloads historical data and plots the high, low and close prices along with the volume.
library(quantmod)
library(PerformanceAnalytics)
download_plot_data <- function(symbol, start_date = "2020-01-01", end_date = "2023-01-01") {
data <- getSymbols(symbol, from = start_date, to = end_date, auto.assign = FALSE)
barChart(data, name = symbol, bar.type = "hlc", up.col = "green", dn.col = "red", theme = "white")
return(data)
}
prices <- download_plot_data("NVDA")
Create Indicators
Let’s calculate a simple moving average (SMA) as our technical indicator. The quantmod
package provides the SMA()
function to calculate the moving average, along with a wide variety of other functions you can combine to create your own strategies.
calculate_sma <- function(symbol, price_data, window = 20) {
sma_values <- SMA(Cl(price_data), n = window)
sma_values[is.na(sma_values)] <- 0
barChart(price_data, name = symbol, bar.type = "hlc", theme = "white")
plot(addTA(sma_values, col = 'green', legend = 'SMA'))
return(sma_values)
}
interval = 10
SMA(prices$NVDA.Close, n = interval)
Add the indicators to chart
Now, lets add the technical indicators to the chart.
ticker = "NVDA"
barChart(prices, name = ticker, bar.type = "hlc", theme = "white")
addTA(indicators, col = 'green', legend = 'Simple Moving Average')
Create functions
This function generates trading signals by returning 1 when the closing price is above the indicator and -1 otherwise.
signals <- function(prices, indicators) {
signals <- ifelse(Cl(prices) > indicators, 1, -1)
return(signals)
}
This function calculates strategy returns by multiplying the lagged trading signals by the daily percentage change in closing price, replaces any missing values with zero, and labels the result with the ticker name.
get_returns <- function(prices, indicators, signals, ticker) {
returns <- Lag(signals) * (Cl(prices) / Lag(Cl(prices)) - 1)
returns[is.na(returns)] <- 0
names(returns)[1] <- ticker
return(returns)
}
This function performs a backtest by displaying the top 5 drawdowns, downside risk metrics, and a performance summary chart for the given returns.
backtest <- function(prices, returns, signals) {
table.Drawdowns(returns, top=5)
table.DownsideRisk(returns)
charts.PerformanceSummary(returns)
}
Backtest
Lets put everything together and run the strategy using the functions we defined previously.
ticker <- "NVDA"
from <- "2020-01-01"
to <- "2023-01-01"
prices <- download_plot_data(ticker, from, to)
indicators <- calculate_sma(ticker, prices, 30)
signals_data <- signals(prices,indicators)
returns_data <- get_returns(prices, indicators, signals_data, ticker)
backtest(prices, returns_data, signals_data)
Interpreting the results
Analysing the first cumulative returns output:
The strategy started with a brief positive return in early 2020 but quickly entered a significant drawdown.
Most of the period shows negative cumulative returns, with losses as deep as -60% at the worst point.
There is some partial recovery towards the end of the period, but the cumulative return remains negative overall.
Analysing the middle daily returns output:
Daily returns are mostly clustered around zero which indicates that the strategy does not take large positions or generate highly volatile results.
There are some spikes both positive and negative, but no obvious consistent pattern of positive returns.
Analysing the last drawdowns output:
Drawdown reached extreme depths (over -60%) which means that the strategy lost a large portion of its value from its highest point.
Prolonged periods of heavy drawdown, with little to no quick recovery, imply that risk was not managed effectively - essentially we had no risk management!
Conclusions
The strategy significantly underperformed compared to holding NVDA and this strategy subjected the portfolio to high drawdowns. We could test the strategy on all companies in the S&P500.