1. Introduction

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.

2. Install Required Packages

# 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")

3. Required Libraries

library(blotter)      
library(quantmod)     
library(TTR)          
library(FinancialInstrument)  
library(PerformanceAnalytics) 
library(plotly)       
library(dygraphs)     
library(DT)           

4. Environment Setup

# 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"
stock("SPY", currency="USD", multiplier=1)
## [1] "SPY"

5. Data Acquisition and RSI Calculation

# 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
# Create a data frame for visualization
spy_df <- data.frame(
  Date = index(SPY),
  Close = as.numeric(Cl(SPY)),
  RSI = as.numeric(SPY$RSI)
)

6. Visualize SPY and RSI

# 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)

7. RSI Distribution Analysis

# 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)

8. Initialize Portfolio and Account

# 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"
# Create orders environment
orderbook <- paste0("order_book.", strat_name)
assign(orderbook, list(), envir = .blotter)

9. Strategy Implementation

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
# Trade summary
cat("Total trades executed:", trade_count, "\n")
## Total trades executed: 19
cat("Long entries:", long_trades, "\n")
## Long entries: 10
cat("Exits:", exit_trades, "\n")
## Exits: 9

10. Visualize Trades

# 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")
# Display interactive trade log
datatable(trade_log, 
          options = list(pageLength = 10, 
                         order = list(list(0, 'asc'))),
          caption = "Complete Trade Log")

11. Performance Analysis

# 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")

# Interactive equity curve visualization
dygraph(equity, main = "Strategy Equity Curve") %>%
  dyOptions(fillGraph = TRUE, fillAlpha = 0.1) %>%
  dyAxis("y", label = "Account Value ($)") %>%
  dyRangeSelector()

12. Drawdown Analysis

# Calculate drawdowns
dd <- Drawdowns(strategy_returns)
plot(dd, main = "Strategy Drawdowns", col = "red")

# Table of worst drawdowns
table.Drawdowns(strategy_returns, top = 5)
##         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

13. Compare with Buy and Hold

# 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()

14. Monthly Returns Analysis

# 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 (%)")

# Interactive monthly returns comparison
monthly_df <- data.frame(
  Date = index(monthly_returns),
  Strategy = monthly_returns[,1] * 100,
  BuyHold = monthly_returns[,2] * 100
)

15. Save Envirenment

save.image()

16. Conclusion

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:

  1. The strategy executed trades successfully during the 10-year backtest period
  2. Performance comparison with buy-and-hold shows the strengths and weaknesses of the mean reversion approach
  3. The strategy tends to perform better during sideways or volatile markets and worse during strong trends