A cash-flow engine for checking accounts. It categorizes transactions, detects recurring income and bills, projects each account's daily balance 14 days forward, and warns several days before the balance is projected to go low or overdraft. A simulation then checks how many overdrafts a suggested transfer from a linked reserve account would have prevented.
It is the decision logic behind the low-balance early-warning features some
banks ship with checking accounts, rebuilt on synthetic data. Every account
comes from a seeded generator in cashlens/gen/; there is no real bank data
or bank API in the repo.
categorize transactions -> project daily balance -> warn before overdraft -> suggest a reserve transfer
Categorization is TF-IDF over merchant strings plus amount and day-of-month into a logistic regression, the only learned component. Recurring detection groups history by normalized merchant key and keeps streams that repeat on a regular interval with near-constant amounts; variable spend falls through to a flat discretionary estimate. Projection is rule-based on top of that.
The warning flags when the projected minimum over the next 14 days drops under a $50 buffer, while the avoidance simulation scores against actual $0 crossings. Flagging at the buffer rather than at zero leaves room to act.
The code follows the same order: cashlens/gen, categorize, forecast,
warn, and api (FastAPI over a SQLite ledger).
Run on 2026-08-05 with seed 42: 60 accounts, 11,712 transactions, 180 days.
The personas skew thin-buffer on purpose (41 of 60 accounts dip below zero at
some point). Full tables in RESULTS.md, raw JSON in results/.
Categorization on 20 held-out accounts (split by account, so no test
account's transactions are seen in training): 93.92% accuracy, 0.9537
macro-F1. Stable-name recurring classes all score F1 1.00; error sits in
other (0.778) and groceries (0.878) because big-box retailers and cafes
deliberately appear under two ground-truth labels.
Recurrence detection: precision 0.980, recall 1.000. Balance projection over 89,040 held-out case-days: MAE $348.78, median absolute error $188.36.
Warning, day-level over 6,360 account/as-of decisions: precision 0.7659, recall 0.8431. Event-level, all 32 overdraft episodes in the evaluation window were flagged in advance (median lead 14.0 days, mean 12.22).
Avoidance simulation, baseline of no warnings versus warn-and-transfer:
| baseline overdrafts | avoided | missed | too late | reserve too small |
|---|---|---|---|---|
| 32 | 8 (25.0%) | 0 | 0 | 24 |
The 25% needs the breakdown next to it: the warning caught every overdraft at least a day early, and what limited avoidance was that in 24 of 32 episodes the reserve could not cover the full shortfall.
GET /alerts, which re-projects all 60 accounts per request, measures p50
25.05 ms and p95 29.03 ms over real HTTP.
python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python -m scripts.run_all # full pipeline -> results/*.json
python -m scripts.measure_latency # boots uvicorn, times /alerts and /categorize
pytest -q --cov=cashlens # 24 tests, 96% coverage
uvicorn cashlens.api.app:app --reloadEverything is seeded (SEED = 42 in cashlens/config.py); re-running the
pipeline regenerates identical JSON.
tests/test_split_no_leakage.py proves the train and test account sets are
disjoint. tests/test_warning_catches_dips.py builds an account whose rent
lands before its paycheck and checks the flag fires ahead of the dip (and
not on a healthy account). The generator, recurring detector, and every
endpoint have their own tests.
The numbers reflect the simulator, not production transactions, which are messier (typos, MCC codes, refunds, pending versus posted). Only 32 overdraft episodes land in the evaluation window, so 32/32 is a small-sample result. Median lead equals the horizon because rent-driven dips get flagged the moment they enter the 14-day window. Grace periods, incoming-deposit logic, payment execution, and any UI are not modeled.