finances 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- finances-0.3.0/LICENSE +21 -0
- finances-0.3.0/PKG-INFO +496 -0
- finances-0.3.0/README.md +469 -0
- finances-0.3.0/finances/__init__.py +5 -0
- finances-0.3.0/finances/__main__.py +6 -0
- finances-0.3.0/finances/analyze.py +1206 -0
- finances-0.3.0/finances/anonymize.py +509 -0
- finances-0.3.0/finances/api.py +832 -0
- finances-0.3.0/finances/ask.py +934 -0
- finances-0.3.0/finances/backtest.py +1122 -0
- finances-0.3.0/finances/benchmark_descriptors.py +234 -0
- finances-0.3.0/finances/budgets.py +298 -0
- finances-0.3.0/finances/cashflow.py +1395 -0
- finances-0.3.0/finances/categorizer.py +1261 -0
- finances-0.3.0/finances/cli.py +245 -0
- finances-0.3.0/finances/cpi.py +1272 -0
- finances-0.3.0/finances/database.py +293 -0
- finances-0.3.0/finances/demo.py +272 -0
- finances-0.3.0/finances/engine.py +420 -0
- finances-0.3.0/finances/export.py +319 -0
- finances-0.3.0/finances/financial_auditor.py +1521 -0
- finances-0.3.0/finances/fx.py +133 -0
- finances-0.3.0/finances/importer.py +1104 -0
- finances-0.3.0/finances/llm_enrich.py +850 -0
- finances-0.3.0/finances/llm_parse.py +184 -0
- finances-0.3.0/finances/models.py +181 -0
- finances-0.3.0/finances/parse.py +1468 -0
- finances-0.3.0/finances/planner.py +975 -0
- finances-0.3.0/finances/reference_data.py +842 -0
- finances-0.3.0/finances/utils.py +398 -0
- finances-0.3.0/finances/web_dist/assets/index-BFvQ5mOC.js +70 -0
- finances-0.3.0/finances/web_dist/assets/index-D77oFOCN.css +2 -0
- finances-0.3.0/finances/web_dist/favicon.svg +1 -0
- finances-0.3.0/finances/web_dist/icons.svg +24 -0
- finances-0.3.0/finances/web_dist/index.html +27 -0
- finances-0.3.0/finances/wrapped.py +190 -0
- finances-0.3.0/finances.egg-info/PKG-INFO +496 -0
- finances-0.3.0/finances.egg-info/SOURCES.txt +51 -0
- finances-0.3.0/finances.egg-info/dependency_links.txt +1 -0
- finances-0.3.0/finances.egg-info/entry_points.txt +2 -0
- finances-0.3.0/finances.egg-info/requires.txt +1 -0
- finances-0.3.0/finances.egg-info/top_level.txt +1 -0
- finances-0.3.0/pyproject.toml +53 -0
- finances-0.3.0/setup.cfg +4 -0
- finances-0.3.0/tests/test_api.py +74 -0
- finances-0.3.0/tests/test_database.py +477 -0
- finances-0.3.0/tests/test_demo.py +76 -0
- finances-0.3.0/tests/test_engine.py +490 -0
- finances-0.3.0/tests/test_features.py +324 -0
- finances-0.3.0/tests/test_importer.py +218 -0
- finances-0.3.0/tests/test_models.py +243 -0
- finances-0.3.0/tests/test_parser_golden.py +435 -0
- finances-0.3.0/tests/test_utils.py +571 -0
finances-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 roshanlam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
finances-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finances
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: See where your money goes from your own bank statements and exports, checked against their printed totals. No bank login; nothing leaves your machine.
|
|
5
|
+
Author: roshanlam
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/roshanlam/finances
|
|
8
|
+
Project-URL: Issues, https://github.com/roshanlam/finances/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/roshanlam/finances/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: personal-finance,bank-statements,pdf,reconciliation,budgeting,local-first,privacy,ofx,beancount,ledger
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pdfplumber>=0.11.10
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# finances
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/finances/)
|
|
31
|
+
[](pyproject.toml)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
See where your money goes from your own bank statements and exports. No bank
|
|
35
|
+
login, no Plaid, no account linking, no cloud. Everything runs on your machine
|
|
36
|
+
and nothing is uploaded anywhere.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uvx finances demo && uvx finances web --db demo.db --open # try it on made-up data
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
[](docs/demo.mp4)
|
|
43
|
+
|
|
44
|
+
*20 seconds, on the made-up data from `finances demo`. Click for the full video with sound.*
|
|
45
|
+
|
|
46
|
+
The thing that makes it different: **a statement prints its own totals, so the
|
|
47
|
+
extraction can be proved correct.** Every figure this produces traces back to a
|
|
48
|
+
line on a statement, and the tool tells you when it cannot.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
$ finances check
|
|
52
|
+
|
|
53
|
+
Store finances.db
|
|
54
|
+
Rows 1180 transactions, 2025-01-03 to 2025-12-29
|
|
55
|
+
Statements 36
|
|
56
|
+
|
|
57
|
+
Reconciled 36/36 (100%)
|
|
58
|
+
|
|
59
|
+
Every statement reconciles and no months are missing. The totals downstream
|
|
60
|
+
are traceable to a statement line.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A tool built on a bank API cannot do this. It receives a feed of transactions
|
|
64
|
+
with no independent total to check them against, so a dropped charge or a
|
|
65
|
+
flipped sign is invisible. Here the statement is the ground truth, and
|
|
66
|
+
reconciliation is enforced rather than assumed.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# Running it
|
|
71
|
+
|
|
72
|
+
Three ways in. Pick one; they all end at the same dashboard on
|
|
73
|
+
`http://localhost:8787`.
|
|
74
|
+
|
|
75
|
+
## Try it first, with made-up data
|
|
76
|
+
|
|
77
|
+
Nobody should have to hand over a year of bank statements to find out whether
|
|
78
|
+
a tool is any good. `demo` writes twelve months of an invented checking account
|
|
79
|
+
and credit card — paychecks, rent, a subscription that raises its price, a
|
|
80
|
+
holiday spike — into a separate `demo.db`. It goes through the same
|
|
81
|
+
reconciliation and categorizer as a real statement.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# with uv, nothing to clone
|
|
85
|
+
uvx finances demo
|
|
86
|
+
uvx finances web --db demo.db --open
|
|
87
|
+
|
|
88
|
+
# or from a checkout
|
|
89
|
+
git clone https://github.com/roshanlam/finances && cd finances
|
|
90
|
+
uv run finances demo
|
|
91
|
+
uv run finances web --db demo.db --open
|
|
92
|
+
|
|
93
|
+
# or with Docker, nothing else installed
|
|
94
|
+
docker compose --profile demo up demo # then open http://localhost:8787
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## With your statements: uv
|
|
98
|
+
|
|
99
|
+
Requires [uv](https://docs.astral.sh/uv/) (it fetches Python 3.12+ if you do
|
|
100
|
+
not have it). For the browser dashboard you also need Node 20+; without it
|
|
101
|
+
everything in the terminal still works.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
uv sync # one dependency: pdfplumber
|
|
105
|
+
mkdir -p PDFs # drop your statement PDFs in here
|
|
106
|
+
uv run finances pipeline PDFs/ # parse, categorize, check, report
|
|
107
|
+
uv run finances web --open # builds the dashboard on first run
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Any folder layout works — subfolders are searched too. Tested against Amex,
|
|
111
|
+
Chase (card and checking), Capital One and Discover. For any other bank, use
|
|
112
|
+
its export (next section), or let a local model read the PDF.
|
|
113
|
+
|
|
114
|
+
## Any bank: CSV, OFX/QFX, CAMT.053, MT940
|
|
115
|
+
|
|
116
|
+
Nearly every bank lets you download your transactions. `import` reads those
|
|
117
|
+
files, works out the columns, the date format, the decimal separator and which
|
|
118
|
+
way the amounts are signed, and checks them against whatever balances the file
|
|
119
|
+
carries:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv run finances import ~/Downloads/activity.csv
|
|
123
|
+
uv run finances import exports/ # a whole folder, any mix of formats
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Format | Who exports it | How it is checked |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| CSV with a running balance | most bank accounts | every row has to move the balance by exactly its amount |
|
|
129
|
+
| CSV without one | most card sites | imported, and marked unverified — honestly |
|
|
130
|
+
| OFX / QFX / QBO | US banks ("Quicken" / "Money" download) | each export's opening balance must equal the previous export's closing balance |
|
|
131
|
+
| CAMT.053 | European banks (ISO 20022 XML) | opening balance + every booked entry = closing balance |
|
|
132
|
+
| MT940 | European banks (SWIFT) | opening balance + every entry = closing balance |
|
|
133
|
+
|
|
134
|
+
Headers are recognised in English, German, French, Spanish, Italian, Dutch and
|
|
135
|
+
the Nordic languages, `1.234,56` and `05.01.2026` are read the European way, and
|
|
136
|
+
a summary block above the header (Bank of America does this) is skipped. When
|
|
137
|
+
downloads overlap, rows already in the store for that account are skipped, not
|
|
138
|
+
counted twice. If a CSV is ambiguous, say so: `--sign out-negative`,
|
|
139
|
+
`--date-format %d.%m.%Y`, `--account 1234 --issuer "My Bank" --kind card`.
|
|
140
|
+
|
|
141
|
+
## A bank the parser does not know: a local model, checked
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv run finances parse PDFs/ --llm
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When a statement's layout is not understood (nothing extracted, or the
|
|
148
|
+
extraction does not reconcile), its text goes to a model running **on your
|
|
149
|
+
machine** — Ollama, LM Studio or llama.cpp — which lists the transaction rows.
|
|
150
|
+
The statement's printed totals are still read by the deterministic parser, so
|
|
151
|
+
the model never supplies both the answer and the check: its rows are kept only
|
|
152
|
+
if they add up to those totals. Otherwise they are discarded and the statement
|
|
153
|
+
stays marked as a mismatch. `FINANCES_LLM_FALLBACK=1` turns this on for the
|
|
154
|
+
dashboard's uploads too.
|
|
155
|
+
|
|
156
|
+
**Always look at the `check` verdict at the end before trusting anything
|
|
157
|
+
else.** If it does not say 100%, the numbers downstream are built on a bad
|
|
158
|
+
extraction, and every other report will tell you so too.
|
|
159
|
+
|
|
160
|
+
## With your statements: Docker
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
mkdir -p PDFs # drop your statement PDFs in here
|
|
164
|
+
docker compose up # then open http://localhost:8787
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
On start the container parses whatever is in `./PDFs` (unchanged files are
|
|
168
|
+
skipped), categorizes it, prints the `check` verdict and serves the dashboard.
|
|
169
|
+
The store and every generated report land in `./data`. Every command works the
|
|
170
|
+
same way inside it:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
docker compose run --rm finances check
|
|
174
|
+
docker compose run --rm finances cashflow
|
|
175
|
+
docker compose run --rm finances ask --example top-merchants
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The port is published on `127.0.0.1` only, and `.dockerignore` is a whitelist:
|
|
179
|
+
your PDFs and database are never sent to the Docker daemon or baked into an
|
|
180
|
+
image.
|
|
181
|
+
|
|
182
|
+
## The dashboard
|
|
183
|
+
|
|
184
|
+
Your statements, the reconciliation verdict for each one, spending by month and
|
|
185
|
+
category, detected subscriptions, and a searchable transaction table. **You
|
|
186
|
+
can drag new statement PDFs or bank exports straight onto it**: they are read
|
|
187
|
+
on drop, and the reply tells you how many transactions came out and whether the
|
|
188
|
+
file checks out.
|
|
189
|
+
|
|
190
|
+
- **Wrapped** — your year in money: where it went, your regular, the month that
|
|
191
|
+
got away from you, what subscriptions cost you all year, and how much of it
|
|
192
|
+
traces back to a statement that reconciles.
|
|
193
|
+
- **Budgets & goals** — a monthly limit per category, projected to the end of
|
|
194
|
+
the month so you hear about it before it is broken; savings goals against the
|
|
195
|
+
surplus your statements actually show.
|
|
196
|
+
- **Cancel these** — recurring charges ranked by what dropping them saves, with
|
|
197
|
+
the reason each one is there (a price that went up, three services doing one
|
|
198
|
+
job). Mark one cancelled and any charge after that date is flagged.
|
|
199
|
+
|
|
200
|
+
The server makes no outbound requests, binds to localhost, and refuses any
|
|
201
|
+
request that does not come from a page on localhost — so another website open
|
|
202
|
+
in the same browser cannot read your transactions from it.
|
|
203
|
+
|
|
204
|
+
While working on the frontend, run the two halves separately —
|
|
205
|
+
`uv run finances web` in one terminal, `cd web && npm run dev` in another, then
|
|
206
|
+
use `http://localhost:5173` (it proxies `/api` through).
|
|
207
|
+
|
|
208
|
+
## Or stay in the terminal
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
uv run finances analyze # where the money went
|
|
212
|
+
uv run finances wrapped # your year in money
|
|
213
|
+
uv run finances budget # this month against your limits
|
|
214
|
+
uv run finances budget cancel # the subscriptions worth cancelling
|
|
215
|
+
uv run finances cashflow # the next 60 days
|
|
216
|
+
uv run finances audit # the reckoning
|
|
217
|
+
uv run finances cpi # your own inflation rate
|
|
218
|
+
uv run finances backtest # how accurate are the forecasts
|
|
219
|
+
uv run finances ask "how much did I spend on coffee in March"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`uv run finances` on its own lists every command; `uv run finances <command>
|
|
223
|
+
--help` shows that tool's own options. Inside an activated virtualenv
|
|
224
|
+
(`source .venv/bin/activate`), drop the `uv run`. Without uv at all:
|
|
225
|
+
`python3 -m venv .venv && .venv/bin/pip install -e .` and then
|
|
226
|
+
`.venv/bin/finances`.
|
|
227
|
+
|
|
228
|
+
## Configuration
|
|
229
|
+
|
|
230
|
+
Everything is a flag, and the few things you would otherwise repeat on every
|
|
231
|
+
command are also environment variables:
|
|
232
|
+
|
|
233
|
+
| Variable | Default | What it sets |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `FINANCES_DB` | `finances.db` | the store every command reads and writes |
|
|
236
|
+
| `FINANCES_PDF_DIR` | `./PDFs` | where dashboard uploads are saved |
|
|
237
|
+
| `FINANCES_HOST` / `FINANCES_PORT` | `127.0.0.1` / `8787` | where `finances web` listens |
|
|
238
|
+
| `FINANCES_CURRENCY` | `USD` (or `finances fx --base`) | the currency every total is reported in |
|
|
239
|
+
| `FINANCES_LLM_ENDPOINT` | Ollama, LM Studio, llama.cpp on localhost | the model server for `ask`, `enrich` and `parse --llm` |
|
|
240
|
+
| `FINANCES_LLM_FALLBACK` | off | `1` lets a local model read unknown PDF layouts, uploads included |
|
|
241
|
+
| `FINANCES_PLAN` | `plan.json` | where savings goals are kept |
|
|
242
|
+
| `FINANCES_WEB_DIST` | `web/dist`, or the copy inside the package | a prebuilt dashboard to serve |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
# Command reference
|
|
247
|
+
|
|
248
|
+
| Command | What it does | Writes |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| `parse PDFs/` | extract transactions from statement PDFs (`--llm` for unknown layouts) | `finances.db` |
|
|
251
|
+
| `import exports/` | bank CSV, OFX/QFX, CAMT.053 and MT940 files | `finances.db` |
|
|
252
|
+
| `check` | reconciliation and coverage report | — |
|
|
253
|
+
| `categorize apply` | label transactions, write back to the store | `finances.db` |
|
|
254
|
+
| `analyze` | spending dashboard | `finance_dashboard.html`, `insights.md` |
|
|
255
|
+
| `cashflow` | daily balance projection with a low-point warning | `cashflow.html` |
|
|
256
|
+
| `plan status` | savings goals against your real surplus | `plan.json` |
|
|
257
|
+
| `budget` | monthly limits per category; `budget cancel` for the cancel list | `finances.db` |
|
|
258
|
+
| `wrapped` | your year in money | — |
|
|
259
|
+
| `export beancount` | beancount, hledger/ledger, YNAB or Actual files | the file you name |
|
|
260
|
+
| `fx` | base currency and exchange rates | `finances.db` |
|
|
261
|
+
| `audit` | a strict grandfather goes through your statements | `auditor_ledger.json` |
|
|
262
|
+
| `cpi` | personal inflation, per merchant and per category | `cpi.html` |
|
|
263
|
+
| `backtest` | grades the forecasts against what actually happened | `backtest.html` |
|
|
264
|
+
| `ask "..."` | plain-English questions, answered offline | — |
|
|
265
|
+
| `anonymize` | a copy safe to share | `transactions_shareable.csv` |
|
|
266
|
+
| `enrich` | local-LLM merchant cleanup | `finances.db` |
|
|
267
|
+
| `web` | serve the browser dashboard, with PDF drag-and-drop | — |
|
|
268
|
+
| `demo` | a year of made-up statements, to try everything on | `demo.db` |
|
|
269
|
+
| `pipeline PDFs/` | parse + categorize + analyze in one run | all of the above |
|
|
270
|
+
|
|
271
|
+
Every command accepts either the SQLite store or a CSV, interchangeably:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
uv run finances analyze --db finances.db
|
|
275
|
+
uv run finances analyze cleanedData/transactions.csv
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Things you will probably need
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# Encrypted PDFs
|
|
282
|
+
uv run finances parse PDFs/ --password 1234
|
|
283
|
+
|
|
284
|
+
# A statement extracted wrong — see what the parser saw
|
|
285
|
+
uv run finances parse "PDFs/one bad file.pdf" --dump-columns
|
|
286
|
+
|
|
287
|
+
# Fail the build if anything does not reconcile (for a cron job)
|
|
288
|
+
uv run finances check --strict
|
|
289
|
+
|
|
290
|
+
# Re-parse everything, ignoring the cache
|
|
291
|
+
uv run finances parse PDFs/ --force
|
|
292
|
+
|
|
293
|
+
# Tell it your take-home pay, so it can work out what is left over
|
|
294
|
+
uv run finances analyze --income 6200
|
|
295
|
+
|
|
296
|
+
# Teach the categorizer, biggest money first
|
|
297
|
+
uv run finances categorize review
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Hand it to the tool you already use
|
|
301
|
+
|
|
302
|
+
This gets complete, checked transactions out of statements. Budgeting apps and
|
|
303
|
+
plain-text ledgers are better at other things, so it hands the rows over:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
uv run finances export beancount -o money.beancount --balances
|
|
307
|
+
uv run finances export hledger -o money.journal --balances # ledger-cli reads it too
|
|
308
|
+
uv run finances export ynab -o exports/ # one CSV per account
|
|
309
|
+
uv run finances export actual -o exports/
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
With `--balances`, every reconciled statement's closing balance becomes a
|
|
313
|
+
balance assertion, so `bean-check` or `hledger check` re-verifies the whole
|
|
314
|
+
history independently of this tool. Card payments from checking go through an
|
|
315
|
+
`Assets:Transfers` account, so they net to zero instead of counting twice.
|
|
316
|
+
|
|
317
|
+
## More than one currency
|
|
318
|
+
|
|
319
|
+
Each transaction keeps the currency its file was in. Totals are reported in one
|
|
320
|
+
base currency, at rates you set — there is no rate feed, because nothing here
|
|
321
|
+
goes online:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
uv run finances fx --base EUR # report in euros
|
|
325
|
+
uv run finances fx USD=0.92 GBP=1.17 # 1 USD is 0.92 of the base currency
|
|
326
|
+
uv run finances fx # what is set, and what is still missing
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Rows in a currency with no rate are left out of the totals and counted, and the
|
|
330
|
+
dashboard says so, rather than quietly adding euros to dollars.
|
|
331
|
+
|
|
332
|
+
## Asking questions offline
|
|
333
|
+
|
|
334
|
+
`ask` needs a local model server. Without one, `--sql` and `--examples` still
|
|
335
|
+
work:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
uv run finances ask --examples
|
|
339
|
+
uv run finances ask --sql "SELECT category, SUM(amount) FROM transactions GROUP BY 1 ORDER BY 2 DESC"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
With one:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
brew install ollama && ollama pull llama3.1:8b
|
|
346
|
+
uv run finances ask "what did I spend on delivery last month" --explain
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Something in the 3B–8B range is plenty. Avoid reasoning models here — they take
|
|
350
|
+
minutes per question for no benefit on this task. Pick explicitly with
|
|
351
|
+
`--model llama3.1:8b`.
|
|
352
|
+
|
|
353
|
+
## If something goes wrong
|
|
354
|
+
|
|
355
|
+
| Symptom | Cause and fix |
|
|
356
|
+
|---|---|
|
|
357
|
+
| `finances.db does not exist` | Run `parse` first. |
|
|
358
|
+
| `check` reports mismatches | The extraction is wrong for those files. Run `finances parse <file> --dump-columns` to see the columns it found, or `--llm` to let a local model try. |
|
|
359
|
+
| A bank is not supported | Download its CSV/OFX export and `finances import` it, or [ask for the bank](https://github.com/roshanlam/finances/issues/new?template=new-bank.yml). |
|
|
360
|
+
| `check` reports gaps | A statement month is missing from your folder. Totals are being computed on incomplete data. |
|
|
361
|
+
| Lots of `Uncategorized` | `categorize review` to teach it, or `enrich` to use a local LLM. |
|
|
362
|
+
| `cashflow` will not project | It could not find a checking-account balance. Pass `--balance 2400`. |
|
|
363
|
+
| Amounts look wrong | Almost always a column-detection problem. `--dump-columns`, then `--amount-col last` or `second-last` to override. |
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
# How it fits together
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
PDFs ─────▶ parse.py ───┐
|
|
371
|
+
(llm_parse.py, checked) ├──▶ finances.db ──▶ engine.py ──▶ every other tool
|
|
372
|
+
exports ─▶ importer.py ─┘ │ │
|
|
373
|
+
│ the read model: what counts as
|
|
374
|
+
each file checked against │ spending, which months are complete,
|
|
375
|
+
its own totals or balances │ what repeats, what is verified
|
|
376
|
+
└──▶ export.py ──▶ beancount, hledger, YNAB, Actual
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
finances/ the package — one module per tool, plus the shared core below
|
|
381
|
+
cli.py `finances <command>`: the front door, dispatches to each tool
|
|
382
|
+
parse.py PDFs -> the store, reconciling each statement as it goes
|
|
383
|
+
importer.py bank exports (CSV, OFX, CAMT.053, MT940) -> the store, balance-checked
|
|
384
|
+
llm_parse.py a local model for unknown PDF layouts, kept only if it reconciles
|
|
385
|
+
engine.py the read model every tool shares
|
|
386
|
+
export.py the store -> beancount, hledger, YNAB, Actual
|
|
387
|
+
api.py the dashboard's JSON API and static server
|
|
388
|
+
demo.py the made-up store
|
|
389
|
+
... analyze, cashflow, cpi, backtest, ask, planner, anonymize, ...
|
|
390
|
+
web/ the dashboard (React + Vite), served by `finances web`
|
|
391
|
+
tests/ stdlib unittest, no test dependencies
|
|
392
|
+
docker/ Dockerfile docker-compose.yml
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Every module still runs on its own too: `python -m finances.parse --help`.
|
|
396
|
+
|
|
397
|
+
- **`models.py`** — one definition of a transaction and a statement. The SQLite
|
|
398
|
+
schema and the CSV columns are both generated from it, so adding a field is a
|
|
399
|
+
single edit and the store migrates itself.
|
|
400
|
+
- **`database.py`** — the store, plus a content-hash cache so re-running the
|
|
401
|
+
parser skips files that have not changed.
|
|
402
|
+
- **`utils.py`** — money, date and merchant parsing, the debt formulas, and the
|
|
403
|
+
recurring-charge detector.
|
|
404
|
+
- **`engine.py`** — the semantics. Everything downstream agrees about what a
|
|
405
|
+
month, a surplus and a subscription are because they all ask this.
|
|
406
|
+
|
|
407
|
+
# What it does that other tools do not
|
|
408
|
+
|
|
409
|
+
**Proves its own numbers.** Extracted charges are checked against the total the
|
|
410
|
+
statement prints for itself, and the verdict is stored, not just displayed.
|
|
411
|
+
Missing statement months are detected and reported as gaps, so an average is
|
|
412
|
+
never quietly computed over incomplete data.
|
|
413
|
+
|
|
414
|
+
This is not decorative. Building it found a noise filter matching the month
|
|
415
|
+
"Apr" and discarding entire statements, sign inversions on two issuers, a
|
|
416
|
+
year-rollover that dated 62 transactions to the wrong December, and a primary
|
|
417
|
+
key that silently dropped 41 of one statement's 181 transactions. All of them
|
|
418
|
+
looked fine in a dashboard. None survive a reconciliation check.
|
|
419
|
+
|
|
420
|
+
**Tells you about next month, not just last month.** `cashflow` projects the
|
|
421
|
+
daily balance forward from known recurring charges and detected paycheck
|
|
422
|
+
cadence — distinguishing semi-monthly from biweekly, because biweekly gives you
|
|
423
|
+
three-paycheck months and that changes the answer. It reports a band rather than
|
|
424
|
+
a line and says plainly that it is not a confidence interval.
|
|
425
|
+
|
|
426
|
+
**Measures your inflation, not the country's.** `cpi` weights per-category price
|
|
427
|
+
changes by your spending and tracks per-merchant prices over time, against real
|
|
428
|
+
BLS Consumer Expenditure and CPI-U reference data shipped with the project.
|
|
429
|
+
|
|
430
|
+
**Grades its own forecasts.** `backtest` walks forward month by month, predicts
|
|
431
|
+
each month using only data from before it, and scores nine predictors against
|
|
432
|
+
what happened. It refuses to name a winner when the sample cannot support one.
|
|
433
|
+
|
|
434
|
+
**Makes your data shareable.** `anonymize` has four levels up to fully
|
|
435
|
+
synthetic, and proves what survived — per-category totals are printed side by
|
|
436
|
+
side so you can see which figures are still exactly true.
|
|
437
|
+
|
|
438
|
+
# Categorization
|
|
439
|
+
|
|
440
|
+
Rules, in order, first hit wins. Every answer records which rule fired and how
|
|
441
|
+
confident it is:
|
|
442
|
+
|
|
443
|
+
1. what you taught it (`learned_rules.json`, always wins)
|
|
444
|
+
2. context rules — `COSTCO`+`GAS` is fuel, `UBER`+`EATS` is delivery
|
|
445
|
+
3. bank phrases — a deposit account names its own transaction type
|
|
446
|
+
4. brand table, exact token match, never substring
|
|
447
|
+
5. keyword rules, word-boundary regex
|
|
448
|
+
6. issuer MCC labels, where the statement prints its own category
|
|
449
|
+
7. behaviour — cadence and amount, when the name says nothing
|
|
450
|
+
|
|
451
|
+
Scored against 159 real-shaped descriptors with processor prefixes, glued
|
|
452
|
+
city/state suffixes and issuer preambles:
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
$ uv run finances categorize eval
|
|
456
|
+
correct 159 100.0%
|
|
457
|
+
wrong 0 0.0% <- these corrupt analysis
|
|
458
|
+
left unknown 0 0.0% <- these are honest
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
# Tests
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
uv run python -m unittest discover tests
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
289 tests, no test dependencies. The parser's golden-file tests skip when
|
|
468
|
+
`PDFs/` is empty, so a fresh clone runs the rest. The golden-file harness
|
|
469
|
+
records per-statement extraction totals so a parser change that moves a number
|
|
470
|
+
is visible:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
uv run python tests/run.py --update-golden # after a deliberate change
|
|
474
|
+
uv run python tests/run.py --check-golden # exits 1 on drift
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
# Privacy
|
|
478
|
+
|
|
479
|
+
Your statements and the store never leave the machine. `PDFs/`, `*.db`, the
|
|
480
|
+
generated dashboards and `anonymize_map.json` are all gitignored. The map file
|
|
481
|
+
is what turns `MERCHANT_007` back into a real name — keep it, do not share it.
|
|
482
|
+
|
|
483
|
+
`ask`, `enrich` and `parse --llm` talk only to a model server on localhost.
|
|
484
|
+
Nothing else in the project makes a network request at all. See
|
|
485
|
+
[SECURITY.md](SECURITY.md) for how the dashboard is locked to your own machine
|
|
486
|
+
and how to report a problem.
|
|
487
|
+
|
|
488
|
+
# Not financial advice
|
|
489
|
+
|
|
490
|
+
Arithmetic on your own statements. The forecasts are extrapolations of your past
|
|
491
|
+
spending and say so, `backtest` will tell you how wrong they have been, and none
|
|
492
|
+
of it knows anything about your life beyond what the statements print.
|
|
493
|
+
|
|
494
|
+
# License
|
|
495
|
+
|
|
496
|
+
MIT. See [LICENSE](LICENSE).
|