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.
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.
# 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
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.
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
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').
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.
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
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.
df[ df['column'] == value ]
df[ (df['column'] == value) & (df['column2'] >= value2) ]
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' )
df.query( 'column == value & column2 >= value2' )['desired_column']
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 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.
weeks
# 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])
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.
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.
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
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.
# 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])
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.
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.
# 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
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.
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
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
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
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')
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.
# 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])
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.
# 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
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.