Personal Finance ETL
My local-first financial data engineering and quantitative decision-support platform for reconciled personal finance, investment analytics, tax evidence, FIRE simulation, and BI.
I built this because I wanted one answer to a deceptively simple question:
Where exactly do I stand financially, and can I prove how I got there?
The first version only needed to make month-end less annoying. Then the questions got harder.
A balance needed evidence. Evidence needed identity. Investment history needed reconstruction. Reconstruction needed reconciliation. Realized gains needed Tax context. Wealth needed both household and market state. FIRE needed all of that to be trustworthy before Monte Carlo was allowed anywhere near it.
So the project grew from ETL into the system I now use to preserve financial evidence, reconstruct state, challenge that state against independent observations, and turn the result into decisions.
Explore: Documentation · ReadTheDocs · Wiki · Local Docs · Architecture · Finance · Developer · Reference
The whole project in one thought
Personal Finance ETL is built around one loop:
preserve evidence → canonicalize meaning → reconstruct state → reconcile against reality → support decisions → preserve provenance
That loop matters more than any individual library in the stack.
A broker file can tell me what it observed. Transactions can reconstruct what should exist. Reconciliation can record where those truths disagree. Quant can turn acquisition history into realized economics. Tax can consume those economics without rebuilding them. Wealth can combine household and investment state. FIRE can then model uncertainty from a state that has actually earned the right to be simulated.
flowchart TB
subgraph EVIDENCE["1 · Financial evidence"]
HH["Household transactions"]
BRK["Broker statements & trades"]
MKT["Market prices & benchmarks"]
FX["FX observations"]
REF["Masters · mappings · policy"]
end
subgraph INGEST["2 · Evidence acquisition & operational memory"]
PROV["File / market providers"]
APPING["Application ingestion"]
CP["SQLite Control Plane\nartifacts · runs · provider requests\nprovenance · recovery history"]
BRONZE["Persistent Bronze\n20 source contracts"]
end
subgraph CANON["3 · Canonical financial state"]
DAG["Polars transformation DAG"]
SILVER["Silver\n24 canonical contracts"]
ID["Deterministic identity\nnamespace + canonical serialization"]
end
subgraph DOMAIN["4 · Financial reconstruction"]
HHDOM["Household\ncash flow · assets · liabilities"]
LOTS["Investment engine\nFIFO lots · realized events"]
RECON["Reconciliation\nobserved vs reconstructed"]
PERF["Performance\nXIRR · benchmark · risk · FX"]
TAX["Tax evidence\nindicative classification"]
WEALTH["Wealth / FI state"]
FIRE["FIRE simulation\nMonte Carlo · regimes · shocks"]
end
subgraph PUBLISH["5 · Analytical publication"]
GOLD["Gold\n22 decision marts"]
META["DuckDB Meta\ncurrent analytical context"]
BI["Power BI / analytics"]
end
subgraph PRODUCT["6 · Product & engineering surface"]
FACADE["PersonalFinanceEngine"]
CLI["CLI"]
DESK["Desktop"]
DOCS["Packaged documentation"]
QA["Quality gates\nRuff · Mypy · Pyright\nPylint · Pytest"]
REL["Build / PR / release / PyPI"]
end
HH --> PROV
BRK --> PROV
MKT --> PROV
FX --> PROV
REF --> APPING
PROV --> APPING
APPING --> CP
APPING --> BRONZE
BRONZE --> DAG
DAG --> SILVER
ID --> SILVER
SILVER --> HHDOM
SILVER --> LOTS
SILVER --> RECON
LOTS --> RECON
LOTS --> PERF
RECON --> PERF
LOTS --> TAX
HHDOM --> TAX
HHDOM --> WEALTH
PERF --> WEALTH
WEALTH --> FIRE
HHDOM --> GOLD
PERF --> GOLD
TAX --> GOLD
FIRE --> GOLD
GOLD --> BI
SILVER --> META
GOLD --> META
FACADE --> APPING
FACADE --> CLI
FACADE --> DESK
FACADE --> DOCS
QA --> REL
This is not a generic framework pretending every bank, broker, Tax system, and household agreed on a schema while I was asleep. It is a deep vertical system built around my real financial environment, with increasingly explicit contracts around the parts that are reusable.
What the system actually proves
1. Data engineering, but with evidence semantics
The pipeline is not just read_csv → clean → dashboard.
The system separates source evidence, canonical financial facts, reconstructed state, analytical presentation, and operational history because they answer different questions.
| Layer | What it owns |
|---|---|
| Control Plane | historical operational truth, run lifecycle, artifacts, provider requests, recovery context |
| Bronze | synchronized source-shaped evidence |
| Silver | canonical financial facts and dimensions |
| Gold | decision-ready analytical marts |
| Meta | current analytical context |
Current registered analytical shape:
| Surface | Contracts / tables |
|---|---|
| Bronze | 20 |
| Silver | 24 |
| Gold | 22 |
| Meta | 6 |
| Control Plane | 10 |
The important bit is not the count. It is the ownership.
SQLite remembers what happened operationally. DuckDB serves current analytical state. Neither database is forced to cosplay as the other one.
Go deeper: Data Lifecycle · Warehouse Architecture · Control Plane Lifecycle
2. Investment evidence becomes financial state
Investment analytics starts with messy evidence:
- purchases and sales;
- broker snapshots;
- market prices;
- benchmarks;
- FX;
- instrument masters;
- geography-specific source behavior.
The engine turns that into explicit state:
trade evidence
→ canonical purchases / sales
→ acquisition lots
→ realized disposal events
→ observed-vs-reconstructed reconciliation
→ performance / benchmark / FX attribution
FIFO is lineage, not just cost basis
The lot engine reconstructs acquisition history chronologically and carries that lineage into realized events.
That matters because a disposal is not merely:
sale proceeds - some cost number
It is linked back to the actual consumed acquisition lots, holding periods, local/base currency basis, benchmark shadow state, and later Tax evidence.
Reconciliation is first-class evidence
A deterministic reconstruction can still disagree with an independent broker observation.
The system does not “fix” that by rewriting one side until everything looks emotionally stable.
It preserves the observation, preserves the reconstructed state, classifies the discrepancy, and emits explicit reconciliation evidence.
That gives future FIFO, performance, Tax, and audit logic a traceable explanation for why state changed.
Go deeper: Investment Data Model · FIFO & Realized Events · Investment Reconciliation
3. Performance is allowed to say “I do not know”
The project calculates and interprets:
- XIRR;
- CAGR;
- benchmark-relative performance;
- drawdown/risk metrics;
- net-injection aware performance;
- local-currency returns;
- reporting-currency returns;
- FX attribution.
But a metric is only useful if its validity state is preserved.
0.0 is not a universal substitute for:
undefined
not computable
missing evidence
invalid evidence
non-convergent
not applicable
Portfolio XIRR is recomputed from portfolio cash flows. It is not a weighted average of instrument XIRRs, because mathematics has already suffered enough.
For foreign assets, missing FX also does not silently become 1.0.
Go deeper: Performance & Risk · Multi-Currency & FX · Metrics & Methodology
4. Tax consumes financial truth, it does not rebuild it
Tax logic consumes canonical household evidence and realized investment events rather than creating its own parallel investment engine.
That boundary is deliberate:
investment reconstruction
owns lots and realized economics
Tax
classifies Tax-relevant evidence
applies configurable policy
produces indicative summaries / review state
The system distinguishes Tax evidence from statutory Tax liability.
Some outcomes are intentionally marked for review when the evidence does not justify a stronger claim.
I would rather have a CHECK_REQUIRED state than a very confident lie with two decimal places.
Go deeper: Tax Methodology · Financial Rules
5. Household finance and investments meet at wealth, not upstream
Household finance has its own semantics:
- income;
- expenses;
- transfers;
- assets;
- liabilities;
- liquidity;
- cash flow;
- budgets;
- net worth.
Investment state has different evidence and reconstruction rules.
The system lets those domains remain distinct until they need to answer shared questions about wealth and planning.
That keeps “money moved between my accounts” from becoming income and keeps “my investment changed market value” from becoming household cash flow.
This sounds obvious until a data model accidentally disagrees.
Go deeper: Financial Model · Cash Flow & Wealth
6. FIRE earns the right to be stochastic
The planning engine does not begin with Monte Carlo.
First it establishes deterministic current-state inputs such as:
- investable wealth;
- current spending;
- FI target;
- coverage ratios;
- runway / Coast-style metrics;
- configured return, inflation, withdrawal, and horizon policy.
Only then does the stochastic layer model future uncertainty through mechanisms including:
- Monte Carlo paths;
- regime transitions;
- inflation processes;
- human-capital shocks;
- glide paths;
- jump behavior;
- adaptive withdrawal rules.
Simulation inputs, model configuration, and outputs are fingerprinted for provenance.
A sophisticated model can widen the questions I ask. It cannot abolish uncertainty with enough NumPy.
Go deeper: FIRE Methodology · FIRE & Simulation Reference
Reliability is part of the financial model
A number is not trustworthy merely because the formula is correct.
I also need to know:
- which source evidence produced it;
- whether ingestion was complete;
- which configuration/rules were active;
- whether analytical publication completed;
- whether SQLite and DuckDB agreed at the end of the run;
- whether restored state was validated;
- whether a later run repaired or superseded something.
That is why reliability is built into the architecture rather than parked in a logging.py file and wished good luck.
Deterministic identity
Identity uses centralized canonical serialization with namespace-specific field ordering and normalization.
That keeps replay, joins, lineage, realized-event continuity, and reconstruction stable across full analytical rebuilds.
Cross-store failure is represented truthfully
SQLite Control Plane and DuckDB warehouse operations are coordinated, but they are not described as one magical distributed ACID transaction.
Commit state and recovery semantics distinguish clean rollback from partial/split outcomes that require repair.
Snapshot / restore distrusts the archive first
Backup and restore coordinate SQLite and DuckDB, stage validation before install, and retain rollback paths around replacement.
“It's just a ZIP” is not a recovery architecture. 😂
Go deeper: Identity & Replay · Reliability & Recovery · Backup & Restore
Software architecture that has to survive real finance
The current package model separates seven responsibilities:
application use cases and orchestration
bootstrap concrete dependency assembly
config validated settings and financial policy
contracts cross-boundary result/status vocabulary
domain financial meaning, algorithms, invariants
infrastructure persistence, providers, files, recovery
presentation CLI, Desktop, docs UI
That boundary model is backed by architecture tests, explicit package APIs, typed contracts, and a single composition root.
The system intentionally mixes styles:
- classes for state, lifecycle, policy, dependencies, and coordinated side effects;
- functions / Polars expressions for deterministic transformations;
- Protocols for capability boundaries;
- dataclasses / TypedDict / Pydantic / enums where each shape actually earns its mechanism.
I am not trying to win OOP bingo. I am trying to make the ownership legible.
Go deeper: System Architecture · Application & Dependency Architecture · Engineering Standards
Performance without pretending every problem is vectorizable
The compute model follows the workload.
| Workload | Primary model |
|---|---|
| canonical table transformations | Polars LazyFrame |
| analytical persistence / SQL | DuckDB |
| operational provenance | SQLite |
| irregular cash-flow returns | PyXIRR |
| numerical simulation | NumPy / Numba |
| stateful lot reconstruction | Python objects / ordered state |
The codebase contains hundreds of lazy-frame interactions, but .collect() is not treated as a moral failure.
Materialization is allowed where a boundary earns it:
publication
stateful algorithms
third-party APIs
scalar extraction
process boundaries
validation diagnostics
FIFO is stateful because acquisition history is stateful. Pretending otherwise would mostly create a more complicated bug.
In one documented production workload with more than 1,600 source artifacts and roughly 147 investment-instrument workloads, fresh ingestion plus complete analytical reconstruction was observed around 40–45 seconds, while a warm-cache run was observed around 24 seconds, publishing roughly 449k Silver + Gold rows.
Those are workload-specific observations, not benchmark promises.
Go deeper: Developer Guide · Testing & Validation
The engineering loop is part of the product
The repository does not stop at “the code works on my machine.”
The developer/release system includes:
- Poe task orchestration;
- Ruff linting and formatting;
- Mypy;
- Pyright;
- Pylint;
- Pytest;
- tracked Git hooks;
- structured audit exports;
- Conventional Commit automation;
- PR creation/certification;
- GitHub release automation;
- PyInstaller desktop builds;
uvpackage builds and PyPI publishing.
The tight loop is intentionally different from the release loop:
edit
→ focused tests
→ fast gate
→ commit
→ full gate
→ PR certification
→ merge
→ build / release / publish
Release infrastructure powered by memory is a particularly brave architectural choice, so I automated the boring parts instead.
Go deeper: Tooling, Quality & Release Engineering
Product surface
The financial engine is exposed through one application-facing API and multiple presentation surfaces.
Install
pip install personal-finance-etl
Desktop
shan-fin
or:
shan-fin-gui
Terminal
shan-fin cli
The package also ships the technical documentation so the same knowledge base is available from the application surfaces.
A successful installation is not automatically a runnable financial environment. The application still needs valid settings, financial rules, reference inputs, and compatible source evidence.
Start with:
Current production shape
The project is large enough that architectural discipline is not hypothetical.
| Surface | Current repository shape |
|---|---|
| Production Python modules | 156 |
| Production Python LOC | ~32k |
| Bronze contracts | 20 |
| Silver contracts | 24 |
| Gold contracts | 22 |
| Meta tables | 6 |
| Control Plane tables | 10 |
| Canonical technical docs | 53 |
The counts are useful context, not trophies.
A 32k-line codebase can still be badly designed. The interesting question is whether another engineer can answer:
who owns this behavior?
what is the grain?
where does identity come from?
what can fail?
what becomes durable?
what proves this output?
That is the standard I care about.
The stack, by responsibility
| Responsibility | Technology |
|---|---|
| Vectorized transformation | Polars |
| Analytical warehouse | DuckDB |
| Operational provenance | SQLite |
| Configuration / validation | Pydantic |
| Irregular cash-flow returns | PyXIRR |
| Numerical simulation | NumPy / Numba |
| Market data | yfinance + provider adapters |
| BI | Power BI |
| Desktop | CustomTkinter / Tkinter / pywebview |
| CLI | Rich + application façade |
| Build / package | Hatchling · uv · PyInstaller |
The point is not the logo collection.
Each tool owns a workload it is actually good at.
Polars does not need to pretend FIFO is vectorized. SQLite does not need to become the analytical warehouse. DuckDB does not need to remember every historical artifact event. Power BI does not need to become the financial engine.
One abstraction does not need to win every argument.
What I trust, and what I deliberately do not claim
Sophisticated systems become suspicious when a README quietly upgrades implementation details into heroic guarantees.
The system does provide
- deterministic identities for defined financial namespaces;
- historical run and artifact provenance;
- persistent Bronze synchronization state;
- explicit source completeness/failure states;
- full analytical rebuild semantics;
- registry-governed publication;
- backward-looking temporal joins in historical market reconstruction;
- explicit FIFO realization and reconciliation;
- multi-currency acquisition/valuation/performance semantics;
- Quant ownership of realized investment economics;
- Tax evidence and indicative classification;
- deterministic FI state plus stochastic FIRE modelling;
- coordinated SQLite + DuckDB snapshot/restore;
- configuration and simulation provenance;
- architecture and quality-gate enforcement.
It deliberately does not claim
- SQLite + DuckDB form one distributed ACID database;
- reconstructed holdings are independent broker-certified daily holdings where that evidence does not exist;
- reconciliation converts derived state into original source evidence;
- indicative Tax estimates are final statutory liability;
- every historical external binary is retained forever;
- Monte Carlo predicts the future;
- every bank, broker, jurisdiction, or statement layout is plug-and-play.
I would rather document a smaller guarantee accurately than a larger one poetically.
How to explore the project
The repository now has four intentionally different knowledge surfaces.
| Surface | Job | Start here |
|---|---|---|
| README | SHOW the project quickly | you are here |
| Docs Portal | INTERACT with visual search & guides | Live Docs (Pages) · ReadTheDocs |
| Wiki | TEACH the mental model | GitHub Wiki |
/docs |
SPECIFY implementation and finance deeply | Technical Docs |
| Code | PROVE the behavior | src/personal_finance_etl/ |
If all four say exactly the same thing at exactly the same depth, I have successfully maintained four copies of one document. That would be impressive in the least useful possible way. 😄
I want the architecture
I want the finance
I need exact contracts
I want to change the system
I want the project story
Why I keep building it this way
The principles below were mostly learned because their opposites eventually hurt.
Preserve evidence before inference
Raw observations should survive long enough to challenge derived state later.
Give every truth an owner
Operational history, canonical facts, reconstruction, analytical presentation, and policy are different authorities.
Respect grain before calculating
A metric with the wrong grain can be perfectly calculated and still be wrong.
Reconcile against independent observations
Agreement is stronger when it is tested, not assumed.
Keep policy explicit
Tax thresholds, FIRE assumptions, mappings, and classification rules should be visible and fingerprintable.
Use the compute model that fits the problem
Lazy vectorization is excellent. Stateful reconstruction is also real. Numerical kernels are allowed to be numerical kernels.
Compute richly, publish selectively
The internal model can remain deep without making every intermediate state a public mart.
Let complexity keep re-earning its place
I have removed calculations, abstractions, and metrics when they stopped supporting a decision.
Sometimes the fastest function is the one I deleted.
Final note
This project began as a personal month-end automation problem.
It became the place where my finance background, data engineering, software architecture, quantitative modelling, reliability thinking, and product engineering finally stopped being separate tracks.
The result is not a generic finance framework and not a toy ETL demo.
It is the financial system I wanted to exist for my own data: local, explainable, reconstructable, challengeable, and useful enough to make decisions from.
And yes, apparently the reasonable response to “I should clean up my monthly finance files” was to build a Control Plane, deterministic lot engine, recovery model, Tax evidence layer, Monte Carlo planner, release pipeline, and a 53-page engineering knowledge base.
The scope may have escalated slightly. 😂
License
Released under the GNU General Public License v3.0 or later.
Metadata
Release files for personal-finance-etl 9.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| personal_finance_etl-9.3.1.tar.gz | 3.2 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| personal_finance_etl-9.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 6.2 MB
Release files / personal_finance_etl-9.3.1.tar.gz
| Download URL | personal_finance_etl-9.3.1.tar.gz |
|---|---|
| Size | 3.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f00076c4ecff61ea31f509086ff088c287b14c6dff5853d2bc5643e51f9f4d2f
|
|
BLAKE2b-256 checksum How to use checksums |
da57b5126c65bb119c66561e1ab8775051dd24b341410501467002a6eb9a5497
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / personal_finance_etl-9.3.1-py3-none-any.whl
| Download URL | personal_finance_etl-9.3.1-py3-none-any.whl |
|---|---|
| Size | 3.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e789584c3bc65424ff7bae3f58dbdace35f170ddd4fc68ef5646ee5dcf279942
|
|
BLAKE2b-256 checksum How to use checksums |
8a3e0592705b533cbc7ca8a0df0a01685d3c84bd84d84de74359b83a03f7a5a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for personal-finance-etl 9.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for personal_finance_etl-9.3.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.