This report presents a simple mean reversion trading strategy for the SPDR S&P 500 ETF Trust (SPY) using the Relative Strength Index (RSI) as the primary indicator. The strategy buys when RSI indicates oversold conditions and sells when RSI indicates overbought conditions.
# Install required packages if not already installed
if (!require("blotter")) install.packages("blotter")
if (!require("quantmod")) install.packages("quantmod")
if (!require("TTR")) install.packages("TTR")
if (!require("FinancialInstrument")) install.packages("FinancialInstrument")
if (!require("PerformanceAnalytics")) install.packages("PerformanceAnalytics")
if (!require("plotly")) install.packages("plotly")
if (!require("dygraphs")) install.packages("dygraphs")
if (!require("DT")) install.packages("DT")
# Clean up existing environments
if(!exists('.blotter')) .blotter <- new.env()
if(!exists('.strategy')) .strategy <- new.env()
# Strategy name
strat_name <- "SimpleRSI"
# Clean up existing strategy
if(exists(paste0("portfolio.", strat_name), envir = .blotter))
rm(list = paste0("portfolio.", strat_name), envir = .blotter)
if(exists(paste0("account.", strat_name), envir = .blotter))
rm(list = paste0("account.", strat_name), envir = .blotter)
# Define currency and instrument
currency("USD")
## [1] "USD"
## [1] "SPY"
# Get SPY data (using a 10-year period for simplicity)
SPY <- getSymbols("SPY", from="2013-01-01", to="2023-01-01", auto.assign=FALSE)
SPY <- adjustOHLC(SPY)
# Calculate RSI with 14-period lookback
SPY$RSI <- RSI(Cl(SPY), n=14)
# Remove NA values
SPY <- na.omit(SPY)
# Preview the data
head(SPY)
## SPY.Open SPY.High SPY.Low SPY.Close SPY.Volume SPY.Adjusted
## 2013-01-23 123.8579 124.1652 123.6337 124.0572 104596100 120.6523
## 2013-01-24 123.8745 124.6967 123.7582 124.0905 146426400 120.6846
## 2013-01-25 124.4808 124.7881 124.0572 124.7881 147211600 121.3631
## 2013-01-28 124.8213 124.8545 124.1735 124.6386 113357700 121.2177
## 2013-01-29 124.3894 125.2864 124.3064 125.1286 105694400 121.6943
## 2013-01-30 125.1120 125.3612 124.5223 124.6386 137447700 121.2177
## RSI
## 2013-01-23 78.19421
## 2013-01-24 78.35310
## 2013-01-25 81.41503
## 2013-01-28 78.84157
## 2013-01-29 80.96547
## 2013-01-30 73.06679
# Static visualization
par(mfrow=c(2,1), mar=c(1,4,1,2))
plot(Cl(SPY), main="SPY Close Price", xlab="")
plot(SPY$RSI, main="RSI(14)", ylim=c(0,100))
abline(h=30, col="green", lty=2)
abline(h=70, col="red", lty=2)
# Create a histogram of RSI values
hist(SPY$RSI, breaks = 30, main = "RSI Distribution",
xlab = "RSI Value", col = "lightblue", border = "white")
abline(v = 30, col = "green", lwd = 2, lty = 2)
abline(v = 70, col = "red", lwd = 2, lty = 2)
# Initialize portfolio date (one day before first data point)
init_date <- format(index(SPY)[1] - 1, "%Y-%m-%d")
# Initialize portfolio
initPortf(strat_name,
symbols='SPY',
initDate=init_date)
## [1] "SimpleRSI"
# Initialize account with $100,000 starting capital
initAcct(strat_name,
portfolios=strat_name,
initDate=init_date,
initEq=100000)
## [1] "SimpleRSI"
A very simple RSI-based mean reversion strategy: - Buy 100 shares when RSI < 30 (oversold) - Sell all shares when RSI > 70 (overbought) - Only long positions (no shorting)
# Strategy parameters
RSI_oversold <- 30
RSI_overbought <- 70
position_size <- 100 # Fixed position size
# Get dates
dates <- index(SPY)
# Trade tracking
trade_count <- 0
long_trades <- 0
exit_trades <- 0
# Track all trades for visualization
trade_log <- data.frame(
Date = character(),
Type = character(),
Price = numeric(),
Quantity = numeric(),
RSI = numeric(),
stringsAsFactors = FALSE
)
# Run the strategy
for(i in 1:length(dates)) {
current_date <- dates[i]
date_str <- format(current_date, "%Y-%m-%d")
# Current price and RSI
current_price <- as.numeric(Cl(SPY)[i])
current_rsi <- as.numeric(SPY$RSI[i])
# Track progress
if(i %% 500 == 0) {
cat("Processing day", i, "of", length(dates), "Date:", date_str, "\n")
}
# Get current position
current_pos <- try(getPosQty(strat_name, Symbol='SPY', Date=date_str), silent=TRUE)
if(inherits(current_pos, "try-error")) {
current_pos <- 0
}
# Trading rules
if(current_pos == 0 && current_rsi < RSI_oversold) {
# Buy when RSI is oversold
addTxn(strat_name, Symbol='SPY', TxnDate=date_str,
TxnPrice=current_price, TxnQty=position_size, TxnFees=0)
trade_count <- trade_count + 1
long_trades <- long_trades + 1
if(trade_count <= 5) cat("BUY on", date_str, "RSI:", round(current_rsi, 1), "Price:", current_price, "\n")
# Log trade for visualization
trade_log <- rbind(trade_log, data.frame(
Date = date_str,
Type = "BUY",
Price = current_price,
Quantity = position_size,
RSI = current_rsi
))
}
else if(current_pos > 0 && current_rsi > RSI_overbought) {
# Sell when RSI is overbought
addTxn(strat_name, Symbol='SPY', TxnDate=date_str,
TxnPrice=current_price, TxnQty=-current_pos, TxnFees=0)
trade_count <- trade_count + 1
exit_trades <- exit_trades + 1
if(trade_count <= 10) cat("SELL on", date_str, "RSI:", round(current_rsi, 1), "Price:", current_price, "\n")
# Log trade for visualization
trade_log <- rbind(trade_log, data.frame(
Date = date_str,
Type = "SELL",
Price = current_price,
Quantity = -current_pos,
RSI = current_rsi
))
}
# Update portfolio
updatePortf(strat_name, Dates=date_str)
updateAcct(strat_name, Dates=date_str)
updateEndEq(strat_name, Dates=date_str)
}
## [1] "2014-10-15 00:00:00 SPY 100 @ 160.166203289854"
## BUY on 2014-10-15 RSI: 29.8 Price: 160.1662
## [1] "2014-11-18 00:00:00 SPY -100 @ 176.592634599694"
## SELL on 2014-11-18 RSI: 71.9 Price: 176.5926
## Processing day 500 of 2504 Date: 2015-01-15
## [1] "2015-08-21 00:00:00 SPY 100 @ 172.496101270886"
## BUY on 2015-08-21 RSI: 25.6 Price: 172.4961
## [1] "2015-11-03 00:00:00 SPY -100 @ 184.936049391103"
## SELL on 2015-11-03 RSI: 70.2 Price: 184.936
## [1] "2016-01-08 00:00:00 SPY 100 @ 169.214030714825"
## BUY on 2016-01-08 RSI: 29.9 Price: 169.214
## [1] "2016-03-30 00:00:00 SPY -100 @ 182.582742341898"
## SELL on 2016-03-30 RSI: 70.1 Price: 182.5827
## [1] "2016-11-03 00:00:00 SPY 100 @ 186.930491561523"
## [1] "2016-11-25 00:00:00 SPY -100 @ 198.337213964329"
## SELL on 2016-11-25 RSI: 70.9 Price: 198.3372
## Processing day 1000 of 2504 Date: 2017-01-10
## [1] "2018-02-05 00:00:00 SPY 100 @ 242.322885268861"
## [1] "2018-07-25 00:00:00 SPY -100 @ 262.978961270005"
## SELL on 2018-07-25 RSI: 70.3 Price: 262.979
## [1] "2018-10-10 00:00:00 SPY 100 @ 258.85829744271"
## Processing day 1500 of 2504 Date: 2019-01-07
## [1] "2019-02-19 00:00:00 SPY -100 @ 259.948941892753"
## [1] "2019-08-05 00:00:00 SPY 100 @ 268.001076943996"
## [1] "2019-11-07 00:00:00 SPY -100 @ 292.347196676019"
## [1] "2020-02-25 00:00:00 SPY 100 @ 298.045736650847"
## [1] "2020-06-05 00:00:00 SPY -100 @ 306.213336918001"
## Processing day 2000 of 2504 Date: 2020-12-30
## [1] "2022-01-21 00:00:00 SPY 100 @ 430.987957644758"
## [1] "2022-08-12 00:00:00 SPY -100 @ 423.408430427125"
## [1] "2022-09-23 00:00:00 SPY 100 @ 366.268111522976"
## Processing day 2500 of 2504 Date: 2022-12-23
# Close any open positions on the last day
last_date <- tail(dates, 1)
last_date_str <- format(last_date, "%Y-%m-%d")
final_pos <- try(getPosQty(strat_name, Symbol='SPY', Date=last_date_str), silent=TRUE)
if(!inherits(final_pos, "try-error") && final_pos != 0) {
final_price <- as.numeric(Cl(SPY)[length(dates)])
addTxn(strat_name, Symbol='SPY', TxnDate=last_date_str,
TxnPrice=final_price, TxnQty=-final_pos, TxnFees=0)
updatePortf(strat_name, Dates=last_date_str)
updateAcct(strat_name, Dates=last_date_str)
updateEndEq(strat_name, Dates=last_date_str)
cat("Final position closed on", last_date_str, "Qty:", -final_pos, "Price:", final_price, "\n")
# Log final trade
trade_log <- rbind(trade_log, data.frame(
Date = last_date_str,
Type = "FINAL_SELL",
Price = final_price,
Quantity = -final_pos,
RSI = as.numeric(tail(SPY$RSI, 1))
))
}
## [1] "2022-12-30 00:00:00 SPY -100 @ 382.429992675781"
## Final position closed on 2022-12-30 Qty: -100 Price: 382.43
## Total trades executed: 19
## Long entries: 10
## Exits: 9
# Convert trade dates to Date objects for plotting
trade_log$Date <- as.Date(trade_log$Date)
# Create a subset of SPY data for the plot
spy_subset <- data.frame(
Date = index(SPY),
Price = as.numeric(Cl(SPY)),
RSI = as.numeric(SPY$RSI)
)
# Create interactive trade visualization
plot_ly() %>%
add_trace(data = spy_subset, x = ~Date, y = ~Price, type = 'scatter', mode = 'lines',
name = 'SPY Price', line = list(color = 'black')) %>%
add_trace(data = subset(trade_log, Type == "BUY"), x = ~Date, y = ~Price, type = 'scatter',
mode = 'markers', name = 'Buy', marker = list(color = 'green', size = 10, symbol = 'triangle-up')) %>%
add_trace(data = subset(trade_log, Type == "SELL" | Type == "FINAL_SELL"), x = ~Date, y = ~Price,
type = 'scatter', mode = 'markers', name = 'Sell',
marker = list(color = 'red', size = 10, symbol = 'triangle-down')) %>%
layout(title = "SPY Price with Trading Signals",
xaxis = list(title = "Date"),
yaxis = list(title = "Price ($)"),
hovermode = "closest")
# Get all transactions
txns <- getTxns(Portfolio=strat_name, Symbol="SPY")
if(length(txns) > 0) {
print(head(txns, 5)) # First 5 transactions
print(tail(txns, 5)) # Last 5 transactions
}
## Txn.Qty Txn.Price Txn.Fees Txn.Value Txn.Avg.Cost
## 2013-01-22 0 0.0000 0 0.00 0.0000
## 2014-10-15 100 160.1662 0 16016.62 160.1662
## 2014-11-18 -100 176.5926 0 -17659.26 176.5926
## 2015-08-21 100 172.4961 0 17249.61 172.4961
## 2015-11-03 -100 184.9360 0 -18493.60 184.9360
## Net.Txn.Realized.PL
## 2013-01-22 0.000
## 2014-10-15 0.000
## 2014-11-18 1642.643
## 2015-08-21 0.000
## 2015-11-03 1243.995
## Txn.Qty Txn.Price Txn.Fees Txn.Value Txn.Avg.Cost
## 2020-06-05 -100 306.2133 0 -30621.33 306.2133
## 2022-01-21 100 430.9880 0 43098.80 430.9880
## 2022-08-12 -100 423.4084 0 -42340.84 423.4084
## 2022-09-23 100 366.2681 0 36626.81 366.2681
## 2022-12-30 -100 382.4300 0 -38243.00 382.4300
## Net.Txn.Realized.PL
## 2020-06-05 816.7600
## 2022-01-21 0.0000
## 2022-08-12 -757.9527
## 2022-09-23 0.0000
## 2022-12-30 1616.1881
# Calculate returns
strategy_returns <- PortfReturns(Account=strat_name)
if(ncol(strategy_returns) > 1) {
strategy_returns <- strategy_returns[,1]
}
colnames(strategy_returns) <- "Strategy"
# Performance charts
charts.PerformanceSummary(strategy_returns, main="RSI Mean Reversion Performance")
# Performance statistics
stats <- rbind(
"Total Return (%)" = round(Return.cumulative(strategy_returns) * 100, 2),
"Annualized Return (%)" = round(Return.annualized(strategy_returns) * 100, 2),
"Annualized Sharpe" = round(SharpeRatio.annualized(strategy_returns), 2),
"Maximum Drawdown (%)" = round(maxDrawdown(strategy_returns) * 100, 2),
"Volatility (%)" = round(sd(strategy_returns) * sqrt(252) * 100, 2)
)
colnames(stats) <- "Strategy"
print(stats)
## Strategy
## Cumulative Return 2465.87
## Annualized Return 38.60
## Annualized Sharpe Ratio (Rf=0%) 1.22
## Maximum Drawdown (%) 9.08
## Volatility (%) 31.75
# Extract and plot equity curve
equity <- getAccount(strat_name)$summary$End.Eq
plot(equity, main="Equity Curve", ylab="Equity", type="l")
# Calculate drawdowns
dd <- Drawdowns(strategy_returns)
plot(dd, main = "Strategy Drawdowns", col = "red")
## From Trough To Depth Length To Trough Recovery
## 1 2022-03-30 2022-06-16 2022-08-11 -0.0908 93 55 38
## 2 2020-02-26 2020-03-23 2020-06-04 -0.0826 70 19 51
## 3 2018-11-08 2018-12-24 2019-02-19 -0.0414 68 31 37
## 4 2022-02-10 2022-03-08 2022-03-29 -0.0402 33 18 15
## 5 2022-12-01 2022-12-28 2022-12-29 -0.0289 20 19 1
# Calculate buy and hold returns
buy_hold <- SPY[, 6] # Adjusted close
buy_hold_returns <- ROC(buy_hold, n=1, type="discrete")
buy_hold_returns <- buy_hold_returns[index(strategy_returns)]
colnames(buy_hold_returns) <- "Buy.Hold"
# Combine returns
combined_returns <- merge(strategy_returns, buy_hold_returns)
combined_returns <- na.omit(combined_returns)
# Plot comparison
charts.PerformanceSummary(combined_returns, main="Strategy vs. Buy & Hold")
# Calculate comparison metrics
comparison <- rbind(
"Total Return (%)" = round(c(
Return.cumulative(combined_returns[,1]),
Return.cumulative(combined_returns[,2])) * 100, 2),
"Annualized Return (%)" = round(c(
Return.annualized(combined_returns[,1]),
Return.annualized(combined_returns[,2])) * 100, 2),
"Annualized Sharpe" = round(c(
SharpeRatio.annualized(combined_returns[,1]),
SharpeRatio.annualized(combined_returns[,2])), 2),
"Maximum Drawdown (%)" = round(c(
maxDrawdown(combined_returns[,1]),
maxDrawdown(combined_returns[,2])) * 100, 2),
"Volatility (%)" = round(c(
StdDev.annualized(combined_returns[,1]) * 100,
StdDev.annualized(combined_returns[,2]) * 100), 2)
)
colnames(comparison) <- c("Strategy", "Buy & Hold")
print(comparison)
## Strategy Buy & Hold
## Total Return (%) 2465.87 208.27
## Annualized Return (%) 38.64 12.00
## Annualized Sharpe 1.22 0.69
## Maximum Drawdown (%) 9.08 33.72
## Volatility (%) 31.77 17.41
# Interactive cumulative returns comparison
cum_ret <- cumprod(1 + combined_returns)
dygraph(cum_ret, main = "Cumulative Returns: Strategy vs Buy & Hold") %>%
dySeries("Strategy", label = "Strategy", color = "blue") %>%
dySeries("Buy.Hold", label = "Buy & Hold", color = "red") %>%
dyLegend(width = 400) %>%
dyRangeSelector()
# Calculate monthly returns for each approach
monthly_strategy <- apply.monthly(combined_returns[,1], Return.cumulative)
monthly_buyhold <- apply.monthly(combined_returns[,2], Return.cumulative)
monthly_returns <- merge(monthly_strategy, monthly_buyhold)
colnames(monthly_returns) <- c("Strategy", "Buy & Hold")
# Plot monthly returns
barplot(t(monthly_returns * 100),
beside=TRUE,
col=c("blue", "red"),
legend.text=c("Strategy", "Buy & Hold"),
main="Monthly Returns Comparison",
ylab="Return (%)")
This simple RSI-based mean reversion strategy demonstrates how technical indicators can be used to create systematic trading rules. The strategy buys SPY shares when RSI falls below 30 (oversold) and sells all positions when RSI rises above 70 (overbought).
Key findings: