Option Heatmap and OI Strikes

so im really interested in how to get put/call options strike oi to show up like volume profile and the lower heat map similar to this example from @melarue on twitter. I have no idea how to even get started on something like this. Any hints or expert coders out there willing to reverse engineer this?

Ideally, the heat map portion could be shown on the upper chart as well, able to be mapped to strikes on today and future expirations as a dynamic color grid for daily, weekly, monthly options. Options codes should auto-generate.

this could be tweaked to show gamma levels as well -- lots of potential.

anyone up to the challenge? :unsure:

FOlIJqyVEAMJS7T
 

ziongotoptions

Active member
@ziongotoptions Thanks for your effort of trying to help. Not sure if it's my computer does not have enough memory resource, it keeps spinning awfully long time. Unfortunately, I wont be able to use this indicator until I get a new computer, (I am a big fan of Volume Profile TPO :).
You’re welcome it’s possible, I usually wait 2 minutes and then I go to edit studies, I just click the apply button and that usually helps it load faster also might be better to try it with 1 of the studies not both that I sent
 
Last edited:

sudoshu

Member
Been lurking for about a week here, very interested in this idea. New to thinkscript but my day job is software engineer. I'd love to collab on getting something like this to work with gamma exposure levels if possible. (Yes I've gone down the rabbit hole already) Anyways I've been playing with folds and modified the code to utilize this function. I also formatted the concat strings a bit to make it easier to read, its' quite hard to follow when everything is on one line.

Note: This is not the complete script, just an example of using the auto selectable strike logic and folds to get a tally instead of manually finding each level.

Code:
# Depth of strikes to scan
# Todo: this only from ATM options to x levels ITM.
input StrikeDepth = 5;

# Call Open Interest
plot CallOpenInterest =
    fold CallOIIndex = 0 to StrikeDepth
    with cOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(centerStrike - strikeSpacing * CallOIIndex ))
            )
        )
    )
    then 0
    else cOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(centerStrike - strikeSpacing * CallOIIndex ))
        )
    )
;
CallOpenInterest.SetHiding(!ShowOI);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Calls"));
AddLabel(ShowOI, "CallOI: " + CallOpenInterest, GlobalColor("Calls"));
 

sudoshu

Member
Okay, uploading what I have so far. I heavily commented everything and renamed a lot of things, trying my best to understand it all and how thinkscript works in general. It would be 100x easier to do this if we could just dynamically assign a string to a variable. That whole 'series' thing had me stumped for a bit but I found a snippet from Mobius which helps to understand:

"series is the expiry starting at 1 and raising by one for each next expiry"

Using that logic, I setup a dropdown switch to select that instead of a number.
Note this is only setup for open interest right now, the DataType switch does nothing yet.
Change all the open_interest() function calls to volume() for option volume instead.


Code:
#hint: Options Hacker \n This study lets you scan the options chain for a series and given depth. \n<b>Warning: Setting the StrikeDepth to large values requires significant processing power, and will result in slow loading times.</b>


#-----------------------------------------------------------------------------------------------------------------#
# Settings

# Colors
DefineGlobalColor("Call", Color.GREEN);
DefineGlobalColor("Put", Color.RED);
DefineGlobalColor("CallCloud",Color.DARK_GREEN);
DefineGlobalColor("PutCloud",Color.DARK_RED);

#hint Mode: The mode to select an option symbol. \n AUTO will use the option series input code. This will not always work, set to MANUAL to use the center strike and spacing inputs to manually determine the option symbol.
input Mode = {default AUTO, MANUAL};

#hint Series: The option expiration series to search. \n This value is used to determine the option symbol, and for the input to the SeriesVolatility(series = x) function where series is the expiry starting at 1 and raising by one for each next expiry.
input Series = {
    default Weekly,
    Month1,
    Month2,
    Month3,
    Month4,
    Month5,
    Month6,
    Month7,
    Month8,
    Month9
};

#hint DataType: The type of option data to show.
input DataType = {default OPEN_INTEREST, VOLUME};

#hint StrikeDepth: The level of depth to search a series. The search starts at the center strike price, and goes both ITM and OTM.
input StrikeDepth = 5;

#hint CenterStrikeOffset: The offset to use when calculating the center strike based on close price. \n Examples: \n   1 = nearest $1 interval \n   10 = nearest $10 interval.
input CenterStrikeOffset = 1.0;

#hint MaxStrikeSpacing: The maximum dollar amount between two adjacent contracts.
input MaxStrikeSpacing = 25;

#hint ManualCenterStrike: The starting price to use when in MANUAL mode.
input ManualCenterStrike = 440;

#hint ManualStrikeSpacing: The dollar amount between two adjacent contracts to use when in MANUAL mode.
input ManualStrikeSpacing = 1.0;

#hint ShowStrikeInfo: Show the strike info labels.
input ShowStrikeInfo = yes;

#hint ShowLabels: Show the open interest labels.
input ShowLabels = yes;

#hint ShowClouds: Show the open interest clouds.
input ShowClouds = no;

#hint ShowLines: Show the open interest lines.
input ShowLines = yes;

#hint ShowGreeks: Show the estimated Greek calculation labels for the latest bar.
input ShowGreeks = yes;


#-----------------------------------------------------------------------------------------------------------------#
# Date, Symbol, and Strike

# OptionSeries is the expiry starting at 1 and raising by one for each next expiry.
def OptionSeries;
switch (Series) {
    case Weekly:
        OptionSeries = 1;
    case Month1:
        OptionSeries = 2;
    case Month2:
        OptionSeries = 3;
    case Month3:
        OptionSeries = 4;
    case Month4:
        OptionSeries = 5;
    case Month5:
        OptionSeries = 6;
    case Month6:
        OptionSeries = 7;
    case Month7:
        OptionSeries = 8;
    case Month8:
        OptionSeries = 9;
    case Month9:
        OptionSeries = 10;
};

# Open price at Regular Trading Hours
def RTHopen = open(period = AggregationPeriod.DAY);

# Current year, month, and day
def CurrentYear = GetYear();
def CurrentMonth = GetMonth();
def CurrentDayOfMonth = GetDayOfMonth(GetYYYYMMDD());

# Get the first day of this month. 1 (Monday) to 7 (Sunday)
def FirstDayThisMonth = GetDayOfWeek((CurrentYear * 10000) + (CurrentMonth * 100) + 1);

# Get the first friday of this month
def FirstFridayDOM =
    if FirstDayThisMonth < 6 then 6 - FirstDayThisMonth
    else if FirstDayThisMonth == 6 then 7
    else 6
;

# Get the second, third, and fourth fridays of this month
def SecondFridayDOM = FirstFridayDOM + 7;
def ThirdFridayDOM = FirstFridayDOM + 14;
def FourthFridayDOM = FirstFridayDOM + 21;

# Pick up all Fridays of the current month for weekly options
def RollDOM = FirstFridayDOM + 21;
def ExpMonth1 =
    if RollDOM > CurrentDayOfMonth then CurrentMonth + OptionSeries - 1
    else CurrentMonth + OptionSeries
;

# Options month input
def ExpMonth2 = if ExpMonth1 > 12 then ExpMonth1 - 12 else ExpMonth1;

# Options year input
def ExpYear = if ExpMonth1 > 12 then CurrentYear + 1 else CurrentYear;

# First friday expiry calc
def ExpDay1DOW = GetDayOfWeek(ExpYear * 10000 + ExpMonth2 * 100 + 1);
def ExpFirstFridayDOM =
    if ExpDay1DOW < 6 then 6 - ExpDay1DOW
    else if ExpDay1DOW == 6 then 7
    else 6
;

# Options code day of month input
def ExpDOM =
    if CurrentDayOfMonth < ExpFirstFridayDOM -1 then FirstFridayDOM
    else if between(CurrentDayOfMonth, ExpFirstFridayDOM, SecondFridayDOM - 1) then SecondFridayDOM
    else if between(CurrentDayOfMonth, SecondFridayDOM, ThirdFridayDOM - 1) then ThirdFridayDOM
    else if between(CurrentDayOfMonth, ThirdFridayDOM, FourthFridayDOM - 1) then FourthFridayDOM
    else ExpFirstFridayDOM
;

# Centerstrike
def CenterStrike =
    if (Mode == Mode.AUTO and !IsNaN(close)) then Round(close / CenterStrikeOffset, 0) * CenterStrikeOffset
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualCenterStrike
    else CenterStrike[1]
;

# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OPEN_INTEREST and !IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
            )
        )
    )
    then MaxStrikeSpacing - i
    else spacing
;
def StrikeSpacing =
    if (Mode == Mode.AUTO and !IsNaN(close)) then StrikeSpacingC
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualStrikeSpacing
    else StrikeSpacing[1]
;


#-----------------------------------------------------------------------------------------------------------------#
# Option Chain

# Call Open Interest
def ITMCallOpenInterest =
    fold itmCallOIIndex = 0 to StrikeDepth
    with itmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
            )
        )
    )
    then 0
    else itmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
        )
    )
;
def OTMCallOpenInterest =
    fold otmCallOIIndex = 0 to StrikeDepth
    with otmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
            )
        )
    )
    then 0
    else otmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
        )
    )
;

# Put Open Interest
def ITMPutOpenInterest =
    fold itmPutOIIndex = 0 to StrikeDepth
    with itmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
            )
        )
    )
    then 0
    else itmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
        )
    )
;
def OTMPutOpenInterest =
    fold otmPutOIIndex = 0 to StrikeDepth
    with otmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike + strikeSpacing * otmPutOIIndex))
            )
        )
    )
    then 0
    else otmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(centerStrike + strikeSpacing * otmPutOIIndex))
        )
    )
;


#-----------------------------------------------------------------------------------------------------------------#
# Greeks Calculations
#
# K  - Option strike price
# N  - Standard normal cumulative distribution function
# r  - Risk free interest rate
# IV - Volatility of the underlying
# S  - Price of the underlying
# t  - Time to option's expiry
#
# Delta = N(d1)
#           d1 = (ln(S/K) + (r + (sqr(IV)/2))t) / (? (sqrt(t)))
# Gamma = (d2) / S(IV(sqrt(t)))
# d2    =  e -(sqr(d1) / 2) / sqrt(2*pi)
# Theta = ((S d2))IV) / 2 sqrt(t)) - (rK e(rt)N(d4))
#         where phi(d3) = (exp(-(sqr(x)/2))) / (2 * sqrt(t))
#         where d4 = d1 - IV(sqrt(t))
# Vega  = S phi(d1) Sqrt(t)


# TODO: Figure out how to dynamically get this value
input DayToExpiry = 6;

# Get the implied volatility for calculations
# Input: series is the expiry starting at 1 and raising by 1 for each next expiry
def IV = SeriesVolatility(series = OptionSeries);


def K = CenterStrike;
def S = close;
def r = GetInterestRate();
def t = DayToExpiry / 365;
def d1 = (Log(S / K) + ((r + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));
script N {
    input data  = 1;
    def a = AbsValue(data);
    def b1 =  .31938153;
    def b2 = -.356563782;
    def b3 = 1.781477937;
    def b4 = -1.821255978;
    def b5 = 1.330274429;
    def b6 =  .2316419;
    def e = 1 / (1 + b6 * a);
    def i = 1 - 1 / Sqrt(2 * Double.Pi) * Exp(-Power(a, 2) / 2) *
           (b1 * e + b2 * e * e + b3 *
            Power(e, 3) + b4 * Power(e, 4) + b5 * Power(e, 5));
    plot CND = if data < 0
               then 1 - i
               else i;
}


# TODO: These values don't quite line up
# My background on options pricing models is not very good

# Delta
def Delta = N(d1);

# Gamma
def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
def Gamma = d2 / (S * (IV * Sqrt(t)));

# Theta
def Theta = -(-(S*d2*IV*(.5000)/
             (2*sqrt(t)))-
             (r*(exp(-r*t)*K))*N(d2)+(S*N(d1)*(.5000)))/365;
# (.5000) variant less than .5 e(X/t)

# Vega
def Vega = (S*d2*sqrt(t))/100;



#-----------------------------------------------------------------------------------------------------------------#
# Visuals

# Current Option Expiry Label
AddLabel(
    ShowStrikeInfo,
    Concat(".",
        Concat(GetSymbolPart(),
            Concat(
                Concat(ExpYear - 2000,
                    if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                    else Concat("", ExpMonth2)
                ),
                if ExpDOM <= 9 then Concat("0", ExpDOM)
                else Concat("", ExpDOM)
            )
        )
    ),
    Color.GRAY
);

# Center Strike Label
AddLabel(ShowStrikeInfo, "Center Strike: " + AsDollars(CenterStrike), Color.GRAY);

# Chain Depth Label
AddLabel(ShowStrikeInfo, "Chain Depth: +/-" + StrikeDepth, Color.GRAY);

# Strike Spacing Label
AddLabel(ShowStrikeInfo and StrikeSpacing, "Strike Spacing: " + StrikeSpacing, Color.GRAY);

# Call Open Interest
plot CallOpenInterest = ItMCallOpenInterest + OTMCallOpenInterest;
CallOpenInterest.SetHiding(!ShowLines);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels, "CallOI: " + CallOpenInterest, GlobalColor("Call"));

# Put Open Interest
plot PutOpenInterest = -(ITMPutOpenInterest + OTMPutOpenInterest); # Make negative to flip under axis
PutOpenInterest.SetHiding(!ShowLines);
PutOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
PutOpenInterest.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels, "PutOI: " + (ITMPutOpenInterest + OTMPutOpenInterest), GlobalColor("Put"));

# Create a center line
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.WHITE);

# Create Clouds
AddCloud(if ShowClouds then CallOpenInterest else Double.NaN, if ShowClouds then Zeroline else Double.NaN, GlobalColor("CallCloud"), GlobalColor("PutCloud"));
AddCloud(if ShowClouds then Zeroline else Double.NaN, if ShowClouds then PutOpenInterest else Double.NaN, GlobalColor("PutCloud"), GlobalColor("CallCloud"));

# Hull Moving Average of Put Open Interest
plot PutVolumeAverage = hullmovingavg(PutOpenInterest);
PutVolumeAverage.SetHiding(!ShowLines);
PutVolumeAverage.setdefaultcolor(color.ORANGE);
PutVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Hull Moving Average of Call Open Interest
plot CallVolumeAverage = hullmovingavg(CallOpenInterest);
CallVolumeAverage.SetHiding(!ShowLines);
CallVolumeAverage.setdefaultcolor(color.LIGHT_GREEN);
CallVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Color Gradient of Total Average Open Interest
plot TotalVolumeAverage = average(CallOpenInterest + PutOpenInterest);
TotalVolumeAverage.SetHiding(!ShowLines);
def colornormlength = 14;
TotalVolumeAverage.setlineweight(3);
TotalVolumeAverage.definecolor("Default", Color.YELLOW);
TotalVolumeAverage.definecolor("Highest", Color.GREEN);
TotalVolumeAverage.DefineColor("Lowest", Color.RED);
TotalVolumeAverage.AssignNormGradientColor(colorNormLength, TotalVolumeAverage.Color("Lowest"), TotalVolumeAverage.Color("Highest"));
AddCloud(if ShowClouds then TotalVolumeAverage else Double.NaN, if ShowClouds then Zeroline else Double.NaN, color.GREEN, color.RED);

# Greeks
AddLabel(ShowGreeks, "Delta = " + Delta, Color.WHITE);
AddLabel(ShowGreeks, "Gamma = " + Gamma, Color.WHITE);
AddLabel(ShowGreeks, "Theta = " + theta, Color.WHITE);
AddLabel(ShowGreeks, "Vega = " + Vega, Color.WHITE);
 

halcyonguy

Well-known member
VIP
Lifetime
Okay, uploading what I have so far. I heavily commented everything and renamed a lot of things, trying my best to understand it all and how thinkscript works in general. It would be 100x easier to do this if we could just dynamically assign a string to a variable. That whole 'series' thing had me stumped for a bit but I found a snippet from Mobius which helps to understand:

"series is the expiry starting at 1 and raising by one for each next expiry"

Using that logic, I setup a dropdown switch to select that instead of a number.
Note this is only setup for open interest right now, the DataType switch does nothing yet.
Change all the open_interest() function calls to volume() for option volume instead.


Code:
#hint: Options Hacker \n This study lets you scan the options chain for a series and given depth. \n<b>Warning: Setting the StrikeDepth to large values requires significant processing power, and will result in slow loading times.</b>


#-----------------------------------------------------------------------------------------------------------------#
# Settings

# Colors
DefineGlobalColor("Call", Color.GREEN);
DefineGlobalColor("Put", Color.RED);
DefineGlobalColor("CallCloud",Color.DARK_GREEN);
DefineGlobalColor("PutCloud",Color.DARK_RED);

#hint Mode: The mode to select an option symbol. \n AUTO will use the option series input code. This will not always work, set to MANUAL to use the center strike and spacing inputs to manually determine the option symbol.
input Mode = {default AUTO, MANUAL};

#hint Series: The option expiration series to search. \n This value is used to determine the option symbol, and for the input to the SeriesVolatility(series = x) function where series is the expiry starting at 1 and raising by one for each next expiry.
input Series = {
    default Weekly,
    Month1,
    Month2,
    Month3,
    Month4,
    Month5,
    Month6,
    Month7,
    Month8,
    Month9
};

#hint DataType: The type of option data to show.
input DataType = {default OPEN_INTEREST, VOLUME};

#hint StrikeDepth: The level of depth to search a series. The search starts at the center strike price, and goes both ITM and OTM.
input StrikeDepth = 5;

#hint CenterStrikeOffset: The offset to use when calculating the center strike based on close price. \n Examples: \n   1 = nearest $1 interval \n   10 = nearest $10 interval.
input CenterStrikeOffset = 1.0;

#hint MaxStrikeSpacing: The maximum dollar amount between two adjacent contracts.
input MaxStrikeSpacing = 25;

#hint ManualCenterStrike: The starting price to use when in MANUAL mode.
input ManualCenterStrike = 440;

#hint ManualStrikeSpacing: The dollar amount between two adjacent contracts to use when in MANUAL mode.
input ManualStrikeSpacing = 1.0;

#hint ShowStrikeInfo: Show the strike info labels.
input ShowStrikeInfo = yes;

#hint ShowLabels: Show the open interest labels.
input ShowLabels = yes;

#hint ShowClouds: Show the open interest clouds.
input ShowClouds = no;

#hint ShowLines: Show the open interest lines.
input ShowLines = yes;

#hint ShowGreeks: Show the estimated Greek calculation labels for the latest bar.
input ShowGreeks = yes;


#-----------------------------------------------------------------------------------------------------------------#
# Date, Symbol, and Strike

# OptionSeries is the expiry starting at 1 and raising by one for each next expiry.
def OptionSeries;
switch (Series) {
    case Weekly:
        OptionSeries = 1;
    case Month1:
        OptionSeries = 2;
    case Month2:
        OptionSeries = 3;
    case Month3:
        OptionSeries = 4;
    case Month4:
        OptionSeries = 5;
    case Month5:
        OptionSeries = 6;
    case Month6:
        OptionSeries = 7;
    case Month7:
        OptionSeries = 8;
    case Month8:
        OptionSeries = 9;
    case Month9:
        OptionSeries = 10;
};

# Open price at Regular Trading Hours
def RTHopen = open(period = AggregationPeriod.DAY);

# Current year, month, and day
def CurrentYear = GetYear();
def CurrentMonth = GetMonth();
def CurrentDayOfMonth = GetDayOfMonth(GetYYYYMMDD());

# Get the first day of this month. 1 (Monday) to 7 (Sunday)
def FirstDayThisMonth = GetDayOfWeek((CurrentYear * 10000) + (CurrentMonth * 100) + 1);

# Get the first friday of this month
def FirstFridayDOM =
    if FirstDayThisMonth < 6 then 6 - FirstDayThisMonth
    else if FirstDayThisMonth == 6 then 7
    else 6
;

# Get the second, third, and fourth fridays of this month
def SecondFridayDOM = FirstFridayDOM + 7;
def ThirdFridayDOM = FirstFridayDOM + 14;
def FourthFridayDOM = FirstFridayDOM + 21;

# Pick up all Fridays of the current month for weekly options
def RollDOM = FirstFridayDOM + 21;
def ExpMonth1 =
    if RollDOM > CurrentDayOfMonth then CurrentMonth + OptionSeries - 1
    else CurrentMonth + OptionSeries
;

# Options month input
def ExpMonth2 = if ExpMonth1 > 12 then ExpMonth1 - 12 else ExpMonth1;

# Options year input
def ExpYear = if ExpMonth1 > 12 then CurrentYear + 1 else CurrentYear;

# First friday expiry calc
def ExpDay1DOW = GetDayOfWeek(ExpYear * 10000 + ExpMonth2 * 100 + 1);
def ExpFirstFridayDOM =
    if ExpDay1DOW < 6 then 6 - ExpDay1DOW
    else if ExpDay1DOW == 6 then 7
    else 6
;

# Options code day of month input
def ExpDOM =
    if CurrentDayOfMonth < ExpFirstFridayDOM -1 then FirstFridayDOM
    else if between(CurrentDayOfMonth, ExpFirstFridayDOM, SecondFridayDOM - 1) then SecondFridayDOM
    else if between(CurrentDayOfMonth, SecondFridayDOM, ThirdFridayDOM - 1) then ThirdFridayDOM
    else if between(CurrentDayOfMonth, ThirdFridayDOM, FourthFridayDOM - 1) then FourthFridayDOM
    else ExpFirstFridayDOM
;

# Centerstrike
def CenterStrike =
    if (Mode == Mode.AUTO and !IsNaN(close)) then Round(close / CenterStrikeOffset, 0) * CenterStrikeOffset
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualCenterStrike
    else CenterStrike[1]
;

# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OPEN_INTEREST and !IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
            )
        )
    )
    then MaxStrikeSpacing - i
    else spacing
;
def StrikeSpacing =
    if (Mode == Mode.AUTO and !IsNaN(close)) then StrikeSpacingC
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualStrikeSpacing
    else StrikeSpacing[1]
;


#-----------------------------------------------------------------------------------------------------------------#
# Option Chain

# Call Open Interest
def ITMCallOpenInterest =
    fold itmCallOIIndex = 0 to StrikeDepth
    with itmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
            )
        )
    )
    then 0
    else itmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
        )
    )
;
def OTMCallOpenInterest =
    fold otmCallOIIndex = 0 to StrikeDepth
    with otmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
            )
        )
    )
    then 0
    else otmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
        )
    )
;

# Put Open Interest
def ITMPutOpenInterest =
    fold itmPutOIIndex = 0 to StrikeDepth
    with itmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
            )
        )
    )
    then 0
    else itmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
        )
    )
;
def OTMPutOpenInterest =
    fold otmPutOIIndex = 0 to StrikeDepth
    with otmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike + strikeSpacing * otmPutOIIndex))
            )
        )
    )
    then 0
    else otmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(centerStrike + strikeSpacing * otmPutOIIndex))
        )
    )
;


#-----------------------------------------------------------------------------------------------------------------#
# Greeks Calculations
#
# K  - Option strike price
# N  - Standard normal cumulative distribution function
# r  - Risk free interest rate
# IV - Volatility of the underlying
# S  - Price of the underlying
# t  - Time to option's expiry
#
# Delta = N(d1)
#           d1 = (ln(S/K) + (r + (sqr(IV)/2))t) / (? (sqrt(t)))
# Gamma = (d2) / S(IV(sqrt(t)))
# d2    =  e -(sqr(d1) / 2) / sqrt(2*pi)
# Theta = ((S d2))IV) / 2 sqrt(t)) - (rK e(rt)N(d4))
#         where phi(d3) = (exp(-(sqr(x)/2))) / (2 * sqrt(t))
#         where d4 = d1 - IV(sqrt(t))
# Vega  = S phi(d1) Sqrt(t)


# TODO: Figure out how to dynamically get this value
input DayToExpiry = 6;

# Get the implied volatility for calculations
# Input: series is the expiry starting at 1 and raising by 1 for each next expiry
def IV = SeriesVolatility(series = OptionSeries);


def K = CenterStrike;
def S = close;
def r = GetInterestRate();
def t = DayToExpiry / 365;
def d1 = (Log(S / K) + ((r + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));
script N {
    input data  = 1;
    def a = AbsValue(data);
    def b1 =  .31938153;
    def b2 = -.356563782;
    def b3 = 1.781477937;
    def b4 = -1.821255978;
    def b5 = 1.330274429;
    def b6 =  .2316419;
    def e = 1 / (1 + b6 * a);
    def i = 1 - 1 / Sqrt(2 * Double.Pi) * Exp(-Power(a, 2) / 2) *
           (b1 * e + b2 * e * e + b3 *
            Power(e, 3) + b4 * Power(e, 4) + b5 * Power(e, 5));
    plot CND = if data < 0
               then 1 - i
               else i;
}


# TODO: These values don't quite line up
# My background on options pricing models is not very good

# Delta
def Delta = N(d1);

# Gamma
def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
def Gamma = d2 / (S * (IV * Sqrt(t)));

# Theta
def Theta = -(-(S*d2*IV*(.5000)/
             (2*sqrt(t)))-
             (r*(exp(-r*t)*K))*N(d2)+(S*N(d1)*(.5000)))/365;
# (.5000) variant less than .5 e(X/t)

# Vega
def Vega = (S*d2*sqrt(t))/100;



#-----------------------------------------------------------------------------------------------------------------#
# Visuals

# Current Option Expiry Label
AddLabel(
    ShowStrikeInfo,
    Concat(".",
        Concat(GetSymbolPart(),
            Concat(
                Concat(ExpYear - 2000,
                    if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                    else Concat("", ExpMonth2)
                ),
                if ExpDOM <= 9 then Concat("0", ExpDOM)
                else Concat("", ExpDOM)
            )
        )
    ),
    Color.GRAY
);

# Center Strike Label
AddLabel(ShowStrikeInfo, "Center Strike: " + AsDollars(CenterStrike), Color.GRAY);

# Chain Depth Label
AddLabel(ShowStrikeInfo, "Chain Depth: +/-" + StrikeDepth, Color.GRAY);

# Strike Spacing Label
AddLabel(ShowStrikeInfo and StrikeSpacing, "Strike Spacing: " + StrikeSpacing, Color.GRAY);

# Call Open Interest
plot CallOpenInterest = ItMCallOpenInterest + OTMCallOpenInterest;
CallOpenInterest.SetHiding(!ShowLines);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels, "CallOI: " + CallOpenInterest, GlobalColor("Call"));

# Put Open Interest
plot PutOpenInterest = -(ITMPutOpenInterest + OTMPutOpenInterest); # Make negative to flip under axis
PutOpenInterest.SetHiding(!ShowLines);
PutOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
PutOpenInterest.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels, "PutOI: " + (ITMPutOpenInterest + OTMPutOpenInterest), GlobalColor("Put"));

# Create a center line
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.WHITE);

# Create Clouds
AddCloud(if ShowClouds then CallOpenInterest else Double.NaN, if ShowClouds then Zeroline else Double.NaN, GlobalColor("CallCloud"), GlobalColor("PutCloud"));
AddCloud(if ShowClouds then Zeroline else Double.NaN, if ShowClouds then PutOpenInterest else Double.NaN, GlobalColor("PutCloud"), GlobalColor("CallCloud"));

# Hull Moving Average of Put Open Interest
plot PutVolumeAverage = hullmovingavg(PutOpenInterest);
PutVolumeAverage.SetHiding(!ShowLines);
PutVolumeAverage.setdefaultcolor(color.ORANGE);
PutVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Hull Moving Average of Call Open Interest
plot CallVolumeAverage = hullmovingavg(CallOpenInterest);
CallVolumeAverage.SetHiding(!ShowLines);
CallVolumeAverage.setdefaultcolor(color.LIGHT_GREEN);
CallVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Color Gradient of Total Average Open Interest
plot TotalVolumeAverage = average(CallOpenInterest + PutOpenInterest);
TotalVolumeAverage.SetHiding(!ShowLines);
def colornormlength = 14;
TotalVolumeAverage.setlineweight(3);
TotalVolumeAverage.definecolor("Default", Color.YELLOW);
TotalVolumeAverage.definecolor("Highest", Color.GREEN);
TotalVolumeAverage.DefineColor("Lowest", Color.RED);
TotalVolumeAverage.AssignNormGradientColor(colorNormLength, TotalVolumeAverage.Color("Lowest"), TotalVolumeAverage.Color("Highest"));
AddCloud(if ShowClouds then TotalVolumeAverage else Double.NaN, if ShowClouds then Zeroline else Double.NaN, color.GREEN, color.RED);

# Greeks
AddLabel(ShowGreeks, "Delta = " + Delta, Color.WHITE);
AddLabel(ShowGreeks, "Gamma = " + Gamma, Color.WHITE);
AddLabel(ShowGreeks, "Theta = " + theta, Color.WHITE);
AddLabel(ShowGreeks, "Vega = " + Vega, Color.WHITE);


a week here and you made this? you are talented.
thank you for posting the code in #106

---------------------------------------

i would like to offer some feedback, that may make things a little simpler.
you don't have to use concat(). you can wrap each section in parenthesis and join them with a +.

i modified the strike spacing code, by removing the word concat and replacing the commas with a +.
then went through and removed some extra parenthesis.


Code:
# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OPEN_INTEREST and !IsNaN(
      open_interest(
        ("." + GetSymbolPart()) +
        (ExpYear - 2000) + 
        # need the "" +   to convert number to text. can't have 2 different data types as T/F values for if-then
        (if ExpMonth2 <= 9 then ("0" + ExpMonth2) else ("" + ExpMonth2) )  +
        (if ExpDOM <= 9 then ("0" + ExpDOM) else ("" + ExpDOM) )  +
        ("C" + AsPrice(CenterStrike + (MaxStrikeSpacing - i)) )
        )
    ) then MaxStrikeSpacing - i
      else spacing;


Code:
# original code

#        open_interest(
#            Concat(
#                Concat(".",
#                    Concat(GetSymbolPart(),
#                        Concat(
#                            Concat(ExpYear - 2000,
#                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
#                                else Concat("", ExpMonth2)
#                            ),
#                            if ExpDOM <= 9 then Concat("0", ExpDOM)
#                            else Concat("", ExpDOM)
#                        )
#                    )
#                ),
#                Concat("C", AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
#            )
#        )
#    )
#    then MaxStrikeSpacing - i
#    else spacing


something else that may help. sometimes i add bubbles to display variables, to verify the data in my formulas.
the \n forces a new line
i tend to use a multiplier like this, low*0.994, to move objects (bubbles, arrows,..) away from the candles, to make it easier to see the candles.

input test1 = yes;
addchartbubble(test1, low*0.994,
bn + "\n" +
upbar3line + "\n" +
upsell + "\n" +
up3lineb + "\n" +
up3line + "\n"
,color.cyan, no);
 

sudoshu

Member
a week here and you made this? you are talented.
thank you for posting the code in #106
Thanks! That means a lot actually.

I did just figure out a way to consolidate the tally into one fold as well.
Also might have found a better way to get the date but I'm not sure its working.
I plotted the date at the bottom and it seems to show the correct expiry for every bar for the selected series, but the data looks quite a bit different.
Heres the date code:
Code:
def OptionExpiryDate = ExpYear * 10000 + ExpMonth2 * 100 + ExpDOM + 1;
Addlabel(ShowStrikeInfo, "ATM Put: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(0 + CenterStrike), Color.LIGHT_RED);
Addlabel(ShowStrikeInfo, "ATM Call: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(0 + CenterStrike), Color.LIGHT_GREEN);
plot test = OptionExpiryDate - 20000001; # For debugging date

Still working on it but here's the snippet I'm playing with updated to include your feedback (thanks for that!)
Code:
# Total Put Open Interest for selected chain depth and expiry series
def TotalPutOpenInterest =
    fold poiIndex = -StrikeDepth to StrikeDepth
    with poi = 0
    do
        #if IsNAN(poi + open_interest(Concat(Concat(Concat(".", GetSymbol()), Concat(AsPrice(OptionExpiryDate - 20000001), "P")), poiIndex + CenterStrike)))
        if IsNaN(poi + open_interest(("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(poiIndex + CenterStrike)))
        then 0
        else poi + open_interest(("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(poiIndex + CenterStrike))
;

# Total Call Open Interest for selected chain depth and expiry series
def TotalCallOpenInterest =
    fold coiIndex = -StrikeDepth to StrikeDepth
    with coi = 0
    do
        #if IsNAN(coi + open_interest(Concat(Concat(Concat(".", GetSymbol()), Concat(AsPrice(OptionExpiryDate - 20000001), "C")), coiIndex + CenterStrike)))
        if IsNaN(coi + open_interest(("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(coiIndex + CenterStrike)))
        then 0
        else coi + open_interest(("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(coiIndex + CenterStrike))
;
 

ziongotoptions

Active member
Okay, uploading what I have so far. I heavily commented everything and renamed a lot of things, trying my best to understand it all and how thinkscript works in general. It would be 100x easier to do this if we could just dynamically assign a string to a variable. That whole 'series' thing had me stumped for a bit but I found a snippet from Mobius which helps to understand:

"series is the expiry starting at 1 and raising by one for each next expiry"

Using that logic, I setup a dropdown switch to select that instead of a number.
Note this is only setup for open interest right now, the DataType switch does nothing yet.
Change all the open_interest() function calls to volume() for option volume instead.


Code:
#hint: Options Hacker \n This study lets you scan the options chain for a series and given depth. \n<b>Warning: Setting the StrikeDepth to large values requires significant processing power, and will result in slow loading times.</b>


#-----------------------------------------------------------------------------------------------------------------#
# Settings

# Colors
DefineGlobalColor("Call", Color.GREEN);
DefineGlobalColor("Put", Color.RED);
DefineGlobalColor("CallCloud",Color.DARK_GREEN);
DefineGlobalColor("PutCloud",Color.DARK_RED);

#hint Mode: The mode to select an option symbol. \n AUTO will use the option series input code. This will not always work, set to MANUAL to use the center strike and spacing inputs to manually determine the option symbol.
input Mode = {default AUTO, MANUAL};

#hint Series: The option expiration series to search. \n This value is used to determine the option symbol, and for the input to the SeriesVolatility(series = x) function where series is the expiry starting at 1 and raising by one for each next expiry.
input Series = {
    default Weekly,
    Month1,
    Month2,
    Month3,
    Month4,
    Month5,
    Month6,
    Month7,
    Month8,
    Month9
};

#hint DataType: The type of option data to show.
input DataType = {default OPEN_INTEREST, VOLUME};

#hint StrikeDepth: The level of depth to search a series. The search starts at the center strike price, and goes both ITM and OTM.
input StrikeDepth = 5;

#hint CenterStrikeOffset: The offset to use when calculating the center strike based on close price. \n Examples: \n   1 = nearest $1 interval \n   10 = nearest $10 interval.
input CenterStrikeOffset = 1.0;

#hint MaxStrikeSpacing: The maximum dollar amount between two adjacent contracts.
input MaxStrikeSpacing = 25;

#hint ManualCenterStrike: The starting price to use when in MANUAL mode.
input ManualCenterStrike = 440;

#hint ManualStrikeSpacing: The dollar amount between two adjacent contracts to use when in MANUAL mode.
input ManualStrikeSpacing = 1.0;

#hint ShowStrikeInfo: Show the strike info labels.
input ShowStrikeInfo = yes;

#hint ShowLabels: Show the open interest labels.
input ShowLabels = yes;

#hint ShowClouds: Show the open interest clouds.
input ShowClouds = no;

#hint ShowLines: Show the open interest lines.
input ShowLines = yes;

#hint ShowGreeks: Show the estimated Greek calculation labels for the latest bar.
input ShowGreeks = yes;


#-----------------------------------------------------------------------------------------------------------------#
# Date, Symbol, and Strike

# OptionSeries is the expiry starting at 1 and raising by one for each next expiry.
def OptionSeries;
switch (Series) {
    case Weekly:
        OptionSeries = 1;
    case Month1:
        OptionSeries = 2;
    case Month2:
        OptionSeries = 3;
    case Month3:
        OptionSeries = 4;
    case Month4:
        OptionSeries = 5;
    case Month5:
        OptionSeries = 6;
    case Month6:
        OptionSeries = 7;
    case Month7:
        OptionSeries = 8;
    case Month8:
        OptionSeries = 9;
    case Month9:
        OptionSeries = 10;
};

# Open price at Regular Trading Hours
def RTHopen = open(period = AggregationPeriod.DAY);

# Current year, month, and day
def CurrentYear = GetYear();
def CurrentMonth = GetMonth();
def CurrentDayOfMonth = GetDayOfMonth(GetYYYYMMDD());

# Get the first day of this month. 1 (Monday) to 7 (Sunday)
def FirstDayThisMonth = GetDayOfWeek((CurrentYear * 10000) + (CurrentMonth * 100) + 1);

# Get the first friday of this month
def FirstFridayDOM =
    if FirstDayThisMonth < 6 then 6 - FirstDayThisMonth
    else if FirstDayThisMonth == 6 then 7
    else 6
;

# Get the second, third, and fourth fridays of this month
def SecondFridayDOM = FirstFridayDOM + 7;
def ThirdFridayDOM = FirstFridayDOM + 14;
def FourthFridayDOM = FirstFridayDOM + 21;

# Pick up all Fridays of the current month for weekly options
def RollDOM = FirstFridayDOM + 21;
def ExpMonth1 =
    if RollDOM > CurrentDayOfMonth then CurrentMonth + OptionSeries - 1
    else CurrentMonth + OptionSeries
;

# Options month input
def ExpMonth2 = if ExpMonth1 > 12 then ExpMonth1 - 12 else ExpMonth1;

# Options year input
def ExpYear = if ExpMonth1 > 12 then CurrentYear + 1 else CurrentYear;

# First friday expiry calc
def ExpDay1DOW = GetDayOfWeek(ExpYear * 10000 + ExpMonth2 * 100 + 1);
def ExpFirstFridayDOM =
    if ExpDay1DOW < 6 then 6 - ExpDay1DOW
    else if ExpDay1DOW == 6 then 7
    else 6
;

# Options code day of month input
def ExpDOM =
    if CurrentDayOfMonth < ExpFirstFridayDOM -1 then FirstFridayDOM
    else if between(CurrentDayOfMonth, ExpFirstFridayDOM, SecondFridayDOM - 1) then SecondFridayDOM
    else if between(CurrentDayOfMonth, SecondFridayDOM, ThirdFridayDOM - 1) then ThirdFridayDOM
    else if between(CurrentDayOfMonth, ThirdFridayDOM, FourthFridayDOM - 1) then FourthFridayDOM
    else ExpFirstFridayDOM
;

# Centerstrike
def CenterStrike =
    if (Mode == Mode.AUTO and !IsNaN(close)) then Round(close / CenterStrikeOffset, 0) * CenterStrikeOffset
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualCenterStrike
    else CenterStrike[1]
;

# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OPEN_INTEREST and !IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
            )
        )
    )
    then MaxStrikeSpacing - i
    else spacing
;
def StrikeSpacing =
    if (Mode == Mode.AUTO and !IsNaN(close)) then StrikeSpacingC
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualStrikeSpacing
    else StrikeSpacing[1]
;


#-----------------------------------------------------------------------------------------------------------------#
# Option Chain

# Call Open Interest
def ITMCallOpenInterest =
    fold itmCallOIIndex = 0 to StrikeDepth
    with itmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
            )
        )
    )
    then 0
    else itmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
        )
    )
;
def OTMCallOpenInterest =
    fold otmCallOIIndex = 0 to StrikeDepth
    with otmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
            )
        )
    )
    then 0
    else otmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
        )
    )
;

# Put Open Interest
def ITMPutOpenInterest =
    fold itmPutOIIndex = 0 to StrikeDepth
    with itmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
            )
        )
    )
    then 0
    else itmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
        )
    )
;
def OTMPutOpenInterest =
    fold otmPutOIIndex = 0 to StrikeDepth
    with otmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike + strikeSpacing * otmPutOIIndex))
            )
        )
    )
    then 0
    else otmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(centerStrike + strikeSpacing * otmPutOIIndex))
        )
    )
;


#-----------------------------------------------------------------------------------------------------------------#
# Greeks Calculations
#
# K  - Option strike price
# N  - Standard normal cumulative distribution function
# r  - Risk free interest rate
# IV - Volatility of the underlying
# S  - Price of the underlying
# t  - Time to option's expiry
#
# Delta = N(d1)
#           d1 = (ln(S/K) + (r + (sqr(IV)/2))t) / (? (sqrt(t)))
# Gamma = (d2) / S(IV(sqrt(t)))
# d2    =  e -(sqr(d1) / 2) / sqrt(2*pi)
# Theta = ((S d2))IV) / 2 sqrt(t)) - (rK e(rt)N(d4))
#         where phi(d3) = (exp(-(sqr(x)/2))) / (2 * sqrt(t))
#         where d4 = d1 - IV(sqrt(t))
# Vega  = S phi(d1) Sqrt(t)


# TODO: Figure out how to dynamically get this value
input DayToExpiry = 6;

# Get the implied volatility for calculations
# Input: series is the expiry starting at 1 and raising by 1 for each next expiry
def IV = SeriesVolatility(series = OptionSeries);


def K = CenterStrike;
def S = close;
def r = GetInterestRate();
def t = DayToExpiry / 365;
def d1 = (Log(S / K) + ((r + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));
script N {
    input data  = 1;
    def a = AbsValue(data);
    def b1 =  .31938153;
    def b2 = -.356563782;
    def b3 = 1.781477937;
    def b4 = -1.821255978;
    def b5 = 1.330274429;
    def b6 =  .2316419;
    def e = 1 / (1 + b6 * a);
    def i = 1 - 1 / Sqrt(2 * Double.Pi) * Exp(-Power(a, 2) / 2) *
           (b1 * e + b2 * e * e + b3 *
            Power(e, 3) + b4 * Power(e, 4) + b5 * Power(e, 5));
    plot CND = if data < 0
               then 1 - i
               else i;
}


# TODO: These values don't quite line up
# My background on options pricing models is not very good

# Delta
def Delta = N(d1);

# Gamma
def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
def Gamma = d2 / (S * (IV * Sqrt(t)));

# Theta
def Theta = -(-(S*d2*IV*(.5000)/
             (2*sqrt(t)))-
             (r*(exp(-r*t)*K))*N(d2)+(S*N(d1)*(.5000)))/365;
# (.5000) variant less than .5 e(X/t)

# Vega
def Vega = (S*d2*sqrt(t))/100;



#-----------------------------------------------------------------------------------------------------------------#
# Visuals

# Current Option Expiry Label
AddLabel(
    ShowStrikeInfo,
    Concat(".",
        Concat(GetSymbolPart(),
            Concat(
                Concat(ExpYear - 2000,
                    if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                    else Concat("", ExpMonth2)
                ),
                if ExpDOM <= 9 then Concat("0", ExpDOM)
                else Concat("", ExpDOM)
            )
        )
    ),
    Color.GRAY
);

# Center Strike Label
AddLabel(ShowStrikeInfo, "Center Strike: " + AsDollars(CenterStrike), Color.GRAY);

# Chain Depth Label
AddLabel(ShowStrikeInfo, "Chain Depth: +/-" + StrikeDepth, Color.GRAY);

# Strike Spacing Label
AddLabel(ShowStrikeInfo and StrikeSpacing, "Strike Spacing: " + StrikeSpacing, Color.GRAY);

# Call Open Interest
plot CallOpenInterest = ItMCallOpenInterest + OTMCallOpenInterest;
CallOpenInterest.SetHiding(!ShowLines);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels, "CallOI: " + CallOpenInterest, GlobalColor("Call"));

# Put Open Interest
plot PutOpenInterest = -(ITMPutOpenInterest + OTMPutOpenInterest); # Make negative to flip under axis
PutOpenInterest.SetHiding(!ShowLines);
PutOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
PutOpenInterest.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels, "PutOI: " + (ITMPutOpenInterest + OTMPutOpenInterest), GlobalColor("Put"));

# Create a center line
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.WHITE);

# Create Clouds
AddCloud(if ShowClouds then CallOpenInterest else Double.NaN, if ShowClouds then Zeroline else Double.NaN, GlobalColor("CallCloud"), GlobalColor("PutCloud"));
AddCloud(if ShowClouds then Zeroline else Double.NaN, if ShowClouds then PutOpenInterest else Double.NaN, GlobalColor("PutCloud"), GlobalColor("CallCloud"));

# Hull Moving Average of Put Open Interest
plot PutVolumeAverage = hullmovingavg(PutOpenInterest);
PutVolumeAverage.SetHiding(!ShowLines);
PutVolumeAverage.setdefaultcolor(color.ORANGE);
PutVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Hull Moving Average of Call Open Interest
plot CallVolumeAverage = hullmovingavg(CallOpenInterest);
CallVolumeAverage.SetHiding(!ShowLines);
CallVolumeAverage.setdefaultcolor(color.LIGHT_GREEN);
CallVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Color Gradient of Total Average Open Interest
plot TotalVolumeAverage = average(CallOpenInterest + PutOpenInterest);
TotalVolumeAverage.SetHiding(!ShowLines);
def colornormlength = 14;
TotalVolumeAverage.setlineweight(3);
TotalVolumeAverage.definecolor("Default", Color.YELLOW);
TotalVolumeAverage.definecolor("Highest", Color.GREEN);
TotalVolumeAverage.DefineColor("Lowest", Color.RED);
TotalVolumeAverage.AssignNormGradientColor(colorNormLength, TotalVolumeAverage.Color("Lowest"), TotalVolumeAverage.Color("Highest"));
AddCloud(if ShowClouds then TotalVolumeAverage else Double.NaN, if ShowClouds then Zeroline else Double.NaN, color.GREEN, color.RED);

# Greeks
AddLabel(ShowGreeks, "Delta = " + Delta, Color.WHITE);
AddLabel(ShowGreeks, "Gamma = " + Gamma, Color.WHITE);
AddLabel(ShowGreeks, "Theta = " + theta, Color.WHITE);
AddLabel(ShowGreeks, "Vega = " + Vega, Color.WHITE);
it's great having a software engineer help with this, I definitely like what you've done so far with calculating OI and the greeks, it won't be completely accurate given these are theoretical calculations and there are bound to be nuisances but I definitely think you're on the right track. This can be used to calculate total volume and open interest but correct me if I'm wrong, the greek values are only for the center strike correct?
 

ziongotoptions

Active member
Okay, uploading what I have so far. I heavily commented everything and renamed a lot of things, trying my best to understand it all and how thinkscript works in general. It would be 100x easier to do this if we could just dynamically assign a string to a variable. That whole 'series' thing had me stumped for a bit but I found a snippet from Mobius which helps to understand:

"series is the expiry starting at 1 and raising by one for each next expiry"

Using that logic, I setup a dropdown switch to select that instead of a number.
Note this is only setup for open interest right now, the DataType switch does nothing yet.
Change all the open_interest() function calls to volume() for option volume instead.


Code:
#hint: Options Hacker \n This study lets you scan the options chain for a series and given depth. \n<b>Warning: Setting the StrikeDepth to large values requires significant processing power, and will result in slow loading times.</b>


#-----------------------------------------------------------------------------------------------------------------#
# Settings

# Colors
DefineGlobalColor("Call", Color.GREEN);
DefineGlobalColor("Put", Color.RED);
DefineGlobalColor("CallCloud",Color.DARK_GREEN);
DefineGlobalColor("PutCloud",Color.DARK_RED);

#hint Mode: The mode to select an option symbol. \n AUTO will use the option series input code. This will not always work, set to MANUAL to use the center strike and spacing inputs to manually determine the option symbol.
input Mode = {default AUTO, MANUAL};

#hint Series: The option expiration series to search. \n This value is used to determine the option symbol, and for the input to the SeriesVolatility(series = x) function where series is the expiry starting at 1 and raising by one for each next expiry.
input Series = {
    default Weekly,
    Month1,
    Month2,
    Month3,
    Month4,
    Month5,
    Month6,
    Month7,
    Month8,
    Month9
};

#hint DataType: The type of option data to show.
input DataType = {default OPEN_INTEREST, VOLUME};

#hint StrikeDepth: The level of depth to search a series. The search starts at the center strike price, and goes both ITM and OTM.
input StrikeDepth = 5;

#hint CenterStrikeOffset: The offset to use when calculating the center strike based on close price. \n Examples: \n   1 = nearest $1 interval \n   10 = nearest $10 interval.
input CenterStrikeOffset = 1.0;

#hint MaxStrikeSpacing: The maximum dollar amount between two adjacent contracts.
input MaxStrikeSpacing = 25;

#hint ManualCenterStrike: The starting price to use when in MANUAL mode.
input ManualCenterStrike = 440;

#hint ManualStrikeSpacing: The dollar amount between two adjacent contracts to use when in MANUAL mode.
input ManualStrikeSpacing = 1.0;

#hint ShowStrikeInfo: Show the strike info labels.
input ShowStrikeInfo = yes;

#hint ShowLabels: Show the open interest labels.
input ShowLabels = yes;

#hint ShowClouds: Show the open interest clouds.
input ShowClouds = no;

#hint ShowLines: Show the open interest lines.
input ShowLines = yes;

#hint ShowGreeks: Show the estimated Greek calculation labels for the latest bar.
input ShowGreeks = yes;


#-----------------------------------------------------------------------------------------------------------------#
# Date, Symbol, and Strike

# OptionSeries is the expiry starting at 1 and raising by one for each next expiry.
def OptionSeries;
switch (Series) {
    case Weekly:
        OptionSeries = 1;
    case Month1:
        OptionSeries = 2;
    case Month2:
        OptionSeries = 3;
    case Month3:
        OptionSeries = 4;
    case Month4:
        OptionSeries = 5;
    case Month5:
        OptionSeries = 6;
    case Month6:
        OptionSeries = 7;
    case Month7:
        OptionSeries = 8;
    case Month8:
        OptionSeries = 9;
    case Month9:
        OptionSeries = 10;
};

# Open price at Regular Trading Hours
def RTHopen = open(period = AggregationPeriod.DAY);

# Current year, month, and day
def CurrentYear = GetYear();
def CurrentMonth = GetMonth();
def CurrentDayOfMonth = GetDayOfMonth(GetYYYYMMDD());

# Get the first day of this month. 1 (Monday) to 7 (Sunday)
def FirstDayThisMonth = GetDayOfWeek((CurrentYear * 10000) + (CurrentMonth * 100) + 1);

# Get the first friday of this month
def FirstFridayDOM =
    if FirstDayThisMonth < 6 then 6 - FirstDayThisMonth
    else if FirstDayThisMonth == 6 then 7
    else 6
;

# Get the second, third, and fourth fridays of this month
def SecondFridayDOM = FirstFridayDOM + 7;
def ThirdFridayDOM = FirstFridayDOM + 14;
def FourthFridayDOM = FirstFridayDOM + 21;

# Pick up all Fridays of the current month for weekly options
def RollDOM = FirstFridayDOM + 21;
def ExpMonth1 =
    if RollDOM > CurrentDayOfMonth then CurrentMonth + OptionSeries - 1
    else CurrentMonth + OptionSeries
;

# Options month input
def ExpMonth2 = if ExpMonth1 > 12 then ExpMonth1 - 12 else ExpMonth1;

# Options year input
def ExpYear = if ExpMonth1 > 12 then CurrentYear + 1 else CurrentYear;

# First friday expiry calc
def ExpDay1DOW = GetDayOfWeek(ExpYear * 10000 + ExpMonth2 * 100 + 1);
def ExpFirstFridayDOM =
    if ExpDay1DOW < 6 then 6 - ExpDay1DOW
    else if ExpDay1DOW == 6 then 7
    else 6
;

# Options code day of month input
def ExpDOM =
    if CurrentDayOfMonth < ExpFirstFridayDOM -1 then FirstFridayDOM
    else if between(CurrentDayOfMonth, ExpFirstFridayDOM, SecondFridayDOM - 1) then SecondFridayDOM
    else if between(CurrentDayOfMonth, SecondFridayDOM, ThirdFridayDOM - 1) then ThirdFridayDOM
    else if between(CurrentDayOfMonth, ThirdFridayDOM, FourthFridayDOM - 1) then FourthFridayDOM
    else ExpFirstFridayDOM
;

# Centerstrike
def CenterStrike =
    if (Mode == Mode.AUTO and !IsNaN(close)) then Round(close / CenterStrikeOffset, 0) * CenterStrikeOffset
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualCenterStrike
    else CenterStrike[1]
;

# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OPEN_INTEREST and !IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
            )
        )
    )
    then MaxStrikeSpacing - i
    else spacing
;
def StrikeSpacing =
    if (Mode == Mode.AUTO and !IsNaN(close)) then StrikeSpacingC
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualStrikeSpacing
    else StrikeSpacing[1]
;


#-----------------------------------------------------------------------------------------------------------------#
# Option Chain

# Call Open Interest
def ITMCallOpenInterest =
    fold itmCallOIIndex = 0 to StrikeDepth
    with itmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
            )
        )
    )
    then 0
    else itmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike - strikeSpacing * itmCallOIIndex))
        )
    )
;
def OTMCallOpenInterest =
    fold otmCallOIIndex = 0 to StrikeDepth
    with otmCOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
            )
        )
    )
    then 0
    else otmCOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("C", AsPrice(CenterStrike + strikeSpacing * otmCallOIIndex))
        )
    )
;

# Put Open Interest
def ITMPutOpenInterest =
    fold itmPutOIIndex = 0 to StrikeDepth
    with itmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
            )
        )
    )
    then 0
    else itmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(CenterStrike - strikeSpacing * itmPutOIIndex))
        )
    )
;
def OTMPutOpenInterest =
    fold otmPutOIIndex = 0 to StrikeDepth
    with otmPOI
    do if IsNaN(
        open_interest(
            Concat(
                Concat(".",
                    Concat(GetSymbolPart(),
                        Concat(
                            Concat(ExpYear - 2000,
                                if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                                else Concat("", ExpMonth2)
                            ),
                            if ExpDOM <= 9 then Concat("0", ExpDOM)
                            else Concat("", ExpDOM)
                        )
                    )
                ),
                Concat("P", AsPrice(CenterStrike + strikeSpacing * otmPutOIIndex))
            )
        )
    )
    then 0
    else otmPOI + open_interest(
        Concat(
            Concat(".",
                Concat(GetSymbolPart(),
                    Concat(
                        Concat(ExpYear - 2000,
                            if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                            else Concat("", ExpMonth2)
                        ),
                        if ExpDOM <= 9 then Concat("0", ExpDOM)
                        else Concat("", ExpDOM)
                    )
                )
            ),
            Concat("P", AsPrice(centerStrike + strikeSpacing * otmPutOIIndex))
        )
    )
;


#-----------------------------------------------------------------------------------------------------------------#
# Greeks Calculations
#
# K  - Option strike price
# N  - Standard normal cumulative distribution function
# r  - Risk free interest rate
# IV - Volatility of the underlying
# S  - Price of the underlying
# t  - Time to option's expiry
#
# Delta = N(d1)
#           d1 = (ln(S/K) + (r + (sqr(IV)/2))t) / (? (sqrt(t)))
# Gamma = (d2) / S(IV(sqrt(t)))
# d2    =  e -(sqr(d1) / 2) / sqrt(2*pi)
# Theta = ((S d2))IV) / 2 sqrt(t)) - (rK e(rt)N(d4))
#         where phi(d3) = (exp(-(sqr(x)/2))) / (2 * sqrt(t))
#         where d4 = d1 - IV(sqrt(t))
# Vega  = S phi(d1) Sqrt(t)


# TODO: Figure out how to dynamically get this value
input DayToExpiry = 6;

# Get the implied volatility for calculations
# Input: series is the expiry starting at 1 and raising by 1 for each next expiry
def IV = SeriesVolatility(series = OptionSeries);


def K = CenterStrike;
def S = close;
def r = GetInterestRate();
def t = DayToExpiry / 365;
def d1 = (Log(S / K) + ((r + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));
script N {
    input data  = 1;
    def a = AbsValue(data);
    def b1 =  .31938153;
    def b2 = -.356563782;
    def b3 = 1.781477937;
    def b4 = -1.821255978;
    def b5 = 1.330274429;
    def b6 =  .2316419;
    def e = 1 / (1 + b6 * a);
    def i = 1 - 1 / Sqrt(2 * Double.Pi) * Exp(-Power(a, 2) / 2) *
           (b1 * e + b2 * e * e + b3 *
            Power(e, 3) + b4 * Power(e, 4) + b5 * Power(e, 5));
    plot CND = if data < 0
               then 1 - i
               else i;
}


# TODO: These values don't quite line up
# My background on options pricing models is not very good

# Delta
def Delta = N(d1);

# Gamma
def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
def Gamma = d2 / (S * (IV * Sqrt(t)));

# Theta
def Theta = -(-(S*d2*IV*(.5000)/
             (2*sqrt(t)))-
             (r*(exp(-r*t)*K))*N(d2)+(S*N(d1)*(.5000)))/365;
# (.5000) variant less than .5 e(X/t)

# Vega
def Vega = (S*d2*sqrt(t))/100;



#-----------------------------------------------------------------------------------------------------------------#
# Visuals

# Current Option Expiry Label
AddLabel(
    ShowStrikeInfo,
    Concat(".",
        Concat(GetSymbolPart(),
            Concat(
                Concat(ExpYear - 2000,
                    if ExpMonth2 <= 9 then Concat("0", ExpMonth2)
                    else Concat("", ExpMonth2)
                ),
                if ExpDOM <= 9 then Concat("0", ExpDOM)
                else Concat("", ExpDOM)
            )
        )
    ),
    Color.GRAY
);

# Center Strike Label
AddLabel(ShowStrikeInfo, "Center Strike: " + AsDollars(CenterStrike), Color.GRAY);

# Chain Depth Label
AddLabel(ShowStrikeInfo, "Chain Depth: +/-" + StrikeDepth, Color.GRAY);

# Strike Spacing Label
AddLabel(ShowStrikeInfo and StrikeSpacing, "Strike Spacing: " + StrikeSpacing, Color.GRAY);

# Call Open Interest
plot CallOpenInterest = ItMCallOpenInterest + OTMCallOpenInterest;
CallOpenInterest.SetHiding(!ShowLines);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels, "CallOI: " + CallOpenInterest, GlobalColor("Call"));

# Put Open Interest
plot PutOpenInterest = -(ITMPutOpenInterest + OTMPutOpenInterest); # Make negative to flip under axis
PutOpenInterest.SetHiding(!ShowLines);
PutOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
PutOpenInterest.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels, "PutOI: " + (ITMPutOpenInterest + OTMPutOpenInterest), GlobalColor("Put"));

# Create a center line
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.WHITE);

# Create Clouds
AddCloud(if ShowClouds then CallOpenInterest else Double.NaN, if ShowClouds then Zeroline else Double.NaN, GlobalColor("CallCloud"), GlobalColor("PutCloud"));
AddCloud(if ShowClouds then Zeroline else Double.NaN, if ShowClouds then PutOpenInterest else Double.NaN, GlobalColor("PutCloud"), GlobalColor("CallCloud"));

# Hull Moving Average of Put Open Interest
plot PutVolumeAverage = hullmovingavg(PutOpenInterest);
PutVolumeAverage.SetHiding(!ShowLines);
PutVolumeAverage.setdefaultcolor(color.ORANGE);
PutVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Hull Moving Average of Call Open Interest
plot CallVolumeAverage = hullmovingavg(CallOpenInterest);
CallVolumeAverage.SetHiding(!ShowLines);
CallVolumeAverage.setdefaultcolor(color.LIGHT_GREEN);
CallVolumeAverage.setstyle(curve.MEDIUM_DASH);

# Color Gradient of Total Average Open Interest
plot TotalVolumeAverage = average(CallOpenInterest + PutOpenInterest);
TotalVolumeAverage.SetHiding(!ShowLines);
def colornormlength = 14;
TotalVolumeAverage.setlineweight(3);
TotalVolumeAverage.definecolor("Default", Color.YELLOW);
TotalVolumeAverage.definecolor("Highest", Color.GREEN);
TotalVolumeAverage.DefineColor("Lowest", Color.RED);
TotalVolumeAverage.AssignNormGradientColor(colorNormLength, TotalVolumeAverage.Color("Lowest"), TotalVolumeAverage.Color("Highest"));
AddCloud(if ShowClouds then TotalVolumeAverage else Double.NaN, if ShowClouds then Zeroline else Double.NaN, color.GREEN, color.RED);

# Greeks
AddLabel(ShowGreeks, "Delta = " + Delta, Color.WHITE);
AddLabel(ShowGreeks, "Gamma = " + Gamma, Color.WHITE);
AddLabel(ShowGreeks, "Theta = " + theta, Color.WHITE);
AddLabel(ShowGreeks, "Vega = " + Vega, Color.WHITE);
I found this article it should help with knowing how to calculate the gamma at each strike, aswell as zero gamma line etc https://perfiliev.co.uk/market-commentary/how-to-calculate-gamma-exposure-and-zero-gamma-level/
 

sudoshu

Member
it's great having a software engineer help with this, I definitely like what you've done so far with calculating OI and the greeks, it won't be completely accurate given these are theoretical calculations and there are bound to be nuisances but I definitely think you're on the right track. This can be used to calculate total volume and open interest but correct me if I'm wrong, the greek values are only for the center strike correct?
Yes only for the ATM strike right now
 

sudoshu

Member
Here is an updated version.
I've massively simplified the fold logic in my opinion. (I think there was a bug in the old code anyways where I was adding ATM data twice)

I've also added gamma exposure based on the math here: https://perfiliev.co.uk/market-commentary/how-to-calculate-gamma-exposure-and-zero-gamma-level/
Although I'm not sure its correct, as the math is quite complicated, and my background is not in finance.
I'm also not sure how to interpret the data just yet ...
It doesn't make it any easier that I realized you can't call a custom script inside the fold using the index of the fold o_O :unsure:
As a workaround, I had to try and implement the formula for deriving the spot gamma in a single line within the fold, as part of the larger calculation for the spot gamma exposure at that level. This is the mobius Greeks formula (which uses Black Scholes), using the OptionPrice() and SeriesVolatility() as inputs.

Some Notes:
  1. Ideally to get the 'Total Market Gamma Exposure' we would need to perform this calculation on all strikes, for every expiry, for every day. I have only tested going +/- 5 to 10 strikes out, for weekly series so far, and it already takes quite a bit of time. For reference I'm running an i9-9900k with 64GB DDR4 RAM, Running this at 5 strike depth weekly on SPY is taking around 15 -20 minutes (hard to tell when its actually done running so I'm just going by when my pc fans/water cooler fans stop spinning at max rpm lol)
  2. For number 1 to work, we would need to further modify the date/strike logic to be able to pick up more than just next series expiries. I also don't know how this effects the SeriesVolatility function.
  3. Maybe another approach would be to modify this so that we can pick up the first couple close to the money strikes, and then skip to picking up a couple way out of the money strikes to get a better overall picture.
Screenshot:

pQI47if.png


Code:

Code:
#hint: Options Hacker \n This study lets you scan the options chain for a series and given depth. \n<b>Warning: Setting the StrikeDepth to large values requires significant processing power, and will result in slow loading times.</b>


#-----------------------------------------------------------------------------------------------------------------#
# Settings

# Colors
DefineGlobalColor("Call", Color.GREEN);
DefineGlobalColor("Put", Color.RED);
DefineGlobalColor("CallCloud",Color.DARK_GREEN);
DefineGlobalColor("PutCloud",Color.DARK_RED);

#hint Mode: The mode to select an option symbol. \n AUTO will try to find the option symbol based on the Series and StrikeDepth inputs. \n MANUAL allow an override of the AUTO behavior by using the ManualCenterStrike and ManualStrikeSpacing inputs to determine the option symbol.
input Mode = {default AUTO, MANUAL};

#hint Series: The option expiration series to search. \n This value is used to determine the option symbol.
input Series = {
    default Weekly,
    Month1,
    Month2,
    Month3,
    Month4,
    Month5,
    Month6,
    Month7,
    Month8,
    Month9
};

#hint DataType: The type of option data to show.
input DataType = {default OpenInterest, Volume, GammaExposure};

#hint StrikeDepth: The level of depth to search a series. (+/- this far from ATM)
input StrikeDepth = 10;

#hint CenterStrikeOffset: The offset to use when calculating the center strike based on close price. \n Examples: \n   1 = nearest $1 interval \n   10 = nearest $10 interval.
input CenterStrikeOffset = 1.0;

#hint MaxStrikeSpacing: The maximum dollar amount between two adjacent contracts.
input MaxStrikeSpacing = 25;

#hint ManualCenterStrike: The starting price to use when in MANUAL mode.
input ManualCenterStrike = 440;

#hint ManualStrikeSpacing: The dollar amount between two adjacent contracts to use when in MANUAL mode.
input ManualStrikeSpacing = 1.0;

#hint ShowStrikeInfo: Show the strike info labels.
input ShowStrikeInfo = yes;

#hint ShowLabels: Show the open interest labels.
input ShowLabels = yes;

#hint ShowClouds: Show the open interest clouds.
input ShowClouds = no;

#hint ShowLines: Show the open interest lines.
input ShowLines = yes;

#hint ShowGreeks: Show the estimated Greek calculation labels for the latest bar.
input ShowGreeks = yes;


#-----------------------------------------------------------------------------------------------------------------#
# Date, Symbol, and Strike

# OptionSeries is the expiry starting at 1 and raising by one for each next expiry.
def OptionSeries;
switch (Series) {
    case Weekly:
        OptionSeries = 1;
    case Month1:
        OptionSeries = 2;
    case Month2:
        OptionSeries = 3;
    case Month3:
        OptionSeries = 4;
    case Month4:
        OptionSeries = 5;
    case Month5:
        OptionSeries = 6;
    case Month6:
        OptionSeries = 7;
    case Month7:
        OptionSeries = 8;
    case Month8:
        OptionSeries = 9;
    case Month9:
        OptionSeries = 10;
};

# Open price at Regular Trading Hours
def RTHopen = open(period = AggregationPeriod.DAY);

# Current year, month, day, and date
def CurrentYear = GetYear(); # number of current bar in CST
def CurrentMonth = GetMonth(); # 1 - 12
def CurrentDay = GetDay(); # 1 - 365 (366 for leap year)
def CurrentDate = GetYYYYMMDD(); # date of the current bar in the YYYYMMDD

# Current day of this month
def CurrentDayOfMonth = GetDayOfMonth(CurrentDate);

# Get the first day of this month - 1 (Monday) to 7 (Sunday)
def FirstDayThisMonth = GetDayOfWeek((CurrentYear * 10000) + (CurrentMonth * 100) + 1);

# Get the first upcoming friday
def FirstUpcomingFriday =
    if FirstDayThisMonth < 6 then 6 - FirstDayThisMonth
    else if FirstDayThisMonth == 6 then 7
    else 6
;

# Get the second, third, and fourth upcoming fridays
def SecondUpcomingFriday = FirstUpcomingFriday + 7;
def ThirdUpcomingFriday = FirstUpcomingFriday + 14;
def FourthUpcomingFriday = FirstUpcomingFriday + 21;

# Pick up all Fridays of the current month for weekly options
def RollDOM = FirstUpcomingFriday + 21;
def ExpMonth1 =
    if RollDOM > CurrentDayOfMonth then CurrentMonth + OptionSeries - 1
    else CurrentMonth + OptionSeries
;

# Options month input
def ExpMonth2 = if ExpMonth1 > 12 then ExpMonth1 - 12 else ExpMonth1;

# Options year input
def ExpYear = if ExpMonth1 > 12 then CurrentYear + 1 else CurrentYear;

# First friday expiry calc
def ExpDay1DOW = GetDayOfWeek(ExpYear * 10000 + ExpMonth2 * 100 + 1);
def ExpFirstFridayDOM =
    if ExpDay1DOW < 6 then 6 - ExpDay1DOW
    else if ExpDay1DOW == 6 then 7
    else 6
;

# Options code day of month input
def ExpDOM =
    if CurrentDayOfMonth < ExpFirstFridayDOM -1 then FirstUpcomingFriday
    else if between(CurrentDayOfMonth, ExpFirstFridayDOM, SecondUpcomingFriday - 1) then SecondUpcomingFriday
    else if between(CurrentDayOfMonth, SecondUpcomingFriday, ThirdUpcomingFriday - 1) then ThirdUpcomingFriday
    else if between(CurrentDayOfMonth, ThirdUpcomingFriday, FourthUpcomingFriday - 1) then FourthUpcomingFriday
    else ExpFirstFridayDOM
;

# Centerstrike
def CenterStrike =
    if (Mode == Mode.AUTO and !IsNaN(close)) then Round(close / CenterStrikeOffset, 0) * CenterStrikeOffset
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualCenterStrike
    else CenterStrike[1]
;

# This is still some voodoo to me ...
def OptionExpiryDate = ExpYear * 10000 + ExpMonth2 * 100 + ExpDOM + 1;

# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OpenInterest or DataType == DataType.GammaExposure and !IsNaN(
        open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
    )
    then MaxStrikeSpacing - i
    else if DataType == DataType.Volume and !IsNaN(
        volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
    )
    then MaxStrikeSpacing - i
    else spacing
;
def StrikeSpacing =
    if (Mode == Mode.AUTO and !IsNaN(close)) then StrikeSpacingC
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualStrikeSpacing
    else StrikeSpacing[1]
;

# Testing ---------
#plot Date = GetYYYYMMDD();
#plot Expiry = (OptionExpiryDate);
#plot DTE  = AbsValue(CountTradingDays(Date, Expiry));

#-----------------------------------------------------------------------------------------------------------------#
# Option Chain Data Gathering


# Total Put Open Interest for selected chain depth and expiry series
def TotalPutOpenInterest =
    fold poiIndex = -(StrikeDepth) to StrikeDepth
    with poi = 0
    do
        if DataType == DataType.OpenInterest and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * poiIndex)))
        )
        then poi + open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * poiIndex)))
        else 0
;


# Total Call Open Interest for selected chain depth and expiry series
def TotalCallOpenInterest =
    fold coiIndex = -(StrikeDepth) to StrikeDepth
    with coi = 0
    do
        if DataType == DataType.OpenInterest and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * coiIndex)))
        )
        then coi + open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * coiIndex)))
        else 0
;


# Total Put Volume for selected chain depth and expiry series
def TotalPutVolume =
    fold pvIndex = -(StrikeDepth) to StrikeDepth
    with pv = 0
    do
        if DataType == DataType.Volume and !IsNaN(
            volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + StrikeSpacing * pvIndex))
        )
        then pv + volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + StrikeSpacing * pvIndex))
        else 0
;


# Total Call Open Interest for selected chain depth and expiry series
def TotalCallVolume =
    fold cvIndex = -(StrikeDepth) to StrikeDepth
    with cv = 0
    do
        if DataType == DataType.Volume and !IsNaN(
            volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + StrikeSpacing * cvIndex))
        )
        then cv + volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + StrikeSpacing * cvIndex))
        else 0
;


#-----------------------------------------------------------------------------------------------------------------#
# Greeks Calculations
#
# K  - Option strike price
# N  - Standard normal cumulative distribution function
# r  - Risk free interest rate
# IV - Volatility of the underlying
# S  - Price of the underlying
# t  - Time to option's expiry
#
# d1    = (ln(S/K) + (r + (sqr(IV)/2))t) / (? (sqrt(t)))
# d2    = e -(sqr(d1) / 2) / sqrt(2*pi)
#
# Delta = N(d1)
# Gamma = (d2) / S(IV(sqrt(t)))
# Theta = ((S d2))IV) / 2 sqrt(t)) - (rK e(rt)N(d4))
#         where phi(d3) = (exp(-(sqr(x)/2))) / (2 * sqrt(t))
#         where d4 = d1 - IV(sqrt(t))
# Vega  = S phi(d1) Sqrt(t)


# TODO: Figure out how to dynamically get this value
def DayToExpiry = AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1);

# Get the implied volatility for calculations
# Input: series is the expiry starting at 1 and raising by 1 for each next expiry
def IV = SeriesVolatility(series = OptionSeries);


def K = CenterStrike;
def S = close;


def r = GetInterestRate();
def t = (DayToExpiry / 365);
def d1 = (Log(S / K) + ((r + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));
def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
script N {
    input data  = 1;
    def a = AbsValue(data);
    def b1 =  .31938153;
    def b2 = -.356563782;
    def b3 = 1.781477937;
    def b4 = -1.821255978;
    def b5 = 1.330274429;
    def b6 =  .2316419;
    def e = 1 / (1 + b6 * a);
    def i = 1 - 1 / Sqrt(2 * Double.Pi) * Exp(-Power(a, 2) / 2) *
           (b1 * e + b2 * e * e + b3 *
            Power(e, 3) + b4 * Power(e, 4) + b5 * Power(e, 5));
    plot CND = if data < 0
               then 1 - i
               else i;
}


# TODO: These values don't quite line up
# My background on options pricing models is not very good

# Delta
def Delta = N(d1);

# Gamma
def Gamma = d2 / (S * (IV * Sqrt(t)));

# Theta
def Theta = -(-(S*d2*IV*(.5000)/
             (2*sqrt(t)))-
             (r*(exp(-r*t)*K))*N(d2)+(S*N(d1)*(.5000)))/365;
# (.5000) variant less than .5 e(X/t)

# Vega
def Vega = (S*d2*sqrt(t))/100;


script SpotGamma {
    input DTE = 0;
    input K = 0;
    input Series = 0;
    def S = close;
    def t = (DTE / 365);
    def IV = SeriesVolatility(series = Series);
    def d1 = (Log(S / K) + ((GetInterestRate() + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));

    # Delta
    def Delta = N(d1);

    # Gamma
    def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
    def Gamma = d2 / (S * (IV * Sqrt(t)));
    
    plot SpotGamma = Gamma;
}


# Total Put Gamma Exposure for selected chain depth and expiry series
def TotalPutGammaExposure =
    fold pgexIndex = -(StrikeDepth) to StrikeDepth
    with pgex = 0
    do
        if DataType == DataType.GammaExposure and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * pgexIndex)))
        )
        then pgex +
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * pgexIndex))) *
            Sqr(OptionPrice((CenterStrike + (StrikeSpacing * pgexIndex)), yes, AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), close, SeriesVolatility(series = OptionSeries), no, 0.0, GetInterestRate())) * 0.01 *
            #SpotGamma(AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), (CenterStrike + (StrikeSpacing * pgexIndex)), OptionSeries) * # Honestly why cant I put the index inside here :(
            (Exp(-(Sqr((Log(close / (CenterStrike + (StrikeSpacing * pgexIndex))) + ((GetInterestRate() + (Sqr(SeriesVolatility(series = OptionSeries)) / 2)) * (AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))) / (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365)))) / 2)) / Sqrt(2 * Double.Pi) / (close * (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))))) *
            100 *
            -1
        else 0
;

# Total Call Gamma Exposure for selected chain depth and expiry series
def TotalCallGammaExposure =
    fold cgexIndex = -(StrikeDepth) to StrikeDepth
    with cgex = 0
    do
        if DataType == DataType.GammaExposure and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * cgexIndex)))
        )
        then cgex +
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * cgexIndex))) *
            Sqr(OptionPrice((CenterStrike + (StrikeSpacing * cgexIndex)), no, AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), close, SeriesVolatility(series = OptionSeries), no, 0.0, GetInterestRate())) * 0.01 *
            #SpotGamma(AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), (CenterStrike + (StrikeSpacing * 0)), OptionSeries) * # Honestly why cant I put the index inside here :(
            (Exp(-(Sqr((Log(close / (CenterStrike + (StrikeSpacing * cgexIndex))) + ((GetInterestRate() + (Sqr(SeriesVolatility(series = OptionSeries)) / 2)) * (AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))) / (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365)))) / 2)) / Sqrt(2 * Double.Pi) / (close * (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))))) *
            100
        else 0
;

plot GammaExposure = TotalPutGammaExposure + TotalCallGammaExposure;


#-----------------------------------------------------------------------------------------------------------------#
# Visuals

# Selected DataType
AddLabel(yes, DataType, Color.LIGHT_GRAY);

# Selected Series
AddLabel(yes, Series, Color.LIGHT_GRAY);

# Center Strike Label
AddLabel(ShowStrikeInfo, "Center Strike: " + AsDollars(CenterStrike), Color.LIGHT_GRAY);

# Chain Depth Label
AddLabel(ShowStrikeInfo, "Strike Depth: +/-" + StrikeDepth, Color.LIGHT_GRAY);

# Strike Spacing Label
AddLabel(ShowStrikeInfo, "Strike Spacing: " + AsDollars(StrikeSpacing), Color.LIGHT_GRAY);

# Current ATM Options Labels
Addlabel(ShowStrikeInfo, "ATM Put: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike), Color.LIGHT_RED);
Addlabel(ShowStrikeInfo, "ATM Call: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike), Color.LIGHT_GREEN);

# Create a center line
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.WHITE);

# Call Open Interest
plot CallOpenInterest = TotalCallOpenInterest;
CallOpenInterest.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels and DataType == DataType.OpenInterest, "CallOI: " + CallOpenInterest, GlobalColor("Call"));

# Put Open Interest
plot PutOpenInterest = -(TotalPutOpenInterest); # Make negative to flip under axis
PutOpenInterest.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
PutOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
PutOpenInterest.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels and DataType == DataType.OpenInterest, "PutOI: " + TotalPutOpenInterest, GlobalColor("Put"));

# Create Clouds for Open Interest
AddCloud(
    if ShowClouds and DataType == DataType.OpenInterest then CallOpenInterest else Double.NaN,
    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
    GlobalColor("CallCloud"), GlobalColor("PutCloud")
);
AddCloud(
    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
    if ShowClouds and DataType == DataType.OpenInterest then PutOpenInterest else Double.NaN,
    GlobalColor("PutCloud"), GlobalColor("CallCloud")
);

# Hull Moving Average of Put Open Interest
plot PutOpenInterestAverage = hullmovingavg(PutOpenInterest);
PutOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
PutOpenInterestAverage.SetDefaultColor(Color.ORANGE);
PutOpenInterestAverage.SetStyle(Curve.MEDIUM_DASH);

# Hull Moving Average of Call Open Interest
plot CallOpenInterestAverage = hullmovingavg(CallOpenInterest);
CallOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
CallOpenInterestAverage.SetDefaultColor(Color.LIGHT_GREEN);
CallOpenInterestAverage.SetStyle(Curve.MEDIUM_DASH);

# Color Gradient of Total Average Open Interest
#plot TotalOpenInterestAverage = average(CallOpenInterest + PutOpenInterest);
#TotalOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
#TotalOpenInterestAverage.SetLineWeight(3);
#TotalOpenInterestAverage.DefineColor("Default", Color.YELLOW);
#TotalOpenInterestAverage.DefineColor("Highest", Color.GREEN);
#TotalOpenInterestAverage.DefineColor("Lowest", Color.RED);
#TotalOpenInterestAverage.AssignNormGradientColor(14, TotalOpenInterestAverage.Color("Lowest"), TotalOpenInterestAverage.Color("Highest"));
#AddCloud(
#    if ShowClouds and DataType == DataType.OpenInterest then TotalOpenInterestAverage else Double.NaN,
#    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
#    color.GREEN, color.RED
#);

# Call Volume
plot CallVolume = TotalCallVolume;
CallVolume.SetHiding(!ShowLines and DataType == DataType.Volume);
CallVolume.SetPaintingStrategy(PaintingStrategy.LINE);
CallVolume.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels and DataType == DataType.Volume, "CallVol: " + CallVolume, GlobalColor("Call"));

# Put Volume
plot PutVolume = -(TotalPutVolume); # Make negative to flip under axis
PutVolume.SetHiding(!ShowLines and DataType == DataType.Volume);
PutVolume.SetPaintingStrategy(PaintingStrategy.LINE);
PutVolume.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels and DataType == DataType.Volume, "PutVol: " + TotalPutVolume, GlobalColor("Put"));

# Create Clouds for Volume
AddCloud(
    if ShowClouds and DataType == DataType.Volume then CallVolume else Double.NaN,
    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
    GlobalColor("CallCloud"), GlobalColor("PutCloud")
);
AddCloud(
    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
    if ShowClouds and DataType == DataType.Volume then PutVolume else Double.NaN,
    GlobalColor("PutCloud"), GlobalColor("CallCloud")
);

# Hull Moving Average of Put Volume
plot PutVolumeAverage = hullmovingavg(PutVolume);
PutVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
PutVolumeAverage.SetDefaultColor(Color.ORANGE);
PutVolumeAverage.SetStyle(Curve.MEDIUM_DASH);

# Hull Moving Average of Call Volume
plot CallVolumeAverage = hullmovingavg(CallVolume);
CallVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
CallVolumeAverage.SetDefaultColor(Color.LIGHT_GREEN);
CallVolumeAverage.SetStyle(Curve.MEDIUM_DASH);

# Color Gradient of Total Average Volume
#plot TotalVolumeAverage = average(CallVolume + PutVolume);
#TotalVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
#TotalVolumeAverage.SetLineWeight(3);
#TotalVolumeAverage.DefineColor("Default", Color.YELLOW);
#TotalVolumeAverage.DefineColor("Highest", Color.GREEN);
#TotalVolumeAverage.DefineColor("Lowest", Color.RED);
#TotalVolumeAverage.AssignNormGradientColor(14, TotalVolumeAverage.Color("Lowest"), TotalVolumeAverage.Color("Highest"));
#AddCloud(
#    if ShowClouds and DataType == DataType.Volume then TotalVolumeAverage else Double.NaN,
#    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
#    Color.GREEN, Color.RED
#);

# Greeks Labels
AddLabel(ShowGreeks, "Delta: " + Delta, Color.WHITE);
AddLabel(ShowGreeks, "Gamma: " + Gamma, Color.WHITE);
AddLabel(ShowGreeks, "Theta: " + Theta, Color.WHITE);
AddLabel(ShowGreeks, "Vega: " + Vega, Color.WHITE);
 
Last edited:

Angrybear

Member
VIP
Here is an updated version.
I've massively simplified the fold logic in my opinion. (I think there was a bug in the old code anyways where I was adding ATM data twice)

I've also added gamma exposure based on the math here: https://perfiliev.co.uk/market-commentary/how-to-calculate-gamma-exposure-and-zero-gamma-level/
Although I'm not sure its correct, as the math is quite complicated, and my background is not in finance.
I'm also not sure how to interpret the data just yet ...
It doesn't make it any easier that I realized you can't call a custom script inside the fold using the index of the fold o_O :unsure:
As a workaround, I had to try and implement the formula for deriving the spot gamma in a single line within the fold, as part of the larger calculation for the spot gamma exposure at that level. This is the mobius Greeks formula (which uses Black Scholes), using the OptionPrice() and SeriesVolatility() as inputs.

Some Notes:
  1. Ideally to get the 'Total Market Gamma Exposure' we would need to perform this calculation on all strikes, for every expiry, for every day. I have only tested going +/- 5 to 10 strikes out, for weekly series so far, and it already takes quite a bit of time. For reference I'm running an i9-9900k with 64GB DDR4 RAM, Running this at 5 strike depth weekly on SPY is taking around 15 -20 minutes (hard to tell when its actually done running so I'm just going by when my pc fans/water cooler fans stop spinning at max rpm lol)
  2. For number 1 to work, we would need to further modify the date/strike logic to be able to pick up more than just next series expiries. I also don't know how this effects the SeriesVolatility function.
  3. Maybe another approach would be to modify this so that we can pick up the first couple close to the money strikes, and then skip to picking up a couple way out of the money strikes to get a better overall picture.
Screenshot:

pQI47if.png


Code:

Code:
#hint: Options Hacker \n This study lets you scan the options chain for a series and given depth. \n<b>Warning: Setting the StrikeDepth to large values requires significant processing power, and will result in slow loading times.</b>


#-----------------------------------------------------------------------------------------------------------------#
# Settings

# Colors
DefineGlobalColor("Call", Color.GREEN);
DefineGlobalColor("Put", Color.RED);
DefineGlobalColor("CallCloud",Color.DARK_GREEN);
DefineGlobalColor("PutCloud",Color.DARK_RED);

#hint Mode: The mode to select an option symbol. \n AUTO will try to find the option symbol based on the Series and StrikeDepth inputs. \n MANUAL allow an override of the AUTO behavior by using the ManualCenterStrike and ManualStrikeSpacing inputs to determine the option symbol.
input Mode = {default AUTO, MANUAL};

#hint Series: The option expiration series to search. \n This value is used to determine the option symbol.
input Series = {
    default Weekly,
    Month1,
    Month2,
    Month3,
    Month4,
    Month5,
    Month6,
    Month7,
    Month8,
    Month9
};

#hint DataType: The type of option data to show.
input DataType = {default OpenInterest, Volume, GammaExposure};

#hint StrikeDepth: The level of depth to search a series. (+/- this far from ATM)
input StrikeDepth = 10;

#hint CenterStrikeOffset: The offset to use when calculating the center strike based on close price. \n Examples: \n   1 = nearest $1 interval \n   10 = nearest $10 interval.
input CenterStrikeOffset = 1.0;

#hint MaxStrikeSpacing: The maximum dollar amount between two adjacent contracts.
input MaxStrikeSpacing = 25;

#hint ManualCenterStrike: The starting price to use when in MANUAL mode.
input ManualCenterStrike = 440;

#hint ManualStrikeSpacing: The dollar amount between two adjacent contracts to use when in MANUAL mode.
input ManualStrikeSpacing = 1.0;

#hint ShowStrikeInfo: Show the strike info labels.
input ShowStrikeInfo = yes;

#hint ShowLabels: Show the open interest labels.
input ShowLabels = yes;

#hint ShowClouds: Show the open interest clouds.
input ShowClouds = no;

#hint ShowLines: Show the open interest lines.
input ShowLines = yes;

#hint ShowGreeks: Show the estimated Greek calculation labels for the latest bar.
input ShowGreeks = yes;


#-----------------------------------------------------------------------------------------------------------------#
# Date, Symbol, and Strike

# OptionSeries is the expiry starting at 1 and raising by one for each next expiry.
def OptionSeries;
switch (Series) {
    case Weekly:
        OptionSeries = 1;
    case Month1:
        OptionSeries = 2;
    case Month2:
        OptionSeries = 3;
    case Month3:
        OptionSeries = 4;
    case Month4:
        OptionSeries = 5;
    case Month5:
        OptionSeries = 6;
    case Month6:
        OptionSeries = 7;
    case Month7:
        OptionSeries = 8;
    case Month8:
        OptionSeries = 9;
    case Month9:
        OptionSeries = 10;
};

# Open price at Regular Trading Hours
def RTHopen = open(period = AggregationPeriod.DAY);

# Current year, month, day, and date
def CurrentYear = GetYear(); # number of current bar in CST
def CurrentMonth = GetMonth(); # 1 - 12
def CurrentDay = GetDay(); # 1 - 365 (366 for leap year)
def CurrentDate = GetYYYYMMDD(); # date of the current bar in the YYYYMMDD

# Current day of this month
def CurrentDayOfMonth = GetDayOfMonth(CurrentDate);

# Get the first day of this month - 1 (Monday) to 7 (Sunday)
def FirstDayThisMonth = GetDayOfWeek((CurrentYear * 10000) + (CurrentMonth * 100) + 1);

# Get the first upcoming friday
def FirstUpcomingFriday =
    if FirstDayThisMonth < 6 then 6 - FirstDayThisMonth
    else if FirstDayThisMonth == 6 then 7
    else 6
;

# Get the second, third, and fourth upcoming fridays
def SecondUpcomingFriday = FirstUpcomingFriday + 7;
def ThirdUpcomingFriday = FirstUpcomingFriday + 14;
def FourthUpcomingFriday = FirstUpcomingFriday + 21;

# Pick up all Fridays of the current month for weekly options
def RollDOM = FirstUpcomingFriday + 21;
def ExpMonth1 =
    if RollDOM > CurrentDayOfMonth then CurrentMonth + OptionSeries - 1
    else CurrentMonth + OptionSeries
;

# Options month input
def ExpMonth2 = if ExpMonth1 > 12 then ExpMonth1 - 12 else ExpMonth1;

# Options year input
def ExpYear = if ExpMonth1 > 12 then CurrentYear + 1 else CurrentYear;

# First friday expiry calc
def ExpDay1DOW = GetDayOfWeek(ExpYear * 10000 + ExpMonth2 * 100 + 1);
def ExpFirstFridayDOM =
    if ExpDay1DOW < 6 then 6 - ExpDay1DOW
    else if ExpDay1DOW == 6 then 7
    else 6
;

# Options code day of month input
def ExpDOM =
    if CurrentDayOfMonth < ExpFirstFridayDOM -1 then FirstUpcomingFriday
    else if between(CurrentDayOfMonth, ExpFirstFridayDOM, SecondUpcomingFriday - 1) then SecondUpcomingFriday
    else if between(CurrentDayOfMonth, SecondUpcomingFriday, ThirdUpcomingFriday - 1) then ThirdUpcomingFriday
    else if between(CurrentDayOfMonth, ThirdUpcomingFriday, FourthUpcomingFriday - 1) then FourthUpcomingFriday
    else ExpFirstFridayDOM
;

# Centerstrike
def CenterStrike =
    if (Mode == Mode.AUTO and !IsNaN(close)) then Round(close / CenterStrikeOffset, 0) * CenterStrikeOffset
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualCenterStrike
    else CenterStrike[1]
;

# This is still some voodoo to me ...
def OptionExpiryDate = ExpYear * 10000 + ExpMonth2 * 100 + ExpDOM + 1;

# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OpenInterest or DataType == DataType.GammaExposure and !IsNaN(
        open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
    )
    then MaxStrikeSpacing - i
    else if DataType == DataType.Volume and !IsNaN(
        volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
    )
    then MaxStrikeSpacing - i
    else spacing
;
def StrikeSpacing =
    if (Mode == Mode.AUTO and !IsNaN(close)) then StrikeSpacingC
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualStrikeSpacing
    else StrikeSpacing[1]
;

# Testing ---------
#plot Date = GetYYYYMMDD();
#plot Expiry = (OptionExpiryDate);
#plot DTE  = AbsValue(CountTradingDays(Date, Expiry));

#-----------------------------------------------------------------------------------------------------------------#
# Option Chain Data Gathering


# Total Put Open Interest for selected chain depth and expiry series
def TotalPutOpenInterest =
    fold poiIndex = -(StrikeDepth) to StrikeDepth
    with poi = 0
    do
        if DataType == DataType.OpenInterest and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * poiIndex)))
        )
        then poi + open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * poiIndex)))
        else 0
;


# Total Call Open Interest for selected chain depth and expiry series
def TotalCallOpenInterest =
    fold coiIndex = -(StrikeDepth) to StrikeDepth
    with coi = 0
    do
        if DataType == DataType.OpenInterest and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * coiIndex)))
        )
        then coi + open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * coiIndex)))
        else 0
;


# Total Put Volume for selected chain depth and expiry series
def TotalPutVolume =
    fold pvIndex = -(StrikeDepth) to StrikeDepth
    with pv = 0
    do
        if DataType == DataType.Volume and !IsNaN(
            volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + StrikeSpacing * pvIndex))
        )
        then pv + volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + StrikeSpacing * pvIndex))
        else 0
;


# Total Call Open Interest for selected chain depth and expiry series
def TotalCallVolume =
    fold cvIndex = -(StrikeDepth) to StrikeDepth
    with cv = 0
    do
        if DataType == DataType.Volume and !IsNaN(
            volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + StrikeSpacing * cvIndex))
        )
        then cv + volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + StrikeSpacing * cvIndex))
        else 0
;


#-----------------------------------------------------------------------------------------------------------------#
# Greeks Calculations
#
# K  - Option strike price
# N  - Standard normal cumulative distribution function
# r  - Risk free interest rate
# IV - Volatility of the underlying
# S  - Price of the underlying
# t  - Time to option's expiry
#
# d1    = (ln(S/K) + (r + (sqr(IV)/2))t) / (? (sqrt(t)))
# d2    = e -(sqr(d1) / 2) / sqrt(2*pi)
#
# Delta = N(d1)
# Gamma = (d2) / S(IV(sqrt(t)))
# Theta = ((S d2))IV) / 2 sqrt(t)) - (rK e(rt)N(d4))
#         where phi(d3) = (exp(-(sqr(x)/2))) / (2 * sqrt(t))
#         where d4 = d1 - IV(sqrt(t))
# Vega  = S phi(d1) Sqrt(t)


# TODO: Figure out how to dynamically get this value
def DayToExpiry = AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1);

# Get the implied volatility for calculations
# Input: series is the expiry starting at 1 and raising by 1 for each next expiry
def IV = SeriesVolatility(series = OptionSeries);


def K = CenterStrike;
def S = close;


def r = GetInterestRate();
def t = (DayToExpiry / 365);
def d1 = (Log(S / K) + ((r + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));
def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
script N {
    input data  = 1;
    def a = AbsValue(data);
    def b1 =  .31938153;
    def b2 = -.356563782;
    def b3 = 1.781477937;
    def b4 = -1.821255978;
    def b5 = 1.330274429;
    def b6 =  .2316419;
    def e = 1 / (1 + b6 * a);
    def i = 1 - 1 / Sqrt(2 * Double.Pi) * Exp(-Power(a, 2) / 2) *
           (b1 * e + b2 * e * e + b3 *
            Power(e, 3) + b4 * Power(e, 4) + b5 * Power(e, 5));
    plot CND = if data < 0
               then 1 - i
               else i;
}


# TODO: These values don't quite line up
# My background on options pricing models is not very good

# Delta
def Delta = N(d1);

# Gamma
def Gamma = d2 / (S * (IV * Sqrt(t)));

# Theta
def Theta = -(-(S*d2*IV*(.5000)/
             (2*sqrt(t)))-
             (r*(exp(-r*t)*K))*N(d2)+(S*N(d1)*(.5000)))/365;
# (.5000) variant less than .5 e(X/t)

# Vega
def Vega = (S*d2*sqrt(t))/100;


script SpotGamma {
    input DTE = 0;
    input K = 0;
    input Series = 0;
    def S = close;
    def t = (DTE / 365);
    def IV = SeriesVolatility(series = Series);
    def d1 = (Log(S / K) + ((GetInterestRate() + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));

    # Delta
    def Delta = N(d1);

    # Gamma
    def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
    def Gamma = d2 / (S * (IV * Sqrt(t)));
   
    plot SpotGamma = Gamma;
}


# Total Put Gamma Exposure for selected chain depth and expiry series
def TotalPutGammaExposure =
    fold pgexIndex = -(StrikeDepth) to StrikeDepth
    with pgex = 0
    do
        if DataType == DataType.GammaExposure and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * pgexIndex)))
        )
        then pgex +
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * pgexIndex))) *
            Sqr(OptionPrice((CenterStrike + (StrikeSpacing * pgexIndex)), yes, AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), close, SeriesVolatility(series = OptionSeries), no, 0.0, GetInterestRate())) * 0.01 *
            #SpotGamma(AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), (CenterStrike + (StrikeSpacing * pgexIndex)), OptionSeries) * # Honestly why cant I put the index inside here :(
            (Exp(-(Sqr((Log(close / (CenterStrike + (StrikeSpacing * pgexIndex))) + ((GetInterestRate() + (Sqr(SeriesVolatility(series = OptionSeries)) / 2)) * (AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))) / (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365)))) / 2)) / Sqrt(2 * Double.Pi) / (close * (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))))) *
            100 *
            -1
        else 0
;

# Total Call Gamma Exposure for selected chain depth and expiry series
def TotalCallGammaExposure =
    fold cgexIndex = -(StrikeDepth) to StrikeDepth
    with cgex = 0
    do
        if DataType == DataType.GammaExposure and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * cgexIndex)))
        )
        then cgex +
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * cgexIndex))) *
            Sqr(OptionPrice((CenterStrike + (StrikeSpacing * cgexIndex)), no, AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), close, SeriesVolatility(series = OptionSeries), no, 0.0, GetInterestRate())) * 0.01 *
            #SpotGamma(AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), (CenterStrike + (StrikeSpacing * 0)), OptionSeries) * # Honestly why cant I put the index inside here :(
            (Exp(-(Sqr((Log(close / (CenterStrike + (StrikeSpacing * cgexIndex))) + ((GetInterestRate() + (Sqr(SeriesVolatility(series = OptionSeries)) / 2)) * (AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))) / (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365)))) / 2)) / Sqrt(2 * Double.Pi) / (close * (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))))) *
            100
        else 0
;

plot GammaExposure = TotalPutGammaExposure + TotalCallGammaExposure;


#-----------------------------------------------------------------------------------------------------------------#
# Visuals

# Selected DataType
AddLabel(yes, DataType, Color.LIGHT_GRAY);

# Selected Series
AddLabel(yes, Series, Color.LIGHT_GRAY);

# Center Strike Label
AddLabel(ShowStrikeInfo, "Center Strike: " + AsDollars(CenterStrike), Color.LIGHT_GRAY);

# Chain Depth Label
AddLabel(ShowStrikeInfo, "Strike Depth: +/-" + StrikeDepth, Color.LIGHT_GRAY);

# Strike Spacing Label
AddLabel(ShowStrikeInfo, "Strike Spacing: " + AsDollars(StrikeSpacing), Color.LIGHT_GRAY);

# Current ATM Options Labels
Addlabel(ShowStrikeInfo, "ATM Put: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike), Color.LIGHT_RED);
Addlabel(ShowStrikeInfo, "ATM Call: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike), Color.LIGHT_GREEN);

# Create a center line
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.WHITE);

# Call Open Interest
plot CallOpenInterest = TotalCallOpenInterest;
CallOpenInterest.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels and DataType == DataType.OpenInterest, "CallOI: " + CallOpenInterest, GlobalColor("Call"));

# Put Open Interest
plot PutOpenInterest = -(TotalPutOpenInterest); # Make negative to flip under axis
PutOpenInterest.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
PutOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
PutOpenInterest.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels and DataType == DataType.OpenInterest, "PutOI: " + TotalPutOpenInterest, GlobalColor("Put"));

# Create Clouds for Open Interest
AddCloud(
    if ShowClouds and DataType == DataType.OpenInterest then CallOpenInterest else Double.NaN,
    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
    GlobalColor("CallCloud"), GlobalColor("PutCloud")
);
AddCloud(
    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
    if ShowClouds and DataType == DataType.OpenInterest then PutOpenInterest else Double.NaN,
    GlobalColor("PutCloud"), GlobalColor("CallCloud")
);

# Hull Moving Average of Put Open Interest
plot PutOpenInterestAverage = hullmovingavg(PutOpenInterest);
PutOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
PutOpenInterestAverage.SetDefaultColor(Color.ORANGE);
PutOpenInterestAverage.SetStyle(Curve.MEDIUM_DASH);

# Hull Moving Average of Call Open Interest
plot CallOpenInterestAverage = hullmovingavg(CallOpenInterest);
CallOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
CallOpenInterestAverage.SetDefaultColor(Color.LIGHT_GREEN);
CallOpenInterestAverage.SetStyle(Curve.MEDIUM_DASH);

# Color Gradient of Total Average Open Interest
#plot TotalOpenInterestAverage = average(CallOpenInterest + PutOpenInterest);
#TotalOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
#TotalOpenInterestAverage.SetLineWeight(3);
#TotalOpenInterestAverage.DefineColor("Default", Color.YELLOW);
#TotalOpenInterestAverage.DefineColor("Highest", Color.GREEN);
#TotalOpenInterestAverage.DefineColor("Lowest", Color.RED);
#TotalOpenInterestAverage.AssignNormGradientColor(14, TotalOpenInterestAverage.Color("Lowest"), TotalOpenInterestAverage.Color("Highest"));
#AddCloud(
#    if ShowClouds and DataType == DataType.OpenInterest then TotalOpenInterestAverage else Double.NaN,
#    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
#    color.GREEN, color.RED
#);

# Call Volume
plot CallVolume = TotalCallVolume;
CallVolume.SetHiding(!ShowLines and DataType == DataType.Volume);
CallVolume.SetPaintingStrategy(PaintingStrategy.LINE);
CallVolume.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels and DataType == DataType.Volume, "CallVol: " + CallVolume, GlobalColor("Call"));

# Put Volume
plot PutVolume = -(TotalPutVolume); # Make negative to flip under axis
PutVolume.SetHiding(!ShowLines and DataType == DataType.Volume);
PutVolume.SetPaintingStrategy(PaintingStrategy.LINE);
PutVolume.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels and DataType == DataType.Volume, "PutVol: " + TotalPutVolume, GlobalColor("Put"));

# Create Clouds for Volume
AddCloud(
    if ShowClouds and DataType == DataType.Volume then CallVolume else Double.NaN,
    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
    GlobalColor("CallCloud"), GlobalColor("PutCloud")
);
AddCloud(
    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
    if ShowClouds and DataType == DataType.Volume then PutVolume else Double.NaN,
    GlobalColor("PutCloud"), GlobalColor("CallCloud")
);

# Hull Moving Average of Put Volume
plot PutVolumeAverage = hullmovingavg(PutVolume);
PutVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
PutVolumeAverage.SetDefaultColor(Color.ORANGE);
PutVolumeAverage.SetStyle(Curve.MEDIUM_DASH);

# Hull Moving Average of Call Volume
plot CallVolumeAverage = hullmovingavg(CallVolume);
CallVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
CallVolumeAverage.SetDefaultColor(Color.LIGHT_GREEN);
CallVolumeAverage.SetStyle(Curve.MEDIUM_DASH);

# Color Gradient of Total Average Volume
#plot TotalVolumeAverage = average(CallVolume + PutVolume);
#TotalVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
#TotalVolumeAverage.SetLineWeight(3);
#TotalVolumeAverage.DefineColor("Default", Color.YELLOW);
#TotalVolumeAverage.DefineColor("Highest", Color.GREEN);
#TotalVolumeAverage.DefineColor("Lowest", Color.RED);
#TotalVolumeAverage.AssignNormGradientColor(14, TotalVolumeAverage.Color("Lowest"), TotalVolumeAverage.Color("Highest"));
#AddCloud(
#    if ShowClouds and DataType == DataType.Volume then TotalVolumeAverage else Double.NaN,
#    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
#    Color.GREEN, Color.RED
#);

# Greeks Labels
AddLabel(ShowGreeks, "Delta: " + Delta, Color.WHITE);
AddLabel(ShowGreeks, "Gamma: " + Gamma, Color.WHITE);
AddLabel(ShowGreeks, "Theta: " + Theta, Color.WHITE);
AddLabel(ShowGreeks, "Vega: " + Vega, Color.WHITE);
I think squeeze @SqueezeMetrics provides a good explaination for Gamma Exposure (GEX) https://squeezemetrics.com/monitor/docs
check out his work as well, sorry for digging the rabbit hole even deeper lol, nice work!
 

sudoshu

Member
I think squeeze @SqueezeMetrics provides a good explaination for Gamma Exposure (GEX) https://squeezemetrics.com/monitor/docs
check out his work as well, sorry for digging the rabbit hole even deeper lol, nice work!
HAH, no problem at all, I actually did read that one already too! Ideally I'd like to get something like that going but I think it is far beyond the capabilities of TOS. Would have to utilize the api and build out the system in a fast compiled language I think. The problem for me is historical options data is not cheap ... I'm working to get a system together to start streaming and storing the api feed going forward but can't test unless market is open :( gonna be a long trial and error process I think.

If your interested in that, I found this nice series on a data pipeline for doing just that. Haven't had a chance to play with it yet.
https://github.com/chrischow/open_options_chains
 

ziongotoptions

Active member
Here is an updated version.
I've massively simplified the fold logic in my opinion. (I think there was a bug in the old code anyways where I was adding ATM data twice)

I've also added gamma exposure based on the math here: https://perfiliev.co.uk/market-commentary/how-to-calculate-gamma-exposure-and-zero-gamma-level/
Although I'm not sure its correct, as the math is quite complicated, and my background is not in finance.
I'm also not sure how to interpret the data just yet ...
It doesn't make it any easier that I realized you can't call a custom script inside the fold using the index of the fold o_O :unsure:
As a workaround, I had to try and implement the formula for deriving the spot gamma in a single line within the fold, as part of the larger calculation for the spot gamma exposure at that level. This is the mobius Greeks formula (which uses Black Scholes), using the OptionPrice() and SeriesVolatility() as inputs.

Some Notes:
  1. Ideally to get the 'Total Market Gamma Exposure' we would need to perform this calculation on all strikes, for every expiry, for every day. I have only tested going +/- 5 to 10 strikes out, for weekly series so far, and it already takes quite a bit of time. For reference I'm running an i9-9900k with 64GB DDR4 RAM, Running this at 5 strike depth weekly on SPY is taking around 15 -20 minutes (hard to tell when its actually done running so I'm just going by when my pc fans/water cooler fans stop spinning at max rpm lol)
  2. For number 1 to work, we would need to further modify the date/strike logic to be able to pick up more than just next series expiries. I also don't know how this effects the SeriesVolatility function.
  3. Maybe another approach would be to modify this so that we can pick up the first couple close to the money strikes, and then skip to picking up a couple way out of the money strikes to get a better overall picture.
Screenshot:

pQI47if.png


Code:

Code:
#hint: Options Hacker \n This study lets you scan the options chain for a series and given depth. \n<b>Warning: Setting the StrikeDepth to large values requires significant processing power, and will result in slow loading times.</b>


#-----------------------------------------------------------------------------------------------------------------#
# Settings

# Colors
DefineGlobalColor("Call", Color.GREEN);
DefineGlobalColor("Put", Color.RED);
DefineGlobalColor("CallCloud",Color.DARK_GREEN);
DefineGlobalColor("PutCloud",Color.DARK_RED);

#hint Mode: The mode to select an option symbol. \n AUTO will try to find the option symbol based on the Series and StrikeDepth inputs. \n MANUAL allow an override of the AUTO behavior by using the ManualCenterStrike and ManualStrikeSpacing inputs to determine the option symbol.
input Mode = {default AUTO, MANUAL};

#hint Series: The option expiration series to search. \n This value is used to determine the option symbol.
input Series = {
    default Weekly,
    Month1,
    Month2,
    Month3,
    Month4,
    Month5,
    Month6,
    Month7,
    Month8,
    Month9
};

#hint DataType: The type of option data to show.
input DataType = {default OpenInterest, Volume, GammaExposure};

#hint StrikeDepth: The level of depth to search a series. (+/- this far from ATM)
input StrikeDepth = 10;

#hint CenterStrikeOffset: The offset to use when calculating the center strike based on close price. \n Examples: \n   1 = nearest $1 interval \n   10 = nearest $10 interval.
input CenterStrikeOffset = 1.0;

#hint MaxStrikeSpacing: The maximum dollar amount between two adjacent contracts.
input MaxStrikeSpacing = 25;

#hint ManualCenterStrike: The starting price to use when in MANUAL mode.
input ManualCenterStrike = 440;

#hint ManualStrikeSpacing: The dollar amount between two adjacent contracts to use when in MANUAL mode.
input ManualStrikeSpacing = 1.0;

#hint ShowStrikeInfo: Show the strike info labels.
input ShowStrikeInfo = yes;

#hint ShowLabels: Show the open interest labels.
input ShowLabels = yes;

#hint ShowClouds: Show the open interest clouds.
input ShowClouds = no;

#hint ShowLines: Show the open interest lines.
input ShowLines = yes;

#hint ShowGreeks: Show the estimated Greek calculation labels for the latest bar.
input ShowGreeks = yes;


#-----------------------------------------------------------------------------------------------------------------#
# Date, Symbol, and Strike

# OptionSeries is the expiry starting at 1 and raising by one for each next expiry.
def OptionSeries;
switch (Series) {
    case Weekly:
        OptionSeries = 1;
    case Month1:
        OptionSeries = 2;
    case Month2:
        OptionSeries = 3;
    case Month3:
        OptionSeries = 4;
    case Month4:
        OptionSeries = 5;
    case Month5:
        OptionSeries = 6;
    case Month6:
        OptionSeries = 7;
    case Month7:
        OptionSeries = 8;
    case Month8:
        OptionSeries = 9;
    case Month9:
        OptionSeries = 10;
};

# Open price at Regular Trading Hours
def RTHopen = open(period = AggregationPeriod.DAY);

# Current year, month, day, and date
def CurrentYear = GetYear(); # number of current bar in CST
def CurrentMonth = GetMonth(); # 1 - 12
def CurrentDay = GetDay(); # 1 - 365 (366 for leap year)
def CurrentDate = GetYYYYMMDD(); # date of the current bar in the YYYYMMDD

# Current day of this month
def CurrentDayOfMonth = GetDayOfMonth(CurrentDate);

# Get the first day of this month - 1 (Monday) to 7 (Sunday)
def FirstDayThisMonth = GetDayOfWeek((CurrentYear * 10000) + (CurrentMonth * 100) + 1);

# Get the first upcoming friday
def FirstUpcomingFriday =
    if FirstDayThisMonth < 6 then 6 - FirstDayThisMonth
    else if FirstDayThisMonth == 6 then 7
    else 6
;

# Get the second, third, and fourth upcoming fridays
def SecondUpcomingFriday = FirstUpcomingFriday + 7;
def ThirdUpcomingFriday = FirstUpcomingFriday + 14;
def FourthUpcomingFriday = FirstUpcomingFriday + 21;

# Pick up all Fridays of the current month for weekly options
def RollDOM = FirstUpcomingFriday + 21;
def ExpMonth1 =
    if RollDOM > CurrentDayOfMonth then CurrentMonth + OptionSeries - 1
    else CurrentMonth + OptionSeries
;

# Options month input
def ExpMonth2 = if ExpMonth1 > 12 then ExpMonth1 - 12 else ExpMonth1;

# Options year input
def ExpYear = if ExpMonth1 > 12 then CurrentYear + 1 else CurrentYear;

# First friday expiry calc
def ExpDay1DOW = GetDayOfWeek(ExpYear * 10000 + ExpMonth2 * 100 + 1);
def ExpFirstFridayDOM =
    if ExpDay1DOW < 6 then 6 - ExpDay1DOW
    else if ExpDay1DOW == 6 then 7
    else 6
;

# Options code day of month input
def ExpDOM =
    if CurrentDayOfMonth < ExpFirstFridayDOM -1 then FirstUpcomingFriday
    else if between(CurrentDayOfMonth, ExpFirstFridayDOM, SecondUpcomingFriday - 1) then SecondUpcomingFriday
    else if between(CurrentDayOfMonth, SecondUpcomingFriday, ThirdUpcomingFriday - 1) then ThirdUpcomingFriday
    else if between(CurrentDayOfMonth, ThirdUpcomingFriday, FourthUpcomingFriday - 1) then FourthUpcomingFriday
    else ExpFirstFridayDOM
;

# Centerstrike
def CenterStrike =
    if (Mode == Mode.AUTO and !IsNaN(close)) then Round(close / CenterStrikeOffset, 0) * CenterStrikeOffset
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualCenterStrike
    else CenterStrike[1]
;

# This is still some voodoo to me ...
def OptionExpiryDate = ExpYear * 10000 + ExpMonth2 * 100 + ExpDOM + 1;

# Strike Spacing
def StrikeSpacingC =
    fold i = 1 to MaxStrikeSpacing
    with spacing = 0
    do if DataType == DataType.OpenInterest or DataType == DataType.GammaExposure and !IsNaN(
        open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
    )
    then MaxStrikeSpacing - i
    else if DataType == DataType.Volume and !IsNaN(
        volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (MaxStrikeSpacing - i)))
    )
    then MaxStrikeSpacing - i
    else spacing
;
def StrikeSpacing =
    if (Mode == Mode.AUTO and !IsNaN(close)) then StrikeSpacingC
    else if (Mode == Mode.MANUAL and !IsNaN(close)) then ManualStrikeSpacing
    else StrikeSpacing[1]
;

# Testing ---------
#plot Date = GetYYYYMMDD();
#plot Expiry = (OptionExpiryDate);
#plot DTE  = AbsValue(CountTradingDays(Date, Expiry));

#-----------------------------------------------------------------------------------------------------------------#
# Option Chain Data Gathering


# Total Put Open Interest for selected chain depth and expiry series
def TotalPutOpenInterest =
    fold poiIndex = -(StrikeDepth) to StrikeDepth
    with poi = 0
    do
        if DataType == DataType.OpenInterest and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * poiIndex)))
        )
        then poi + open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * poiIndex)))
        else 0
;


# Total Call Open Interest for selected chain depth and expiry series
def TotalCallOpenInterest =
    fold coiIndex = -(StrikeDepth) to StrikeDepth
    with coi = 0
    do
        if DataType == DataType.OpenInterest and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * coiIndex)))
        )
        then coi + open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * coiIndex)))
        else 0
;


# Total Put Volume for selected chain depth and expiry series
def TotalPutVolume =
    fold pvIndex = -(StrikeDepth) to StrikeDepth
    with pv = 0
    do
        if DataType == DataType.Volume and !IsNaN(
            volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + StrikeSpacing * pvIndex))
        )
        then pv + volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + StrikeSpacing * pvIndex))
        else 0
;


# Total Call Open Interest for selected chain depth and expiry series
def TotalCallVolume =
    fold cvIndex = -(StrikeDepth) to StrikeDepth
    with cv = 0
    do
        if DataType == DataType.Volume and !IsNaN(
            volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + StrikeSpacing * cvIndex))
        )
        then cv + volume(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + StrikeSpacing * cvIndex))
        else 0
;


#-----------------------------------------------------------------------------------------------------------------#
# Greeks Calculations
#
# K  - Option strike price
# N  - Standard normal cumulative distribution function
# r  - Risk free interest rate
# IV - Volatility of the underlying
# S  - Price of the underlying
# t  - Time to option's expiry
#
# d1    = (ln(S/K) + (r + (sqr(IV)/2))t) / (? (sqrt(t)))
# d2    = e -(sqr(d1) / 2) / sqrt(2*pi)
#
# Delta = N(d1)
# Gamma = (d2) / S(IV(sqrt(t)))
# Theta = ((S d2))IV) / 2 sqrt(t)) - (rK e(rt)N(d4))
#         where phi(d3) = (exp(-(sqr(x)/2))) / (2 * sqrt(t))
#         where d4 = d1 - IV(sqrt(t))
# Vega  = S phi(d1) Sqrt(t)


# TODO: Figure out how to dynamically get this value
def DayToExpiry = AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1);

# Get the implied volatility for calculations
# Input: series is the expiry starting at 1 and raising by 1 for each next expiry
def IV = SeriesVolatility(series = OptionSeries);


def K = CenterStrike;
def S = close;


def r = GetInterestRate();
def t = (DayToExpiry / 365);
def d1 = (Log(S / K) + ((r + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));
def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
script N {
    input data  = 1;
    def a = AbsValue(data);
    def b1 =  .31938153;
    def b2 = -.356563782;
    def b3 = 1.781477937;
    def b4 = -1.821255978;
    def b5 = 1.330274429;
    def b6 =  .2316419;
    def e = 1 / (1 + b6 * a);
    def i = 1 - 1 / Sqrt(2 * Double.Pi) * Exp(-Power(a, 2) / 2) *
           (b1 * e + b2 * e * e + b3 *
            Power(e, 3) + b4 * Power(e, 4) + b5 * Power(e, 5));
    plot CND = if data < 0
               then 1 - i
               else i;
}


# TODO: These values don't quite line up
# My background on options pricing models is not very good

# Delta
def Delta = N(d1);

# Gamma
def Gamma = d2 / (S * (IV * Sqrt(t)));

# Theta
def Theta = -(-(S*d2*IV*(.5000)/
             (2*sqrt(t)))-
             (r*(exp(-r*t)*K))*N(d2)+(S*N(d1)*(.5000)))/365;
# (.5000) variant less than .5 e(X/t)

# Vega
def Vega = (S*d2*sqrt(t))/100;


script SpotGamma {
    input DTE = 0;
    input K = 0;
    input Series = 0;
    def S = close;
    def t = (DTE / 365);
    def IV = SeriesVolatility(series = Series);
    def d1 = (Log(S / K) + ((GetInterestRate() + (Sqr(IV) / 2)) * t)) / (IV * Sqrt(t));

    # Delta
    def Delta = N(d1);

    # Gamma
    def d2 = Exp(-(Sqr(d1) / 2)) / Sqrt(2 * Double.Pi);
    def Gamma = d2 / (S * (IV * Sqrt(t)));
   
    plot SpotGamma = Gamma;
}


# Total Put Gamma Exposure for selected chain depth and expiry series
def TotalPutGammaExposure =
    fold pgexIndex = -(StrikeDepth) to StrikeDepth
    with pgex = 0
    do
        if DataType == DataType.GammaExposure and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * pgexIndex)))
        )
        then pgex +
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike + (StrikeSpacing * pgexIndex))) *
            Sqr(OptionPrice((CenterStrike + (StrikeSpacing * pgexIndex)), yes, AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), close, SeriesVolatility(series = OptionSeries), no, 0.0, GetInterestRate())) * 0.01 *
            #SpotGamma(AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), (CenterStrike + (StrikeSpacing * pgexIndex)), OptionSeries) * # Honestly why cant I put the index inside here :(
            (Exp(-(Sqr((Log(close / (CenterStrike + (StrikeSpacing * pgexIndex))) + ((GetInterestRate() + (Sqr(SeriesVolatility(series = OptionSeries)) / 2)) * (AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))) / (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365)))) / 2)) / Sqrt(2 * Double.Pi) / (close * (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))))) *
            100 *
            -1
        else 0
;

# Total Call Gamma Exposure for selected chain depth and expiry series
def TotalCallGammaExposure =
    fold cgexIndex = -(StrikeDepth) to StrikeDepth
    with cgex = 0
    do
        if DataType == DataType.GammaExposure and !IsNaN(
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * cgexIndex)))
        )
        then cgex +
            open_interest(("." + GetSymbolPart()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike + (StrikeSpacing * cgexIndex))) *
            Sqr(OptionPrice((CenterStrike + (StrikeSpacing * cgexIndex)), no, AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), close, SeriesVolatility(series = OptionSeries), no, 0.0, GetInterestRate())) * 0.01 *
            #SpotGamma(AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1), (CenterStrike + (StrikeSpacing * 0)), OptionSeries) * # Honestly why cant I put the index inside here :(
            (Exp(-(Sqr((Log(close / (CenterStrike + (StrikeSpacing * cgexIndex))) + ((GetInterestRate() + (Sqr(SeriesVolatility(series = OptionSeries)) / 2)) * (AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))) / (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365)))) / 2)) / Sqrt(2 * Double.Pi) / (close * (SeriesVolatility(series = OptionSeries) * Sqrt((AbsValue(CountTradingDays(CurrentDate, OptionExpiryDate) - 1) / 365))))) *
            100
        else 0
;

plot GammaExposure = TotalPutGammaExposure + TotalCallGammaExposure;


#-----------------------------------------------------------------------------------------------------------------#
# Visuals

# Selected DataType
AddLabel(yes, DataType, Color.LIGHT_GRAY);

# Selected Series
AddLabel(yes, Series, Color.LIGHT_GRAY);

# Center Strike Label
AddLabel(ShowStrikeInfo, "Center Strike: " + AsDollars(CenterStrike), Color.LIGHT_GRAY);

# Chain Depth Label
AddLabel(ShowStrikeInfo, "Strike Depth: +/-" + StrikeDepth, Color.LIGHT_GRAY);

# Strike Spacing Label
AddLabel(ShowStrikeInfo, "Strike Spacing: " + AsDollars(StrikeSpacing), Color.LIGHT_GRAY);

# Current ATM Options Labels
Addlabel(ShowStrikeInfo, "ATM Put: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "P" + AsPrice(CenterStrike), Color.LIGHT_RED);
Addlabel(ShowStrikeInfo, "ATM Call: " + ("." + GetSymbol()) + AsPrice(OptionExpiryDate - 20000001) + "C" + AsPrice(CenterStrike), Color.LIGHT_GREEN);

# Create a center line
plot ZeroLine = 0;
ZeroLine.SetDefaultColor(Color.WHITE);

# Call Open Interest
plot CallOpenInterest = TotalCallOpenInterest;
CallOpenInterest.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
CallOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
CallOpenInterest.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels and DataType == DataType.OpenInterest, "CallOI: " + CallOpenInterest, GlobalColor("Call"));

# Put Open Interest
plot PutOpenInterest = -(TotalPutOpenInterest); # Make negative to flip under axis
PutOpenInterest.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
PutOpenInterest.SetPaintingStrategy(PaintingStrategy.LINE);
PutOpenInterest.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels and DataType == DataType.OpenInterest, "PutOI: " + TotalPutOpenInterest, GlobalColor("Put"));

# Create Clouds for Open Interest
AddCloud(
    if ShowClouds and DataType == DataType.OpenInterest then CallOpenInterest else Double.NaN,
    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
    GlobalColor("CallCloud"), GlobalColor("PutCloud")
);
AddCloud(
    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
    if ShowClouds and DataType == DataType.OpenInterest then PutOpenInterest else Double.NaN,
    GlobalColor("PutCloud"), GlobalColor("CallCloud")
);

# Hull Moving Average of Put Open Interest
plot PutOpenInterestAverage = hullmovingavg(PutOpenInterest);
PutOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
PutOpenInterestAverage.SetDefaultColor(Color.ORANGE);
PutOpenInterestAverage.SetStyle(Curve.MEDIUM_DASH);

# Hull Moving Average of Call Open Interest
plot CallOpenInterestAverage = hullmovingavg(CallOpenInterest);
CallOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
CallOpenInterestAverage.SetDefaultColor(Color.LIGHT_GREEN);
CallOpenInterestAverage.SetStyle(Curve.MEDIUM_DASH);

# Color Gradient of Total Average Open Interest
#plot TotalOpenInterestAverage = average(CallOpenInterest + PutOpenInterest);
#TotalOpenInterestAverage.SetHiding(!ShowLines and DataType == DataType.OpenInterest);
#TotalOpenInterestAverage.SetLineWeight(3);
#TotalOpenInterestAverage.DefineColor("Default", Color.YELLOW);
#TotalOpenInterestAverage.DefineColor("Highest", Color.GREEN);
#TotalOpenInterestAverage.DefineColor("Lowest", Color.RED);
#TotalOpenInterestAverage.AssignNormGradientColor(14, TotalOpenInterestAverage.Color("Lowest"), TotalOpenInterestAverage.Color("Highest"));
#AddCloud(
#    if ShowClouds and DataType == DataType.OpenInterest then TotalOpenInterestAverage else Double.NaN,
#    if ShowClouds and DataType == DataType.OpenInterest then Zeroline else Double.NaN,
#    color.GREEN, color.RED
#);

# Call Volume
plot CallVolume = TotalCallVolume;
CallVolume.SetHiding(!ShowLines and DataType == DataType.Volume);
CallVolume.SetPaintingStrategy(PaintingStrategy.LINE);
CallVolume.SetDefaultColor(GlobalColor("Call"));
AddLabel(ShowLabels and DataType == DataType.Volume, "CallVol: " + CallVolume, GlobalColor("Call"));

# Put Volume
plot PutVolume = -(TotalPutVolume); # Make negative to flip under axis
PutVolume.SetHiding(!ShowLines and DataType == DataType.Volume);
PutVolume.SetPaintingStrategy(PaintingStrategy.LINE);
PutVolume.SetDefaultColor(GlobalColor("Put"));
AddLabel(ShowLabels and DataType == DataType.Volume, "PutVol: " + TotalPutVolume, GlobalColor("Put"));

# Create Clouds for Volume
AddCloud(
    if ShowClouds and DataType == DataType.Volume then CallVolume else Double.NaN,
    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
    GlobalColor("CallCloud"), GlobalColor("PutCloud")
);
AddCloud(
    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
    if ShowClouds and DataType == DataType.Volume then PutVolume else Double.NaN,
    GlobalColor("PutCloud"), GlobalColor("CallCloud")
);

# Hull Moving Average of Put Volume
plot PutVolumeAverage = hullmovingavg(PutVolume);
PutVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
PutVolumeAverage.SetDefaultColor(Color.ORANGE);
PutVolumeAverage.SetStyle(Curve.MEDIUM_DASH);

# Hull Moving Average of Call Volume
plot CallVolumeAverage = hullmovingavg(CallVolume);
CallVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
CallVolumeAverage.SetDefaultColor(Color.LIGHT_GREEN);
CallVolumeAverage.SetStyle(Curve.MEDIUM_DASH);

# Color Gradient of Total Average Volume
#plot TotalVolumeAverage = average(CallVolume + PutVolume);
#TotalVolumeAverage.SetHiding(!ShowLines and DataType == DataType.Volume);
#TotalVolumeAverage.SetLineWeight(3);
#TotalVolumeAverage.DefineColor("Default", Color.YELLOW);
#TotalVolumeAverage.DefineColor("Highest", Color.GREEN);
#TotalVolumeAverage.DefineColor("Lowest", Color.RED);
#TotalVolumeAverage.AssignNormGradientColor(14, TotalVolumeAverage.Color("Lowest"), TotalVolumeAverage.Color("Highest"));
#AddCloud(
#    if ShowClouds and DataType == DataType.Volume then TotalVolumeAverage else Double.NaN,
#    if ShowClouds and DataType == DataType.Volume then Zeroline else Double.NaN,
#    Color.GREEN, Color.RED
#);

# Greeks Labels
AddLabel(ShowGreeks, "Delta: " + Delta, Color.WHITE);
AddLabel(ShowGreeks, "Gamma: " + Gamma, Color.WHITE);
AddLabel(ShowGreeks, "Theta: " + Theta, Color.WHITE);
AddLabel(ShowGreeks, "Vega: " + Vega, Color.WHITE);
I really like what youve done so far , but are you sure gamma exposure is a datatype i cant any information on that. the reason i ask is because the total gamma exposure should be in the hundreds of millions if not billions, I think you're forgetting to multiply by 100 because each option contract size is 100 shares
 

sudoshu

Member
Okay here's a question for the gurus.
According to https://perfiliev.co.uk/market-commentary/how-to-calculate-gamma-exposure-and-zero-gamma-level/
Let’s now calculate the total gamma contribution from each option. The formula is:
Option’s Gamma * Contract Size * Open Interest * Spot Price * (-1 if puts)

This gives the total option’s change in delta per ONE POINT move in the index. To convert into percent, we must multiply by how many points 1% is. Hence, we multiply by 1% * Spot Price, giving the final formula:
Option’s Gamma * Contract Size * Open Interest * Spot Price ^ 2 * 0.01

According to https://squeezemetrics.com/monitor/download/pdf/white_paper.pdf
To calculate the GEX (in shares*) of all call options at a particular strike price in a contract:
GEX = Г · OI · 100
Where Г is the option's gamma, OI is the open interest in the particular option strike, and 100 is the adjustment from option contracts to shares of the underlying.
In the case of put options:
GEX = Г · OI · (-100)

The latest code is utilizing the the first approach, second formula to get the percent change.
Why does the second approach by squeezemetrics not use the spot price as well?
What does the 'Gamma Contribution' mean vs 'GEX in shares'?

From my understand of GEX as a concept:
when GEX > 0 = less volatility (MM's hedging by buying into lows, selling into highs)​
and conversely when GEX < 0 = greater volatility (MM's selling into lows, buying into highs thus fueling volatility)​
What approach would be the best to take here? I'll play with adding some switching logic so we can play with the different formulas.
 

sudoshu

Member
I really like what youve done so far , but are you sure gamma exposure is a datatype i cant any information on that. the reason i ask is because the total gamma exposure should be in the hundreds of millions if not billions, I think you're forgetting to multiply by 100 because each option contract size is 100 shares
See my latest response, this is currently showing the percent gamma exposure. I did multiply by 100 for each contract.
 

Similar threads

The Market Trading Game Changer

Join 2,500+ subscribers inside the useThinkScript VIP Membership Club
  • Exclusive indicators
  • Proven strategies & setups
  • Private Discord community
  • ‘Buy The Dip’ signal alerts
  • Exclusive members-only content
  • Add-ons and resources
  • 1 full year of unlimited support

Frequently Asked Questions

What is useThinkScript?

useThinkScript is the #1 community of stock market investors using indicators and other tools to power their trading strategies. Traders of all skill levels use our forums to learn about scripting and indicators, help each other, and discover new ways to gain an edge in the markets.

How do I get started?

We get it. Our forum can be intimidating, if not overwhelming. With thousands of topics, tens of thousands of posts, our community has created an incredibly deep knowledge base for stock traders. No one can ever exhaust every resource provided on our site.

If you are new, or just looking for guidance, here are some helpful links to get you started.

What are the benefits of VIP Membership?
VIP members get exclusive access to these proven and tested premium indicators: Buy the Dip, Advanced Market Moves 2.0, Take Profit, and Volatility Trading Range. In addition, VIP members get access to over 50 VIP-only custom indicators, add-ons, and strategies, private VIP-only forums, private Discord channel to discuss trades and strategies in real-time, customer support, trade alerts, and much more. Learn all about VIP membership here.
How can I access the premium indicators?
To access the premium indicators, which are plug and play ready, sign up for VIP membership here.
Top