One sheet to rule them all. “how much money do I actually have?”
Explore the docs »
Report Bug
·
Request Feature
$ uv run stonksmith --help
usage: stonksmith [-h] [--verbose] [--debug] [--quiet] [--version]
{ally,fidelity,schwab529plan,snaptrade,tsp} ...
==================================================
__ _ _ __ _ _ _
/ _\ |_ ___ _ __ | | __/ _\_ __ ___ (_) |_| |__
\ \| __/ _ \| '_ \| |/ /\ \| '_ ` _ \| | __| '__
_\ \ || (_) | | | | < _\ \ | | | | | | |_| | | |
\__/\__\___/|_| |_|_|\_\___/_| |_| |_|_|\__|_| |_|
==================================================
Aggregate everything in one dashboard
Written by: @Gerrrt
Version : 0.1.0
Codename: Forrest Gump
Brokers:
Available Brokers
{ally,fidelity,schwab529plan,snaptrade,tsp}
ally Brokerage accounts at https://live.invest.ally.com
fidelity Brokerage and retirement accounts at https://www.fidelity.com
schwab529plan College Savings Account at https://www.schwab529plan.com
snaptrade Every brokerage connected through https://snaptrade.com
tsp Thrift Savings Plan, valued from published share pricesTable of Contents
StonkSmith forges your financial data into a single source of truth. It scrapes, aggregates and syncs your investment accounts into one Google Sheets dashboard — holdings, balances, transactions, net worth over time and an asset allocation breakdown, all rendered from databases it owns rather than from whichever app happened to be open.
Absolutely no day-trading.
No more bouncing between a squad of applications each meant for one account, because no current app just works for all the accounts you own. Think of it as a personal financial command center: it scales with your life as you gain wealth, takes new platforms without ceremony, and answers the one question those five apps between them will not.
ONE SHEET TO RULE THEM ALL!
StonkSmith/
|--- src/
| |--- stonksmith/ # the one name this project installs
| |--- main.py # CLI entry point
| |--- etc/ # config, logging, connection, shells, paths,
| | # records, database, the canonical row shape
| | # and the one thing that writes it to a sheet
| |--- brokers/ # one package per broker (broker.py + helpers)
| |--- modules/ # per-broker scrape/sync modules
| |--- loaders/ # dynamic broker and module loading
| |--- helpers/ # db, sheets, logging helpers
|--- docs/ # the reference chapters this file summarises,
| # live verification, what a schedule can carry,
| # and what was decided against
|--- scripts/ # one-off setup and probe scripts
|--- tests/
|--- pyproject.toml
Everything lives under stonksmith/ so that the wheel installs exactly one
importable name. It used to install six — main, etc, helpers, modules,
loaders, brokers — straight into site-packages, where any of them could
collide with an unrelated package in the same environment.
Each broker is one package. brokers/<name>/broker.py holds the login class and
publishes it as Broker, optionally alongside broker_args.py and any
parser.py. A directory containing broker.py is a broker — that is how
BrokerLoader discovers them, scanning src/stonksmith/brokers/ first and then
~/.stonksmith/brokers/.
broker.py is the only file a broker needs. Without a database.py it gets
BrokerDatabase, and without a db_navigator.py it gets BrokerNavigator —
which is what every bundled broker now takes, SnapTrade's navigator aside. A
broker that does ship one and gets it wrong is reported rather than quietly
given the default: the file exists because somebody meant something by it.
A broker that raises while loading is
reported by name and skipped — it registers no subparser and is simply
unavailable for that run, so a half-finished broker under
~/.stonksmith/brokers/ never takes the rest of the tool down with it.
Your own brokers and modules import from stonksmith. — see
what to import, which also covers the
pre-namespace names, still accepted under deprecation until 1.0.
Brokers come in three shapes. A scraper posts a form and reads the response,
and subclasses Connection: Schwab 529. A browser-backed broker has a login
guarded by bot detection, a session worth keeping between runs, and a page that
only exists after JavaScript has run; it subclasses BrowserConnection, which
owns the whole Playwright lifecycle: Fidelity and Ally. An API-backed broker
has no login at all — its key lives in config and the OS keyring — and
subclasses ApiConnection: SnapTrade, and TSP, which holds no key either
because the data it reads is published.
What a module is handed and what it must return is
docs/modules.md; src/stonksmith/modules/example.py is the annotated
template.
Note
Not every claim here rests on a live run. Green tests say the code does
what it was written to do, which is not the same as saying the site still
looks the way it did when the parser was written. Four of the five brokers
have been run against the real thing, and the sheet has been read back off a
real spreadsheet; eleven claims are still open. Five are the fidelity
broker, which has never been run — the Fidelity accounts SnapTrade reaches
are settled, but that is an API answering, not a browser getting past bot
detection to scrape a summary page. Three wait on data rather than on
effort — whether the Transactions tab holds every movement or only the
newest five hundred, whether the Net Worth series carries across brokers
that scraped on different days, and whether a 529 with more than one
beneficiary attributes its movements to the right one. Two wait on a
condition occurring at all: a SnapTrade connection lapsing, and an account's
holdings going stale. One waits only on a run, being whether each allocation
block's slices add up on a real tab.
docs/live-verification.md is the record of
which is which, claim by claim, and this note summarises it rather than being
maintained beside it.
Python 3.14 and uv. Fidelity and Ally drive a real browser, so those two also need the Playwright runtime; the other three brokers need nothing beyond the install below.
From PyPI, which gets you the two console scripts and nothing else:
uv tool install stonksmithOr from a clone, which is what you want to change anything:
git clone https://github.com/Gerrrt/StonkSmith.git
cd StonkSmith
uv syncBoth are supported and both stay current — see
Supported versions. Every command below is
written for the clone, so drop the uv run if you installed the tool.
Then, if you intend to use Fidelity or Ally:
uv run playwright install firefoxRun that again after any uv sync that moves Playwright. Each release pins a
new browser revision, so an already-installed Firefox stops satisfying it and
the next run fails with Could not start browser, naming an executable that is
not there.
List the modules available for a broker:
uv run stonksmith schwab529plan -LRun a module against an account:
uv run stonksmith schwab529plan -M schwab529plan -u <username> -p <password>Or use a credential stored in the database instead of passing it on the command line:
uv run stonksmith schwab529plan -M schwab529plan -id 1Five brokers, and they do not work alike — one needs you to sign in by hand
every time, one needs no credential at all. What each needs and what a run of it
does is docs/brokers.md; this table is the index into it.
| Broker | Shape | What it needs from you |
|---|---|---|
| Fidelity | Browser | A manual sign-in once; the session is reused until it expires. Or link it through SnapTrade and skip the browser |
| Ally Invest | Browser | A manual sign-in every scrape — Ally honours no restored session. --from-prices revalues between scrapes with no browser at all |
| SnapTrade | API | A free Personal API key, and one browser step per brokerage every few weeks. Covers Schwab, Fidelity, Vanguard and the rest through one key |
| Schwab 529 | Scraper | A stored credential. A form post and two page reads — no browser, nothing to expire |
| TSP | API | Nothing daily. Share prices are published; units come from a quarterly statement or a config line |
Adding a brokerage SnapTrade covers is an operator action, not a code change — no new broker, module, database or tab. Vanguard is the standing example.
A run reports what it did. That is the default and it used to not be: every
progress line — including the yellow [!] warnings — logs at INFO, and the
default level was ERROR, so a sync could read a statement, write the snapshot
to the database and update the Google Sheet while printing nothing but a
progress bar. Success and doing nothing at all looked identical.
| Flag | Shows |
|---|---|
| (none) | What the run did, and anything that went wrong. |
--quiet |
Failures only. This is what an unattended run wants. |
--verbose |
Same as the default, but wins over --quiet — useful for seeing inside a wrapper script that hardcodes it. |
--debug |
Everything, including internals. |
All four work on either side of the broker name.
The exit status reflects what the run actually did, so a cron entry, systemd timer or CI step can tell whether the sync worked.
| Code | Meaning |
|---|---|
0 |
The run did its work. A Google Sheets failure after the balances reached the database is still a success — the data is saved, and the log says the dashboard was not updated. |
1 |
The run did not complete: unknown broker or module, could not connect or log in, a module reported it did nothing, only some of the requested modules loaded, or nothing reached the database. |
130 |
Interrupted (128 + SIGINT). Distinct from 1 so a scheduler can page on a real failure and shrug at a human pressing Ctrl-C. |
stonksmithdb <command> reports the same way: 0 when the command did its work,
1 when it did not. For stale that means 1 when any account has gone stale
or a database would not open — a status a crontab can act on, for the one failure
none of the codes above can catch, because nothing broke.
A partial module load still runs the modules that did load — partial data beats
none — but reports 1 rather than claiming success.
Every broker writes to its own SQLite file at
~/.stonksmith/workspaces/<workspace>/<broker>.db, holding four tables:
accounts, account_snapshots, holdings and transactions. Money is stored
as a number with the source's own text kept beside it, so a site that changes
its formatting costs you a parse rather than the record.
Browse and manage it from the shell:
uv run stonksmithdbThe tables, the columns, the shell's commands and what a migration does on open
are docs/database.md.
Ask whether anything has quietly stopped updating — no login, no network:
uv run stonksmithdb staleIt exits 1 when any account's as-of date is missing, unreadable or more than a
week old. That is the one question a schedule cannot otherwise ask: every other
step reports when it breaks, and none reports when it stops happening.
StonkSmith owns five tabs and refuses to touch anything else. Accounts,
Holdings, Transactions, Net Worth and Dashboard are created on the first
sync and rewritten in full every run — so anything of your own goes on a tab of
your own, and pulls across with a formula.
That is a refusal rather than a convention. Each tab's first cell carries a
banner; before clearing, StonkSmith reads it back. A tab carrying the banner is
its own, an empty tab is adopted, and a tab with anything else on it is refused
by name with nothing written. The run still exits 0 — the scrape reached the
database before the sheet was touched.
Rebuild the tabs from the databases at any time, with no login anywhere:
uv run stonksmithdb sheetWhat each tab promises, why the dashboard has to be constructed rather than
read, and what verify checks that a successful sync cannot show are
docs/sheet.md.
The five brokers do not schedule alike, and two of them do not schedule at all. That is the part a crontab cannot tell you, and getting it wrong is expensive in a specific way: a cron job that errors every night gets muted, and after that the portfolio has stopped updating with nothing to say so.
| Broker | On a schedule |
|---|---|
tsp |
Yes. No credential in the daily path |
snaptrade |
Yes, until the connection expires — a browser step every few weeks |
schwab529plan |
Yes. A form post with a stored credential |
ally |
--from-prices only, which reprices a stale unit count rather than scraping |
fidelity |
No. Link it through SnapTrade instead |
Weekdays after the close, one process per broker — broker is a positional
subcommand, so there is no --all — staggered, because two runs inside the same
UTC second collapse into one snapshot:
PATH=/usr/local/bin:/usr/bin:/bin
30 18 * * 1-5 cd ~/StonkSmith && uv run stonksmith tsp -M tsp --quiet
35 18 * * 1-5 cd ~/StonkSmith && uv run stonksmith snaptrade -M snaptrade --quiet
40 18 * * 1-5 cd ~/StonkSmith && uv run stonksmith schwab529plan -M schwab529plan -id 1 --quiet
45 18 * * 1-5 cd ~/StonkSmith && uv run stonksmith ally -M ally --from-prices --quiet
50 18 * * 1-5 cd ~/StonkSmith && uv run stonksmithdb sheetWarning
There is no fidelity line, and the ally line is not a scrape. Ally
honours no restored session, so --from-prices values the account from
today's published close and the units the last signed-in run recorded —
exact arithmetic on a number that goes quietly wrong the moment a deposit
lands. It says so on every account it values, and a schedule that mails only
on failure will never show anybody that line. Re-run --manual-login after a
deposit; the schedule cannot, and will not ask.
The sheet goes last, and it reports: stonksmithdb sheet exits 0 when the tabs
were rewritten and 1 when the sheet was unreachable, a tab refused, or a broker
database could not be read — a total short by a whole broker being the failure
that must not be quiet.
That schedule is committed as
scripts/stonksmith.cron, commented and ready to
paste into crontab -e. Every line in it is a no-op or a nightly failure until
its broker is set up — Ally in particular refuses and exits 1 until a
--manual-login run has recorded some units — so read Before the first night
before installing it.
docs/scheduling.md is the record: what each broker can
do unattended, what --from-prices is and is not, what has to be true before
the first night, and which of these claims a live run has actually settled.
This section summarises it rather than being maintained beside it.
- Multi-broker support (Fidelity, Schwab and anything else via SnapTrade, plus Ally Invest and Schwab 529 scrapers, and TSP from public data, for what SnapTrade does not cover)
- Automatic data scraping (requests + Playwright)
- Google Sheets sync
- CLI commands for automation
- Credentials stored in the OS keyring
- Account history: numeric balances, holdings and transactions over time
- Net worth tracking over time
- Asset allocation breakdown — by account kind and by position, both free
from what the databases already hold, plus by asset class from a mapping
you keep in
~/.stonksmith/stonksmith.conf. Sector and region are still absent: nothing states them and nothing here guesses - Scheduling (cron), for the brokers that run unattended — three of five, plus Ally in a reduced mode. Fidelity is replaced by SnapTrade, not scheduled
- More brokers. Vanguard needs no code at all; link it through SnapTrade
- Settle whether the
Transactionstab windows at five hundred rows (#141) — blocked on a broker with the movement volume - Settle whether the
Net Worthseries carries across brokers (#149) — blocked on a workspace whose brokers scraped on different days
See the open issues for a full list of proposed features (and known issues).
This project handles sensitive financial data.
Current safeguards:
-
Secrets live in the OS keyring (Keychain on macOS, Secret Service on Linux, Credential Locker on Windows). The SQLite database stores only a reference such as
schwab529plan:alice, never the secret itself. -
show credsmasks secrets. Setaudit_modeandreveal_chars_of_pwdin~/.stonksmith/stonksmith.confto reveal a short prefix when you need to tell two credentials apart. -
export credswrites the keyring reference, never the secret. -
Databases created before this change are migrated automatically on first open: each plaintext password moves into the keyring and the column is cleared in place.
-
Everything StonkSmith writes is owner-only —
0600for files,0700for the directories under~/.stonksmith. That covers the databases, the config, the run log, page captures, the saved browser session and the Playwright trace.
Important
Passing -p on the command line still exposes the secret to your shell
history and process list. Prefer add creds in stonksmithdb plus -id.
Never commit credentials to source control.
SECURITY.md is the full account, including the risks this
project accepts rather than solves — the CDP debugging port, the width of the
Google grant, and what a migrated database still has in its freed pages — and
how to report a vulnerability.
This is currently a personal project, but contributions may open up in the future.
CONTRIBUTING.md has the gates, the commit and branch
conventions, the test house rules, and the handful of settings whose reasons are
worth reading before changing them. The four gates are:
uv run ruff check
uv run ruff format --check
uv run ty check
uv run pytest -q --cov --cov-fail-under=87The version lives in pyproject.toml and nowhere else. --version and the
banner read it off the installed distribution, so bumping it is one edit
followed by uv sync — and tests/test_version_single_source.py fails if the
two ever part company. The codename beside it in src/stonksmith/etc/cli.py is the one
piece still written by hand, because nothing can derive one.
Distributed under the MIT License. See LICENSE for more information.
Garrett Allen — @Gerrrt
Project Link: https://github.com/Gerrrt/StonkSmith
The three sources StonkSmith reads that it did not have to build: