Building a Dividend Reinvestment Tracker in Python Against a Brokerage API
A practical data model and sync loop for tracking DRIP lots, cost basis, and yield on cost from a brokerage API — including the corporate actions and 1099 restatements that break naive trackers.
Your broker’s app shows a position size and a total return percentage. Neither answers the question a dividend-growth investor actually asks: how many of these shares did you buy with cash, how many did the dividends buy for you, and what is the cost basis of each reinvested sliver? Brokers track that internally — they have to, for the 1099 — but most surface it only as a year-end PDF, and almost none expose per-lot DRIP history in a form you can chart.
So you write your own. We built one against a REST brokerage API over a few evenings, and the interesting problems were not the ones we expected. Pulling JSON is trivial. Making the same pull twice not corrupt your history, and making a ticker symbol survive a spinoff, is where the work is.
The ledger is the product; the dashboard is a view
The first instinct is a positions table with a shares column you keep updating. Resist it. The broker already owns that number, and any time your copy and theirs disagree you have no way to tell which is wrong or when the drift started.
Store an append-only event log instead, and derive everything by folding it:
# schema.sql — one table you actually write to
CREATE TABLE events (
broker_txn_id TEXT PRIMARY KEY, -- idempotency key, from the broker
instrument_id INTEGER NOT NULL, -- NOT the ticker; see below
kind TEXT NOT NULL, -- BUY | SELL | DIV_CASH | DIV_REINVEST
-- | SPLIT | ROC | FEE | TRANSFER_IN
trade_date TEXT NOT NULL,
settle_date TEXT,
quantity TEXT, -- Decimal as string
price TEXT,
amount TEXT,
currency TEXT NOT NULL DEFAULT 'USD',
status TEXT NOT NULL, -- pending | settled | corrected
raw JSON NOT NULL -- the untouched API payload
);
Three things earn their keep here. broker_txn_id as the primary key makes re-ingestion a no-op, which lets you re-fetch overlapping date windows without dedupe logic. status gives you somewhere to put a dividend that posts as pending and later settles at a different amount. And raw means that when you discover in month eight that the broker was sending a field you ignored, you can backfill from your own database instead of re-paginating three years of API history.
Use decimal.Decimal throughout, never float. DRIP produces fractional shares — a $47.20 dividend on a stock trading at $138.44 buys 0.340941 shares — and floats accumulate error across hundreds of those. Store quantities as strings with at least six decimal places, keep money in Decimal, and quantize() only at the point of display.
SQLite is the right default. A 30-position portfolio held for a decade generates on the order of a few thousand rows. You will not outgrow it, and a single file you can copy is worth more than a Postgres container you have to keep alive.
The sync loop: idempotent pulls and a reconciliation check
Brokerage APIs generally expose an activities or transactions endpoint paginated by date range. The naive loop — fetch since last run, insert — fails in two specific ways.
First, activity is not immutable. A dividend can appear as pending on the pay date and be corrected days later, and some brokers backdate corrections into a window you have already swept past. Re-fetch a rolling window (we use 45 days) on every run and upsert on broker_txn_id. If the payload for an existing ID differs from the stored raw, write the new version and flip status to corrected rather than silently overwriting — you want the diff visible.
Second, nothing tells you when you have quietly lost a transaction. Add a reconciliation step that runs after every sync: fold your event log into a share count per instrument, pull the broker’s current positions endpoint, and compare.
def reconcile(derived, broker_positions, tol=Decimal("0.000001")):
problems = []
for iid, qty in derived.items():
actual = broker_positions.get(iid, Decimal(0))
if abs(qty - actual) > tol:
problems.append((iid, qty, actual, qty - actual))
return problems # non-empty => stop, do not publish numbers
This is the single highest-value 15 lines in the project. Without it, a tracker degrades invisibly; with it, a missed corporate action fails loudly on the next cron run.
On pricing: the reinvestment price is the price on the pay date, not the ex-dividend date. Most APIs give you the executed reinvestment price directly on the transaction — use it, and only fall back to a market data lookup for transfer-in lots where the broker sent no basis.
What breaks in year two
The tracker that works for six months breaks on the first corporate action, and always in the same places.
Tickers are not stable keys. Symbols get reused, companies rename, and a spinoff hands you shares of an instrument you never bought. This is why the schema above keys on instrument_id with the ticker as a mutable attribute. Retrofitting that after you have three years of history is a genuinely unpleasant migration.
Splits rewrite quantities, not events. A 4-for-1 split should be an event in the log, applied during the fold, not a bulk UPDATE against past rows. Rewriting history destroys your ability to reconcile against a broker statement from before the split.
Return of capital reduces basis rather than counting as income. If you fold ROC as ordinary dividend income, your yield on cost is overstated and your eventual capital gain is understated. Give it its own kind and handle it explicitly.
Transfers arrive without basis. Move accounts and lots frequently land with acquisition dates but no cost, sometimes for weeks. Flag those lots and exclude them from cost-basis reporting instead of letting a zero propagate into a return calculation.
Once the ledger is honest, the metrics are a handful of queries: yield on cost (trailing 12 months of distributions divided by cash basis, deliberately excluding DRIP-purchased shares from the denominator), the share of current position acquired through reinvestment, and a projected forward income figure. Those are the numbers no broker dashboard gives you, and they are the reason to build the thing.
Cursor
An AI-native editor that handles the mechanical half of this project well — generating the Pydantic models from a sample API payload, writing the pagination and backoff wrapper, and scaffolding the pytest fixtures for corporate-action edge cases. The data modeling decisions still need to be yours.
Free tier available; Pro around $20/month
Affiliate link · We earn a commission at no cost to you.
Testing the parts that will actually bite
Write fixtures from real payloads, with the account numbers scrubbed. The tests worth having are the awkward ones: a dividend that posts pending and settles at a different amount, a 4-for-1 split mid-year, a spinoff that introduces a new instrument, a partial sale that has to pick lots under FIFO versus specific identification, and a re-run of the same sync window that must leave the database byte-identical.
That last test — sync twice, assert no change — catches more real bugs than any of the others. If it passes, your idempotency key is doing its job, and you can run the job hourly without thinking about it.
FAQ
Do I need a separate market data provider?
SQLite or Postgres?
Why not just use a spreadsheet?
Related reading
2026-06-22
Tiingo vs Polygon.io: Market Data APIs for Indie Quant Projects in 2026
A practical comparison of Tiingo and Polygon.io for solo quant builders in 2026 — pricing, rate limits, data coverage, and which one fits a weekend backtester.
2026-06-22
Building a Portfolio Rebalancing Script in Python: From Drift to Trades
A practical walkthrough of writing a Python rebalancing script: measuring allocation drift, generating a self-funding trade list, and using threshold bands to avoid over-trading.
2026-08-12
Implied Volatility for Beginners: What IV Rank Tells You Before an Earnings Report
IV rank compresses a year of implied volatility into one number. Here is how it is calculated, why it almost always rises into earnings, and the three checks that make it useful instead of misleading.
2026-08-12
Reading a 10-K as a Developer: The Five Sections That Actually Change a Thesis
Most of a 10-K is boilerplate carried over from last year. Here are the five items that carry new information, plus the SEC endpoints that let you diff filings instead of reading them front to back.
2026-06-22
Position Sizing and Risk per Trade: The Math Retail Investors Skip in 2026
How to size a trade from risk instead of conviction: the fixed-fractional formula, fractional Kelly, R-multiples, and the drawdown math that decides whether a losing streak is survivable.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.