Skip to content

This project implements a VolScore trading strategy inspired by a Barclays report on retail-driven volatility surges, using NVDA’s implied vs. sector realized volatility to identify short volatility trade opportunities.

Notifications You must be signed in to change notification settings

prod-ric/volscoreanalysis

Repository files navigation

Introduction

In recent times, retail options trading has surged, and according to a (Barclays Report), the influx of retail traders has significantly affected market volatility, particularly in large-cap tech stocks. Retail traders have increasingly influenced implied volatility levels, creating opportunities for advanced strategies like short volatility trades. One way to take advantage of this is through a VolScore-based strategy, which measures the difference between a stock option's implied volatility and its sector's realized volatility.

In this article, we explore how to use VolScore for trading NVIDIA (NVDA) options by comparing its implied volatility to that of its sector, composed of stocks like Microsoft (MSFT), Apple (AAPL), and AMD. Let’s dive into the strategy and its backtest results. I also reccomend watching this video on youtube, explaining the Barclays report in a simpler way (Link to video)

We will:

  1. Gather implied volatilities and realized volatilities for the selected stocks.
  2. Calculate the sector's realized volatility.
  3. Compare NVDA's implied volatility to the sector's realized volatility to generate a VolScore.
  4. Develop a trading strategy based on VolScore thresholds.
  5. Backtest the strategy and plot the results.

Step 1: Gathering Implied and Realized Volatilities

We have collected implied volatilities and realized volatilities for MSFT, AAPL, and AMD. The data is stored in JSON format.

knitr::opts_chunk$set(warning = FALSE, message = FALSE)

# Load libraries
library(jsonlite)
library(ggplot2)

# Simulated time-series data for implied and realized volatilities from JSON files
msft_data <- fromJSON("MSFT.json")
aapl_data <- fromJSON("AAPL.json")
amd_data <- fromJSON("AMD.json")

# Convert to data frames
msft_df <- as.data.frame(msft_data$data)
aapl_df <- as.data.frame(aapl_data$data)
amd_df <- as.data.frame(amd_data$data)

Step 2: Calculating Sector Realized Volatility

We calculate the sector realized volatility by averaging the realized volatilities of MSFT, AAPL, and AMD.

# Calculate sector-wide realized volatility
# Convert realized volatility columns to numeric (if not already)

msft_df = na.omit(msft_df)
aapl_df = na.omit(aapl_df)
amd_df = na.omit(amd_df)


msft_df$realized_volatility <- as.numeric(msft_df$realized_volatility)
aapl_df$realized_volatility <- as.numeric(aapl_df$realized_volatility)
amd_df$realized_volatility <- as.numeric(amd_df$realized_volatility)

msft_df$implied_volatility <- as.numeric(msft_df$implied_volatility)
aapl_df$implied_volatility <- as.numeric(aapl_df$implied_volatility)
amd_df$implied_volatility <- as.numeric(amd_df$implied_volatility)

msft_df$price <- as.numeric(msft_df$price)
aapl_df$price <- as.numeric(aapl_df$price)
amd_df$price <- as.numeric(amd_df$price)


msft_df$date <- as.Date(msft_df$date)
aapl_df$date <- as.Date(aapl_df$date)
amd_df$date <- as.Date(amd_df$date)


summary(msft_df)
##       date            implied_volatility     price       realized_volatility
##  Min.   :2023-10-10   Min.   :0.1613     Min.   :326.7   Min.   :0.1250     
##  1st Qu.:2024-01-03   1st Qu.:0.1938     1st Qu.:378.0   1st Qu.:0.1705     
##  Median :2024-03-27   Median :0.2165     Median :409.4   Median :0.1892     
##  Mean   :2024-03-27   Mean   :0.2279     Mean   :403.4   Mean   :0.1918     
##  3rd Qu.:2024-06-20   3rd Qu.:0.2636     3rd Qu.:423.0   3rd Qu.:0.2141     
##  Max.   :2024-09-13   Max.   :0.3457     Max.   :467.6   Max.   :0.2713
# Calculate sector-wide realized volatility

sector_realized_vol <- rowMeans(cbind(msft_df$realized_volatility, aapl_df$realized_volatility, amd_df$realized_volatility), na.rm = TRUE)


sector_realized_vol = na.omit(sector_realized_vol)
summary(sector_realized_vol)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.2058  0.2734  0.2956  0.2976  0.3165  0.3936
# Plot the sector realized volatility
plot(msft_df$date, sector_realized_vol, type = "l", col = "blue", main = "Sector Realized Volatility", xlab = "Date", ylab = "Realized Volatility")

Step 3: Calculating VolScore for NVDA

Now, we calculate the VolScore for NVDA, which is the difference between NVDA's implied volatility and the sector's realized volatility, normalized by the sector realized volatility.

# Simulated NVDA data
nvda_data <- fromJSON("NVDA.json")
nvda_df <- as.data.frame(nvda_data$data)
nvda_df = na.omit(nvda_df)
# Convert 'implied_volatility' in nvda_df to numeric
nvda_df$implied_volatility <- as.numeric(nvda_df$implied_volatility)
nvda_df$date = as.Date(nvda_df$date)


# Calculate VolScore
nvda_df$VolScore <- (nvda_df$implied_volatility - sector_realized_vol) / sector_realized_vol
nvda_df$VolScore
##   [1] 0.25911963 0.24504680 0.27318219 0.26107159 0.26278263 0.28118220
##   [7] 0.34886623 0.33593256 0.34127584 0.76256463 0.73199414 0.78765807
##  [13] 0.97564071 1.11525125 1.04042688 1.04853343 0.99657040 1.45401088
##  [19] 1.41490680 1.47648142 1.45487798 1.02317504 1.05205103 0.95054729
##  [25] 1.05352060 1.01963509 1.05566304 1.03568629 1.05141537 0.98461807
##  [31] 0.94412456 0.45162581 0.46271193 0.39720986 0.33369372 0.32078467
##  [37] 0.33954072 0.16542307 0.24318803 0.24705368 0.28063789 0.15534638
##  [43] 0.26861060 0.35771020 0.37837995 0.38427552 0.41062987 0.28644594
##  [49] 0.26531081 0.15942551 0.18901705 0.16151819 0.09542627 0.11707560
##  [55] 0.09871148 0.05473755 0.04107436 0.07646779 0.15489835 0.08594149
##  [61] 0.04904109 0.08313478 0.16956800 0.17067045 0.13844621 0.11056301
##  [67] 0.14665291 0.25650633 0.24357957 0.30900106 0.71759019 0.71563575
##  [73] 0.48911870 0.57594573 0.55837573 0.54210138 0.59169983 0.48337951
##  [79] 0.49112118 0.53264642 0.66650770 0.71113312 0.71211364 0.73905051
##  [85] 0.72278606 0.82880733 0.89589122 0.85089259 0.86759201 0.86038403
##  [91] 0.83601003 0.77394150 0.27638053 0.53914451 0.50288660 0.41789834
##  [97] 0.38233647 0.35148243 0.52365390 0.69726660 0.81596612 0.74512524
## [103] 0.94180590 1.13042874 1.01517746 1.25817015 1.21089441 1.05943739
## [109] 1.16200514 1.07461448 0.68073042 0.60923031 0.54575803 0.69056673
## [115] 0.67187887 0.60074808 0.54595290 0.46385076 0.48148611 0.38653602
## [121] 0.24793204 0.28277498 0.18420868 0.16923096 0.15026325 0.14939445
## [127] 0.17323228 0.23878445 0.30069113 0.29320013 0.31210423 0.34182155
## [133] 0.57186305 1.01464614 0.98194395 1.07731686 1.02464993 1.07135584
## [139] 0.99732048 1.03257675 1.02564633 1.35298677 1.33364921 1.69856380
## [145] 1.68229643 1.70759727 1.67174272 1.43212727 0.97639679 0.89588246
## [151] 0.92118817 1.04542447 1.07495809 0.88227668 0.77332507 0.79431022
## [157] 0.34460093 0.32383871 0.59663490 0.65580694 0.69731607 0.66886025
## [163] 0.54402475 0.51911009 0.51316670 0.60696639 0.50724968 0.38413569
## [169] 0.35789948 0.47990985 0.63499468 0.76233731 0.44426269 0.39413723
## [175] 0.63249499 0.69288195 0.76949863 0.47328771 0.42750643 0.39479708
## [181] 0.33946630 0.31718777 0.29722599 0.26638302 0.27130230 0.22400789
## [187] 0.22240636 0.21488268 0.29816856 0.28236949 0.28213788 0.23192519
## [193] 0.34257781 0.43365531 0.49605384 0.35441602 0.35796749 0.51500839
## [199] 0.65722966 0.67276395 1.10943970 1.26494029 1.09354005 1.45503032
## [205] 1.93834872 1.79599780 1.72349303 1.86578453 1.56181180 1.64304399
## [211] 1.47567185 1.29201088 1.30572533 1.19959602 1.26997706 1.28550226
## [217] 1.44257524 1.14791984 1.37398478 1.32609067 1.42891003 1.33630557
## [223] 1.33786176 0.76328163 0.57354718 0.77472253 1.03262317 1.02113252
## [229] 1.06715379 0.99654573 0.94611287 0.98236017 1.03777682 0.92219172
class(nvda_df$VolScore)
## [1] "numeric"
ggplot(nvda_df) +
  aes(x = date) +
  geom_line(aes(y = VolScore, colour = "Nvda VolScore"), size = 1) +  # Line for Nvda VolScore
  geom_line(aes(y = sector_realized_vol, colour = "Sector Realized"), size = 1) +  # Line for Sector VolScore
  labs(x = "Date", y = "Volscore", title = "Nvda vs Sector Realized") +
  scale_color_manual(values = c("Nvda VolScore" = "#9941AC", "Sector Realized" = "#1C84C6")) +  # Define colors
  theme_minimal()

# Plot NVDA VolScore
plot(nvda_df$date, nvda_df$VolScore, type = "l", col = "red", main = "NVDA VolScore", xlab = "Date", ylab = "VolScore")

Step 4: Developing a Trading Strategy

The trading strategy involves:

  1. Entering a short volatility (straddle) position when the VolScore exceeds a certain threshold (1.2).
  2. Exiting the position when the VolScore falls below a specified threshold (0.7) or after a fixed holding period.
# Initialize position tracking
# Trading parameters
entry_threshold <- 1.2
exit_threshold <- 0.7
holding_period <- 30  # in days

# Initialize position tracking
positions <- list()
cash <- 100000  # Starting cash
position_size <- 10000  # Trade size

entry_price <- 0
entry_date <- NA
pnl <- 0  # To track profit/loss

# Loop through NVDA data to simulate trades
for (i in 1:nrow(nvda_df)) {
  # Entry condition
  if (nvda_df$VolScore[i] > entry_threshold && is.na(entry_date)) {
    entry_date <- nvda_df$date[i]
    entry_price <- nvda_df$implied_volatility[i]
    positions[[length(positions) + 1]] <- list(
      type = "Entry", 
      date = entry_date, 
      price = entry_price, 
      VolScore = nvda_df$VolScore[i],
      pnl = NA  # No P/L for entry
    )
  }
  
  # Exit condition
  if (nvda_df$VolScore[i] < exit_threshold && !is.na(entry_date)) {
    exit_date <- nvda_df$date[i]
    exit_price <- nvda_df$implied_volatility[i]
    pnl <- position_size * (entry_price - exit_price)  # Calculate P/L
    cash <- cash + pnl
    positions[[length(positions) + 1]] <- list(
      type = "Exit", 
      date = exit_date, 
      price = exit_price, 
      VolScore = nvda_df$VolScore[i],
      pnl = pnl  # Include P/L for exit
    )
    
    # Reset entry after exit
    entry_date <- NA
    entry_price <- 0
  }
}

Step 5: Plotting the Final Results

We now plot the entries and exits on the same chart with VolScore and implied volatility to visualize the strategy.

# Ensure the relevant columns are numeric
nvda_df$VolScore <- as.numeric(nvda_df$VolScore)
nvda_df$realized_volatility <- as.numeric(nvda_df$realized_volatility)

positions_df <- do.call(rbind, lapply(positions, function(x) as.data.frame(x, stringsAsFactors = FALSE)))


# Ensure the positions dataframe also has numeric VolScore
positions_df$VolScore <- as.numeric(positions_df$VolScore)




# Plotting VolScore with realized volatility, sector volatility, and vertical lines for entry/exit points
ggplot(nvda_df) +
  aes(x = date) +
  geom_line(aes(y = VolScore, colour = "VolScore"), size = 0.5) +  # Line for VolScore
#  geom_line(aes(y = realized_volatility, colour = "Realized Volatility"), size = 0.5) +  # Line for Realized Volatility
  geom_line(aes(y = sector_realized_vol, colour = "Sector Volatility"), size = 0.5) +  # Line for Sector Volatility
  geom_line(aes(y = implied_volatility, colour = "IV"), size = 0.5) + 
  # Add vertical lines for Entry points
  geom_vline(data = subset(positions_df, type == "Entry"), aes(xintercept = as.numeric(date)), 
              color = "green", size = 1, linetype = "dotted") +
    # Add vertical lines for Exit points
  geom_vline(data = subset(positions_df, type == "Exit"), aes(xintercept = as.numeric(date)), 
             color = "red", size = 1, linetype = "dotted") +
  
  labs(x = "Date", y = "Volscore", title = "Nvda Volscore, Implived Volatility, and Sector Volatility with Entry/Exit Points") +
  scale_color_manual(values = c("VolScore" = "#9941AC", "Realized Volatility" = "#FF5733", "Sector Volatility" = "#1C84C6", "IV" = "black")) +  # Custom colors
  theme_minimal()

positions_df
##    type       date     price  VolScore      pnl
## 1 Entry 2023-11-02 0.5246691 1.4540109       NA
## 2  Exit 2023-11-22 0.3503089 0.4516258 1743.602
## 3 Entry 2024-03-12 0.6294907 1.2581702       NA
## 4  Exit 2024-03-19 0.5004700 0.6807304 1290.207
## 5 Entry 2024-05-02 0.6229188 1.3529868       NA
## 6  Exit 2024-05-23 0.3948357 0.3446009 2280.832
## 7 Entry 2024-07-30 0.7206185 1.2649403       NA
## 8  Exit 2024-08-30 0.4633520 0.5735472 2572.665

Conclusion

This report demonstrates how to calculate a VolScore and develop a simple short-volatility trading strategy based on implied and realized volatilities. The strategy involves entering positions when the VolScore is high and exiting when the VolScore drops or after a set holding period. The backtest results show the P&L for each trade and the overall performance of the strategy.

About

This project implements a VolScore trading strategy inspired by a Barclays report on retail-driven volatility surges, using NVDA’s implied vs. sector realized volatility to identify short volatility trade opportunities.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published