fix(portfolio): derive holding currency from assets, stop USD fallback poisoning #85
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gertjan/bodega!85
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/holding-currency-from-assets"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Root cause
Every stored holding has
currency="USD", including EUR/CHF-listed assets.HoldingRepository.replace_for_portfolio(the sole holdings write path whenportfolio_behavior.transactions_authoritative=true, which istrueinprod) defaulted a symbol's currency to
Currency.USDthe first time it sawthat symbol, since its only source of currency was a pre-existing holding of
the same symbol:
Transactions carry no currency field at all (
TransactionCreate,ParsedTransactionfrom the DEGIRO parser), so on first import the USDdefault was baked in — and every subsequent resync (create/update/delete of
any transaction, or a re-import) carried that same wrong value forward via
metadata_by_symbol, self-perpetuating the bug forever.create_holdinghad a parallel issue: it overrides
body.asset_typefrom the resolved assetbut never touched
body.currency.Blast radius
Display-only. Currency is never used in any position-sizing, P&L, or
analytics math in this codebase — it's a label on the holding response.
Existing bad data doesn't need a migration: the read-time fix below covers
it immediately, and the write-time fix self-heals it in Mongo on the next
resync (which happens on every transaction create/update/delete/import for
that portfolio).
Fix
(a) Read-time derivation —
list_holdings,get_holdings_valuation,and
get_holdinginsrc/api/routes/portfolios.pyalready look up assets bysymbol to build a
full_namemap; the same lookup now also overrides eachholding's served
currencyfromassets.currencywhen known, falling backto the stored value when the asset can't be resolved. This fixes what users
see for already-poisoned holdings without touching the database.
(b) Write-path fix (self-healing storage) —
HoldingRepository.replace_for_portfolionow accepts a
currency_by_symbolmap and applies precedenceassets.currency > prior stored currency > Currency.USD.value(previouslyjust
prior stored currency > USD, with no asset lookup at all).TransactionRepository.sync_holdings_from_transactionstakes an optionalasset_repoand builds that map from the projected positions beforecalling
replace_for_portfolio. Every route that triggers a resync(
create_transaction,update_transaction/put_transaction,delete_transaction,import_transactions) now depends onAssetRepoandthreads it through —
import_transactionsalready had the dependency forISIN resolution, the others gained it.
create_holdingalso now overridesbody.currencyfrom the resolved asset next to the existingasset_typeoverride.
No migration needed: every future resync — which happens automatically on
essentially every write to a portfolio's transaction ledger — rewrites the
holding with the correct currency. Combined with the read-time override,
users never see a wrong currency, and storage self-heals within the normal
write path.
Tests
tests/unit/test_repositories_portfolios.py(new):replace_for_portfoliocurrency precedence — first-time symbol + known asset -> asset currency;
first-time symbol + unknown asset -> USD fallback; existing bad-USD
holding + known asset -> self-heals to asset currency; existing holding +
unknown asset -> prior stored currency preserved (regression guard).
tests/unit/test_routes_portfolios.py: extendedtest_create_holding_auto_resolves_asset_typeto also assert the currencyoverride (submits USD, asset is EUR, persisted body and response are EUR);
added
test_list_holdings_currency_overridden_from_asset,test_get_holding_currency_overridden_from_asset, andtest_get_holding_keeps_stored_currency_when_asset_unknown; updated thetransaction-route tests (create/patch/put/delete) to override
get_asset_repositorynow that those routes depend on it.ruff check src/,mypy src/, andpytest -m "not integration"are allgreen (1069 passed).
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
4d02d97dac8c6b51e8e2