Privacy-first, local-only personal cash flow forecasting and financial anomaly detection.
CashPulse turns raw bank exports (CSV, OFX, QFX) into auto-categorized transactions, 30-day cash flow forecasts with confidence intervals, and actionable anomaly alerts (subscription creep, spending drift, lifestyle inflation, overdraft risk).
Everything runs locally on your machine. No bank credentials. No cloud uploads. No telemetry.
Feature-complete across all six build phases:
| Phase | Subsystem | Status |
|---|---|---|
| 1 | Ingestion layer (parsers + dedup + DB) | Done |
| 2 | Hybrid categorization engine | Done |
| 3 | Feature engineering + recurrence/payday | Done |
| 4 | Model layer (forecasters + 5 detectors) | Done |
| 5 | Output layer, dashboard, PDF, CLI polish | Done |
| 6 | Model training + evaluation + model card | Done |
533 tests pass / ruff check + mypy --strict clean across both
cashpulse/ and tests/. Line coverage is 52 % overall; the Phase 6
training-script package (cashpulse/training/) is intentionally
exercised end-to-end via the integration test rather than via unit
tests, so excluding it brings runtime-code coverage to ~84 %. See
docs/MODEL_CARD.md for trained-model metrics
with the actual numbers from each training run.
Most personal-finance tools are rearview mirrors — they tell you what already happened. CashPulse is forward-looking:
- Predicts the next 30 days of cash flow with confidence intervals
- Catches subscription creep before you do — including zombie subscriptions you never use
- Detects spending drift in specific categories ("Dining Out is up 34% over 3 months")
- Flags lifestyle inflation when discretionary spending scales faster than income
- Warns of overdraft risk with SHAP-backed feature contributions
Everything stays on your machine. No Plaid. No bank APIs. No cloud.
# Install (dev mode)
pip install -e ".[dev]"
# Step-by-step pipeline
cashpulse scan --input ./bank_exports/ # ingest
cashpulse categorize # label
cashpulse featurize # features + recurrence
cashpulse train # train all 7 models
cashpulse predict # forecasts + alerts
# Or one-shot
cashpulse analyze --input ./bank_exports/ --dashboard
# PDF export
cashpulse report --input ./bank_exports/ --format pdf --output vitals.pdf+----------------------------------------------------------------+
| CashPulse Vital Signs - May 2026 |
+----------------------------------------------------------------+
| Burn rate $3,240/mo (^ 6% vs 3-mo avg) |
| Savings rate 11.2% (v from 14.8%) |
| Subscription load $187/mo (12.4% of income) |
| Emergency runway 18.3 days (v from 24.1 days) |
| Income regularity 0.87 (HIGH - stable) |
| Overdraft risk LOW (next 30 days) |
| |
| ANOMALIES DETECTED: 3 |
| - Dining Out drift: +$116/mo vs baseline |
| - 2 subscription price increases: +$36/yr |
| - Lifestyle inflation: Shopping elasticity 2.1x |
| |
| 30-DAY FORECAST |
| Balance projection: $1,420 - $2,890 (80% CI) |
| Next cash crunch: June 3 (rent + insurance overlap) |
+----------------------------------------------------------------+
- Overview — vital signs + forecast envelope chart
- Cash Flow — daily balance projection, income/expense bars
- Categories — spend breakdown, pie chart, month-over-month trends
- Subscriptions — every recurring charge, zombie flags, price history
- Anomalies — timeline of detected anomalies + SHAP factor breakdown
- Forecast — interactive what-if scenarios ("what if I cancel Netflix?")
A self-contained one-page report with the headline metrics table, the forecast-envelope chart, and the top-5 alert table. Includes a privacy footer reminding the reader that nothing left their machine.
+----------+ +-------------+ +-------------+ +---------+ +----------+
| Ingest |-->| Categorize |-->| Features |-->| Models |-->| Output |
+----------+ +-------------+ +-------------+ +---------+ +----------+
| | | | |
v v v v v
Parser Zoo Rule + ML + Rolling windows Forecasters Vital signs
CSV / OFX Active learn Recurrence detect + 5 detectors Terminal +
Dedup + DB Taxonomy Payday detect + SHAP PDF + dash
Strict one-way dependency flow: a downstream module never imports from an
upstream module. Every persistence boundary is SQLite (one local DB at
~/.cashpulse/data.db). Model weights live under ~/.cashpulse/models/.
Goal: drop any supported bank export onto disk, get a clean, deduplicated, unified transaction ledger.
- CSV parsers with auto-detection: Chase (checking + credit card),
Bank of America, Capital One. Each implements
BankParser.detect() -> floatfor confidence scoring andnormalize() -> pd.DataFramefor the unified schema. - OFX / QFX parser via
ofxparse, handles both SGML and XML variants. Timezone-aware timestamps are normalized to UTC before the calendar date is taken (stable across machines). - Encoding fallback chain (
utf-8->utf-8-sig->cp1252->latin-1) to survive banks that mis-declare their export encoding.
UNIFIED_SCHEMA = {
"date": "datetime64[ns]",
"description": "str",
"amount": "float64", # negative = outflow
"balance": "float64 | null",
"account_id": "str",
"source_file": "str",
"raw_category": "str | null",
}- Groups by
(date ± 1 day, abs(amount), description similarity > 0.85). Rows with blank descriptions are never clustered together. - Detects transfer pairs (opposite amounts across accounts within the
date window). Both sides must look transfer-related (shared keywords
such as
transfer,zelle,card payment) or have very similar descriptions — coincidental same-amount payments are not linked. - Cross-account aware so combining checking + credit-card + savings works out of the box.
- Idempotent
INSERT OR IGNOREkeyed on(date, amount, description, account_id). - Schema bootstrapped on first connection; no migrations needed.
cashpulse scan --input <file_or_directory>Goal: every transaction gets a (category, subcategory, confidence)
label.
Hybrid three-stage classifier per the architecture doc:
- Merchant normalizer strips dates, card-number fragments, location codes, transaction IDs.
- Curated merchant dictionary with 1,082 entries across 11
top-level categories (see
data/merchants/merchants.json). Each entry can carry aliases;rapidfuzzhandles fuzzy matching.
- Base model:
all-MiniLM-L6-v2(384-dim embeddings, ~80MB). - For each category, computes a centroid embedding from the dictionary entries; new descriptions are scored by cosine similarity to every centroid.
- Returns top-1 label with a confidence score.
- Transactions below a confidence threshold (default
0.7) are flaggedneeds_review. - User corrections persist to the
correctionsSQLite table, keyed by normalized merchant, so the same merchant won't get mislabeled twice. - Future categorize runs treat corrections as ground truth (overriding both the rule engine and the embedder).
11 top-level categories, ~50 subcategories total: Income, Housing,
Transportation, Food, Subscriptions & Recurring, Health, Financial,
Shopping, Education, Travel, Miscellaneous. See
cashpulse/categorize/taxonomy.py
for the full tree.
cashpulse categorize --threshold 0.7Goal: raw labeled transactions become a rich daily feature matrix ready for model consumption.
day_of_week, day_of_month, week_of_year, month, is_weekend,
is_payday_proximity, is_recurring, merchant_frequency,
amount_zscore (z-score within category), and
time_since_last_same_merchant.
features/balance_utils.py
builds a combined end-of-day balance: per-account last balance each day,
forward-filled per account, then summed across accounts (or per-account
cashflow integration when the bank omits balance). Used by rolling
features, overdraft risk, and vital-signs forecast envelopes.
| Feature | Notes |
|---|---|
daily_net_cashflow |
Signed sum |
daily_spend, daily_income |
Outflow / inflow magnitudes |
rolling_spend_{7,14,30}d |
Outflow sums |
rolling_income_{7,14,30}d |
Inflow sums |
balance |
Combined EOD balance (NaN when unavailable) |
rolling_balance_trend_{7,14,30}d |
Linear-regression slope on EOD balance |
spend_velocity |
First derivative of 7-day rolling spend |
burn_rate_acceleration |
Second derivative of cumulative spend |
savings_rate_30d |
(income - spend) / income over 30d |
subscription_burden_ratio |
Recurring outflows / income |
emergency_runway_days |
balance / avg_daily_spend |
income_regularity_score |
1 - CV of inter-arrival times between income days |
category_spend_ratio_{cat}_30d |
Per-top-level-category 30-day shares |
- Mines per-merchant inter-transaction intervals against canonical periods (weekly, biweekly, monthly, quarterly, annual).
- A stream is flagged recurring when periodicity score > 0.7 AND amount coefficient-of-variation < 0.15.
- Emits
RecurringChargerecords carrying merchant, amount, period, full price history, and a stable/increasing/decreasing trend label.
- Classifies inflows as
weekly/biweekly/semi-monthly/monthly/irregularper account. - Returns the typical day(s) of the month, the median interval, the detection confidence, and the projected next payday.
Persists four tables in the same SQLite DB:
transaction_features, daily_features (with category-share JSON),
recurring_charges, payday_patterns. Every write is idempotent.
cashpulse featurizeGoal: trained models that forecast cash flow and detect anomalies.
Seven models behind a common CashPulseModel interface (fit,
predict, save, load).
| Model | Module | Purpose |
|---|---|---|
| LSTM-Transformer | forecaster/lstm_transformer.py |
Primary 30-day cash-flow forecaster (90-day lookback, MSE + pinball loss) |
| Prophet + XGBoost | forecaster/prophet_xgb.py |
Fallback forecaster, OLS-stacked, residual-driven 80% intervals |
| Spending drift | anomaly/spending_drift.py |
PyTorch MLP autoencoder, per-category 2σ reconstruction-error threshold |
| Cash-flow anomaly | anomaly/cashflow_anomaly.py |
Isolation Forest on 6 daily features; detect(as_of=, lookback_days=) scopes alerts to a recent window |
| Subscription creep | anomaly/subscription_creep.py |
Rule-based: 4 alert kinds — price increase, cumulative creep, new sub, zombie |
| Lifestyle inflation | anomaly/lifestyle_inflation.py |
Income-elasticity-of-spending regression on discretionary categories |
| Overdraft risk | anomaly/overdraft_risk.py |
XGBoost classifier + SHAP feature contributions |
Per-day feature vector
|
v
Temporal Embedding (day_of_week, day_of_month, week_of_year, payday_proximity)
|
v
2-layer LSTM (128 hidden units)
|
v
4-head Multi-Head Self-Attention
|
v
Mean-pool -> [Point head | Quantile head]
Combined MSE + pinball-quantile loss; 90-day lookback, configurable horizon. Persona training can normalize each lookback window independently; inference uses the same window-local normalization when that mode is active. Falls back to Prophet+XGBoost when history is shorter than 150 days.
models/walk_forward.py implements
expanding-window and sliding-window validation with a model-agnostic
callable API. Returns per-fold MAE / RMSE / MAPE so callers can pick a
forecaster on real held-out folds, not in-sample fit.
models/store.pyhandles save/load and meta-json versioning (name + version validated on load).models/runner.pyis therun_train/run_predictorchestrator. It gracefully degrades when history is short:- LSTM-Transformer skipped < 150 days
- Prophet+XGBoost skipped < 60 days
- Autoencoder skipped < 60 days
- Anomaly detectors skipped < 30 days
- Rule-based detectors always trainable.
cashpulse train # trains every model that meets its data threshold
cashpulse predict # loads + runs every available modelGoal: turn structured model output into a usable product.
output/vital_signs.py consumes the
PredictResult + the daily feature frame and produces a single
VitalSigns dataclass: burn rate, savings rate, subscription burden
(from transaction_features.is_recurring when available), emergency
runway, income regularity, overdraft band, the 30-day forecast envelope
(when balance history exists), the projected cash-crunch date, and a
severity-sorted top-5 alert list across every detector (including
overdraft).
output/report_generator.py
renders the vital-signs panel via Rich, with coloured trend arrows and a
fallback ASCII text mode shared with the PDF exporter.
output/pdf_export.py produces a
self-contained two-page PDF via matplotlib + reportlab:
- Page 1: title, headline metrics table, forecast chart (cumulative balance only when daily balance exists; otherwise raw forecast target), top-alerts table, optional cash-crunch warning.
- Page 2: privacy footer ("CashPulse runs entirely on your local machine; no network calls, no telemetry").
cashpulse/dashboard/ ships as an importable
sub-package. cashpulse dashboard (or cashpulse analyze --dashboard)
spawns streamlit run against dashboard/app.py.
Six sidebar-navigated pages cover Overview, Cash Flow, Categories,
Subscriptions, Anomalies, and Forecast (with interactive cancel-a-sub
what-if).
output/runner.py implements run_analyze
(end-to-end scan -> categorize -> featurize -> train -> predict -> vital
signs) and run_report (full pipeline + terminal/PDF render). The
analyze runner reuses already-trained models on disk unless --retrain
is passed.
Goal: trained, evaluated, serialised weights for every ML model — so the project ships with a working classifier, forecaster, and anomaly detectors instead of architecture-only stubs.
Training data comes from two complementary sources, both deterministic and local:
PersonaGenerator(cashpulse/training/persona_generator.py) produces 12-month synthetic financial lives for 8 persona archetypes (stable salaried, high-earner, freelancer, paycheck-to-paycheck, subscription zombie, couple, college student, retiree). Clean personas establish baselines; "dirty" personas inject one of five anomalies (drift, creep, inflation, overdraft, cashflow) for labelled evaluation. 780 personas total, regenerable viapython -m cashpulse.training.build_datasets.- HuggingFace
DoDataThings/us-bank-transaction-categories-v2— 68K rows of realistic US bank-export descriptions, mapped to the CashPulse 11-category / ~50-subcategory taxonomy bycategory_mapping.py.
| Model | Source data | Headline metric | Target | Hit? |
|---|---|---|---|---|
| Sentence-transformer categorizer (fine-tuned MiniLM + multi-task head) | 68K HF + 89K synthetic | Top-1 cat accuracy = 88.8% (baseline 63.5%) | > 92% | close |
| LSTM-Transformer forecaster | 160 personas × 12mo | 30-day MAE = 1.0% of avg balance | < 15% | yes |
| Prophet+XGBoost fallback | 160 personas, 120-day slice, 27-point grid | Test walk-forward MAE = $896 | — | n/a |
| Spending-drift autoencoder | 130 clean personas (per-user fit) | Recall 0.58, precision 0.50 | > 0.70 / 0.80 | partial |
| Cash-flow Isolation Forest | 130 clean personas (per-user fit) | Day-level FP rate 3.48%, persona recall 1.00 | < 5% | yes |
| Overdraft XGBoost + SHAP | 100 personas × 12-point grid | AUC=0.52, SHAP top-2 = balance + days_to_next_payday (as expected) |
> 0.85 | no — data limit |
| Subscription creep (threshold-tuned) | 10 zombie persona pairs × 27 combos | F1=0.67, recall=1.00 | — | n/a |
| Lifestyle inflation (threshold-tuned) | 10 high-earner pairs + 10 controls × 24 combos | F1=0.48 | — | n/a |
10 fresh, unseen personas (5 clean + 5 dirty) exported as Chase CSVs
and run through cashpulse analyze --retrain=False against the
pre-trained models. Full per-persona metric summary at
test_integration/metric_summary.json:
96.25% avg category accuracy, 0.0% uncategorised, 10/10 valid
forecast envelopes, 10/10 complete vital signs, all 6 dashboard pages
import cleanly.
Several models miss their architecture-spec targets. The gaps trace
to synthetic-data realism more than to model architecture — see
docs/MODEL_CARD.md for the per-model
root-cause analysis (where the metric breaks down: detector code,
training data, or integration boundary). The model card distinguishes
"detector working as designed" from "detector underperforming" rather
than burying the gap.
All Phase 6 training scripts live in
cashpulse/training/:
persona_generator.py # 8 archetypes, 5 anomaly injectors
persona_loader.py # Shared loader for trainers
build_datasets.py # Step 1+2 dataset orchestrator
category_mapping.py # HuggingFace -> CashPulse taxonomy
train_categorizer.py # Step 3 (sentence-transformer fine-tune)
train_forecaster.py # Step 4 (LSTM-Transformer)
train_fallback_forecaster.py # Step 5 (Prophet+XGBoost)
train_spending_drift.py # Step 6A (autoencoder, per-user)
train_cashflow_anomaly.py # Step 6B (Isolation Forest, per-user)
train_overdraft.py # Step 6C (XGBoost + SHAP)
tune_subscription_creep.py # Step 6D (rule-based thresholds)
tune_lifestyle_inflation.py # Step 6E (rule-based thresholds)
integration_test.py # Step 7 (10-persona E2E)
Trained weights are persisted via the existing ModelStore to
~/.cashpulse/models/<model_name>/, each with a sidecar
training_metadata.json carrying dataset sizes, hyperparameters, and
every reported metric.
cashpulse scan --input <path> # Parse + dedupe + persist
cashpulse categorize --threshold 0.7 # Rule + transformer classifier
cashpulse featurize # Rolling features + recurrence
cashpulse train # Train every Phase 4 model
cashpulse predict # Run every detector / forecaster
cashpulse analyze --input <path> # Full pipeline end-to-end
--dashboard # Launch Streamlit after
--retrain # Force fresh training
cashpulse report --format pdf # Export PDF vital signs
--output vitals.pdf
cashpulse dashboard # Streamlit only (no analysis)
Global flags accepted by every subcommand:
--config <path.yaml> # Override config
--log-level DEBUG|INFO|WARNING|ERROR # Verbosity
--strict / --no-strict # Fail fast vs. continue
A single CashPulseConfig Pydantic model
(cashpulse/config.py) holds every tunable knob.
Defaults work out of the box; override anything via a YAML file passed
with --config:
data_dir: ~/.cashpulse # SQLite DB + model artifacts live here
log_level: INFO
strict: false # Fail loud on recoverable issues
default_encoding: utf-8
fallback_encodings: [utf-8-sig, cp1252, latin-1]
min_parser_confidence: 0.5
dedup_date_window_days: 1
dedup_description_similarity: 0.85Nothing in the library hard-codes a file path; everything flows through this object.
+-------------------------------------------+
| User's Machine |
| |
| CSV files --> CashPulse --> SQLite DB |
| | |
| v |
| Local Models |
| (no network calls) |
| | |
| v |
| Reports & Dashboard |
| |
| - No cloud uploads |
| - No API keys required |
| - No telemetry |
| - No bank credentials |
+-------------------------------------------+
CashPulse downloads no model weights at runtime, makes no outgoing
network requests during analysis, and writes nothing outside the user's
chosen data_dir. The sentence-transformer is pulled once at install
time and cached locally by sentence-transformers itself.
cashpulse/
cli.py # Click CLI entry point
config.py # CashPulseConfig (Pydantic)
exceptions.py # CashPulseError hierarchy
ingest/ # Phase 1
base_parser.py # BankParser ABC + UnifiedTransaction
csv_parsers/ # Chase, BoA, Capital One
ofx_parser.py # OFX/QFX
csv_reader.py # Encoding fallback chain
deduplicator.py # Dedup + transfer-pair detection
scan.py # End-to-end scan pipeline
categorize/ # Phase 2
normalizer.py # Merchant string cleaning
merchant_db.py # Curated dictionary loader
rule_engine.py # Stage 1
embedder.py # Stage 2 (sentence-transformer)
classifier.py # 3-stage orchestration
active_learner.py # Stage 3 (SQLite-backed corrections)
taxonomy.py # 11 categories / ~50 subcategories
store.py # CategorizationStore
runner.py # cashpulse categorize pipeline
features/ # Phase 3
balance_utils.py # Multi-account combined EOD balance
transaction_features.py # Per-row features
rolling_features.py # Daily rolling windows
recurrence_detector.py # RecurringCharge mining
payday_detector.py # PaydayPattern detection
feature_store.py # 4 feature tables
runner.py # cashpulse featurize pipeline
models/ # Phase 4
base.py # CashPulseModel ABC
store.py # ModelStore (filesystem)
walk_forward.py # WalkForwardValidator
forecaster/
lstm_transformer.py # Primary forecaster (PyTorch)
prophet_xgb.py # Fallback ensemble
anomaly/
spending_drift.py # Autoencoder (PyTorch)
cashflow_anomaly.py # Isolation Forest
subscription_creep.py # Rule-based
lifestyle_inflation.py # Income elasticity
overdraft_risk.py # XGBoost + SHAP
runner.py # cashpulse train / predict
output/ # Phase 5
vital_signs.py # VitalSigns aggregator
report_generator.py # Rich terminal renderer
pdf_export.py # matplotlib + reportlab PDF
runner.py # cashpulse analyze / report
dashboard/ # Phase 5
app.py # Streamlit entry (6 pages)
components.py # Chart + table builders
state.py # Shared data loader + cache
db/
schema.py # 7-table SQLite DDL
store.py # TransactionStore
data/
merchants/merchants.json # 1,082 curated merchants
sample/ # 5 realistic sample bank exports
tests/ # pytest (533 tests; 52% overall, ~84% ex-training)
helpers.py # Shared test fixtures (stub classifier)
test_parsers/ # CSV / OFX parser tests
test_categorize/ # Categorization pipeline
test_features/ # Feature engineering
test_models/ # 7 model test modules + walk-forward
test_output/ # Vital signs + renderer + PDF
test_dashboard/ # Dashboard components + state
test_cli_pipeline.py # Click CLI subprocess smoke tests
test_deduplicator.py
test_base_parser.py
test_scan_cli.py
test_scaffold.py
| Model | Metric | Target |
|---|---|---|
| Categorization | Top-1 accuracy | > 92% |
| Categorization | Top-3 accuracy | > 98% |
| Cash-flow forecast | 7-day MAE | < 8% |
| Cash-flow forecast | 30-day MAE | < 15% |
| Overdraft prediction | AUC-ROC | > 0.85 |
| Overdraft prediction | Precision @ 80% recall | > 0.70 |
| Anomaly detection | Precision | > 0.80 |
| Recurring detection | F1 score | > 0.90 |
These targets assume 12+ months of real data. The bundled sample fixtures are ~60 days — long enough to validate the pipeline end-to-end, not long enough to hit every metric. Test data uses synthetic frames designed to exercise each detector under controlled conditions.
# Install (dev mode)
pip install -e ".[dev]"
# Run the full test suite with coverage
pytest --cov=cashpulse
# Lint + format (ruff config in pyproject.toml)
ruff check cashpulse tests
ruff format cashpulse tests
# Strict type-check
mypy cashpulse
# Launch the dashboard against any data directory
streamlit run cashpulse/dashboard/app.py -- --data-dir ~/.cashpulse- Python 3.11+, packaged via
pyproject.toml(PEP 621). - ruff — 88-char line width, double quotes,
E/F/W/I/UP/B/SIM/RUFenabled. - mypy --strict — Pydantic plugin on; pandas-stubs handled with
targeted
# type: ignore[code]comments where the stubs are stricter than runtime behaviour. - pytest + hypothesis for property-based parser tests.
Five realistic bundled exports under data/sample/ cover every parser:
bofa_checking.csv
capital_one.csv
chase_checking.csv
chase_credit.csv
wells_fargo.ofx
Try the full pipeline against them:
cashpulse analyze --input data/sample/| Component | Library |
|---|---|
| Data | pandas, numpy |
| Categorization | sentence-transformers, scikit-learn, rapidfuzz |
| Forecasting | PyTorch (LSTM-Transformer), Prophet, XGBoost |
| Anomaly detection | PyTorch (autoencoder), scikit-learn (Isolation Forest), XGBoost |
| Explainability | SHAP |
| File parsing | ofxparse |
| Storage | SQLite (stdlib) |
| CLI | Click + Rich |
| Dashboard | Streamlit |
| matplotlib + reportlab | |
| Config / validation | Pydantic, PyYAML |
| Testing | pytest, hypothesis |
| Lint / type | ruff, mypy |
MIT