From adf2460979fe2da4c659852d936c08dcc9a7e0db Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Sat, 29 Aug 2026 17:03:49 -0700 Subject: [PATCH] fix(investments): type unrecognized tickers as 'other', not 'stock' 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. --- src/lib/simplefin/sync.test.ts | 14 ++++++++++++-- src/lib/simplefin/sync.ts | 13 ++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/lib/simplefin/sync.test.ts b/src/lib/simplefin/sync.test.ts index 3ca7e2f..32e462e 100644 --- a/src/lib/simplefin/sync.test.ts +++ b/src/lib/simplefin/sync.test.ts @@ -173,9 +173,19 @@ describe("processHoldings", () => { expect(result[0].type).toBe("bond"); }); - it("falls back to type stock for an unrecognized ticker", () => { + it("falls back to type other, not stock, for an unrecognized ticker", () => { const result = processHoldings([makeAccount({ holdings: [makeHolding({ symbol: "NVDA" })] })]); - expect(result[0].type).toBe("stock"); + expect(result[0].type).toBe("other"); + }); + + it("does not chart an unlisted ETF as equity", () => { + // IBIT is a spot-bitcoin ETF absent from KNOWN_ETF_SYMBOLS. Guessing + // "stock" inflated the equity slice of the allocation chart; "other" + // reports the uncertainty instead of misstating it. + const result = processHoldings([ + makeAccount({ holdings: [makeHolding({ symbol: "IBIT", description: "iShares Bitcoin Trust" })] }), + ]); + expect(result[0].type).toBe("other"); }); it("classifies a SimpleFIN currency symbol as cash, not stock", () => { diff --git a/src/lib/simplefin/sync.ts b/src/lib/simplefin/sync.ts index ff752db..186a0b0 100644 --- a/src/lib/simplefin/sync.ts +++ b/src/lib/simplefin/sync.ts @@ -17,8 +17,8 @@ import { withHousehold } from "@/lib/household-context"; // SimpleFIN brokerages don't send a security type the way Plaid does. These // static lists cover the tickers common enough to be worth a dedicated -// allocation bucket; anything else with a ticker is assumed to be an -// individual stock, and anything with no ticker at all falls back to "other". +// allocation bucket. Anything we cannot place -- an unlisted ticker, or no +// ticker at all -- falls back to "other" rather than being guessed at. const KNOWN_CRYPTO_SYMBOLS = new Set(["BTC", "ETH", "SOL", "DOGE", "LTC", "BCH", "ADA", "XRP", "USDC", "USDT"]); const KNOWN_BOND_SYMBOLS = new Set(["BND", "AGG", "TLT", "IEF", "SHY", "LQD", "HYG", "MUB", "BNDX", "VCIT", "VCSH"]); const KNOWN_ETF_SYMBOLS = new Set([ @@ -26,8 +26,8 @@ const KNOWN_ETF_SYMBOLS = new Set([ "SCHD", "ARKK", "IWM", "DIA", "GLD", "SLV", "XLK", "XLF", "XLE", "XLV", ]); // Sweep/money-market positions. Without these they'd fall through to the -// "stock" default and be charted as equity exposure, which is the opposite of -// what they are. +// "other" default and sit in the unclassified bucket, when they are in fact +// known cash and belong in the cash slice. const KNOWN_CASH_SYMBOLS = new Set([ "SPAXX", "VMFXX", "SWVXX", "FDRXX", "VMRXX", "SPRXX", "FZFXX", "SNVXX", "SNSXX", ]); @@ -47,7 +47,10 @@ function inferHoldingType(symbol: string | null): HoldingRow["type"] { if (KNOWN_CRYPTO_SYMBOLS.has(ticker)) return "crypto"; if (KNOWN_BOND_SYMBOLS.has(ticker)) return "bond"; if (KNOWN_ETF_SYMBOLS.has(ticker)) return "etf"; - return "stock"; + // Unrecognized ticker. The allowlists above cannot cover the long tail, so + // guessing "stock" turns an unknown into a confident misclassification -- + // IBIT (a spot-bitcoin ETF) was charted as equity. "other" is honest. + return "other"; } // ---------------------------------------------------------------------------