How to Build a DGI Dashboard in Python and Plotly

Some Background

Over a year ago, I built a custom Dividend Growth Investing (DGI) Dashboard with Google Sheets. I wanted a tool where I could look at a few years of dividend growth, along with some other values like 5-year compounded annual growth rate, historic yield, and dividend increase trends.

Since then, I’ve learned a little code and thought it would be fun to reproduce my Google Sheets using Python.

Google Sheets on the left, Python on the right

I’ll explain how to set up Python, write the code, and run the program. A full snippet of the code is included toward the very bottom of the post.

If you have any problems getting this to work, please let me know in the comments – I’d love to troubleshoot if necessary. I’d also love to reproduce my entire portfolio spreadsheet in Python, so let me know how I might improve this type of post.

And, if you’re a better Python writer than me (very possible), let me know how I can get better!

I’ll write this from a Mac perspective. I haven’t tried running this on Windows.

Install Python

I’ve been working in Python 3.7.4 for a while. I tried 3.8, had some issues, so came back to 3.7.4. Visit https://www.python.org/downloads/release/python-374/ for the Mac or PC download, and choose 3.7.4.

After installing, open Terminal (command + space bar and search Terminal).

Type python3 and hit return. You should see an interactive Python session. Try 1 + 2 and hit return to see if you return 3.
* Note: Python 3.7.4 may alias itself as py. If that’s the case, just type py instead of python3. I’ll use python3 for the rest of this post. Enter exit() to get out of Python.

# In the Terminal:

~ python3
Python 3.7.4 (default, Jul  9 2019, 18:13:23) 
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.

>>> 1 + 2
3

>>> exit()

Update Pip

Pip helps us download Python packages, which are like tool sets. One package we use will let us easily download stock and dividend information. Pip is the package manager. Luckily, Pip comes with Python starting with 3.4, so you should already have it with Python 3.7.4.

Upgrade Pip to its latest version in Terminal with:

# In the Terminal:

~ pip install -U pip

Download a Text Editor

My preference is Visual Studio Code (NOT the full Visual Studio!) (Made by Microsoft):
https://code.visualstudio.com/download

Atom is another great option (made by Github):
https://atom.io/

Create a .py File

In your Terminal, type cd and hit return. This brings you to your user folder. Now, use mkdir, cd, and touch to create a project folder, move into it, and create a runner.py file.

# In the Terminal:

~ mkdir python_portfolio_tracker/

~ cd python_portfolio_tracker/

~ /python_portfolio_tracker touch runner.py

Open your text editor (VS Code or Atom) and File > Open … > python_portfolio_tracker.

Use the text editor’s file tree to select runner.py.

Test Run Your First Code

To make sure we can run a Python file, let’s make it do something really simple. In your runner.py file, add print("Hello, DGI world!") and save the file. You’ll need to save the file every time before running it in the future, or turn on the text editor’s auto-save feature.

# runner.py

print("Hello, DGI world!")

Now, run the python file from your Terminal with python3 runner.py (and make sure you are in the python_portfolio_tracker folder):

# In the Terminal:

~/python_portfolio_tracker python3 runner.py

Hello, DGI world!

We just made the terminal print out Hello, DGI world! by writing a Python program and running it in the Terminal!

Begin Building the Dashboard

Install Python Packages

We’ll start by installing and importing some Python packages.

  • yfinance: downloads historical stock data from Yahoo Finance
  • pandas: yfinance uses pandas behind the scenes to structure data
  • plotly: browser-based graphing library
# In the terminal: 

~ pip install yfinance

~ pip install pandas

~ pip install plotly

Set Up Package Imports

Delete your hello world message because we know we can run a simple program. Import the following packages. math and datetime are packages that come installed already with Python.

# runner.py
import yfinance as yf
import plotly.graph_objects as go
import plotly.figure_factory as ff
import math
from datetime import datetime

Begin Setting Some Variables For the Table and Charts

We will start with JNJ and a time range of 5 years.

# Define the ticker and time range
ticker = 'jnj'
time_range = 5
ticker = ticker.upper()

# Get Yahoo Finance data
yahoo_data = yf.Ticker(ticker)

# Create a dataframe of Yahoo Finance data
# actions=True includes dividend data
stock_price_history_df = yahoo_data.history(
    actions=True, 
    period=f"{time_range}y"
)

# Calculates years of returned data
years = round((
    stock_price_history_df.index[-1] - 
    stock_price_history_df.index[-0]
).days/365.25)

#### Define price data
# Gets high price on first day of Yahoo data
start_price = stock_price_history_df['High'].iloc[0]

# Gets current Yahoo price
current_price = yahoo_data.info['regularMarketPrice']

# Gets max of all daily highs
max_price = max(stock_price_history_df['High'])

# Calculates compound Annual Growth Rate (CAGR)
stock_cagr = ((current_price / start_price) ** (1 / years)) - 1

# Filters stock history data on dividend payment dates
dividend_df = stock_price_history_df[(stock_price_history_df['Dividends'] > 0)]

Define Some Dividend Data

Extracts dividend data from our dividend dataframe. Also accounts for stocks with no dividend history, like AMZN.

#Extracts dividend data
try: 
    start_dividend = dividend_df['Dividends'].iloc[0]
    end_dividend = dividend_df['Dividends'].iloc[-1]
    dividend_cagr = ((end_dividend / start_dividend) ** (1 / years)) - 1
    dividend_payments = [x for x in dividend_df.itertuples(index=True, name='Pandas')]
    dividend_frequency = round(len(dividend_payments) / years)
# Exception for stocks with dividend history like AMZN
except IndexError as error: 
    start_dividend = 0
    end_dividend = 0
    dividend_cagr = 0
    dividend_payments = []
    dividend_frequency = 0

Gets table information from Yahoo info, but handles cases where the info is missing (yfinance is a great project, but it is still being improved upon).

# Extracts table information
# Accounts for cases when yfinance doesn't include some info
try:
    payout_ratio = '<b>{0:.2%}</b>'.format(yahoo_data.info['payoutRatio'])
except:
    payout_ratio = '<b>Not in<br>yfinance</b>'

try:
    dividend_yield = '<b>{0:.2%}</b>'.format(yahoo_data.info['dividendYield'])
except:
    dividend_yield = '<b>Not in<br>yfinance</b>'

try:
    forward_pe = f"<b>{round(yahoo_data.info['forwardPE'], 2)}</b>"
except:
    forward_pe = '<b>Not in<br>yfinance</b>'

Set Table Information

Set the table header and cells. Everything we’ve defined so far will auto-populate this table when we run the program.

table_data = [
    [
        'Ticker',
        'Time<br>Range<br>(years)', 
        'Current<br>Share<br>Price', 
        f'{years}-Year<br>Dividend<br>CAGR',
        'Current<br>Yield',
        'Payout<br>Ratio<br>(Forward)', 
        'PE<br>Ratio',
        f'{years}-Year<br>Stock<br>CAGR',
        'Consecutive<br>Dividend<br>Increases<br>(years)', 
        'Dividends/<br>Year<br>(frequency)'
    ], [
        f'<b>{ticker}</b>', 
        f'<b>{years}</b>', 
        '<b>${:,.2f}</b>'.format(current_price),
        '<b>{0:.2%}</b>'.format(dividend_cagr),
        dividend_yield,
        payout_ratio,
        forward_pe, 
        '<b>{0:.2%}</b>'.format(stock_cagr),
        '<b>TBD</b>', 
        f'<b>{dividend_frequency}</b>'
    ]
]

Set Color Palette

# Set dashboard color palette variables
color_dividends = '#3d85c6'
color_yields = '#f4b400'
color_white = '#FFFFFF'
color_gray_shade_1 = '#D3D3D3'
color_gray_shade_2 = '#a8a8a8'
color_background = '#434343'
color_line_area = '#42576b'
color_black = 'black'
color_table_header = '#b7b7b7'
color_table_cells = "#6ea8dc"

# Builds colorscale (used by table)
colorscale = [
    [0, color_table_header],
    [.5, color_white],
    [1, color_table_cells]
]

Build the Table!

We imported Plotly’s Figure Factory as ff, and we will put it to use now. Figure factory will build a table that can be vertically aligned with our future charts.

# Build the table
fig = ff.create_table(
    table_data, 
    colorscale=colorscale,
    font_colors=[color_black],
)

If you want to see what we have now, add fig.show() to the end of the runner.py file and run python3 runner.py in the Terminal. Here’s what we have so far:

High Quality It ain't much, but it's honest work Blank Meme Template
These seems appropriate

Begin Building the Charts

We will add two charts. The first chart will be the stock price, and the second chart will contain dividend amount, historic dividend yield, and dividend increases. Both charts will feed off the same x-axis, which is date.

First, we will initialize empty arrays for each of these plot points, and then fill them in by iterating through the dividend_payments objects we created earlier.

#### Generate x/y charting data
# Initialize the four y-axis data columns
dates = []
amounts = []
percent_increases = []
yields = []

# Add data to the y-axis data columns for each dividend payment
for row in dividend_payments:
    # Removes errant yfinance duplication (two dividends in under ten days)
    if dates and (row[0] - datetime.strptime(dates[-1], "%Y-%m-%d")).days < 10:
        dates.pop()
    
    # Adds date
    dates.append(str(row[0].to_pydatetime())[0:10])
    
    # Adds amount
    amounts.append(row[6])
    
    # Adds historic yield
    share_price = row[2]
    yields.append(row[6] * dividend_frequency / share_price)
    
    # Adds percent increase if there was an increase
    if len(amounts) > 1 and amounts[-2] != row[6]:
        increase = (row[6] - amounts[-2]) / amounts[-2]
        percent_increases.append('{0:.2%}'.format(increase))
    else:
        percent_increases.append(None)

We really haven’t done much yet, but if you could peak inside the dates, amounts, percent_increases, and yields, you can see what we have now for JNJ:

(Pdb) pp dates
['2015-05-21',
 '2015-08-21',
 '2015-11-20',
 '2016-02-19',
 '2016-05-20',
 '2016-08-19',
 '2016-11-18',
 '2017-02-24',
 '2017-05-25',
 '2017-08-25',
 '2017-11-27',
 '2018-02-26',
 '2018-05-25',
 '2018-08-27',
 '2018-11-26',
 '2019-02-25',
 '2019-05-24',
 '2019-08-26',
 '2019-11-25',
 '2020-02-24']
(Pdb) pp amounts
[0.75,
 0.75,
 0.75,
 0.75,
 0.8,
 0.8,
 0.8,
 0.8,
 0.84,
 0.84,
 0.84,
 0.84,
 0.9,
 0.9,
 0.9,
 0.9,
 0.95,
 0.95,
 0.95,
 0.95]
(Pdb) pp percent_increases
[None,
 None,
 None,
 None,
 '6.67%',
 None,
 None,
 None,
 '5.00%',
 None,
 None,
 None,
 '7.14%',
 None,
 None,
 None,
 '5.56%',
 None,
 None,
 None]
(Pdb) pp yields
[0.03305420890260026,
 0.034395780784223805,
 0.032722513089005235,
 0.03190131858783496,
 0.031323414252153486,
 0.029282576866764276,
 0.03010064904524504,
 0.028241108463507195,
 0.028332911712623324,
 0.027103331451157536,
 0.025818349469801754,
 0.02659700783661838,
 0.03073245688919242,
 0.027554535017221583,
 0.026223776223776224,
 0.02691588785046729,
 0.027810304449648715,
 0.030065669752353825,
 0.027781839450212016,
 0.025503355704697986]

Normalize the Gridlines

We have the data to plot, but now we need to tell Plotly how to plot it. The first thing we will do is figure out how to make the y-axes match on both sides between Amounts and Yields.

I have to give full credit to https://github.com/VictorBezak/Plotly_Multi-Axes_Gridlines for a great writeup on this topic. I used a lot of Victor’s code for this part.

# Set y gridlines
# https://github.com/VictorBezak/Plotly_Multi-Axes_Gridlines
gridlines = 4

# Function which takes in y-axis values and number of gridlines
# Outputs information used to optimize gridlines for multiple y-axes
def normalize_axis(y_values, gridlines):
    if y_values:
        y_max = max(y_values)
    else: # Exception for when there are no dividend payments
        y_max = 5
    y_range = y_max * 1000 # because min is always 0 and to account for ranges < 1
    y_len = len(str(math.floor(y_range))) # Gets number of digits
    y_pow10_divisor = math.pow(10, y_len - 1)
    y_firstdigit = math.floor(y_range / y_pow10_divisor)
    y_max_base = y_pow10_divisor * y_firstdigit / 1000
    y_dtick = y_max_base / gridlines
    y_dtick_ratio = y_range / y_dtick

    return {
        'max': y_max,
        'range': y_range,
        'dtick': y_dtick,
        'dtick_ratio': y_dtick_ratio
    }

# Create variables for amounts and yields
y1 = normalize_axis(amounts, gridlines)
y2 = normalize_axis(yields, gridlines)

# Optimizes matching y-axis ticks for amounts and yields
global_dtick_ratio = max(y1['dtick_ratio'], y2['dtick_ratio'])
y1_positive_ratio = abs(y1['max'] / y1['range']) * global_dtick_ratio
y2_positive_ratio = abs(y2['max'] / y2['range']) * global_dtick_ratio
global_positive_ratio = max(y1_positive_ratio, y2_positive_ratio) + 0.1

# Outputs y-axes ranges for use when building the chart
y1_range_max = (global_positive_ratio) * y1['dtick']
y2_range_max = (global_positive_ratio) * y2['dtick']

Time to Make Some Lines on a Graph

Plotly calls uses the term “traces” for data sets. We will use Bar and Scatter traces to plot our Dividend Amounts, Historic Yields, Percent Increases, and Historic Stock Prices.

To be honest, I wanted to go through in more detail, but realized the screenshots and post length would get out of control. Feel free to ask in the comments about why any of these values or parameters are being set if any of them aren’t intuitive (lots aren’t).

########################
# Make traces for graph
trace1 = go.Bar(
    x=dates, 
    y=amounts, 
    xaxis='x2', 
    yaxis='y2',
    text=amounts,
    textposition='inside',
    texttemplate='$%{y:.2f}',
    marker=dict(color=color_dividends),
    name='Dividend Amount ($)',
    hoverinfo='x+y'
)
trace2 = go.Scatter(
    x=dates, 
    y=yields, 
    xaxis='x2', 
    yaxis='y3',
    name='Historic Yield (%)',
    line=dict(color=color_yields, width=3),
    hoverinfo='x+y'
)
trace3 = go.Scatter(
    x=dates, 
    y=percent_increases, 
    xaxis='x2', 
    yaxis='y4',
    marker=dict(color=color_white),
    mode='markers+text',
    text=percent_increases,
    textposition='top center',
    textfont=dict(color=color_white),
    name='Percent Increase (%)',
    hoverinfo='x+text'
)
trace4 = go.Scatter(
    x=stock_price_history_df.index, 
    y=stock_price_history_df['High'], 
    xaxis='x2', 
    yaxis='y5',
    name='Share Price ($)',
    fill='tozeroy',
    fillcolor=color_line_area,
    line=dict(color=color_dividends, width=3),
    hoverinfo='x+y'
)

# Add trace data to figure
fig.add_traces([trace1, trace2, trace3, trace4])

Set the X- and Y-Axes

We have our data traces, and now we need to set up our x- and y-axes.

Again, I would love to explain each line, but it would be a very long post. Feel free to ask in the comments.

# initialize xaxis2 and yaxis2
fig['layout']['xaxis2'] = {}
fig['layout']['yaxis2'] = {}
fig['layout']['yaxis3'] = {}
fig['layout']['yaxis4'] = {}
fig['layout']['yaxis5'] = {}

# Edit screen layout for subplots
fig.layout.yaxis.update({
    'domain': [.7, .95]
})
fig.layout.xaxis2.update({
    'tickfont': dict(color=color_gray_shade_2),
    'hoverformat': '%B %d, %Y',
    'anchor': 'y2',
})
fig.layout.yaxis2.update({
    'domain': [.1, .45], 
    'title': 'Dividend Amount',
    'range': [0, y1_range_max],
    'dtick': y1['dtick'],
    'gridcolor': color_gray_shade_2,
    'title_font': dict(color=color_gray_shade_1),
    'tickfont': dict(color=color_gray_shade_2),
    'anchor': 'x2',
    'tickprefix': '$',
    'tickformat': ',.2f',
})
fig.layout.yaxis3.update({
    'domain': [.1, .45], 
    'title': 'Historic Yield and Dividend Increase (%)',
    'side': 'right',
    'overlaying': 'y2',
    'anchor': 'x2',
    'range': [0, y2_range_max * 2],
    'dtick': y2['dtick'] * 2,
    'showgrid': False,
    'title_font': dict(color=color_gray_shade_1),
    'tickfont': dict(color=color_gray_shade_2),
    'hoverformat': ',.2%',
    'tickformat': ',.1%',
    'zeroline': True,
    'zerolinewidth': 2, 
    'zerolinecolor': color_gray_shade_2,
})
fig.layout.yaxis4.update({
    'domain': [.1, .45], 
    'overlaying': 'y2',
    'anchor': 'x2',
    'rangemode': 'tozero',
    'showgrid': False,
    'showticklabels': False,
    'tickformat': ',.1%',
    'zeroline': False
})
fig.layout.yaxis5.update({
    'domain': [.5, .65], 
    'anchor': 'x2',
    'tickfont': dict(color=color_gray_shade_2),
    'showgrid': False,
    'title': 'Share Price ($)',
    'title_font': dict(color=color_gray_shade_1),
    'range': [0, max_price],
    'tickprefix': '$',
    'tickformat': ',.0f',
    'hoverformat': ',.2f',
    'zeroline': True,
    'zerolinewidth': 2, 
    'zerolinecolor': color_gray_shade_2,
})

# Update the margins to add a title and see graph x-labels.
fig.layout.margin.update({'t': 75, 'l': 50})

title = (
    f"{years}-Year Dividend Summary for {ticker}: {yahoo_data.info['longName']}" + 
    f"<br><span style='font-size: 12px;'>As of {datetime.now().strftime('%B %d, %Y')}</span>" +
    "<br><span style='font-size: 12px;'>Information accuracy and completeness not guaranteed. Not all yfinance dividends are adjusted for splits</span>"
)

fig.layout.update({
    'title': title,
    'plot_bgcolor': color_background,
    'paper_bgcolor': color_background,
    'legend': dict(
        x=.14,
        y=0.69,
        traceorder="normal",
        font=dict(color=color_gray_shade_1),
    ),
    'legend_orientation': 'h',
    'font': dict(color=color_white),
    'height': 800,
    'hovermode': 'x'
})

Generate the Final Dashboard

Add fig.show() to the end of the runner.py file:

fig.show()

Run python3 runner.py in your Terminal, and wait for your browser to show the dashboard!

# In the Terminal:

~/python_portfolio_tracker python3 runner.py

Don’t forget to save the runner.py file before running it. If you want to change the stock ticker or the range of years, change lines 9 and 10 of the runner file.

Summary

As promised, the entire runner.py file code:

# runner.py
import yfinance as yf
import plotly.graph_objects as go
import plotly.figure_factory as ff
import math
from datetime import datetime

# Define the ticker and time range
ticker = 'jnj'
time_range = 5
ticker = ticker.upper()

# Get Yahoo Finance data
yahoo_data = yf.Ticker(ticker)

# Create a dataframe of Yahoo Finance data
# actions=True includes dividend data
stock_price_history_df = yahoo_data.history(
    actions=True, 
    period=f"{time_range}y"
)

# Calculates years of returned data
years = round((
    stock_price_history_df.index[-1] - 
    stock_price_history_df.index[-0]
).days/365.25)

#### Define price data
# Gets high price on first day of Yahoo data
start_price = stock_price_history_df['High'].iloc[0]

# Gets current Yahoo price
current_price = yahoo_data.info['regularMarketPrice']

# Gets max of all daily highs
max_price = max(stock_price_history_df['High'])

# Calculates compound Annual Growth Rate (CAGR)
stock_cagr = ((current_price / start_price) ** (1 / years)) - 1

# Filters stock history data on dividend payment dates
dividend_df = stock_price_history_df[(stock_price_history_df['Dividends'] > 0)]

#Extracts dividend data
try: 
    start_dividend = dividend_df['Dividends'].iloc[0]
    end_dividend = dividend_df['Dividends'].iloc[-1]
    dividend_cagr = ((end_dividend / start_dividend) ** (1 / years)) - 1
    dividend_payments = [x for x in dividend_df.itertuples(index=True, name='Pandas')]
    dividend_frequency = round(len(dividend_payments) / years)
# Exception for stocks with dividend history like AMZN
except IndexError as error: 
    start_dividend = 0
    end_dividend = 0
    dividend_cagr = 0
    dividend_payments = []
    dividend_frequency = 0

# Extracts table information
# Accounts for cases when yfinance doesn't include some info
try:
    payout_ratio = '<b>{0:.2%}</b>'.format(yahoo_data.info['payoutRatio'])
except:
    payout_ratio = '<b>Not in<br>yfinance</b>'

try:
    dividend_yield = '<b>{0:.2%}</b>'.format(yahoo_data.info['dividendYield'])
except:
    dividend_yield = '<b>Not in<br>yfinance</b>'

try:
    forward_pe = f"<b>{round(yahoo_data.info['forwardPE'], 2)}</b>"
except:
    forward_pe = '<b>Not in<br>yfinance</b>'


table_data = [
    [
        'Ticker',
        'Time<br>Range<br>(years)', 
        'Current<br>Share<br>Price', 
        f'{years}-Year<br>Dividend<br>CAGR',
        'Current<br>Yield',
        'Payout<br>Ratio<br>(Forward)', 
        'PE<br>Ratio',
        f'{years}-Year<br>Stock<br>CAGR',
        'Consecutive<br>Dividend<br>Increases<br>(years)', 
        'Dividends/<br>Year<br>(frequency)'
    ], [
        f'<b>{ticker}</b>', 
        f'<b>{years}</b>', 
        '<b>${:,.2f}</b>'.format(current_price),
        '<b>{0:.2%}</b>'.format(dividend_cagr),
        dividend_yield,
        payout_ratio,
        forward_pe, 
        '<b>{0:.2%}</b>'.format(stock_cagr),
        '<b>TBD</b>', 
        f'<b>{dividend_frequency}</b>'
    ]
]

# Set dashboard color palette variables
color_dividends = '#3d85c6'
color_yields = '#f4b400'
color_white = '#FFFFFF'
color_gray_shade_1 = '#D3D3D3'
color_gray_shade_2 = '#a8a8a8'
color_background = '#434343'
color_line_area = '#42576b'
color_black = 'black'
color_table_header = '#b7b7b7'
color_table_cells = "#6ea8dc"

# Builds colorscale (used by table)
colorscale = [
    [0, color_table_header],
    [.5, color_white],
    [1, color_table_cells]
]

# Build the table
fig = ff.create_table(
    table_data, 
    colorscale=colorscale,
    font_colors=[color_black],
)

#### Generate x/y charting data
# Initialize the four y-axis data columns
dates = []
amounts = []
percent_increases = []
yields = []

# Add data to the y-axis data columns for each dividend payment
for row in dividend_payments:
    # Removes errant yfinance duplication (two dividends in under ten days)
    if dates and (row[0] - datetime.strptime(dates[-1], "%Y-%m-%d")).days < 10:
        dates.pop()
    
    # Adds date
    dates.append(str(row[0].to_pydatetime())[0:10])
    
    # Adds amount
    amounts.append(row[6])
    
    # Adds historic yield
    share_price = row[2]
    yields.append(row[6] * dividend_frequency / share_price)
    
    # Adds percent increase if there was an increase
    if len(amounts) > 1 and amounts[-2] != row[6]:
        increase = (row[6] - amounts[-2]) / amounts[-2]
        percent_increases.append('{0:.2%}'.format(increase))
    else:
        percent_increases.append(None)

# Set y gridlines
# https://github.com/VictorBezak/Plotly_Multi-Axes_Gridlines
gridlines = 4

# Function which takes in y-axis values and number of gridlines
# Outputs information used to optimize gridlines for multiple y-axes
def normalize_axis(y_values, gridlines):
    if y_values:
        y_max = max(y_values)
    else: # Exception for when there are no dividend payments
        y_max = 5
    y_range = y_max * 1000 # because min is always 0 and to account for ranges < 1
    y_len = len(str(math.floor(y_range))) # Gets number of digits
    y_pow10_divisor = math.pow(10, y_len - 1)
    y_firstdigit = math.floor(y_range / y_pow10_divisor)
    y_max_base = y_pow10_divisor * y_firstdigit / 1000
    y_dtick = y_max_base / gridlines
    y_dtick_ratio = y_range / y_dtick

    return {
        'max': y_max,
        'range': y_range,
        'dtick': y_dtick,
        'dtick_ratio': y_dtick_ratio
    }

# Create variables for amounts and yields
y1 = normalize_axis(amounts, gridlines)
y2 = normalize_axis(yields, gridlines)

# Optimizes matching y-axis ticks for amounts and yields
global_dtick_ratio = max(y1['dtick_ratio'], y2['dtick_ratio'])
y1_positive_ratio = abs(y1['max'] / y1['range']) * global_dtick_ratio
y2_positive_ratio = abs(y2['max'] / y2['range']) * global_dtick_ratio
global_positive_ratio = max(y1_positive_ratio, y2_positive_ratio) + 0.1

# Outputs y-axes ranges for use when building the chart
y1_range_max = (global_positive_ratio) * y1['dtick']
y2_range_max = (global_positive_ratio) * y2['dtick']

########################
# Make traces for graph
trace1 = go.Bar(
    x=dates, 
    y=amounts, 
    xaxis='x2', 
    yaxis='y2',
    text=amounts,
    textposition='inside',
    texttemplate='$%{y:.2f}',
    marker=dict(color=color_dividends),
    name='Dividend Amount ($)',
    hoverinfo='x+y'
)
trace2 = go.Scatter(
    x=dates, 
    y=yields, 
    xaxis='x2', 
    yaxis='y3',
    name='Historic Yield (%)',
    line=dict(color=color_yields, width=3),
    hoverinfo='x+y'
)
trace3 = go.Scatter(
    x=dates, 
    y=percent_increases, 
    xaxis='x2', 
    yaxis='y4',
    marker=dict(color=color_white),
    mode='markers+text',
    text=percent_increases,
    textposition='top center',
    textfont=dict(color=color_white),
    name='Percent Increase (%)',
    hoverinfo='x+text'
)
trace4 = go.Scatter(
    x=stock_price_history_df.index, 
    y=stock_price_history_df['High'], 
    xaxis='x2', 
    yaxis='y5',
    name='Share Price ($)',
    fill='tozeroy',
    fillcolor=color_line_area,
    line=dict(color=color_dividends, width=3),
    hoverinfo='x+y'
)

# Add trace data to figure
fig.add_traces([trace1, trace2, trace3, trace4])

# initialize xaxis2 and yaxis2
fig['layout']['xaxis2'] = {}
fig['layout']['yaxis2'] = {}
fig['layout']['yaxis3'] = {}
fig['layout']['yaxis4'] = {}
fig['layout']['yaxis5'] = {}

# Edit screen layout for subplots
fig.layout.yaxis.update({
    'domain': [.7, .95]
})
fig.layout.xaxis2.update({
    'tickfont': dict(color=color_gray_shade_2),
    'hoverformat': '%B %d, %Y',
    'anchor': 'y2',
})
fig.layout.yaxis2.update({
    'domain': [.1, .45], 
    'title': 'Dividend Amount',
    'range': [0, y1_range_max],
    'dtick': y1['dtick'],
    'gridcolor': color_gray_shade_2,
    'title_font': dict(color=color_gray_shade_1),
    'tickfont': dict(color=color_gray_shade_2),
    'anchor': 'x2',
    'tickprefix': '$',
    'tickformat': ',.2f',
})
fig.layout.yaxis3.update({
    'domain': [.1, .45], 
    'title': 'Historic Yield and Dividend Increase (%)',
    'side': 'right',
    'overlaying': 'y2',
    'anchor': 'x2',
    'range': [0, y2_range_max * 2],
    'dtick': y2['dtick'] * 2,
    'showgrid': False,
    'title_font': dict(color=color_gray_shade_1),
    'tickfont': dict(color=color_gray_shade_2),
    'hoverformat': ',.2%',
    'tickformat': ',.1%',
    'zeroline': True,
    'zerolinewidth': 2, 
    'zerolinecolor': color_gray_shade_2,
})
fig.layout.yaxis4.update({
    'domain': [.1, .45], 
    'overlaying': 'y2',
    'anchor': 'x2',
    'rangemode': 'tozero',
    'showgrid': False,
    'showticklabels': False,
    'tickformat': ',.1%',
    'zeroline': False
})
fig.layout.yaxis5.update({
    'domain': [.5, .65], 
    'anchor': 'x2',
    'tickfont': dict(color=color_gray_shade_2),
    'showgrid': False,
    'title': 'Share Price ($)',
    'title_font': dict(color=color_gray_shade_1),
    'range': [0, max_price],
    'tickprefix': '$',
    'tickformat': ',.0f',
    'hoverformat': ',.2f',
    'zeroline': True,
    'zerolinewidth': 2, 
    'zerolinecolor': color_gray_shade_2,
})

# Update the margins to add a title and see graph x-labels.
fig.layout.margin.update({'t': 75, 'l': 50})

title = (
    f"{years}-Year Dividend Summary for {ticker}: {yahoo_data.info['longName']}" + 
    f"<br><span style='font-size: 12px;'>As of {datetime.now().strftime('%B %d, %Y')}</span>" +
    "<br><span style='font-size: 12px;'>Information accuracy and completeness not guaranteed. Not all yfinance dividends are adjusted for splits</span>"
)

fig.layout.update({
    'title': title,
    'plot_bgcolor': color_background,
    'paper_bgcolor': color_background,
    'legend': dict(
        x=.14,
        y=0.69,
        traceorder="normal",
        font=dict(color=color_gray_shade_1),
    ),
    'legend_orientation': 'h',
    'font': dict(color=color_white),
    'height': 800,
    'hovermode': 'x'
})

fig.show()

This seems great, but there’s a small bug in yfinance where it won’t render information for some tickers. This is the first time I’ve modified a package, so I felt pretty cool. First, run pip show yfinance in your Terminal to get the base.py location.

# In the Terminal

~/python_portfolio_tracker pip show yfinance
Name: yfinance
Version: 0.1.54
Summary: Yahoo! Finance market data downloader
Home-page: https://github.com/ranaroussi/yfinance
Author: Ran Aroussi
Author-email: ran@aroussi.com
License: Apache
Location: /usr/local/lib/python3.7/site-packages
Requires: multitasking, requests, numpy, pandas
Required-by: 

Copy the /usr/local/lib/python3.7/site-packages.

Open Finder
Menu > Go > Go to Folder >
Enter /usr/local/lib/python3.7/site-packages
Open yfinance > base.py

Replace lines 286-292 with:

if len(holders) > 1:
    self._institutional_holders = holders[1]
    if 'Date Reported' in self._institutional_holders:
        self._institutional_holders['Date Reported'] = _pd.to_datetime(
            self._institutional_holders['Date Reported'])
    if '% Out' in self._institutional_holders:
        self._institutional_holders['% Out'] = self._institutional_holders[
            '% Out'].str.replace('%', '').astype(float)/100

This avoids an error for certain stock tickers that apparently have one or fewer “holders” – I haven’t looked deeply into it enough to know what a holder is in this case.

Let me know if you enjoyed this or what I could do to make it better. I plan to convert my portfolio tracker into a Python app at some point and document it along the way.