DEV Community

pickuma
pickuma

Posted on • Originally published at pickuma.com

Building a Dividend Tracking Dashboard in Python: From CSV to Visualization

I own a modest dividend portfolio — maybe 25 positions across a mix of REITs, ETFs, and a handful of individual stocks. For the first two years, I tracked everything in a Google Sheet. Every month I'd copy the dividend payment from my brokerage statement into a cell, manually update the share count, and squint at a formula that calculated yield on cost. It worked. It was also annoying, error-prone, and gave me exactly zero insight into what my income might look like six months out.

The commercial alternatives — DividendMax, Simply Safe Dividends, TrackYourDividends — all do the job. But they cap your portfolio size on free tiers (10-15 holdings), lock projections behind paid plans, and own your data. After the third "you've hit your limit" modal, I decided to build my own.

The tools you don't need to pay for

Before writing a line of code, the first question is: where does the data come from? I use two free sources that cover everything a retail investor needs.

Yahoo Finance via yfinance. This Python library wraps Yahoo's publicly available endpoints and gives you historical price data, dividend history, and basic company info. No API key required. The tradeoff is rate limiting — Yahoo will throttle you if you hammer it. For a personal dashboard pulling data once a day, you'll never hit the limit.

import yfinance as yf

ticker = yf.Ticker("O")
dividends = ticker.dividends  # Pandas Series with date index
Enter fullscreen mode Exit fullscreen mode

The dividends attribute returns a pandas Series where each row is a payment date and each value is the per-share amount. That one line replaces the monthly copy-paste ritual.

Alpha Vantage free tier. If yfinance goes dark (it happens — Yahoo changes their endpoints without warning every year or two), Alpha Vantage gives you 25 API calls per day on the free tier. That's enough to pull dividend data for 25 positions once daily. Register for a key at alphavantage.co, and you get structured JSON responses with a documented schema rather than screen-scraped HTML.

import requests

url = f"https://www.alphavantage.co/query"
params = {
    "function": "DIVIDENDS",
    "symbol": "O",
    "apikey": "YOUR_KEY"
}
Enter fullscreen mode Exit fullscreen mode

Designing the data model

The Google Sheet approach breaks because it conflates two things: your portfolio state (what you own, at what cost basis) and market data (what the ticker pays, when). A clean data model separates these.

I landed on three tables in a SQLite database — light enough to not need Postgres, structured enough to query:

holdings — one row per position. Columns: ticker (text, PK), shares (real), cost_basis_per_share (real), first_purchased (date), account_type (text — taxable, Roth IRA, etc.).

dividends — one row per payment per ticker. Columns: ticker (FK to holdings), ex_date (date), pay_date (date), amount_per_share (real), source (text — 'yfinance' or 'alphavantage'). The source column is worth adding from day one; when yfinance returns stale data because of a cache bug, you'll want to know which pipeline produced each row.

prices — daily close prices, useful for calculating current yield and portfolio value. Columns: ticker, date, close.

Why SQLite and not just a CSV? Because GROUP BY ticker and SUM(dividends.amount_per_share * holdings.shares) across three joined tables is trivial in SQL and a mess in spreadsheet formulas once you pass 10 positions with quarterly pay schedules. And SQLite is zero-config — the database is a single file you can back up by copying.

This is not investment advice. The code and approach described here are for personal education and portfolio tracking. Past dividends do not guarantee future payouts, and yield calculations based on historical data should not be the sole basis for investment decisions.

Calculating yield on cost (the only metric that matters)

Most dividend tracking apps show you current dividend yield — the annual dividend divided by the current share price. That number is useless for evaluating your own returns. If you bought Realty Income (O) at $48 and it's now trading at $62, the current yield looks lower than what you're actually earning on your invested capital.

Yield on cost fixes this:

yield_on_cost = (annual_dividend_per_share / cost_basis_per_share) * 100
Enter fullscreen mode Exit fullscreen mode

Implementing this in Python means summing the trailing 12 months of dividends per position and dividing by your cost basis. The gotcha here is handling positions with variable dividends (looking at you, BDCs and some REITs) — you can't just take the last quarterly payment and multiply by four. You actually need to sum the last 365 days of payments.

def yield_on_cost(ticker_symbol, db_path):
    conn = sqlite3.connect(db_path)
    # Get cost basis from holdings
    cost = conn.execute(
        "SELECT cost_basis_per_share FROM holdings WHERE ticker = ?",
        (ticker_symbol,)
    ).fetchone()[0]

    # Sum trailing 12 months of dividends
    annual_div = conn.execute("""
        SELECT SUM(amount_per_share)
        FROM dividends
        WHERE ticker = ?
        AND pay_date >= date('now', '-365 days')
    """, (ticker_symbol,)).fetchone()[0]

    return (annual_div / cost) * 100 if cost else 0
Enter fullscreen mode Exit fullscreen mode

Projecting forward income

Historical data tells you what happened. Projections tell you what to expect — and they're the single biggest reason to build your own tracker instead of using a free-tier app.

The simplest projection that's actually useful: take each position's most recent quarterly dividend, assume it holds steady (no cuts, no hikes), and multiply by remaining quarters in the year. This gives you a baseline — a floor, not a prediction. You can layer on dividend growth assumptions later if you want, but the floor alone is actionable. It tells you the minimum income your portfolio will generate if everything stays constant.

Visualizing this is where the project gets satisfying. A stacked bar chart with one bar per month, each segment colored by ticker, makes it immediately obvious that your January income is dominated by one position's quarterly payout while March is a desert. You'll notice patterns in your own portfolio that you'd never catch in a table of numbers.

For the visualization layer, plotly is worth the import over matplotlib for one reason: interactivity. Hovering over a bar segment to see the exact dollar amount and ticker saves you from having to cross-reference a legend. The code is straightforward:

import plotly.express as px

fig = px.bar(monthly_projections, x="month", y="amount",
             color="ticker", title="Projected Monthly Dividend Income")
fig.write_html("dashboard.html")
Enter fullscreen mode Exit fullscreen mode

That write_html call outputs a self-contained HTML file — no server, no framework, just open it in a browser. This is the dashboard.

Making it run without you

The script is only useful if it runs. I have mine scheduled with a daily cron job:

0 8 * * * /usr/bin/python3 /home/owen/dividend-tracker/update.py
Enter fullscreen mode Exit fullscreen mode

Every morning at 8 AM, it fetches the latest dividends, recalculates yield on cost, regenerates the HTML dashboard, and drops the updated file into a directory served by a static file server.

If you'd rather not keep a machine running, GitHub Actions handles scheduled execution for free on public repos. A .github/workflows/update.yml that runs python update.py on a cron trigger, commits the updated data back to the repo, and deploys the HTML to GitHub Pages. The free tier gives you 2,000 minutes of runner time per month — a daily 30-second data fetch consumes about 15 minutes total.

Where the free tools still fall short

Three things I still can't do without paying someone:

  1. Upcoming ex-dividend dates for your specific holdings. Both yfinance and Alpha Vantage free tier are backward-looking. They'll tell you what was paid, not what's declared for next month. For that, you need a premium data feed or you accept a one-month lag.

  2. Dividend safety scores. Knowing the yield is 6% doesn't tell you if the payout ratio is 120%. Morningstar and Simply Safe Dividends do this analysis; free APIs don't.

  3. Tax-lot-level tracking. The data model above treats all shares of a ticker as one block. If you need to track specific tax lots for capital gains planning, you either build a significantly more complex schema or accept that this tool won't replace your brokerage's cost basis tracker.

The dashboard I ended up with isn't going to compete with a paid service on features. But it cost me nothing except a Saturday afternoon, runs on a free GitHub Actions runner, and answers the three questions I actually care about: what am I earning, what is my yield on cost, and what does the next 12 months look like. For a 25-position portfolio, that's enough.

Can I start with just a CSV instead of SQLite?

Yes — and you probably should. Start with a CSV of ticker,shares,cost_basis, read it with pandas.read_csv(), and only migrate to SQLite when you find yourself needing joins or historical snapshots. The simpler the starting point, the more likely you'll actually finish the project.

Does yfinance need an API key?

No. It scrapes publicly accessible Yahoo Finance endpoints. The risk is that Yahoo changes those endpoints without notice, which happens roughly once every 12-18 months. The library maintainers usually patch within a few days.

How do I handle stock splits?

yfinance adjusts historical dividend data for splits automatically. If you bought 100 shares of a stock that later split 2:1, yfinance's dividend history for the pre-split period will show the split-adjusted amount. Your cost basis tracking, however, is your responsibility — the API doesn't know what you paid.

Can I deploy this as a web app?

The static HTML approach described above is the simplest path. If you want a live dashboard with filters, Streamlit is the quickest route from Python script to web app — about 20 lines of additional code gets you dropdowns for account type and date range selectors. Host it on Streamlit Cloud (free tier) and you get a public URL without managing infrastructure.


Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)