openportfolio

Apache-2.0 · self-hosted · no provider API key

Every account as one book.
Every call on the record.

openportfolio is an open-source, self-hosted portfolio tracker. It pulls every brokerage, pension, wallet and bank account into a single net worth, stores the investor flows behind the price, and Brier-scores the forecasts you registered before the fact.

Get started See the live demo

git clone · pnpm install · npx convex dev

Net worthFlowsTrack recordDecisions
Demo book · viewer

£186,136.74 5 accounts · 30 snapshots · base GBP

By asset class
ETF 38.6%Equity 34.9%Pension 14.2%Crypto 9.5%Cash 2.8%
Positions
Symbol Account Venue Qty Price Value
VWRL isa ibkr 340 £118.20 £40,188.00
CSPX isa ibkr 42 £754.10 £31,672.20
WORKPLACE-DC pension manual 1 £26,500 £26,500.00
MSFT gia ibkr 60 $526.40 £23,466.90
035420 gia ibkr 210 ₩197,500 £23,093.30
ASML gia ibkr 18 €812.40 £12,184.00

6 of 10 rows · every figure on this page is invented

Two problems that look unrelated

A portfolio is scattered by construction, and market commentary is unaccountable. Both are bookkeeping failures, so openportfolio treats them as bookkeeping.

01

One net worth

A broker here, a pension there, an ISA, an exchange account, a holding no API will ever return. Every app shows you a number and none of them shows you yours, so the figures that decide things get estimated, generously, in the direction that avoids a decision.

02

Flows, not just prices

Price is the output of who was buying and who was made to sell. Net buying by investor type, turnover and a calendar of dated forward events are stored as first-class series, not derived when somebody remembers to ask. A forced seller is on a schedule.

03

A scored track record

Being wrong costs nothing and leaves no trace, so a forecaster worth reading and one who is merely fluent are indistinguishable. Register the call before the fact with a probability and a resolution criterion, and the machine settles it and scores it.

Track record

The reliability diagram is the product

A mean Brier score alone hides the direction you are wrong in. The diagram puts what you said next to what happened, bucket by bucket, and the gap between the two bars is the entire point.

Calls with a parseable criterion resolve themselves when the horizon passes. Nothing waits on you remembering that you made a prediction three weeks ago.

  • Probability, horizon and resolution criterion required at registration
  • Automatic settlement on horizon expiry, scored on resolve
  • Calibration error and per-bucket observed rate
Net worthFlowsTrack recordDecisions
Demo book · viewer

0.187 mean Brier over 74 scored calls · coin flip is 0.25 · calibration error 6.8%

Reliability
Said Calls Happened Said vs happened
0% to 20% 9 11.1%
20% to 40% 14 28.6%
40% to 60% 19 52.6%
60% to 80% 21 61.9%
80% to 100% 11 72.7%
Calls
Subject Criterion Said Brier
MSFT MSFT close > 520.00 55% 0.203
Euro area CPI HICP yoy < 2.2 66% 0.116
ASML bookings beat consensus 58% 0.176
VWRL VWRL close > 124.00 61% open
ETH ETHUSD > 4000 37% open

Flows

Who was buying, and who was made to sell

Net buying by investor type and session turnover, per market or per symbol, kept next to the tape rather than under it. Retail absorbing a foreign dump on collapsing volume is a fact about supply, and it is visible before it is a story.

  • Per-session net buying by investor type
  • Turnover alongside every flow row, never without it
  • Dated catalyst calendar for forced-seller events
Net worthFlowsTrack recordDecisions
Demo book · viewer

KOSPI net buying over the last 30 sessions

Investor Net bought (₩bn)
individual +7,218
institution −1,884
foreigner −5,602
other_corporation +268
Sessions
Date Investor Net (₩bn) Turnover (₩bn)
2026-08-07 foreigner −811.4 37,412
2026-08-07 individual +206.9 37,412
2026-08-07 institution +598.3 37,412
2026-08-06 foreigner −2,967.5 41,265
2026-08-06 individual +3,104.8 41,265
2026-08-06 institution −241.6 41,265

Decisions

A deferred decision is still a decision

"Wait for the print, then decide" evaporates the moment it is said out loud. The queue holds each one with a trigger condition and an outcome, and marks the ones that quietly went past their date.

  • Trigger condition and due date on every entry
  • Overdue rows sorted first and flagged
  • Closed only with a written outcome
Net worthFlowsTrack recordDecisions
Demo book · viewer

Semiconductor hold review

6 days overdue

Held until the book is at cost plus the target, not until one name recovers.

trigger · first of each month

Re-enter the metals tranche after CPI

in 2 days

Deferred once already. Deferring twice is a decision to skip it.

trigger · CPI print clears

Consolidate the old workplace pension

in 17 days

Manual row until the provider exposes an API.

trigger · provider statement arrives

Trim the ETF overweight

in 44 days

ETF share is above the band written at the last review.

trigger · quarterly rebalance

No provider API key. Anywhere.

Watching a book is only useful if something is actually watching. Metered inference is the wrong shape for that.

When each run bills per token, every autonomous check becomes a purchase, and a product that spends your money unprompted has to ask first, or batch, or ration. All three turn a portfolio that watches itself into a portfolio that asks permission to look.

So every model call is dispatched to an agent CLI you are already signed in to, with a per-task fallback order. There is no key in this repo and no field to put one in. Agent work is bounded by quota and wall clock rather than by spend, so it never has to be justified one invocation at a time.

  • codex, antigravity and claude supported out of the box
  • Per-task provider order with automatic fallback
  • MCP server with 25 tools, so the agent reads and writes the book directly
// actor.mts — the provider chain, not an API client
const ORDERS = {
  sync:    ["codex", "claude"],
  review:  ["claude", "codex"],
  resolve: ["codex", "antigravity", "claude"],
}

// Each entry is a CLI you already signed in to.
// There is no apiKey field to fill in, in any of them.

Where it sits

Between a spreadsheet that never updates and a hosted tracker that wants your broker password.

openportfolio compared with a spreadsheet and a hosted portfolio tracker
  openportfolio Spreadsheet Hosted tracker
Data stays on your infrastructure Yes Yes No
Updates without you Yes No Yes
Holdings no API returns Manual adapter Yes Rarely
Investor flow series First class No No
Scored forecast record Brier + reliability No No
Readable by an agent MCP, 25 tools No No
Places orders Never No Sometimes

Running in five commands

Node 22, pnpm, and a free Convex deployment. Nothing else is required to see a real number.

git clone https://github.com/seonglae/openportfolio.git
cd openportfolio
pnpm install

cp .env.example .env.local
npx convex dev --once

# create the first book
npx convex env set OPENPORTFOLIO_DEV_TENANT home
npx convex run tenants:create '{"slug":"home","name":"Home","baseCurrency":"GBP"}'

# the UI, then the sync loop
pnpm --filter openportfolio-browser dev
npx tsx sync-worker.mts --once

With nothing linked it registers the venues it can serve and records a net worth of zero, which is correct. Point OPENPORTFOLIO_MANUAL_HOLDINGS at a JSON file and the same command gives you a real total, with crypto re-quoted through the keyless CoinGecko adapter and every row converted into one base currency.

Full quick start · Writing a venue adapter · Multi-tenancy

Questions

Is this a trading bot?

No. The backend has no function that places an order, the adapters that ship declare canPlaceOrders: false, and PlaceOrderRequest requires an OrderConfirmation naming who confirmed it and when. That field has no default on purpose. It aggregates, watches and keeps score.

Where does my financial data live?

In your own Convex deployment. Broker credentials stay in the worker process on your machine; the backend never receives them and this repository has nowhere to put them.

Which accounts can it read?

Two adapters ship: a keyless CoinGecko quote source, and a manual adapter that reads a JSON file you maintain, which is how a pension or an unlisted holding gets into the total instead of being left out of it. A keyed broker adapter is about forty lines against a documented contract.

Can several people share one deployment?

Yes. Every table carries a tenant id and every index leads with it. A caller never says which tenant it is: the id comes from membership rows or from the service key's own row. A document belonging to another tenant reads as missing rather than forbidden, because "forbidden" confirms the row exists.

What does it cost to run?

A Convex free tier, a machine you already own, and the agent CLI subscriptions you already pay for. There is no inference bill because there is no inference key.

Start keeping score

Apache-2.0. Clone it, point it at one account, and register one call with a date on it.

Star on GitHub Read the docs