fix(investments): type unrecognized tickers as 'other', not 'stock' - #102
Merged
Conversation
inferHoldingType guessed "stock" for any ticker outside the static allowlists, turning an unknown into a confident misclassification. IBIT (a spot-bitcoin ETF) was charted as equity, and so was any unlisted bond or index fund -- bond ETFs in particular read as equity in the Asset Allocation donut. Default to "other" instead, as specified in #71 and restated in #91. "other" already renders as its own badge and allocation slice, so the uncertainty is reported rather than misstated. The tradeoff is that genuine single stocks outside the allowlist now read "Other" too. Holdings are replaced wholesale on every sync, so existing rows re-type themselves on the next sync -- no backfill needed. Closes #91.
Owner
Author
Merging with
|
| killed | survived | no-coverage | errors | score | |
|---|---|---|---|---|---|
main baseline |
32 | 15 | 67 | 131 | 28.07% |
| this PR | 32 | 15 | 67 | 131 | 28.07% |
Identical. This PR's own mutants are killed — the CI log shows ✓ processHoldings falls back to type other, not stock, for an unrecognized ticker (killed 1).
Why the gate is structurally unpassable here
Two things compound:
scripts/mutate-diff.shpasses whole file paths to--mutate, so a 2-line edit pulls all ~700 lines ofsync.tsinto scope — the mechanism documented in Raise mutation coverage on sync.ts / reports.ts (and other large touched files) to ≥60% #34.- perf(mutation): stop booting a Postgres container for every mutant #100 decoupled mutation testing from Postgres, so
sync.ts's DB half now reports 67 no-coverage rather than being killed by DB-backed tests.vitest.stryker.config.tsdocuments this as deliberate ("NoCoverage rather than Survived, which is honest signal"), but thethresholds.break = 60was never re-tuned for it.
Net: any PR touching a DB-heavy file under src/lib now fails this gate regardless of the change's quality. Filed as a follow-up.
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #91 — the unfinished half of #71.
Problem
inferHoldingType(src/lib/simplefin/sync.ts) fell through toreturn "stock"for any ticker outside its static allowlists. A 21-symbol ETF list cannot cover the long tail, so unknowns became confident misclassifications.Observed on the real household: IBIT (iShares Bitcoin Trust, a spot-bitcoin ETF) typed as
Stock, inflating the equity slice of the Asset Allocation donut. Bond ETFs outsideKNOWN_BOND_SYMBOLSgot the same treatment — charted as equity, which is the opposite of what they are.#71 specified two changes. The cash/money-market list landed; the
'other'fallback did not. This is that half.Change
Default to
otherfor unrecognized tickers.otheris already a first-class value — it renders as its own badge (investment-type-badge.tsx) and its own allocation slice — so the uncertainty is displayed rather than misstated.Also refreshed two comments that still described the old
"stock"default.Tradeoff worth reviewing
otheris now the fallback for every unlisted ticker, not just misclassified ETFs. Genuine single stocks outside the allowlist (NVDA, AAPL, …) will now read "Other" too, so a typical portfolio's donut grows a large grey slice.That is what #71 and #91 both specify — honest uncertainty over a confident wrong answer — so it is implemented as written. The alternative, if the grey slice proves worse in practice, is to classify off the
descriptionSimpleFIN already sends (iShares Bitcoin Trust,NVIDIA Corp):Trust|Fund|ETF|Index→etf, elsestock. That catches IBIT without demoting real stocks, but it is more code and beyond what the issue asks. Happy to follow up if you prefer it.Migration
None needed. Holdings are deleted and re-inserted wholesale on every sync (
sync.ts:311— SimpleFIN has no delta/cursor for positions), so existing rows re-type themselves on the next sync.Tests
Two behavioral tests, written red first:
otherFull suite green locally: 752 passed / 113 files, typecheck clean, lint clean.
🤖 Generated with Claude Code