dahai.tang

2026 · Architecture only

Finanz cockpit

Three banking systems, two continents and three currencies, normalised into one schema — and the masking rules that make it safe to look at.

  • Python
  • PSD2 / open banking (Enable Banking)
  • IBKR Flex Query
  • JSONL normalised layer
Currencies
3EUR base, with CAD and CNY converted at the correct date per row
Ingestion paths
3PSD2 API, brokerage flex query, and CSV for banks with neither
Pipeline
1,595 LOCnine scripts, one shared schema module every parser must satisfy

The problem is not the maths

Money spread across two continents does not produce a hard arithmetic problem. It produces a reconciliation problem: a European bank, a brokerage account, a Canadian bank and a Chinese bank each describe a transaction differently, in different currencies, with different date semantics, and none of them will ever agree to a common format.

The naive approach — open four apps, read four balances, add them up — fails in a specific way. It gives you a number you cannot check, cannot reproduce next month, and cannot decompose when it looks wrong.

One normalised layer, three ways in

Everything lands in a single normalised schema. What differs is only how it gets there.

Source Method Automation
EU banks PSD2 account-information API Fully automated; consent must be re-authorised every ≤180 days
Brokerage Flex Query web service (token + query id) Fully automated
Canadian / Chinese banks CSV or Excel export → parser Manual export, automated parsing

The rule that keeps this from decaying: analysis only ever runs on the normalised layer, never on a raw export. Raw responses are kept, but they are inputs, not sources of truth. Adding a new EU bank requires no code at all — it is one authorisation command, because PSD2 is a standard. Adding a CSV-only bank means writing one parser function that maps columns onto the shared schema. That asymmetry is the whole reason the schema module exists as a separate file every parser must satisfy.

Currency conversion is a date problem

The mistake that makes cross-currency numbers meaningless is converting everything at today’s rate.

Flows convert at the transaction date. Stocks convert at the snapshot date. A rent payment received in March is a March-rate event forever; a balance sitting in an account is worth what it is worth on the day you are asking. Rates come from a central-bank reference series and are cached locally, so a recomputation of last year’s figures produces last year’s answer rather than a new one.

This sounds pedantic until a tax return needs a figure that must still be defensible two years later.

Privacy as a schema property, not a habit

This pipeline touches the most sensitive data I have, and it exists to be read in conversation with an AI assistant. So the protections are structural rather than behavioural:

  • Raw API responses and raw exports are git-ignored. They never enter version control.
  • The normalised layer masks account identifiers at write time — only the last four characters survive. Not at display time, at write time, so no downstream consumer can leak what it never received.
  • Credentials live in a git-ignored config file, with a committed example template carrying the shape but no values.
  • Analysis output shows aggregates and masked counterparties. Full account numbers and tokens are never printed, including into a chat transcript.

The distinction I care about here: masking at write time means the safety property holds for code I have not written yet. Masking at display time means every future feature is one forgotten call away from a leak.

Boundaries between systems

The pipeline feeds a tax workflow — brokerage dividend and interest rows on one side, foreign rental transactions on the other. It would have been easy to let it also compute the tax treatment.

It doesn’t. Tax logic lives in a separate cross-border tax workflow, and this pipeline only hands it rows. Two systems computing the same figure from different assumptions is the failure mode that makes both untrustworthy, and personal finance offers no shortage of figures that are almost the same.

The same reasoning governs the manual layer: property valuations, lease obligations and liabilities that no API will ever report live in a hand-maintained file with explicit skip_if_actual markers. When real synced data eventually covers a period, the estimate yields to it automatically rather than being silently double-counted. Estimates are labelled as estimates in the schema, so nothing downstream has to remember which is which.


← All systems