This shows how to calculate gamma exposure for stock options, specifically in this example the SPX (S&P 500). With ever increasing levels of options trading activity, it is useful to understand the dynamics of options gamma, given the impact on equities as a result of market maker hedging activities.

We look at total put and call gamma at each strike (chart 1), total gamma at each strike (aggregate put & call - chart 2).

Gamma is then calculated over a range of spot prices as an estimate of what gamma looks like at each level as prices change (chart 3).

Credit goes to Perfiliev Financial Training.Their blog details how to calculate gamma exposure in Python. I’ve taken that and translated to R, as R is my preferred language and I was unable to find any R implementations.

Getting Started

First, we need the options data, which is available from the CBOE. click on Options tab -> set Options Range == All -> set Expiration == All -> click View Chain. To download the entire options chain, scroll all the way down, and click on Download CSV.

Now that we have that, we can get started.

The libraries I’ve used:

library(data.table)
library(lubridate)
library(timeDate)
library(stringr)
library(formattable)
library(dplyr)
library(plotly)
library(tidyr)
library(purrr)


Here the file is being read, we are then looking at the specific line that includes the spot price, along with other information. We split that string up and extract the spot price out of it.

The current spot price is around 4,000. Because the file includes all strike prices, from 100 up to 10,000, some of this is filtered out to show more relevant information and for graphs to be more visually appealing. We set the range of strike prices to be displayed (which is used in first 2 of 3 charts) to be +/- 20% from spot. This gives around 800 points above and below spot, which is more than enough.

#import option chain file downloaded from CBOE
option_chain <- fread ("spx_quotedata.csv")

# Get SPX Spot
spotLine <- fread("spx_quotedata.csv", skip=1, nrows = 1)
spotLineData <- strsplit(as.character(colnames(spotLine[,2])), ":")
spotPrice <- as.numeric(str_trim(spotLineData[[1]][2]))

#specify range of strike prices to view gamma for
fromStrike = 0.8 * spotPrice
toStrike = 1.2 * spotPrice


Extracting the current date in a similar approach to getting the spot price.

The date is in the format ‘Thur 28 July 2022’. Each of the steps extracts the day, month, year, with the final step changing the month from ‘July’ to the numerical equivalent ‘07’. This helps with subsequent date processing.

# Get Today's Date
dateLine <-  fread("spx_quotedata.csv", skip=2, nrows = 1)
todayDateData <- strsplit(as.character(colnames(dateLine[,1])), " ")
todayYear <- todayDateData[[1]][4]
todaymonth <- todayDateData[[1]][2]
todayDay <- todayDateData[[1]][3]
todayDate <- as.Date(lubridate::ymd(paste0(todayYear, '-', todaymonth, '-', todayDay)))


Column names are updated to explicitly state whether the columns relate to puts or calls. In the raw file most names are ambiguous.

The expiration date is also in the same format as ‘Thur 28 July 2022’. All days are abbreviated to 4 characters, so it is easy to drop those and the subsequent white space and then convert to Y-mm-dd

# Make col names less ambiguous
colnames(option_chain) <- c('ExpirationDate','Calls','CallLastSale','CallNet','CallBid','CallAsk','CallVol',
                            'CallIV','CallDelta','CallGamma','CallOpenInt','StrikePrice','Puts','PutLastSale',
                            'PutNet','PutBid','PutAsk','PutVol','PutIV','PutDelta','PutGamma','PutOpenInt')

# change date format of expiration date
option_chain$ExpirationDate <- as.Date(lubridate::mdy(substring(option_chain$ExpirationDate,5)))


The below function determines whether the expiry date is the third Friday of the month. The SPX options offers 2 different types of expirations; either weekly or monthly. The monthly expirations expire on the third Friday of the month.

We are interested in what the gamma exposure looks like currently, but also after the next weekly and monthly expirations. There could be significant gamma rolling off that could impact the market in either of these expirations.

There are 2 approaches that could be used here; looking at the expiration dates and determining if it is a 3rd Friday, which I have done, or by looking at the contract name pre-fix. Weekly contracts have “SPXW” before a series of digits and monthly have “SPX” before the digits. Splitting the string is easier and would give a line or 2 less code thank what I have done.

#determines if the date passed is the 3rd Friday of month
isThirdFriday <- function(expDate){
  #passing date directly to timeNthNdayInMonth in the last step gives unreliable results.
  #day of month appears to need to be the 1st if giving a single date and not a range of dates
  #truncate expiration date day and add 01 in its place
  
  ym <- str_sub(expDate, end=-4)
  ym01 <- ymd(paste0(ym,'-01'))
  ThirdFriday <- as.Date(timeNthNdayInMonth(ym01, nday = 5, nth = 3))
  
  ifelse(ThirdFriday == expDate, TRUE, FALSE)
}

Calculating Spot Gamma Exposure

For charts 1 and 2 we need to calculate spot gamma exposure, in terms of 1% move of spot price:

Option’s Gamma * Contract Size * Open Interest * Spot Price ^ 2 * 0.01

Call gamma is positive, negative for put.

The CBOE data has individual option gamma included, so arriving at gamma exposure is simple.

# spot gamma exposure calls, puts & total 
option_chain$'CallGEX' <- option_chain$'CallGamma' * option_chain$'CallOpenInt'  * 100 * spotPrice^2 * 0.01
option_chain$'PutGEX' <- option_chain$'PutGamma' * option_chain$'PutOpenInt' * 100 * spotPrice^2* 0.01 * -1
option_chain$'TotalGamma' <- (option_chain$CallGEX + option_chain$PutGEX) / 10^9

# create data frame for plotting charts 1 & 2 showing call, put and total gamma exposure
# over a range of strike prices +/- 20% of spot. 
dfAgg <- option_chain %>% 
  filter(StrikePrice > fromStrike, StrikePrice < toStrike) %>%
  group_by(StrikePrice) %>% 
  summarise(CallGEX = sum(CallGEX),
            PutGEX = sum(PutGEX),
            TotalGamma = sum(TotalGamma)
  )

#defining variables for plotting
strikes <- dfAgg$StrikePrice
TotalGamma <- round(sum(dfAgg$TotalGamma),3) 

Charting Spot Gamma Exposure

Chart 1 is the spot gamma exposure, showing total gamma over the specified strikes, with the current spot price overlaid for reference.

chart1 <- plot_ly(data = dfAgg, x = ~StrikePrice, y = ~TotalGamma, type = 'bar', name = 'Total Gamma')
chart1 <- chart1 %>% add_lines(x=spotPrice,
                               line=list(color= rgb(3, 74, 23, maxColorValue = 255), dash = 'dot'),
                               name= paste('Spot Price: ', round(spotPrice,0))
)
chart1 <- chart1 %>%layout(
  title = paste("Total Gamma: $", TotalGamma,  " Bn per 1% SPX Move ",  todayDate),
  xaxis=list(title='Strike Price'),
  yaxis=list(title='Spot Gamma Exposure ($ billions/1% move)')
)
chart1
350040004500−101234
Total GammaSpot Price: 4072Total Gamma: $ 16.296 Bn per 1% SPX Move 2022-07-28Strike PriceSpot Gamma Exposure ($ billions/1% move)


This graph shows the total gamma in billions per 1% of move. The significance of this chart is that there is a relatively large amount of call gamma (positive) at 4,100, just 28 points from spot. This is significant because call walls (large call gamma, credit to SpotGamma for coining this phrase) tend to act as resistance, with reduced volatility about the wall, which was the case in the days prior to the 29th July.

The assumption is that the majority of calls are from investors selling the calls short (covered calls) and the market maker is long calls and short the underlying stock. As price in the underlying increases, the delta needs to be hedged and marker makers will sell. The result is generally price of the underlying hovering around the call wall during this hedging, suppressing volatility or fall back to prior support.

Chart 2 breaks down the total gamma into calls and puts.

chart2 <- plot_ly(data = dfAgg, x = ~StrikePrice, y = ~CallGEX, type = 'bar', name = 'Call Gamma')
chart2 <- chart2 %>% add_bars(
  x = ~StrikePrice,
  y = ~PutGEX,
  marker = list(color = 'red'),
  name = 'Put Gamma'
)
chart2 <- chart2 %>% add_lines(x=spotPrice,
                               line=list(color= rgb(3, 74, 23, maxColorValue = 255), dash = 'dot'),
                               name= paste('Spot Price: ', round(spotPrice,0))
)
chart2 <- chart2 %>%layout(
  title = paste("Total Gamma: $", TotalGamma,  " Bn per 1% SPX Move ",  todayDate),
  xaxis=list(title='Strike Price'),
  yaxis=list(title='Spot Gamma Exposure ($ billions/1% move)')
)
chart2
350040004500−4B−2B02B4B6B
Call GammaPut GammaSpot Price: 4072Total Gamma: $ 16.296 Bn per 1% SPX Move 2022-07-28Strike PriceSpot Gamma Exposure ($ billions/1% move)

Calculating Gamma Exposure Profile


Next we look at the gamma exposure profile, which is an estimate of gamma over a range of spot prices. We are trying to determine what gamma may look like if price in the underlying changes. It is an estimate only because we are trying to assume what the gamma exposure will look like as price changes based on the information that we have have available now. Price will change with the passage of time, so too volatility, days to expiry, open interest and possibly even the risk free rate and yield, which will all affect the gamma.

The first step in calculating the gamma exposure profile is the below function, which calculates option gamma.It takes arguments that feed into the black-scholes formulas for d1 and then option gamma, accounting for change to spot price (for gamma, the formula is the same for both put and calls).

calcGammaEx <- Vectorize(function(S, K, vol, T, r, q, OI){
  
  d1 = (log(S/K) + T*(r - q + ((vol^2)/2))) / (vol*sqrt(T))
  gamma = exp(-q*T) * dnorm(d1) / (S * vol * sqrt(T))
  
  ifelse (T == 0 || vol == 0, result <- 0, result <-  OI * 100 * S^2* 0.01 * gamma )
  
  return(result)
}
)


Some further steps include:

#range of prices to calculate curve over
levels <- seq(from = fromStrike, to = toStrike, length.out =60)

# for use in the subsequent step, determining number of days (business) until expiry
# For 0 days to expiry options, set DTE = 1 day, otherwise they get excluded
# throws error if negative days, though shouldn't be any expired contracts in the current options chain
Nweekdays <- Vectorize(function(datesToCheck)
  ifelse(datesToCheck == todayDate, 1/252,sum(!weekdays(seq(todayDate, datesToCheck, "days")) %in% c("Saturday", "Sunday"))/252)
)

option_chain$daysTillExp <- Nweekdays(option_chain$ExpirationDate)
nextExpiry <- min(option_chain$ExpirationDate)

option_chain$IsThirdFriday <- map(option_chain$ExpirationDate, isThirdFriday)
thirdFridays <- which(option_chain$IsThirdFriday == TRUE)
nextMonthlyExp <- option_chain$ExpirationDate[min(thirdFridays)]


Now we can compute the gamma for each 60 spot prices.

First create 3 lists, for capturing the total gamma, total gamma excluding next expiry and excluding next Friday expiry. As we loop through each of the 60 spot prices we append to these lists which are then later used for the graph.

Next we actually run through the loop. For each spot price the gamma exposure is calculated using the previously defined calcGammaEx function.

totalGamma <- c()
totalGammaExNext <- c()
totalGammaExFri <- c()

# For each spot level, calc gamma exposure at that point
for (level in levels) {
  option_chain$callGammaEx <- calcGammaEx(level,option_chain$StrikePrice, option_chain$CallIV, option_chain$daysTillExp,0,0,option_chain$CallOpenInt)
  
  option_chain$putGammaEx <- calcGammaEx(level,option_chain$StrikePrice, option_chain$PutIV, option_chain$daysTillExp,0,0,option_chain$PutOpenInt)
  
  totalGamma <- append(totalGamma,(sum(option_chain$callGammaEx) - sum(option_chain$putGammaEx)))
  
  exNxt <- which(option_chain$ExpirationDate != nextExpiry)
  dfExNext <- option_chain[exNxt,]
  totalGammaExNext <- append(totalGammaExNext, sum(dfExNext$callGammaEx) - sum(dfExNext$putGammaEx)) 
  
  exFri = which(option_chain$ExpirationDate != nextMonthlyExp)
  dfExNextFri <- option_chain[exFri,]
  totalGammaExFri <- append(totalGammaExFri, sum(dfExNextFri$callGammaEx) - sum(dfExNextFri$putGammaEx))
}

totalGamma <- totalGamma / 10^9
totalGammaExNext <- totalGammaExNext / 10^9
totalGammaExFri <- totalGammaExFri / 10^9
dfTotalGamma <- data.frame(levels,totalGamma,totalGammaExNext,totalGammaExFri)


Next we want to know at what spot price does gamma exposure flip from positive to negative and define the ranges for positive and negative gamma exposure. Volatility is higher in negative gamma and lower in positive.

# Find Gamma Flip Point
zeroCrossIdx <- which(diff(sign(dfTotalGamma$totalGamma)) != 0)

negGamma <- dfTotalGamma$totalGamma[zeroCrossIdx]
posGamma <- dfTotalGamma$totalGamma[zeroCrossIdx+1]
negStrike = dfTotalGamma$levels[zeroCrossIdx]
posStrike = dfTotalGamma$levels[zeroCrossIdx+1]

zeroGamma = posStrike - ((posStrike - negStrike) * posGamma/(posGamma-negGamma))

Plotting Gamma Exposure


Now we plot

# Chart 3: Gamma Exposure Profile
chart3 <- plot_ly(data = dfTotalGamma,
               x = ~levels, 
               y = ~totalGamma,
               type = "scatter",
               mode = "lines",
               name = "All Expiries",
               line=list(color = "blue")
)
chart3 <- chart3 %>% add_trace(x = ~levels,
                         y = ~totalGammaExFri,
                         line =list(color="cornflowerblue"),
                         name = "Ex-Next Monthly Expiry"
)
chart3 <- chart3 %>% add_trace(x = ~levels,
                         y = ~totalGammaExNext,
                         line =list(color = rgb(242, 66, 245, maxColorValue = 255)),
                         name = "Ex-Next Expiry"
)
chart3 <- chart3 %>% add_trace(x=spotPrice,
                         line=list(color= rgb(3, 74, 23, maxColorValue = 255), dash = 'dot'),
                         name= paste('Spot Price: ', round(spotPrice,0))
)
chart3 <- chart3 %>% add_trace(x=zeroGamma,
                         line=list(color= rgb(247, 40, 202,maxColorValue = 255), dash = 'dot'),
                         name= paste('Gamma Flip: ', round(zeroGamma,0))
)
chart3 <- chart3 %>%layout(
  title = paste("Gamma Exposure Profile, SPX",  todayDate),
  xaxis=list(title='Strike Price'),
  yaxis=list(title='Gamma Exposure ($ billions/1% move)'),
  shapes = list(
    list(type = "rect",
         fillcolor = rgb(247, 178, 178, maxColorValue = 255), line = list(color = rgb(247, 178, 178, maxColorValue = 255)), opacity = 0.3,
         x0 = min(dfTotalGamma$levels), x1 = zeroGamma, xref = "x",
         y0 = min(dfTotalGamma$totalGamma), y1 = max(dfTotalGamma$totalGamma), yref = "y"),
    list(type = "rect",
         fillcolor = rgb(178, 247, 183, maxColorValue = 255), line = list(color = rgb(178, 247, 183, maxColorValue = 255)), opacity = 0.3,
         x0 = zeroGamma, x1 = max(dfTotalGamma$levels), xref = "x",
         y0 = min(dfTotalGamma$totalGamma), y1 = max(dfTotalGamma$totalGamma), yref = "y")
  )
)
chart3
350040004500−40−30−20−1001020
All ExpiriesEx-Next Monthly ExpiryEx-Next ExpirySpot Price: 4072Gamma Flip: 4028Gamma Exposure Profile, SPX 2022-07-28Strike PriceGamma Exposure ($ billions/1% move)


Only 2 of the 3 plots are visible. ‘All Expiry’ and ‘Ex-Next Expiry’ overlap, with Ex-Next Expiry in the foreground as it is drawn last. The reason for this is that the next expiry date is 28 July, which is the date of the option chain data, so the gamma that is due to expire has done so in the data.

#next monthly expiry, previously defined as:
#nextExpiry <- min(option_chain$ExpirationDate)
paste("The next expiry is: ", nextExpiry)
## [1] "The next expiry is:  2022-07-28"
#date of current option chain, defined in initial step
paste("The date of the current option chain is: ", todayDate)
## [1] "The date of the current option chain is:  2022-07-28"