15.Expenses + receipt OCR — Schedule C without a SaaS
Statement parsers for Amazon / BoA / Chase / Apple Card, on-device receipt OCR via Apple Vision (macOS) or Tesseract (fallback), 20-bucket Schedule-C taxonomy, merchant canonicalization with learned mappings, duplicate detection, full-text search. $0 / receipt, no LLMs, no paid APIs.
Why this is in a trading app
If you trade as a business (sole prop / single-member LLC / S-corp), you file Schedule C for expenses against your trading income. The categories — Office / Software / Education / Equipment / Travel / Internet / etc. — match cleanly across self-employed individuals. The expense tracker is built into TraderView so the receipts that justify your home-office deduction, your data-feed subscription, your trading-education book sit in the same database as the trades they support.
Statement parsers
POST /api/expense/import accepts a PDF or CSV statement; the parser dispatches by signature column / heading:
- Amazon order history CSV — itemized order lines with shipping / tax broken out.
- Bank of America statement PDF — debit / credit transactions, balance / type columns.
- Chase statement PDF — credit card monthly statements with merchant + amount + date.
- Apple Card monthly statement PDF — Daily Cash + purchase rows + merchant categories.
Output: one row per transaction in expense_transactions. The merchant string from the statement is normalized through a canonicalization layer (see below). The same idempotent insert pattern as broker import (dedupe on date + amount + last-4 + raw description) prevents duplicates on re-import.
Receipt OCR — on device, no cloud
Receipts are images / PDFs you drop into the Expense → Receipts pane. traderview-ocr extracts:
- Merchant — top-of-receipt name.
- Total — bottom-of-receipt amount.
- Date — parsed flexibly (handles US / EU / ISO).
- Itemized lines — for receipts that include them.
Two backends:
- Apple Vision (macOS only) — wraps
VNRecognizeTextRequestvia a Swift bridge. Fast, accurate on printed receipts, free. - Tesseract (fallback) — used on Linux / Windows and on macOS if Vision isn't available. Slower, less accurate on thermal-printed receipts, free.
The OCR pass is async (Tokio task per receipt); the upload returns immediately and the parsed fields populate when ready. Re-OCR is a button per row — useful when you tweak the OCR config or rotate the source image.
20-bucket Schedule-C taxonomy
Categories match Schedule C Part II line items, expanded for trader-specific buckets:
Advertising · Car & Truck · Commissions/Fees · Contract Labor · Depreciation · Insurance · Interest · Legal & Professional · Office · Pension/Profit-sharing · Rent (Office) · Rent (Equipment) · Repairs · Supplies · Taxes & Licenses · Travel · Meals · Utilities · Other (specify) · + Trading subcategories: Data Feeds · Trading Software · Trading Education · Internet (home office)
Each expense_transactions row carries a category. Sub-totals roll up on the Schedule C report (GET /api/expense/report/schedule_c?year=).
Merchant canonicalization + learned mappings
Statement descriptions are noisy: "AMZN MKTP US*N04QV0RT3" is Amazon, "COSTCO WHSE #1142" is Costco. The canonicalizer normalizes to a canonical merchant name + applies your learned-category mapping.
- Regex passes — strip transaction IDs, store numbers, terminal codes.
- Prefix table — built-in mappings for the ~200 most common merchants.
- Learned mappings — when you re-categorize a row (e.g. "this Amazon order was actually office supplies, not personal"), the canonical merchant → category mapping is recorded in
expense_rules. Future imports with the same merchant + similar amount pattern auto-categorize. - Bulk re-categorize — selecting multiple rows + setting category writes one rule that covers all selected merchants.
Duplicate detection
When a receipt is uploaded for a transaction that's already in the database (because you also imported the statement that contained it), the receipt is attached to the existing row rather than creating a duplicate. The matcher:
- Same date ± 3 days — receipts and statement postings often differ by a few days.
- Same total (exact).
- Merchant fuzzy match — case-insensitive, token-bag.
Multiple candidates → manual pick in the UI. No-match → new transaction row is created from the receipt alone (useful for cash purchases that don't show on a card statement).
Reports + export
- Schedule C report (
GET /api/expense/report/schedule_c?year=) — Part II line items rolled up. One-click CSV export. - Top merchants — where the money goes.
- Category trend — monthly bar chart per category.
- Receipt missing — list of transactions over $X (configurable threshold) without an attached receipt. The audit-trail companion for "do I have documentation if I get audited?".