Native macOS SwiftUI app for tracking personal cash and investments in one place, with GBP as the headline reporting currency.
- Tracks UK bank accounts and supported credit cards through TrueLayer Open Banking
- Tracks investment accounts from either:
- CSV files as the account source of truth
- SnapTrade sync for supported brokerages
- Prices equities, ETFs, and funds with Financial Modeling Prep
- Stores analyst consensus ratings for supported stocks and ETFs via StockAnalysis
- Converts non-GBP holdings to GBP for portfolio totals
- Shows per-account detail and a combined overview
Current account sources:
TrueLayer: bank/current accounts and supported credit cardsCSV File: investment and fund accountsSnapTrade: supported brokerage investment accounts
- A bank connection is stored as one app account
- Multi-currency balances remain grouped under that account
- The headline value is converted to GBP
- Account detail shows:
- per-currency balances
- A TrueLayer card connection is stored as one app account
- Card balances are treated as liabilities and stored as negative balances
- The headline value reduces net worth in the same way as other negative cash balances
- The connection flow is shared with bank accounts, using the provider's normal authentication and 2FA
- Holdings and cash are stored separately
- Headline value is shown in GBP
- Mixed-currency accounts also show the original currency breakdown
- For CSV-backed accounts, the CSV is the source of truth and can be re-imported
- Holdings can show open P&L percentage when an average purchase price is available
- Analyst consensus ratings are cached in shared security metadata when available
- macOS 14+
- Xcode 16+
- XcodeGen
Credentials are entered in-app via Settings and stored locally in:
~/Library/Application Support/Accounts/credentials.json
They are not committed to the repo.
Supported credentials:
FMP API KeyTrueLayer Client IDTrueLayer Client SecretSnapTrade Client IDSnapTrade Consumer Key
The checked-in source of truth is project.yml. Generate the Xcode project first:
xcodegen generateBuild from the command line:
xcodebuild -project Accounts.xcodeproj -scheme Accounts -destination 'platform=macOS' buildRelease build:
xcodebuild -project Accounts.xcodeproj -scheme Accounts -configuration Release -destination 'platform=macOS' buildOpen in Xcode:
open Accounts.xcodeprojThe default bundle identifier in project.yml is io.github.le-big-mac.accountsmanager. If you plan to sign or distribute your own build, change it to an identifier you control.
Run from Xcode, or launch the built app from DerivedData after a successful build.
The app uses a custom URL scheme for auth callbacks:
accounts://truelayer-callbackaccounts://snaptrade-callback
For personal use on the same Mac, build Release and copy Accounts.app into /Applications.
The app's data is stored outside the app bundle, so replacing the installed app does not remove:
~/Library/Application Support/Accounts/Accounts.store~/Library/Application Support/Accounts/credentials.json
Those files contain your local data and credentials and are not affected by replacing /Applications/Accounts.app.
SwiftData store:
~/Library/Application Support/Accounts/Accounts.store
Legacy migrations:
- older builds may have used
~/Library/Application Support/default.store - current builds migrate that legacy store into the app-specific path above on launch
Debug log:
~/Library/Application Support/Accounts/debug.log
Debug logging is disabled by default. To enable it for a debug run:
ACCOUNTS_DEBUG_LOG=1The repo ignores local credentials, logs, and store files via .gitignore.
CSV-backed investment accounts are intended to use one file per account as the persistent source of truth.
Current preferred format is a generic portfolio CSV with one header row and one row per holding or cash balance.
Expected columns:
nameassetClassunitscurrency- optional
averagePurchasePrice - optional
ticker - optional
isin - optional
sedol
Rules:
assetClassshould be one ofcash,stock,etf,fund, orgilt- non-cash rows become holdings
cashrows become cash balances- for cash rows, the amount is stored in
units currencyis the holding price currency or the cash currencyaveragePurchasePriceis optional and stores per-unit cost basis for open P&L displayticker,isin, andsedolare optional, but at least one identifier is useful for live pricing- gilt rows use
unitsas nominal held - gilt prices refresh from Hargreaves Lansdown by
sedol;currentCleanPriceis optional and is mainly an exported cached value - gilt HTM calculations use
dirtyPricePaidper GBP 100 nominal as the cost basis
Example:
name,assetClass,units,currency,averagePurchasePrice,ticker,isin,sedol,currentCleanPrice,couponRate,maturityDate,settlementDate,cleanPricePaid,dirtyPricePaid,couponDates
FTSE Global All Cap Index Fund Accumulation,fund,36.9627,GBP,252.40,VAFTGAG,GB00BD3RZ582,
Apple Inc,stock,12,USD,182.15,AAPL,US0378331005,
USD Cash,cash,18003.45,USD,,,,
GBP Cash,cash,130.12,GBP,,,,
1% Treasury Gilt 2032,gilt,10000,GBP,,,,GB00BM8Z2S21,93.40,1%,2032-01-31,2026-04-29,92.80,93.15,31 Jan;31 JulFor conventional gilts:
couponRateis the annual coupon rate;1%,1, and0.01are acceptedmaturityDateandsettlementDateacceptyyyy-MM-dd,dd/MM/yyyy, or UK month-name datescouponDatesis a semicolon-separated pair of coupon days, such as31 Jan;31 Jul- displayed value is nominal held multiplied by the refreshed clean price divided by 100
- displayed returns are gross annual HTM yield and gross total HTM return if held to maturity
- HTM entitlement excludes payments where settlement is on or after the seven-business-day ex-dividend date, using an England and Wales weekday/bank-holiday calendar
Other import paths still exist for older Vanguard UK, Robinhood, and Interactive Investor exports, but the generic portfolio format is the format the current app model is built around.
If the format changes in future, inspect:
Accounts/Services/CSVParser.swiftAccounts/Services/PortfolioImportService.swift
The main window includes an Export CSV button that writes the current live portfolio state to one CSV file.
Export columns:
accountNameaccountTypesourceTypenameassetClassunitscurrencyaveragePurchasePricetickerisinsedolcurrentCleanPricecouponRatematurityDatesettlementDatecleanPricePaiddirtyPricePaidcouponDates
Notes:
- investment holdings export one row per holding
- investment cash exports as
assetClass = cash, with the amount stored inunits - bank balances also export as
assetClass = cash, one row per currency balance - TrueLayer credit card balances export as cash liability rows, with negative values in
units - the extra account/source columns are informational; the current generic importer ignores them
Analyst consensus ratings are optional enrichment for holdings with a ticker and asset class stock or etf.
- Source: StockAnalysis forecast pages
- Stored in shared security metadata and reused until stale
- Refreshed on launch for missing/stale rows and during investment price syncs
- UI shows only the consensus rating: Strong Buy, Buy, Hold, Sell, or Strong Sell
- Rating colors follow the app's gain/loss palette, with muted colors for Buy/Sell and stronger colors for Strong Buy/Strong Sell
- Scrape/source errors are retained on the holding so page-shape breakage is visible in the app
Current refresh policy:
- if a holding has no stored rating, it is eligible immediately
- if a holding has a rating, it refreshes when older than 1 day
- if a scrape failed before a rating was stored, it is eligible on the next launch or sync
- cash and most fund/OEIC rows do not participate
Accounts/AccountsApp.swift- app entry point, model container, callback wiringAccounts/Models/- SwiftData modelsAccounts/Services/- TrueLayer, SnapTrade, pricing, CSV import, syncingAccounts/Views/- SwiftUI screensAccounts/Utilities/- formatting, debug logging, store backup
- This is a personal-use app, not a production product
- Some external integrations are intentionally lightweight and rely on local credentials and local persistence
- The repo is safe to publish as code, but the app still uses local filesystem storage for secrets and data
This project was built in a heavily iterative, AI-assisted way. Treat it as practical personal software, not as a polished reference implementation. Review the code carefully before extending it, publishing derivatives, or trusting it with anything important.