Jorge's Tutorials 🏠

Click the house emoji to go back home
***GitHub***

Introduction

This tutorial will show the reader how to create interactive plots with the Python libraries Matplotlib and Plotly (Plotly Express, Plotly Graph Objects Figure Widget, and Plotly Dash). The data that will be used for these interactive plots is the stock option chain data from Yahoo Finance. Specifically, there will be two subplots for each plot that will showcase the calls and the puts available for a user specified stock in a user specified range. The plots will have colored backgrounds signifying in the money and out of the money options. The user will be able to interact with the plots by choosing a Selected Call and Selected Put at different expiration dates with a certain amount of shares. This will calculate how much premium will be received for Covered Calls and Cash Secured Puts. The interactive buttons will highlight the Selected Call and Selected Put and display its breakeven price along with other information on the plot which can be seen below.

  • Shares
  • Expiration
  • Strike
  • Bid
  • Breakeven
  • Premium
  • Value/Cost
  • I.T.M. Received/Paid

Disclaimer:

I do not provide personal investment advice and I am not a qualified licensed investment advisor. The information provided may include errors or inaccuracies. Conduct your own due diligence, or consult a licensed financial advisor or broker before making any and all investment decisions. Any investments, trades, speculations, or decisions made on the basis of any information found on this site and/or script, expressed or implied herein, are committed at your own risk, financial or otherwise. No representations or warranties are made with respect to the accuracy or completeness of the content of this entire site and/or script, including any links to other sites and/or scripts. The content of this site and/or script is for informational purposes only and is of general nature. You bear all risks associated with the use of the site and/or script and content, including without limitation, any reliance on the accuracy, completeness or usefulness of any content available on the site and/or script. Use at your own risk.

In [8]:
# This is needed for matplotlib jupyter notebook ipywidgets to work
%matplotlib notebook 
import pandas as pd
import numpy as np
import yfinance as yf
import datetime

#Jupyter Widgets
from ipywidgets import widgets, interact

# Matplotlib
import matplotlib.pyplot as plt

#Plotly Express
import plotly.express as px

#Plotly Graph Objects
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# Plotly Dash
import dash
from jupyter_dash import JupyterDash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

Stock and Plotting Options

Before we start plotting, the user can modify the four variables below depending on the stock, amount of strike prices, and number of weeks in the option chain they want to see.

The four variables in the block below are the only ones that need to be modified.

  • symbol: Desired stock option chain from Yahoo Finance.

  • percentOTM: Out of the money strike price percent limit based off of the current price

  • percentITM: In the money strike price percent limit based off of the current price

  • weeks: Number of weeks in the option chain that will be plotted starting from the current week.

In [9]:
symbol='AMC'#Desired stock
percentOTM=.25 #Out of the money x-axis limit percent based off of current price
percentITM=.05 #In the money x-axis limit percent based off of current price
weeks=4 #Number of option chain weeks that will be plotted including current week

Setup

The option chain data for the desired stock can now be downloaded and cleaned up. The option_chain() function gathers the selected stock's option chain data using Yahoo Finance. The variables in the previous cell are then used to bound the data to be plotted. Those same variables are also used to bound the selectable values in the interactive buttons ('widgets').

Option Chain data from Yahoo Finance

The function below returns the option chain data options and current price cp of the desired stock symbol chosen earlier. It uses the Python yfinance library and is from the Medium article Webscrapping Options Data with Python and YFinance which has been slightly modified to return the current price of the stock as well. The option chain data is returned as a Pandas DataFrame.

It is important to note that the data is static once it has been downloaded and needs to be re-downloaded manually if the most current option chain data is to be seen.

In [10]:
def option_chain(symbol):

    # Grabbing information for specific symbol
    tk = yf.Ticker(symbol) 
    
    # Grabbing today's information to get current price
    todays_data = tk.history(period='1d') # Today's data
    cp = round(todays_data['Close'][0],2) # Current price is latest price
    
    # Expiration dates
    exps = tk.options

    # Get options for each expiration
    options = pd.DataFrame()
    for e in exps:
        opt = tk.option_chain(e) # Get option chain information for specific date
        opt = pd.DataFrame().append(opt.calls).append(opt.puts) # Append calls and puts
        opt['expirationDate'] = e # Add expiration date
        options = options.append(opt, ignore_index=True) # Append option date for specific date to ALL dates

    # Bizarre error in yfinance that gives the wrong expiration date
    # Add 1 day to get the correct expiration date
    options['expirationDate'] = pd.to_datetime(options['expirationDate']) + datetime.timedelta(days = 1)
    options['dte'] = (options['expirationDate'] - datetime.datetime.today()).dt.days # Days till expiration
    
    # Boolean column if the option is a CALL
    # SYMBOL211231*C*00010000 is a CALL
    # SYMBOL240119*P*00070000 is a PUT
    options['CALL'] = options['contractSymbol'].str[4:].apply(lambda x: "C" in x) # Looks for 'C' in contract symbol to determine if Call
    
    options[['bid', 'ask', 'strike']] = options[['bid', 'ask', 'strike']].apply(pd.to_numeric) # Convert columns to numeric for maths
    options['mark'] = (options['bid'] + options['ask']) / 2 # Calculate the midpoint of the bid-ask
    
    # Drop unnecessary and meaningless columns
    options = options.drop(columns = ['contractSize', 'currency', 'change', 'percentChange', 'lastTradeDate', 'lastPrice'])

    return cp, options

Plot Limits

The cell below calculates the x-limits and y-limits of the call and put plots using the input from the user at the beginning of the notebook. The x-limits are based off of the current price using the percentOTM and percentITM variables. The y-limits are based off of the maximum bid within the weeks variable and the calculated x-limits.

In order to calculate the ymax of the plots, the bids that fall within the selected weeks and x-limit strike prices must be extracted. The value ymax is 1.5 times the maximum between the maximum call bid and maximum put bid. The ymax_ann is the y location of the selected option annotation that will be displayed on the plots (the x location is the midpoint of the x-limits). Two methods for filtering a Pandas DataFrame to extract specific values are shown below.

We also filter out unique dates, unique call_strikes, and unique put_strikes from the option chain dataframe in order to create widgets later on.

  • Boolean Indexing Method
    • Takes the form:
      df[ df['column'] == value ]
      
    • To chain filters:
      df[ (df['column'] == value) & (df['column2'] >= value2) ]
      
    • To only get a specific column as an output:
      df[ (df['column'] == value) & (df['column2'] >= value2) ]['desired_column']
      
  • Query Method

    • Takes the form:

      df.query( 'column == value' )
      
        * Note: if you want to use a variable, it needs an at @ right before the variable name 
      
      
      df.query( 'column == @value' )
      
        * Note: if you want to use a string, it needs double quotes "" 
      
      
      df.query( 'column == "value"' )
      
    • To chain filters:

      df.query( 'column == value & column2 >= value2' )
      
    • To only get a specific column as an output:
      df.query( 'column == value & column2 >= value2' )['desired_column']
      
In [11]:
cp, opts=option_chain(symbol) # Current price and option chain acquired from function in previous cell

leftcall=cp-cp*percentITM # Left x-limit for call plot
rightcall=cp+cp*percentOTM # Right x-limit for call plot
leftput=cp-cp*percentOTM # Left x-limit for put plot
rightput=cp+cp*percentITM # Right x-limit for put plot

dates = opts['expirationDate'].unique() # Unique dates in the option chain 

##### Boolean Indexing Method #####
# Filtering out call bid data based on certain columns
call_limit=opts[(opts['expirationDate']<=dates[weeks-1]) & \
          (opts['strike']>=leftcall) & \
          (opts['strike']<=rightcall) & \
          (opts['CALL']==True)]['bid']

# Filtering out put bid data based on certain columns
put_limit=opts[(opts['expirationDate']<=dates[weeks-1]) & \
          (opts['strike']>=leftput) & \
          (opts['strike']<=rightput) & \
          (opts['CALL']==False)]['bid']

##### Query Method #####
# call_limit=opts.query('expirationDate <= @dates[@weeks-1] & \
#                         strike >= @leftcall & \
#                         strike <= @rightcall & \
#                         CALL == True')['bid']

# put_limit=opts.query('expirationDate <= @dates[@weeks-1] & \
#                         strike >= @leftput & \
#                         strike <= @rightput & \
#                         CALL == False')['bid']

# Filtering out call strike data based on certain columns and acquiring unique values
call_strike_values=opts[(opts['expirationDate']<=dates[weeks-1]) & \
          (opts['strike']>=leftcall) & \
          (opts['strike']<=rightcall) & \
          (opts['CALL']==True)]['strike'].unique()

# Filtering out put strike data based on certain columns and acquiring unique values
put_strike_values=opts[(opts['expirationDate']<=dates[weeks-1]) & \
          (opts['strike']>=leftput) & \
          (opts['strike']<=rightput) & \
          (opts['CALL']==False)]['strike'].unique()

ymax=max(max(call_limit), max(put_limit))*1.5 # Top y-limit for call and put plot
ymax_ann=ymax*.8 # The y location for selected option information

Widgets

Widgets are buttons, dropdowns, etc... that are implemented in order for the user to be able to interact with and update the plots. These widgets are from the Jupyter Widgets ipywidgets library. There are three widgets each for the call plot and put plot which can be seen below. Widget containers are also used for the organization and grouping of the widgets.

  • Call/Put Shares Bounded Integer Text
    • Shares of current symbol held or able to be purchased from 100 to 10000 in steps of 100
  • Call/Put Expiration Dates Dropdown
    • Expiration date of option from current week through weeks
  • Call/Put Strike Prices Dropdown
    • Strike prices of available options within the x-limits of the plots
In [12]:
# Create two bounded int text boxes that allow only integers for number of shares
call_shares = widgets.BoundedIntText(
    value=100, # Default value
    min=100, # Minimum value
    max=10000, # Maximum value
    step=100, # Value step
    description='Call Shares:', # Label
    disabled=False, # Disabled is false so that user can interact with widget
)

put_shares = widgets.BoundedIntText(
    value=100, # Default value
    min=100, # Minimum value
    max=10000, # Maximum value
    step=100, # Value step
    description='Put Shares:', # Label
    disabled=False, # Disabled is false so that user can interact with widget
)


# Create two dropdown menus that allow only selected dates for expiration dates
call_exp = widgets.Dropdown(
    value=np.min(dates), # Default value
    options=dates[0:weeks], # Available choices
    description='Call Expiration Date:', # Label
    disabled=False, # Disabled is false so that user can interact with widget
)

put_exp = widgets.Dropdown(
    value=np.min(dates), # Default value
    options=dates[0:weeks], # Available choices
    description='Put Expiration Date:', # Label
    disabled=False, # Disabled is false so that user can interact with widget
)


# Create two dropdown menus that allow only available strike prices
call_strikes = widgets.Dropdown(
    value=np.min(call_strike_values), # Default value
    options=np.sort(call_strike_values), # Available choices
    description='Call Strike:', # Label
    disabled=False, # Disabled is false so that user can interact with widget
)

put_strikes = widgets.Dropdown(
    value=np.min(put_strike_values), # Default value
    options=np.sort(put_strike_values), # Available choices
    description='Put Strike:', # Label
    disabled=False, # Disabled is false so that user can interact with widget
)

#Horizontal Boxes for widget organization and grouping
container = widgets.HBox([call_exp, call_strikes, call_shares])
container2 = widgets.HBox([put_exp, put_strikes, put_shares])

Using Matplotlib

The first method I used for the interactive option chain plot was the plotting library that comes to most peoples' minds when thinking about plotting in Python: Matplotlib. Matplotlib was combined with the Jupyter Widgets library ipywidgets to create the buttons for interacting with the plots.

Matplotlib plot function

The first step was to create the function matplotlib_plot_options() which plots all the static data. The function returns the figure and axes of the created plot which will be handed to another function that will add the Selected Call and Selected Put information to the plot.

The figure and axes are created using the command below. We state the figure size in inches using figsize = ( width , height ) and specify the number of subplots using nrows = 1, ncols = 2 signifying we want two subplots in the form of one row and two columns. The first subplot is the Call Option Chain while the second subplot is the Put Option Chain. Once we have the axes object, we can add data, labels, limits, etc... to each subplot by using the corresponding subplot index. For instance ax[0] refers to the first subplot while ax[1] refers to the second subplot. If we had four subplots in a square formation nrows=2, ncols=2, the indexing would be in the matrix form: ax[0,0], ax[0,1], ax[1,0], and ax[1,1].

fig, ax = plt.subplots(figsize=(10, 5), nrows = 1, ncols = 2)

The function then cycles through each of the weeks and acquires the call and put option chain data for that week from the option chain Pandas DataFrame created in the option_chain() function. Each week is plotted as a different color in their respective subplot for calls and puts. An annotation is added for each point on the plot which lists its strike, bid, and breakeven price. After the option chains are plotted, the current price vertical line is drawn for reference, the areas for I.T.M. and O.T.M. are filled in, and the plots are labeled.

In [13]:
def matplotlib_plot_options():

    # Create Matplotlib plot and pass in some settings
    fig, ax = plt.subplots(figsize=(10, 5), nrows=1, ncols=2)
    
    # Plot title
    plt.suptitle(f'The Wheel for {symbol}')
    
    # Cycle through dates to plot option chains
    for date in dates[0:weeks]:
        
        # Removing time from current date for plot labels
        datelegend=pd.to_datetime(str(date)).strftime('%Y.%m.%d') 

        
        #############################
        ##### Call subplot data #####
        #############################
        
        # Filter out call options for current date
        calldata=opts[(opts['expirationDate']==date) & (opts['CALL']==True) ] 
        # Plot option chain for current date
        ax[0].plot(calldata['strike'],calldata['bid'],label=datelegend,marker='o') 
        
        # Adding annotations to each point
        for x,y in zip(calldata['strike'],calldata['bid']): # Pair data together for individual annotations
            label=f'Strike: {round(x,2)} \n Bid: {round(y,2)} \n Breakeven: {round(x+y,2)}' # Annotation label
            ax[0].annotate(label, # Label
                (x,y), # Label coordinates corresponding to specific option
                textcoords="offset points", # Label coordinate type, offset by a certain value
                xytext=(10,10), # Label offset distance from coordinates
                ha='center', # Label horizontal alignment
                fontsize=5, # Label fontsize
                weight='bold') # Label weight

            
        ############################    
        ##### Put subplot data #####
        ############################
        
        # Filter out put options for current date
        putdata=opts[(opts['expirationDate']==date) & (opts['CALL']==False) ] 
        # Plotting option chain for current date
        ax[1].plot(putdata['strike'],putdata['bid'],label=datelegend,marker='o')
        
        # Adding annotations to each point
        for x,y in zip(putdata['strike'],putdata['bid']): # Pair data together for individual annotations
            label=f'Strike: {round(x,2)} \n Bid: {round(y,2)} \n Breakeven: {round(x-y,2)}' # Annotation label
            ax[1].annotate(label, # Label
                (x,y), # Label coordinates corresponding to specific option
                textcoords="offset points", # Label coordinate type, offset by a certain value
                xytext=(10,10), # Label offset distance from coordinates
                ha='center', # Label horizontal alignment
                fontsize=5, # Label fontsize
                weight='bold') # Label weight

            
    ####################################
    ##### Call subplot information #####
    ####################################
    
    ax[0].plot([cp,cp],[0,ymax],label=f'Current Price: {cp}',color='black') # Current Price line
    ax[0].fill_between([cp,rightcall],[ymax,ymax],[0,0],color='honeydew') # Out of the money plot fill
    ax[0].fill_between([leftcall,cp],[ymax,ymax],[0,0],color='mistyrose') # In the money plot fill
    ax[0].set_xlim(leftcall,rightcall) # Plot x-limit
    ax[0].set_ylim(0,ymax) # Plot y-limit
    ax[0].set_title('Call Options') # Plot title
    ax[0].set_xlabel('Call Strike') # Plot x-label
    ax[0].set_ylabel('Call Premium') # Plot y-label
    ax[0].legend(prop={'size': 'xx-small'}) # Plot legend size
        
        
    ###################################    
    ##### Put subplot information #####
    ###################################
    
    ax[1].plot([cp,cp],[0,ymax],label=f'Current Price: {cp}',color='black') # Current Price line
    ax[1].fill_between([leftput,cp],[ymax,ymax],[0,0],color='honeydew') # Out of the money plot fill
    ax[1].fill_between([cp,rightput],[ymax,ymax],[0,0],color='mistyrose') # In the money plot fill
    ax[1].set_xlim(leftput,rightput) # Plot x-limit
    ax[1].set_ylim(0,ymax) # Plot y-limit
    ax[1].set_title('Put Options') # Plot title
    ax[1].set_xlabel('Put Strike') # Plot x-label
    ax[1].set_ylabel('Put Premium') # Plot y-label
    ax[1].legend(prop={'size': 'xx-small'}) # Plot legend size

        
    return fig, ax

Matplotlib update function

Now that the Matplotlib plotting function has been defined, we can generate the function that adds the user selected call and put to the plot. The update function matplotlib_update() filters the option chain Pandas DataFrame according to the widget values to search for a matching call and put for the specified date. If there is a match, it highlights the point on the subplot and adds an annotation with more information. If there isn't a match, it warns the user to choose another strike and date option. Note that the update function is called whenever any of the widgets observe a change in their values using the method below.

widget.observe(update_function, 'value')

Note: The interaction between the plot and the widgets seems to only work if the objects are defined in a certain order.

  1. fig, ax
  2. def update():
  3. widgets
  4. display widgets

In [14]:
# Seems to work in a certain order only : 1. fig, ax 2. def update(): 3. widgets 4. display widgets

fig, ax = matplotlib_plot_options() # Call Matplotlib Option Chain plotting function

ann_list = [] # Create empty list to append annotations that will be removed
selected_option_list =[] # Create empty list to append options that will be removed

# Function that is activated when the widget values are changed
def matplotlib_update(change):

    # Clear appended annotations to remove them from plot
    for a in ann_list:
        a.remove()
    ann_list[:] = []
    
    # Clear appended options to remove them from plot
    for o in selected_option_list:
        o.remove()
    selected_option_list[:] = []
    
    # Grab specific call bid based on call widget values
    call_bid=opts[(opts['expirationDate'] == call_exp.value) & \
                  (opts['CALL'] == True) & \
                  (opts['strike'] == call_strikes.value)]['bid']
    
    # Grab specific put bid based on put widget values
    put_bid=opts[(opts['expirationDate'] == put_exp.value) & \
                 (opts['CALL'] == False) & \
                 (opts['strike'] == put_strikes.value)]['bid']

    # If call bid and put bid return real values update title 
    if not call_bid.empty and not put_bid.empty:
        plt.suptitle(f'The Wheel for {symbol}: ' + \
                        f'Call Premium: {round(call_shares.value*call_bid.values[0],2)} + ' + \
                        f'Put Premium: {round(put_shares.value*put_bid.values[0],2)} = ' + \
                        f'{round(call_shares.value*call_bid.values[0]+put_shares.value*put_bid.values[0],2)}')

        
    #############################
    ##### Call subplot data #####
    #############################
    
    # If call bid does not return a value, update annotation to warn user
    if call_bid.empty:
        
        label=f'Selected Call Expiration and Call Strike Combination Does Not Exist' # Annotation label
        x=(leftcall+rightcall)/2 # Annotation x coordinate
        y=ymax_ann # Annotation y coordinate
        ann = ax[0].annotate(label,  # Label
            (x,y), # Label coordinates corresponding to overall annotation
            ha='center', # Label horizontal alignment
            fontsize=5, # Label fontsize
            weight='bold') # Label weight
        
        ann_list.append(ann) # Append annotation to annotation list
        
    # If call bid does return a value, update annotation with corresponding data 
    else:
        
        # Plotting selected option and assigning it to variable in order to append it to the selected option list
        # Need the , since you want the actual line not the line object
        # A zorder of 1 sends this point to the background
        selected_option , =ax[0].plot(call_strikes.value,call_bid.values[0],label='Selected Call',marker='o',color='red', markersize = 12, zorder=1)
        selected_option_list.append(selected_option) # Append plotted option to option list
        
        calldate=pd.to_datetime(str(call_exp.value)).strftime('%Y.%m.%d') # Removing time from current date for plot labels
        # Annotation label
        label=f'Calls with {call_shares.value} shares \n' + \
                    f'Expiration: {calldate} \n' + \
                    f'Strike: {round(call_strikes.value,2)} \n' + \
                    f'Bid: {round(call_bid.values[0],2)} \n' + \
                    f'Breakeven: {round(call_strikes.value+call_bid.values[0],2)} \n' + \
                    f'Premium: {round(call_shares.value*call_bid.values[0],2)} \n' + \
                    f'Value: {round(call_shares.value*call_strikes.value,2)} \n' + \
                    f'I.T.M. Received: +{round(call_shares.value*(call_strikes.value+call_bid.values[0]),2)}'
        x=(leftcall+rightcall)/2 # Annotation x coordinate
        y=ymax_ann # Annotation y coordinate
        ann = ax[0].annotate(label, # Label
            (x,y), # Label coordinates corresponding to overall annotation
            ha='center', # Label horizontal alignment
            fontsize=5, # Label fontsize
            weight='bold') # Label weight 
        
        ann_list.append(ann) # Append annotation to annotation list
        
     
    ############################
    ##### Put subplot data #####
    ############################
    
    # If put bid does not return a value, update annotation to warn user
    if put_bid.empty:
        
        label=f'Selected Put Expiration and Put Strike Combination Does Not Exist' # Annotation label
        x=(leftput+rightput)/2 # Annotation x coordinate
        y=ymax_ann # Annotation y coordinate
        ann=ax[1].annotate(label, # Label
            (x,y), # Label coordinates corresponding to overall annotation
            ha='center', # Label horizontal alignment
            fontsize=5, # Label fontsize
            weight='bold') # Label weight
        
        ann_list.append(ann) # Append annotation to annotation list
        
    # If put bid does return a value, update annotation with corresponding data         
    else:
        
        # Plotting selected option and assigning it to variable in order to append it to the selected option list
        # Need the , since you want the actual line not the line object
        # A zorder of 1 sends this point to the background
        selected_option,=ax[1].plot(put_strikes.value,put_bid.values[0],label='Selected Put',marker='o',color='red', markersize = 12,zorder=1)
        selected_option_list.append(selected_option) # Append plotted option to option list
        
        putdate=pd.to_datetime(str(put_exp.value)).strftime('%Y.%m.%d') # Removing time from current date for plot labels
        # Annotation label
        label=f'Puts with {put_shares.value} shares \n' + \
                    f'Expiration: {putdate} \n' + \
                    f'Strike: {round(put_strikes.value,2)} \n' + \
                    f'Bid: {round(put_bid.values[0],2)} \n' + \
                    f'Breakeven: {round(put_strikes.value-put_bid.values[0],2)} \n' + \
                    f'Premium: {round(put_shares.value*put_bid.values[0],2)} \n' + \
                    f'Cost: {round(put_shares.value*put_strikes.value,2)} \n' + \
                    f'I.T.M. Paid: -{round(put_shares.value*(put_strikes.value-put_bid.values[0]),2)}'
        x=(leftput+rightput)/2 # Annotation x coordinate
        y=ymax_ann # Annotation y coordinate
        ann=ax[1].annotate(label, # Label
            (x,y), # Label coordinates corresponding to overall annotation
            ha='center', # Label horizontal alignment
            fontsize=5, # Label fontsize
            weight='bold') # Label weight
        
        ann_list.append(ann) # Append annotation to annotation list

    # Redraw plot with updated data
    fig.canvas.draw()
    

# Widgets calling matplotlib_update function every time they observe a change
call_exp.observe(matplotlib_update, 'value')
call_strikes.observe(matplotlib_update, 'value')
call_shares.observe(matplotlib_update, 'value')
put_exp.observe(matplotlib_update, 'value')
put_strikes.observe(matplotlib_update, 'value')
put_shares.observe(matplotlib_update,'value')

# Displaying widgets in vertical box containing the horizontal box containers from the widgets cell
widgets.VBox([container, container2])

Using Plotly

After creating the interactive option chain plot using Matplotlib, I wanted a plot with more interactivity. I searched for various Python plotting libraries and stumbled upon Plotly. I decided to go with Plotly because it seemed to have a lot more features. Plotly allows the user to zoom, pan, hide/show data, hover over data for more information, and much more. Plotly has three main plotting options: Plotly Express, Plotly Graph Objects, and Plotly Dash. Plotly Dash has much more customization since it is written on top of React.js and can user either Plotly Express or Plotly Graph Objects as inputs.

I originally tried using Plotly Express by itself to create the interactive option chain plot but ran into some trouble trying to use dropdown menus for interactivity therefore I switched to using Plotly Graph Objects Figure Widget. Plotly Graph Objects Figure Widget connects Plotly Graph Objects and Jupyter Widgets (ipywidgets) much like the Matplotlib example created earlier in this tutorial. I then used Plotly Dash with Plotly Graph Objects as Plotly Dash has its own 'widgets' so there is no need for Jupyter Widgets (ipywidgets). As mentioned above, you can also use Plotly Dash with Plotly Express.

Using Plotly Express

Plotly Express, as its name suggests, is the express method of creating plots using Plotly. I found this method really intuitive and easy to use as it is based on passing in your whole Pandas DataFrame to Plotly Express. The user can then select columns just by using the column names since Plotly Express already knows which dataframe you are using. If you are looking for a method to quickly create interactive plots and your data is in a dataframe, I highly recommend Plotly Express (Plotly Graph Objects is more akin to Matplotlib's ax.plot() method: Plotly Express v.s. Plotly Graph Objects).

However, when I first wrote the script for Plotly Express, I was using custom buttons (which are also applicable to Plotly Graph Objects) which are meant for updating the style and visibility of data that is already plotted rather than modifying data (at least from what I have found). I was essentially barking up the wrong tree when trying to implement 'widgets' when I should have been using Plotly Graph Objects Figure Widget, Plotly Dash with Plotly Graph Objects, Plotly Dash with Plotly Express. Nevertheless, another lesson learned.

Now to actually creating a plot with Plotly Express. We first create the figure using the command below (this is a simplified version of what is in the actual code cell). We pass in the Pandas DataFrame as the first argument so that Plotly Express knows which dataframe to use. The x and y values can now be assigned just by writing out the column name. We then specify how to differentiate each set of x and y lines by passing in a column name to the color attribute (this is how each line is distinguished in the plot legend). The facet_col is used to create the different subplots (here we are passing in the 'is it a call' column). There is no need to cycle through the data to plot it since Plotly Express knows how to separate the data just by passing in the dataframe which is why this method of plotting in Plotly is so straightforward.

fig = px.line( dataframe, # Dataframe to use
            x ='strike', # x values column
            y ='bid',# y values column
            color ='expirationDate',# Column to distinguish each plotted line.
            facet_col ='CALL', # Column to separate subplots
)

For the interactive aspect we create the buttons to update the plot, plot all the different call and put options separately, set the visibility of the call and put options to false, and assign a button to each of those call and put options that toggles their visibility. This is where I ran into trouble because I couldn't figure out how to update the values based on the number of shares held call_shares_px or number of shares that could be bought put_shares_px. I could set these values at the beginning, but couldn't figure out how to update it once the plot was created (you would have to update the shares and rerun the code cell every time). A way around this is to create a button for every single combination of calls/puts and stocks owned/able to be purchased. However, this would create a giant dropdown menu which isn't really user friendly. Therefore, I moved on to Plotly Graph Objects Figure Widget and Plotly Dash with Plotly Graph Objects before I realized I was barking up the wrong tree as I mentioned previously.

In [15]:
# Interactive widgets don't seem to be available for Plotly Express so keep these as constants
call_shares_px=100
put_shares_px=100


#############################################
##### Creating Plotly Express Main Plot #####
#############################################

# Create empty dataframe for the selected weeks
callput=pd.DataFrame()

# Cycle through dates to grab option chains
for date in dates[0:weeks]:
    
    # Filter out call options for current date
    calldata=opts[(opts['expirationDate']==date) & (opts['CALL']==True) ]
    putdata=opts[(opts['expirationDate']==date) & (opts['CALL']==False) ]
    
    # Append options to new dataframe
    callput=callput.append(calldata).append(putdata)

# Plotly Express doesn't like datetime
callput['expirationDate'] = callput['expirationDate'].astype(str) 

# Plotly Express has a convenient plotting method using Pandas dataframes
fig_px = px.line(callput, # Dataframe to use
            x='strike', # x values column
            y='bid',# y values column
            color='expirationDate',# Column to distinguish each plotted line. This is the plot label for legend
            facet_col='CALL', # Column to separate subplots
            title=f'The Wheel for {symbol}', # Main title
            markers=True, # Enable markers on lines
            labels=dict(strike='Strike',bid='Bid'), # Hover over label names for x and y values
            text='<b>Strike: ' + round(callput['strike'],2).astype(str) + '<br>' + # Plotly uses HTML to format text
                    'Bid: ' + round(callput['bid'],2).astype(str) + '<br>' +
                    'Breakeven: ' + round(callput['strike']+callput['bid'],2).astype(str) + '<br>',
 ) 
    
#############################################
##### Creating Plotly Express 'Widgets' #####
#############################################

# Plotly Express has a different method of interacting and updating values than Plotly Graph Ojbects Figure Widget

# Empty list for buttons
buttonscall = []
buttonsput = []

n=2*weeks # Number of Calls and Puts lines

# False array to hide Selected Calls and Selecteds Put by default
# Array length is length callput + n since we need to include buttons 
    # for all individual Selected Calls and Selected Puts + Number of Calls and Puts lines
args = np.full(len(callput)+n, False, dtype=bool)
args[0:n] = True # Don't hide the Calls and Puts lines since we want those drawn by default

#Create Initial None buttons
buttoncall = dict(label = 'Select Call', # Button label
                  method = "update", # Method type to update data
                  args=[{"visible": args}] # Which lines should be visible when this choice is selected
                 )
buttonput = dict(label = 'Select Put', # Button label
                method = "update", # Method type to update data
                args=[{"visible": args}] # Which lines should be visible when this choice is selected
                )

# Append to list of buttons
buttonscall.append(buttoncall)
buttonsput.append(buttonput)

# Cycle through callput dataframe to create a button for each Option and Put
for index, row in callput.iterrows(): #using this 
    
    # Resetting the args array for line visibility for each button
    args = np.full(len(callput)+n, False, dtype=bool)
    args[0:n] = True # Set original Call and Put lines visible
    args[index+n] = True # Set the current call/put visible. This will be the Selected Call/Selected Put
    
    # Call buttons
    if row['CALL'] == True:
        
        label= str(row['expirationDate']) + ' Call ' + str(round(row['strike'],2)) # Button label
        button = dict(label = label, # Button label
                      method = "update", # Method type to update data
                      args=[{"visible": args}] # Which lines should be visible when this choice is selected
                     )
        buttonscall.append(button) # Append call button
        
        # Annotation label
        text='<b>Calls with ' + str(call_shares_px) + ' shares<br>' + \
                'Expiration: ' + row['expirationDate'] + '<br>' + \
                'Strike: ' + str(round(row['strike'],2)) + '<br>' + \
                'Bid: ' + str(round(row['bid'],2)) + '<br>' + \
                'Breakeven: ' + str(round(row['strike']+row['bid'],2)) + '<br>' + \
                'Premium: ' + str(round(call_shares_px*row['bid'],2)) + '<br>' + \
                'Value: ' + str(round(call_shares_px*row['strike'],2)) + '<br>' + \
                'I.T.M. Received: +' + str(round(call_shares_px*(row['strike']+row['bid']),2)) + '</b>'
        
        fig_px.add_scatter( # Add scatter
            x=[ row['strike'] ,(leftcall + rightcall)/2 ], # x values: one for selected option one for annotation
            y=[ row['bid'] ,ymax_ann ], # y values: one for Selected Option one for annotation
            name=str(row['expirationDate']) + ' Call ' + str(row['strike']), # Plot label for legend
            row=1,col=1, # Subplot
            visible=False, # Turn selected option off by default. Dropdown menu will make it visible
            text=['',text], # Don't add text to selected option but add text to dummy annotation point
            mode='markers+text' # No line since you don't want to connect selected option to dummy annotation point
        ) 
        
    # Put buttons
    else:
        
        label= str(row['expirationDate']) + ' Put ' + str(round(row['strike'],2)) # Button label
        button = dict(label = label, # Button label
                      method = "update", # Method type to update data
                      args=[{"visible": args}] # Which lines should be visible when this choice is selected
                    )
        buttonsput.append(button) # Append put button
        
        # Annotation label
        text='<b>Puts with ' + str(put_shares_px) + ' shares<br>' + \
                'Expiration: ' + row['expirationDate'] + '<br>' + \
                'Strike: ' + str(round(row['strike'],2)) + '<br>' + \
                'Bid: ' + str(round(row['bid'],2)) + '<br>' + \
                'Breakeven: ' + str(round(row['strike']-row['bid'],2)) + '<br>' + \
                'Premium: ' + str(round(put_shares_px*row['bid'],2)) + '<br>' + \
                'Cost: ' + str(round(put_shares_px*row['strike'],2)) + '<br>'  + \
                'I.T.M. Paid: -' + str(round(put_shares_px*(row['strike']-row['bid']),2)) + '</b>'
        
        fig_px.add_scatter(
            x=[ row['strike'] ,(leftput + rightput)/2 ], # x values: one for selected option one for annotation 
            y=[ row['bid'] ,ymax_ann ], # y values: one for Selected Option one for annotation
            name=str(row['expirationDate']) + ' Put ' + str(row['strike']), # Plot label for legend
            row=1,col=2, # Subplot
            visible=False, # Turn selected option off by default. Dropdown menu will make it visible
            text=['',text], # Don't add text to selected option but add text to dummy annotation point
            mode='markers+text' # No line since you don't want to connect selected option to dummy annotation point
        ) 


# Update figure layout with buttons
fig_px.update_layout(
    
    # Add buttons
    updatemenus=[ 
        
        # Selected Call button
        dict(
            type="dropdown", # Type of button
            direction="down", # How the dropdown menu opens up
            x = 0, # Location of button on figure (0,0) is bottom left corner
            y = 1, # Location of button on figure (0,0) is bottom left corner
            buttons = buttonscall # Selected Call button list
        ),

        # Selected Put button
        dict(
            type="dropdown", # Type of button
            direction="down", # How the dropdown menu opens up
            x = 0, # Location of button on figure (0,0) is bottom left corner
            y = .9, # Location of button on figure (0,0) is bottom left corner
            buttons = buttonsput # Selected Put button list
        )
        
    ]
)


#####################################
##### Overalll Plot Information #####
#####################################

fig_px.update_traces(marker={'size': 10}, textfont_size=6,textposition='bottom center') # Update marker text

titles = {'CALL=True':'Call Options', 'CALL=False':'Put Options'} # Dictionary matches facet values (subplots)
fig_px.for_each_annotation(lambda a: a.update(text = titles[a.text])) # Update facet values (subplots)

# Update axes limits
fig_px.update_xaxes(matches=None) # Uncouple subplot x-axes so they can haver their own limits
fig_px.update_xaxes(range=[leftcall, rightcall], title='Call Strike',col=1) # Call subplot x-axis limits
fig_px.update_xaxes(range=[leftput, rightput], title='Put Strike', col=2) # Put subplot x-axis limits
fig_px.update_yaxes(matches=None) # Uncouple subplot y-axes so they can haver their own limits
fig_px.update_yaxes(range=[0, ymax],title='Call Bid',col=1) # Call subplot y-axis limits
fig_px.update_yaxes(range=[0, ymax],title='Put Bid', col=2) # Put subplot y-axis limits

fig_px.show() # Show figure

Plotly Graph Objects Functions

Both the Plotly Graph Objects Figure Widget and Plotly Dash with Plotly Graph Objects methods for plotting use Plotly Graph Objects so a common function for creating the graph plotly_plot_options() and a common function for updating the graph plotly_update() were made.

Plotly Graph Objects plot function

The plotly_plot_options() function below creates the plots for both the Plotly Graph Objects Figure Widget and Plotly Dash with Plotly Graph Objects with the only difference being the first if statement which determines which type of Graph Object to use. We create the figure using one of the Graph Object commands below depending on whether we want to plot using Graph Objects or Graph Objects Figure Widget. We also need to call out that we are creating subplots using the make_subplots() built-in function. We use the Graph Objects Figure Widget with subplots and Graph Objects with subplots for the Plotly Graph Objects Figure Widget and Plotly Dash with Plotly Graph Objects tutorials, respectively. The Graph Objects Figure Widget allows the user to add Jupyter Widgets using the ipywidgets library. These widgets are the same exact ones created for the Matplotlib plot. We don't use Plotly Graph Objects Figure Widget for Dash since Dash has its own built in 'widgets' that can be used. As mentioned previously, you can use Plotly Express or Plotly Graph Objects for Dash but we will only be using Plotly Graph Objects for this tutorial.

  • Graph Object

    fig = go.Figure()
    
  • Graph Object Figure Widget

    fig = go.FigureWidget()
    

  • Graph Object with subplots

    fig = go.Figure(make_subplots(rows=1, cols=2, subplot_titles=(['Call Options','Put Options']) ) )
    
  • Graph Object Figure Widget with subplots

    fig = go.FigureWidget(make_subplots(rows=1, cols=2, subplot_titles=(['Call Options','Put Options']) ) )
    

Now that we have our figure set up, we can add individual sets of data more akin to Matplotlib's ax.plot() method that we used in the Matplotlib tutorial. In order to add data to the Graph Objects, we use the command below depending on what type of plot you want to create (go.Scatter() can be replaced by go.Bar() and so on). The Graph Objects do not take in data using a Pandas DataFrame directly therefore the x and y data must be passed independently. Note that you can also use the add trace method to add additional data to Plotly Express once the main plot is created.

fig.add_trace( go.Scatter() )

The function plotly_plot_options() plots each item by using the fig.add_trace() method which draws them in the order they are added. That means that the I.T.M., O.T.M., and current price data is plotted first so they don't cover any of the option plots. We then add dummy traces and annotations for the Selected Call and Selected Put that will be updated later in the plotly_update() function. The Plotly Graph Object fig is a giant object which contains dictionaries for each trace (and other information). This means we can cycle through each set of data to find the index for the Selected Call ic and Selected Put ip trace in order to update it later in the plotly_update() function. We finally update the overall plot information.

# Cycle through all the data arrays
    for i, data in enumerate(fig.data):

        if data.name == 'Selected Call': # Find the data array that is named Selected Call
            ic = i # Index for Selected Call found

        if data.name == 'Selected Put': # Find the data array that is named Selected Put
            ip= i # Index for Selected Put found
In [16]:
def plotly_plot_options(type):
        
    # This if statement is the only difference between type 'Figure Widget' and 'Dash'
    # Everything else is the same when creating the Graph Object
    if type=='Figure Widget':
        # Create Plotly Graph Object Figure Widget plot since we are using ipywidgets
        fig = go.FigureWidget(make_subplots(rows=1, cols=2, subplot_titles=(['Call Options','Put Options']) ) )
    elif type == 'Dash':
        # Create Plotly Graph Object plot (NO Figure Widget) since Dash doesn't use ipywidgets
        fig = go.Figure(make_subplots(rows=1, cols=2, subplot_titles=(['Call Options','Put Options']) ) )
    else:
        return
    
    # Traces appear in the order they are added, the newer traces are drawn on top of the older traces

    ###########################
    ##### Add fill traces #####
    ###########################
    
    # Add the fill plots first so they remain in the background
    # In the money plot fill call
    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[cp,cp], # x values
            y=[0,ymax], # y values
            fill='tozerox', # Fill type
            line=dict(color='mistyrose'), # Line color
            showlegend=False # No need to show this on legend
        ),

        # Call subplot
        row=1, 
        col=1

    )

    # Out of the money plot fill call
    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[rightcall,rightcall], # x values
            y=[0,ymax], # y values
            fill='tonextx', # Fill type
            line=dict(color='honeydew'), # Line color
            showlegend=False # No need to show this on legend
        ),

        # Call subplot
        row=1, 
        col=1

    )

    # Out of the money plot fill put
    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[cp,cp], # x values
            y=[0,ymax], # y values
            fill='tozerox', # Fill type
            line=dict(color='honeydew'), # Line color
            showlegend=False # No need to show this on legend
        ),

        # Put subplot
        row=1, 
        col=2

    )

    # In the money plot fill put
    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[rightput,rightput], # x values 
            y=[0,ymax], # y values
            fill='tonextx', # Fill type
            line=dict(color='mistyrose'), # Line color
            showlegend=False # No need to show this on legend
        ),

        # Put subplot
        row=1, 
        col=2

    )

    ####################################
    ##### Add current price traces #####
    ####################################
    
    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[cp,cp], # x values   
            y=[0,ymax], # y values
            mode="lines", # Type of scatter plot, this one only includes lines
            line=dict( # Line information
                color='black', 
                width=4
            ),
            name='Current Price ' + str(cp) # Plot label for legend
        ),

        #Call subplot
        row=1, 
        col=1,

    )

    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[cp,cp], # x values   
            y=[0,ymax], # y values
            mode="lines", # Type of scatter plot, this one only includes lines
            line=dict( # Line information
                color='black', 
                width=4
            ),
            name='Current Price', # Plot label for legend
            showlegend=False # No need to show this on legend since the other current price is already on
        ),

        # Put subplot
        row=1, 
        col=2,

    )
    
    # Plotting Call and Puts separately so they are grouped together in the legend
    
    ###########################
    ##### Add call traces #####
    ###########################
    
    # Cycle through dates to plot option chains
    for date in dates[0:weeks]:

        # Removing time from current date for plot labels
        datelegend=pd.to_datetime(str(date)).strftime('%Y.%m.%d')

        # Filter out call options for current date
        calldata=opts[(opts['expirationDate']==date) & (opts['CALL']==True) ]

        # Plotting option chain for current date
        fig.add_trace( # Add trace to Graphic Object

            go.Scatter( # Scatterplot
                x=calldata['strike'], # x values  
                y=calldata['bid'], # y values
                mode="lines+markers+text", # Type of scatter plot, this one includes lines, markers, and text for annotation
                text='<b>Strike: ' + round(calldata['strike'],2).astype(str) + '<br>' + # Annotation label
                        'Bid: ' + round(calldata['bid'],2).astype(str) + '<br>' + # Plotly uses HTML to format text
                        'Breakeven: ' + round((calldata['strike']+calldata['bid']),2).astype(str) + '<br>',
                textposition="top center", # Text position relative to x y coordinates
                textfont=dict( # Font information
                    family="sans serif",
                    size=6,
                    color="black"
                ),
                name='Call ' + str(datelegend), # Plot label for legend
            ),

            # Call subplot
            row=1, 
            col=1

        )

    ##########################
    ##### Add put traces #####
    ##########################
    
    # Cycle through dates to plot option chains
    for date in dates[0:weeks]:

        # Removing time from current date for plot labels
        datelegend=pd.to_datetime(str(date)).strftime('%Y.%m.%d')

        # Filter out call options for current date
        putdata=opts[(opts['expirationDate']==date) & (opts['CALL']==False) ]

        # Plotting option chain for current date
        fig.add_trace( # Add trace to Graphic Object

            go.Scatter( # Scatterplot
                x=putdata['strike'], # x values  
                y=putdata['bid'], # y values
                mode="lines+markers+text", # Type of scatter plot, this one includes lines, markers, and text for annotation
                text='<b>Strike: ' + round(putdata['strike'],2).astype(str) + '<br>' + # Annotation label
                        'Bid: ' + round(putdata['bid'],2).astype(str) + '<br>' + # Plotly uses HTML to format text
                        'Breakeven: ' + round((putdata['strike']-putdata['bid']),2).astype(str) + '<br>',
                textposition="top center", # Text position relative to x y coordinates
                textfont=dict( # Font information
                    family="sans serif",
                    size=6,
                    color="black"
                ),
                name='Put ' + str(datelegend), # Plot label for legend
            ),

            # Put subplot        
            row=1, 
            col=2

        )

    ##########################################################################################
    ##### Add dummy traces for Selected Call and Selected Put that will be updated later #####
    ##########################################################################################
    
    # Dummy Selected Call
    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[0,0], # x values  
            y=[0,0], # y values
            mode="markers", # Type of scatter plot, this one only includes markers
            name='Selected Call', # Plot label for legend
            visible= False, # Turn off at beginning
            marker=dict( # Marker information
                color='blue',
                size=15
            )
        ),

        # Call subplot   
        row=1, 
        col=1,

    )

    # Dummy Selected Put
    fig.add_trace( # Add trace to Graphic Object

        go.Scatter( # Scatterplot
            x=[0,0], # x values   
            y=[0,0], # y values
            mode="markers", # Type of scatter plot, this one only includes markers
            name='Selected Put', # Plot label for legend
            visible= False, # Turn off at beginning
            marker=dict( # Marker information
                color='red',
                size=15
            )
        ),

        # Put subplot   
        row=1, 
        col=2,

    )
    
    
    ###########################################################
    ##### Acquire index of Selected Call and Selected Put #####
    ###########################################################
    
    # The Plotly Graph Object 'g' is a giant object with a data array filled with dictionaries for each trace
    # We can find the index of Selected Call and Selected Put trace in order to update it later

    # Cycle through all the data arrays
    for i, data in enumerate(fig.data):

        if data.name == 'Selected Call': # Find the data array that is named Selected Call
            ic = i # Index for Selected Call found

        if data.name == 'Selected Put': # Find the data array that is named Selected Put
            ip= i # Index for Selected Put found

            
    ###############################################################################################
    ##### Add dummy annotations for Selected Call and Selected Put that will be updated later #####
    ###############################################################################################
    
    # Dummy Selected Call
    fig.add_annotation( # Add annotation to Graphic Object
        x=(leftcall+rightcall)/2, # x value 
        y=ymax_ann, # y value
        text='', # Empty text for now
        showarrow=False, # No arrow to mark annotation
        # Call subplot
        row=1,
        col=1
    )

    # Dummy Selected Put
    fig.add_annotation( # Add annotation to Graphic Object
        x=(leftput+rightput)/2, # x value 
        y=ymax_ann, # y value
        text='', # Empty text for now
        showarrow=False, # No arrow to mark annotation
        # Put subplot
        row=1,
        col=2
    )
    
    
    ####################################
    ##### Overall plot information #####
    ####################################
    
    #Note that the subplot information (xaxis1, xaxis2, etc...) are numbered left to right, top to bottom
    fig.update_layout(
        height=600, 
        title_text=f"The Wheel for {symbol}", 
        xaxis1_title = 'Call Strike',
        xaxis2_title = 'Put Strike',
        yaxis1_title = 'Call Bid',
        yaxis2_title = 'Put Bid',
        xaxis1_range=[leftcall,rightcall],
        xaxis2_range=[leftput,rightput],
        yaxis1_range=[0, ymax],
        yaxis2_range=[0, ymax],

    )

    return fig, ic, ip 

Plotly Graph Objects update function

The plotly_update() function takes in the current values of the 'widgets' and updates the Plotly Graph Objects accordingly using the index of the Selected Call ic and Selected Put ip trace. This function is very similar to the matplotlib_update() function except we update specific traces each time instead of creating and deleting new ones. We access the data of the Graph Object by using the commands below where index is the index of the specific trace you want to update and is based on the order you added the traces. The annotations are also updated in a similar fashion and can be seen in the code cell. Note that we use with fig.batch_update(): in order for the updates to be pushed to the fig all at once after they are all complete, otherwise you will get a staggered/choppy look as each update is pushed individually.

fig['data'][index].x
fig['data'][index].y

or

fig.data[index].x
fig.data[index].y
In [17]:
def plotly_update(call_exp, call_strikes, call_shares,
                  put_exp, put_strikes, put_shares,
                  fig, ic, ip ): 

    # Grab specific call bid based on call widget values
    call_bid=opts[(opts['expirationDate'] == call_exp) & \
                  (opts['CALL'] == True) & \
                  (opts['strike'] == call_strikes)]['bid']
    
    # Grab specific put bid based on call widget values
    put_bid=opts[(opts['expirationDate'] == put_exp) & \
                 (opts['CALL'] == False) & \
                 (opts['strike'] == put_strikes)]['bid']
    
    # Pushes updates only when all of them are done, otherwise will have staggered update
    with fig.batch_update():
        
        ######################
        ##### Main Title #####
        ######################
    
        # If call bid and put bid return real values update title 
        if not call_bid.empty and not put_bid.empty:

            # The Graph Object title
            fig.layout.title.text = f'The Wheel for {symbol}: ' + \
                                  f'Call Premium: {round(call_shares*call_bid.values[0],2)} + ' + \
                                  f'Put Premium: {round(put_shares*put_bid.values[0],2)} = ' + \
                                  f'{round(call_shares*call_bid.values[0] + put_shares*put_bid.values[0],2)}'

            
        ####################################################
        ##### Update Call Dummy Traces and Annotations #####
        ####################################################

        # If call bid does not return a value, update annotation to warn user
        if call_bid.empty:

            # The Graph Object suplot 1 annotation
            # The annotations[0] is subplot 1 title and annotations[1] is subplot 2 title
            fig.layout.annotations[2].text = 'Selected Call Expiration and Call Strike Combination Does Not Exist'

        # If put bid does return a value, update annotation with corresponding data              
        else:

            # Removing time from current date for annotation
            calldate=pd.to_datetime(str(call_exp)).strftime('%Y.%m.%d')

            # The Graph Object suplot 1 annotation
            # The annotations[0] is subplot 1 title and annotations[1] is subplot 2 title
            fig.layout.annotations[2].text = f'Calls with {call_shares} shares <br>' + \
                                           f'Expiration: {calldate} <br>' + \
                                           f'Strike: {round(call_strikes,2)} <br>' + \
                                           f'Bid: {round(call_bid.values[0],2)} <br>' + \
                                           f'Breakeven: {round(call_strikes+call_bid.values[0],2)} <br>' + \
                                           f'Premium: {round(call_shares*call_bid.values[0],2)} <br>' + \
                                           f'Value: {round(call_shares*call_strikes,2)} <br>' + \
                                           f'I.T.M. Received: +{round(call_shares*(call_strikes+call_bid.values[0]),2)}'

            # The Graph Object Selected Call data
            # Can also do fig['data'][ic]
            fig.data[ic].visible = True # Make visible
            fig.data[ic].x = [call_strikes, call_strikes] # Update x values
            fig.data[ic].y = [call_bid.values[0], call_bid.values[0]] # Update y values


            
        ###################################################
        ##### Update Put Dummy Traces and Annotations #####
        ###################################################
        
        # If put bid does not return a value, update annotation to warn user
        if put_bid.empty:

            # The Graph Object suplot 2 annotation
            # The annotations[0] is subplot 1 title and annotations[1] is subplot 2 title
            fig.layout.annotations[3].text = 'Selected Put Expiration and Put Strike Combination Does Not Exist'

        # If put bid does return a value, update annotation with corresponding data  
        else:

            # Removing time from current date for annotation
            putdate=pd.to_datetime(str(put_exp)).strftime('%Y.%m.%d')

            # The Graph Object suplot 2 annotation
            # The annotations[0] is subplot 1 title and annotations[1] is subplot 2 title
            fig.layout.annotations[3].text = f'Puts with {put_shares} shares <br>' + \
                                           f'Expiration: {putdate} <br>' + \
                                           f'Strike: {round(put_strikes,2)} <br>' + \
                                           f'Bid: {round(put_bid.values[0],2)} <br>' + \
                                           f'Breakeven: {round(put_strikes-put_bid.values[0],2)} <br>' + \
                                           f'Premium: {round(put_shares*put_bid.values[0],2)} <br>' + \
                                           f'Cost: {round(put_shares*put_strikes,2)} <br>'  + \
                                           f'I.T.M. Paid: -{round(put_shares*(put_strikes-put_bid.values[0]),2)}'

            # The Graph Object Selected Put data
            # Can also do fig['data'][ip]
            fig.data[ip].visible = True # Make visible
            fig.data[ip].x = [put_strikes, put_strikes] # Update x values
            fig.data[ip].y = [put_bid.values[0], put_bid.values[0]] # Update y values

            
    return fig #this is the 'figure' for the @app.callback output: Output('option-chain', 'figure')

Using Plotly Graph Objects Figure Widget

The code cell below creates the Plotly Graph Objects Figure Widget with its widgets. This code cell is very similar to the Matplotlib Jupyter Widgets ipywidgets update cell and is using the same exact ipywidgets as the Matplotlib plot. We first call plotly_plot_options('Figure Widget') to get the figure, Selected Call index, and Selected Put index.

Afterwards, the update function plotly_go_fw_update(change) is defined and is activated when the widgets observe a change in their values (this is the same as the Matplotlib plot). However, the actual update function plotly_update() is nested inside of plotly_go_fw_update(change). This was done because I wanted to create one single update function for both Plotly Graph Objects Figure Widget and Plotly Dash with Plotly Graph Objects. Otherwise, all the data manipulation could have been placed in the top level update function plotly_go_fw_update(change) like in the Matplotlib update function. We also had to modify the update function to take fig, ic, and ip as inputs since we needed to make it compatible with Dash Callback System. Otherwise, it would have been similar to the Matplotlib version where it had only the widget values as inputs.

Finally, the widgets and a vertical box are added much like the Matplotlib update cell.

In [18]:
# Call Plotly Option Chain plotting function signify type 'Figure Widget'
fig_fw, ic_fw, ip_fw = plotly_plot_options('Figure Widget')

# Function that is activated when the widget values are changed
def plotly_go_fw_update(change):

    # Call the update function with the values of each dropdown menu and with fig, ic, ip
        # Have to pass in extra variables besides the widget values (fig_fw, ic_fw, ip_fw)
        # since made it compatible with Dash Callback System. Otherwise it would have been 
        # similar to Matplotlib version where it had only the widget values as inputs.
    plotly_update(call_exp.value, call_strikes.value, call_shares.value,
                  put_exp.value, put_strikes.value, put_shares.value,
                     fig_fw, ic_fw, ip_fw) 

# Widgets calling plotly_go_fw_update function every time they observe a change
call_exp.observe(plotly_go_fw_update, names="value")
call_strikes.observe(plotly_go_fw_update, names="value")
call_shares.observe(plotly_go_fw_update, names="value")
put_exp.observe(plotly_go_fw_update, names="value")
put_strikes.observe(plotly_go_fw_update, names="value")
put_shares.observe(plotly_go_fw_update, names="value")


# Adding widgets and Plotly Graph Object Figure Widget plot in vertical box
widgets.VBox([container, container2, fig_fw])

Using Plotly Dash with Plotly Graph Objects

Plotly Dash has a lot of customization capabilities as it is built on top of React.js. If one is familiar with HTML, CSS, and React, customizing the Plotly Dash layout should be pretty straightforward.

Initiate the app by using app = JupyterDash(__name__) at the very top of the code cell. The front end of the Ploty Dash layout is then created using app.layout = html.Div(). We create the main title with a H1 tag and then create two sets of divs for the Call and Put buttons which are styled with a dictionary style={}. We finally create an element for the Graph Object at the bottom. Each element within the Plotly Dash layout can have an id which is how the Ploty Dash Callbacks know what to update.

app.layout = html.Div(

    children=[

        html.H1(
                children='The Wheel'
        ),

        # Call Menus
        html.Div(
            children=[], 
        ),

        # Put Menus
        html.Div(
            children=[], 
        ),

        # The Plotly graph
        dcc.Graph(
            id='option-chain',
            # Do not call out figure if it has 'widgets', otherwise won't update
                # The @app.callback() produces the plot and updates it
            #figure = fig 
        ),
    ]
)

The Plotly Dash Callbacks are somewhat the equivalent of the Jupyter Widgets ipywidgets observe technique which watches for changes in the buttons in order to call a function. The Output is what the callback will produce after its function gets activated. Here we want to output a fig to the dcc.Graph(id='option-chain'). The Input(s) are the widgets that activate the callback. The different Inputs are called out by their id and we want to observe a change in their value to activate the callback.

@app.callback(

    # The output is what the callback will produce
        # Here we are outputting the 'figure' to the div 'option-chain'
    Output('option-chain', 'figure'),

    # The input is what the callback observes for a change to activate the callback
        # Here we are observing for a change in the 'value' in all the different Call and Put options divs
    [Input('call-exp', 'value'),
    Input('put-exp', 'value')],
)

You may have noticed that the @app.callback() doesn't specify a function to call when the widgets observe a change. This is because the way that Plotly Dash is set up, the @app.callback() calls the function that is defined immediately after the callback and the arguments to the function are in the order of the Inputs. The function plotly_dash_update() calls on the common functions plotly_plot_options() and plotly_update() to acquire the figure and its update. It is important to note that this update function must return a fig which will be used to populate the Output in the @app.callback(). This is a bit different than the Plotly Graph Objects Figure Widget plot where a fig does not need to be returned.

def plotly_dash_update(call_exp, call_strikes, call_shares,put_exp, put_strikes, put_shares): 

    # Call the Plotly Option Chain plotting function signify type 'Dash'
    fig_dash, ic_dash, ip_dash = plotly_plot_options(...)

    # Call the update function with the values of each dropdown menu and with fig, ic, ip
    fig_dash = plotly_update(...)

    return fig_dash # This is the 'figure' for the @app.callback output: Output('option-chain', 'figure')

Finally, run the Plotly Dash app by using app.run_server(mode="inline") at the bottom.

In [19]:
# app = dash.Dash(__name__) # for Dash without Jupyter notebook
app = JupyterDash(__name__)

app.layout = html.Div(
    
    children=[
            
        html.H1(
            children='The Wheel'
        ),


        # Call Menus
        html.Div(
            children=[

                # Call Expiration Date Dropdown Menu
                html.Div(
                    children=[
                        html.Label(['Call Expiration Date:'], style={'font-weight': 'bold', "text-align": "center"}),
                        dcc.Dropdown(
                            id='call-exp',
                            options=[{'label': pd.to_datetime(str(date)).strftime('%Y.%m.%d'), 'value': pd.to_datetime(str(date)).strftime('%Y.%m.%d')} for date in dates[0:weeks]],
                            value=pd.to_datetime(str(np.min(dates))).strftime('%Y.%m.%d'),
                            searchable = False
                        ),
                    ], 
                    style={'width': '33%', 'display': 'inline-block'}
                ),

                # Call Strike Dropdown Menu
                html.Div(
                    children=[
                        html.Label(['Call Strike:'], style={'font-weight': 'bold', "text-align": "center"}),
                        dcc.Dropdown(
                            id='call-strike',
                            options=[{'label': x, 'value': x} for x in np.sort(call_strike_values) ],
                            value=np.min(call_strike_values),
                            searchable = False
                        ),
                    ], 
                    style={'width': '33%', 'display': 'inline-block'}
                ),

                # Call Shares Dropdown Menu
                html.Div(
                    children=[
                        html.Label(['Call Shares:'], style={'font-weight': 'bold', "text-align": "center"}),
                        dcc.Dropdown(
                            id='call-shares',
                            options=[{'label': x, 'value': x} for x in range(100,10100,100) ],
                            value=100,
#                             searchable = False
                        ),
                    ], 
                    style={'width': '33%', 'display': 'inline-block'}
                ),

            ], 
        ),


        # Put Menus
        html.Div(
            children=[

                # Put Expiration Date Dropdown Menu
                html.Div(
                    children=[
                        html.Label(['Put Expiration Date:'], style={'font-weight': 'bold', "text-align": "center"}),
                        dcc.Dropdown(
                            id='put-exp',
                            options=[{'label': pd.to_datetime(str(date)).strftime('%Y.%m.%d'), 'value': pd.to_datetime(str(date)).strftime('%Y.%m.%d')} for date in dates[0:weeks]],
                            value=pd.to_datetime(str(np.min(dates))).strftime('%Y.%m.%d'),
                            searchable = False,
                        ),
                    ], 
                    style={'width': '33%', 'display': 'inline-block'}
                ),

                # Put Strike Dropdown Menu
                html.Div(
                    children=[
                        html.Label(['Put Strike:'], style={'font-weight': 'bold', "text-align": "center"}),
                        dcc.Dropdown(
                            id='put-strike',
                            options=[{'label': x, 'value': x} for x in np.sort(put_strike_values) ],
                            value=np.min(put_strike_values),
                            searchable = False
                        ),
                    ], 
                    style={'width': '33%', 'display': 'inline-block'}
                ),

                # Put Shares Dropdown Menu
                html.Div(
                    children=[
                        html.Label(['Put Shares:'], style={'font-weight': 'bold', "text-align": "center"}),
                         dcc.Dropdown(
                            id='put-shares',
                            options=[{'label': x, 'value': x} for x in range(100,10100,100) ],
                            value=100,
#                             searchable = False
                        ),
                    ], 
                    style={'width': '33%', 'display': 'inline-block'}
                ),

            ], 
        ),


        # The Plotly graph
        dcc.Graph(
            id='option-chain',
            # Do not call out figure if it has 'widgets', otherwise won't update
                # The @app.callback() produces the plot and updated plot
            #figure = fig 
        ),
    ]
)



# Callbacks are how the dash app gets updated
@app.callback(
    
    # The output is what the callback will produce
        # Here we are outputting the 'figure' to the div 'option-chain'
    Output('option-chain', 'figure'),
    
    # The input is what the callback observes for a change to activate the callback
        # Here we are observing for a change in the 'value' in all the different Call and Put options divs
    [Input('call-exp', 'value'),
    Input('call-strike', 'value'),
    Input('call-shares', 'value'),
    Input('put-exp', 'value'),
    Input('put-strike', 'value'),
    Input('put-shares', 'value')],
)
def plotly_dash_update(call_exp, call_strikes, call_shares,
                       put_exp, put_strikes, put_shares): 

# The @app.callback calls the function directly below it.
# There must be no space between the @app.callback and function
# The variables in the function are in the order of the inputs in the @app.callback

    # Call the Plotly Option Chain plotting function signify type 'Dash'
    fig_dash, ic_dash, ip_dash = plotly_plot_options('Dash')

    # Call the update function with the values of each dropdown menu and with fig, ic, ip
    fig_dash = plotly_update(call_exp, call_strikes, call_shares,
                  put_exp, put_strikes, put_shares,
                      fig_dash, ic_dash, ip_dash)

    return fig_dash # This is the 'figure' for the @app.callback output: Output('option-chain', 'figure')



app.run_server(mode="inline")
# app.run_server() # Open in different tab with debug features

Conclusion

I wanted to learn about interactive plots and how to update them using interactive buttons. I started off with the classic Matplotlib and learned how to use Jupyter Widgets. After searching for other plotting libraries that provide more interactivity, I stumbled across Plotly and its many different flavors of plotting each providing their own pros and cons. Overall, you can't go wrong with the classic Matplotlib but if you want more interactive and eye catching plots go with Plotly. If you want super quick and intuitive plotting I would go with Plotly Express as you can use Pandas DataFrames right off the bat. If you want more customization and interactivity, I would go with Plotly Dash with Plotly Graph Objects as you can have all the HTML, CSS, and React.js options. I wouldn't go with Plotly Graph Objects Figure Widget since Matplotlib with Jupyter Widgets is essentially the same (minus the toggling of data visibility) and Plotly Dash with Plotly Graph Objects offers a lot more customization especially with its own built in in 'widgets'. Ploty Graph Objects Figure Widget (to me) is essentially an in between point that could be simplified (use Matplotlib since it is a standard Python library) or expanded on (used Plotly Dash with Plotly Graph Objects for more customization) based on your needs. Nevertheless, researching how to plot using all these different methods made me learn more about what sort of plotting libraries are out there, added one more library to my Python repertoire, and I had fun working on the project which is the whole point of learning something new.

In [ ]: