Self-hosted · Single-user

PortfolioDB

AGPL-3.0 · No telemetry

The ledger you own.

Your own up-to-date record of what you hold, across every broker you use, in one place you control. Every trade is an append-only row in your own Postgres, tagged with the account it happened in; positions, cost basis and P&L are recomputed from raw lots on every read — never persisted, never rewritten — per account, and merged across all of them.

The Ledger

A full history. Nothing up my sleeve.

Scroll for fees & trade date →

How to read these listings

Each line is one lot: side and a positive quantity encode direction, and a correction is a new row, never an edit. BUY fees inflate cost basis; SELL fees reduce proceeds. Every lot carries an account — matching is scoped per symbol and account, so one broker’s cost basis is never matched against a sale at another, while the VOO lots split across IBKR and PENSION still read as one merged position at right. A SELL exceeding open BUYs is truncated with a warning — shorts are not supported, and the data-health page will name the orphaned sell in print.

Computed on read

OpenQtyAvg costUnrlzd
AAPL6228.23112.00
MSFT6415.47159.80
NVDA7175.59271.35
TSM25170.87463.25
VOO12521.13481.40
Realized P&L245.00
Unrealized1,487.80

No positions table exists — these figures are derived in your browser from the rows at left (against synthetic last prices), exactly as the product derives them from Postgres on every read.

Read the source on GitHubgithub.com/amosgeva/PortfolioDB — the README is the manual

Quick start

curl -fsSLO …/docker-compose.yml
curl -fsSL  …/.env.template -o .env
docker compose up -d

Full notice below — Docker is the only requirement

Two engines, side by side

FIFO and moving-average lot matching run in parallel, with per-match fee attribution — BUY fees inflate cost basis, SELL fees reduce proceeds. Monetary math is Decimal end to end; floats appear only at the display layer.

There is no positions table. Recomputing from lots on every read is the design.

Prices that refuse to lie

Snapshots are collected on your schedule into an append-only (symbol, ts) series. Quotes that are stale upstream are rejected rather than written under a fresh timestamp. Splits live in corporate_actions and are adjusted at read time — history is never rewritten, and an adjustment is undone by deleting one row.

Your AI agents, via MCP

An optional read-only MCP server exposes the same engines the dashboard uses to Claude Code, Claude Desktop or Cursor — over Streamable HTTP with Bearer auth.

47 tools · 7 resources · 7 prompts, including get_portfolio_review_snapshot, get_concentration, pre_trade_check and compare_methods.

Can I trust this number?

A data-health page answers it per symbol: price freshness judged against the collector’s own runs, missing cost basis, orphaned sells, suspected splits. When a figure is doubtful, the paper says so — in print.

500+ tests across two suites, including a parity suite asserting the MCP server’s KPIs match the dashboard byte for byte.

The PortfolioDB dashboard: KPI row, time-weighted returns, portfolio value history and allocation charts
The portfolio view — KPI row, time-weighted returns, value history and allocation. Pictured with make demo-seed data: a fictional portfolio with random-walk prices, not anyone’s holdings.

Known limitations, printed in full

Deliberate scope, stated before you invest an evening — the same section the README leads with.

Single currency
The engines assume one currency end to end. Multi-currency is the top roadmap item — it touches every engine, so it will not be a patch.
No broker sync — by design
You enter trades or import a CSV. No Plaid, no scraping: no third party gets credentials to your brokerage, and the ledger can’t be silently rewritten by an integration.
No built-in login
Single-user by design; your network is the access control. The docs cover LAN-only defaults, tailnets and reverse proxies.
Equities and ETFs
Anything yfinance quotes will value. No options, bonds or crypto-native accounting; shorts are truncated with a warning and reported by data health.

The standing guarantees

You own your data

Runs on your infrastructure. Your keys, your disks, your rules.

Open source, forever

AGPL-3.0: run a modified version as a service and those modifications must be published too.

Postgres is the truth

Four core tables, all append-only. The database lives in a volume you control and back up.

Nothing phones home

Outbound requests: price lookups, your LLM provider if you enable the advisor, Financial Datasets if you enable enrichment. That’s the list.

One image, every service

Dashboard, scheduler, MCP server and CLIs ship in one pulled Docker image. No toolchain on your machine.

Used daily by its author

Which is why the correctness work is real — and why the roadmap follows one portfolio’s needs.

Public notice: the whole install

Docker with Compose. Nothing else.

Quick start

# a directory and two fetched files
mkdir portfoliodb && cd portfoliodb
curl -fsSLO https://raw.githubusercontent.com/amosgeva/PortfolioDB/main/docker-compose.yml
curl -fsSL  https://raw.githubusercontent.com/amosgeva/PortfolioDB/main/.env.template -o .env

# set two matching passwords in .env, then
docker compose up -d
docker compose run --rm dashboard python app/apply_schema.py

# dashboard at http://localhost:8501

Want the 47 MCP tools too? They sit behind a separate, off-by-default profile. Set PORTFOLIODB_MCP_TOKEN in .env, then docker compose --profile mcp up -d mcp.

Read the source on GitHubgithub.com/amosgeva/PortfolioDB — the README is the manual

The image is pulled, not built: no Python, no toolchain, no compile step. Upgrade with docker compose pull.