Skip to content

fix(investments): type unrecognized tickers as 'other', not 'stock' - #102

Merged
KenTaniguchi-R merged 1 commit into
mainfrom
fix/91-unknown-ticker-other
Aug 30, 2026
Merged

fix(investments): type unrecognized tickers as 'other', not 'stock'#102
KenTaniguchi-R merged 1 commit into
mainfrom
fix/91-unknown-ticker-other

Conversation

@KenTaniguchi-R

Copy link
Copy Markdown
Owner

Closes #91 — the unfinished half of #71.

Problem

inferHoldingType (src/lib/simplefin/sync.ts) fell through to return "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 outside KNOWN_BOND_SYMBOLS got 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 other for unrecognized tickers. other is 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

other is 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 description SimpleFIN already sends (iShares Bitcoin Trust, NVIDIA Corp): Trust|Fund|ETF|Indexetf, else stock. 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:

  • the existing "falls back to type stock" test flipped to expect other
  • a new regression test pinning IBIT specifically, so the reported symptom cannot come back

Full suite green locally: 752 passed / 113 files, typecheck clean, lint clean.

🤖 Generated with Claude Code

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.
@KenTaniguchi-R

Copy link
Copy Markdown
Owner Author

Merging with mutation (diff) red — the red is pre-existing, not from this diff

Verified by running Stryker locally on a clean checkout of main's sync.ts, which is byte-identical to this PR's (git diff main -- src/lib/simplefin/sync.ts is empty for everything but the 13 changed lines):

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:

  1. scripts/mutate-diff.sh passes whole file paths to --mutate, so a 2-line edit pulls all ~700 lines of sync.ts into scope — the mechanism documented in Raise mutation coverage on sync.ts / reports.ts (and other large touched files) to ≥60% #34.
  2. 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.ts documents this as deliberate ("NoCoverage rather than Survived, which is honest signal"), but the thresholds.break = 60 was 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

inferHoldingType still returns 'stock' for unknown tickers — the 'other' fallback from #71 never landed (IBIT charted as equity)

1 participant