Dan Matthew
Service

Operator test

System
Nimbus CMS
Skin
Attract mode (arcade)
Entries
on this screen
Menus
8 items
Path
/blog/divvy-replay-your-real-investing-history
Credits
Free play
Press ~ to toggle · Esc to close

Writing · 6 Sep 2026

I replayed the money I actually invested into portfolios I never owned

Divvy is a Python backtester that replays your real contribution dates and amounts into a counterfactual portfolio, dividends included.

#python #showdev #finance #opensource

Every backtester I could find answers a question I never asked: what if you had put $500 in on the first of every month, without fail, for ten years? I did not do that. I doubt anyone does. I skipped months when money was tight and dropped lump sums in when a bonus landed. My investing history is lumpy, and any tool that smooths it into a tidy monthly schedule is backtesting somebody else.

Divvy takes the money I actually invested, on the dates I actually invested it, and replays it into a portfolio I did not own. Then it tells me what dividends and returns that portfolio would have paid. It is a Python package (divvy-backtest on PyPI, built with uv), a CLI, and a local Streamlit app.

One thing to say up front: it is a backtester, not advice. It answers "what already happened over this window," and one window says nothing about the next.

private by default: stays on your machine Your broker history Fidelity history CSVs (ledger.py) or any date,amount CSV Contribution calendar real dates, real dollars (date, contributed) Counterfactual portfolio tickers + target weights YAML, --holdings, or sidebar replay engine.run_backtest one sorted event loop contribution: split by weight, bought at that day's close ex-dividend: cash, then DRIP back into the same symbol optional: tax, expense drag, periodic rebalance same-day: dividends settle before buys Market data yfinance close + per-share dividends cached as parquet in data/cache/ report.summarize XIRR (money-weighted) dividends: lifetime + trailing 12mo ending value, total return drawdown + vol (basket-only NAV) same calendar for every bucket Experiment Lab Streamlit, runs locally (divvy ui) sliders, comparison table, charts or CLI report to results/date/
⤢ Enlarge

The whole idea is a two-column table

The engine's only required input is a table with two columns: date, contributed. That is the contract. Everything upstream of the engine exists to produce it, and nothing downstream cares where it came from.

Three things produce it today. divvy.contributions reads any broker's export as a generic CSV, tolerant of header names, summing same-day rows. divvy.ledger is a Fidelity-specific parser that classifies raw transaction rows: "YOU BOUGHT" is new money, "REINVESTMENT" is not. It emits the same calendar, and also the dividends I really received, so my actual account can sit in the comparison table as its own row next to the hypotheticals. And synthetic.monthly_contributions generates the flat monthly schedule for people who want to try the tool without exporting anything.

Other brokers slot in as sibling parsers. The rest of the pipeline never knows.

Replay, not simulation

engine.run_backtest is a small event loop. It builds one sorted list of events: a contribution for every row in the calendar, and a dividend for every ex-dividend date of every symbol in the bucket, pulled from yfinance and cached as parquet. Then it walks the list in order.

A contribution is split by target weight and bought at that day's close. If the market was shut, it rolls forward to the next trading day. A dividend pays shares_held * per_share in cash, which is immediately reinvested into the same symbol at that day's close. That is DRIP, and it is why holdings drift away from target weights over time, exactly as they do in a real account.

The detail that matters most is same-day ordering. If a contribution and a dividend land on the same date, the dividend settles first, because shares bought today were not held on the record date. Get that backwards and you pay yourself dividends on shares you did not own yet, and the error compounds for years.

Three realism knobs default to off. A flat dividend_tax_rate reinvests only the after-tax cash (gross is still reported). An expense_ratio shaves shares continuously over the days between events. And rebalance injects periodic events that reset holdings to target weights.

Money-weighted, on purpose

Most performance numbers you see are time-weighted, which deliberately strips out when you added money so a fund manager can be judged on picks alone. That is the wrong metric here. My timing is the point. If I happened to have cash when a fund was cheap, I want to see that reflected.

So report.summarize computes XIRR. Every contribution is a negative cashflow on its real date, the ending value is a single positive cashflow on the last price date, and a Newton solve finds the annualized rate that zeroes the net present value. Because every bucket is fed the identical calendar, the XIRRs are apples to apples. Two portfolios can show the same total return and very different XIRRs if one of them happened to be cheap when I happened to be buying.

Dividends get reported as a first-class metric, twice: the lifetime total, and trailing-twelve-month income, which is the run rate a dividend investor actually watches. Max drawdown and annualized volatility are computed on a contribution-free growth-of-$1 NAV, so they describe the basket's own risk and not my cash-flow luck.

The Experiment Lab

divvy ui launches a Streamlit app wrapped around the same run_backtest call. The sidebar picks a contribution source (synthetic DCA or an uploaded CSV), exposes up to four portfolios as editable ticker and weight tables, and holds the realism knobs. Hit Run and it renders the headline metrics, the comparison table with an SPY benchmark row, and charts for value, cumulative dividends, and dividends by year. Market data is memoized with st.cache_data, so tweaking a weight and re-running is quick.

Private by default

The real numbers never leave the machine. data/, results/, personal buckets/, and .env are gitignored; only code and fake examples/ live in the repo. The hosted demo on Streamlit Community Cloud runs on synthetic data only, and the Fidelity importer is CLI-only by design. There is no upload-your-brokerage-statement path on the public site.

What it is not

Divvy is a replay of the past. It is not a forecast and it is not advice. Forward planning lives in a separate divvy project command (a closed-form target plus a Monte Carlo range), kept in its own module so nobody mistakes an assumption-driven projection for something that actually happened.

Try the demo at divvy-backtest.streamlit.app, install it with pip install divvy-backtest (PyPI), or read the code at github.com/DanMat/Divvy. If you have a broker export you want supported, it is one parser away.