| storage | Postgres, not SQLite | Already running and already used by guestbook; SQLite would be a second engine in one project, and its CLI is not installed here so it would be harder to inspect. |
| storage | Hand-written SQL, not an ORM | “Positions are a query” is the idea worth learning. An ORM would generate exactly the query worth reading. |
| ledger | Cash is an instrument | Buying debits cash implicitly, so nobody enters a balance and nobody can enter one wrongly. Without it money vanishes between a sale and the next purchase. |
| ledger | FX conversion is two linked rows | Withdrawing sterling and depositing euros is one act; neither leg means anything alone. |
| ledger | BUY/SELL for equities, DEPOSIT/WITHDRAW for cash | One word for both would hide the difference between an investment decision and a funding one. Enforced by a composite foreign key. |
| ledger | Whole shares only | You cannot buy 0.37 of a share. The residual is real money; it also makes foreign cash net to exactly zero. |
| ledger | Append-only enforced by triggers | Policy that lives only in a comment gets broken by whoever is in a hurry. TRUNCATE needed its own guard. |
| ledger | numeric, never floating point | A ledger that does not add up exactly is not a ledger. The returns maths stays on floats — measurement, not accounting. |
| ledger | Backdating allowed, amendment not | A trade entered late is legitimate; history changing is the feature. A correction is a reversing entry. |
| ledger | Average cost, not FIFO | Simpler, and it matches HMRC’s Section 104 pooling for a GBP investor — a domain reason rather than a convenience. |
| ledger | Buy dates from car numbers, rolled forward | 63 happens to be a Friday; most numbers are not. Forward, never back — a purchase cannot predate its instruction. |
| maths | Attribute in money, not percentages | Percentages compound so they do not add. In pounds the three components sum to the total exactly, with no smoothing factor. |
| maths | Units held through a day are yesterday’s | Trades execute at the close, so a trade cannot earn the move it was not there for. |
| maths | Returns per share, P&L in money | They answer different questions. A holding sold down shows a positive return and a small P&L; both are correct. |
| maths | TWR labelled, not assumed | With one subscription and no flows since, TWR and MWR are the same number. They diverge later; the label is not yet earned. |
| currency | Threshold still tests fx_return | A −1.5% limit is a statement about how far a currency moved. Re-pointing it would change what a documented number means while leaving it looking identical. |
| currency | Both breach counts published | 1 currency and 3 holdings are one event counted two ways. A reader must never have to guess which number they are looking at. |
| currency | Disagreeing rates raise, never average | It would mean holdings were priced against different bases, and an average would hide it under a plausible number. |
| currency | Cash gets a currency row | Otherwise the weights quietly fail to add to 100%. |
| currency | Hedge cost not netted off | Pricing a forward needs interest-rate differences; Frankfurter publishes FX only. Not estimated, not faked. |
| serving | Service runs under the project venv | The core needs pandas/numpy/yfinance; none are installed system-wide and PEP 668 says they should not be. |
| serving | Core imported lazily | That process serves the whole site. A broken import must be a 503 on one route, not a dead website. |
| serving | Static file is a fallback, not retired | It works when the API is down and under a plain http.server, and stays the record of what the tool said on a given day. |
| serving | A page never depends on a restart | Learned the hard way. Every block the page did not write itself is optional. |