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"; } // ---------------------------------------------------------------------------