https://github.com/milouk/personal-portfolio-tracker
Self-hosted dashboard for tracking your net worth across brokers, banks, T-bills, crypto, and cash. Live prices, automated sync, full privacy — all data stays on your machine.
https://github.com/milouk/personal-portfolio-tracker
Last synced: about 2 months ago
JSON representation
Self-hosted dashboard for tracking your net worth across brokers, banks, T-bills, crypto, and cash. Live prices, automated sync, full privacy — all data stays on your machine.
- Host: GitHub
- URL: https://github.com/milouk/personal-portfolio-tracker
- Owner: milouk
- Created: 2026-04-30T15:38:26.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-30T21:56:23.000Z (about 2 months ago)
- Last Synced: 2026-04-30T23:19:57.490Z (about 2 months ago)
- Language: TypeScript
- Homepage: https://milouk.github.io/personal-portfolio-tracker
- Size: 263 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Personal Portfolio Tracker
[](https://nextjs.org)
[](https://react.dev)
[](https://tailwindcss.com)
[](https://www.typescriptlang.org)
[](https://github.com/milouk/personal-portfolio-tracker/actions/workflows/deploy-demo.yml)
[](https://github.com/milouk/personal-portfolio-tracker/actions/workflows/release.yml)
[](https://github.com/milouk/personal-portfolio-tracker/pkgs/container/personal-portfolio-tracker)
[](https://github.com/milouk/personal-portfolio-tracker/releases/latest)
[](#license)
A self-hosted dashboard for tracking your net worth across brokers, banks,
T-bills, crypto, and cash — with live prices, automated sync, and per-position
P/L. **All data stays on your own machine.**
> **Live demo:** [milouk.github.io/personal-portfolio-tracker](https://milouk.github.io/personal-portfolio-tracker)
## Features
- **Live valuations** — ETFs, stocks, crypto and FX update on every page load.
- **Multi-broker sync** — pull positions and cash directly from your accounts.
- **Bond / T-bill ladder** — maturity dates, YTM, accrued profit, blended yield.
- **Greek T-bill auctions** — upcoming auctions and a profit calculator.
- **Calendar** — bond maturities, ex-dividend dates, dividend payments.
- **Email + push reminders** — N days before each event.
- **Privacy mode** — one click blurs every number on screen for screenshots.
- **JSON API** — drop-in widget for self-hosted dashboards (Glance, Homepage…).
- **Dark / light theme** — Nova preset, readable in both modes.
## Quick start
```bash
git clone https://github.com//personal-portfolio-tracker
cd personal-portfolio-tracker
npm install
cp .env.example .env.local # add the credentials you want
npm run dev # → http://localhost:3000
```
That's enough to see the empty dashboard. Add assets with the **+ Add asset**
button or by editing `data/portfolio.json` directly.
## Data layout
Everything lives in `data/`:
```text
data/
├── portfolio.json your assets (single source of truth)
├── events.jsonl append-only change log
├── prices.json price cache
├── fx.json EUR/USD cache
├── ecb.json ECB Deposit Facility Rate cache
└── …
```
`data/` is git-ignored. Backup = copy the folder.
## Live data sources
| Asset class | Source | Refresh |
| ----------------------- | ---------------- | -------------- |
| ETFs / stocks (EUR) | Yahoo Finance | 60 s |
| ETFs / stocks (LSE/USD) | Stooq (fallback) | 60 s |
| Crypto | CoinGecko | 60 s |
| EUR/USD FX | ECB / Frankfurter| 1 h |
| ECB rate | ECB SDW | 24 h |
| Greek T-bill auctions | PDMA | 24 h |
| Dividend calendar | Yahoo | 24 h |
| Broker positions / cash | per-broker sync | on demand |
## Broker sync
Some sources have no public API, so this project drives them with the same
tools you'd use yourself — a Python helper for Trade Republic and a headless
browser for NBG. Both run **only on your machine**, only when you ask them to.
```bash
npm run sync:tr # Trade Republic (push / SMS code on first login)
npm run sync:nbg # National Bank of Greece (Viber OTP each login)
npm run sync:all # both, sequentially
```
Or click **Sync** in the header. When an OTP is needed, a modal pops up — paste
the code and submit.
## Calendar reminders
The dashboard surfaces upcoming events automatically. To get **email or
ntfy.sh push notifications** N days ahead, add this to a cron job:
```cron
0 9 * * * cd /path/to/personal-portfolio-tracker && npm run notify:calendar
```
Configure SMTP / ntfy in `.env.local` (see `.env.example`).
## JSON API
Read-only summary + sync triggers — drop into Glance, Homepage, or any custom
dashboard:
| Endpoint | Method | Purpose |
| ------------------ | ------ | ------------------------------------ |
| `/api/summary` | GET | Net worth, P/L, allocation, FX, ECB |
| `/api/prices` | GET | Live prices + FX (cached) |
| `/api/calendar` | GET | Upcoming maturities + dividends |
| `/api/tbills` | GET | Greek T-bill auction calendar |
| `/api/sync` | GET | Current sync status |
| `/api/sync` | POST | Trigger a sync (`tr` / `nbg` / `all`)|
Set `PORTFOLIO_API_TOKEN` in `.env.local` and pass it via `?token=…`,
`x-api-token: …`, or `Authorization: Bearer …`. With no token configured the
endpoints are open — keep the dashboard on localhost or behind a tunnel.
### Glance widget recipe
```yaml
- type: custom-api
title: Net worth
cache: 5m
url: http://portfolio-tracker:3000/api/summary?token=YOUR_TOKEN
template: |
€{{ printf "%.0f" (.JSON.Float "totalEur") }}
P/L {{ if gt (.JSON.Float "gainEur") 0.0 }}+{{ end }}€{{ printf "%.0f" (.JSON.Float "gainEur") }}
({{ printf "%.1f" (multiply (.JSON.Float "gainPct") 100.0) }}%)
```
## Docker
Multi-arch images (`linux/amd64`, `linux/arm64`) are published on every
release to **GitHub Container Registry**:
```text
ghcr.io/milouk/personal-portfolio-tracker
```
Tags follow semver: each release gets `vX.Y.Z`, `vX.Y`, `vX`, plus `latest`.
Images are public — no `docker login` needed to pull.
### One-line up (pulls from GHCR)
```bash
cp .env.example .env.local # fill in the vars you actually want
docker compose up -d # dashboard at http://localhost:3000
```
`compose.yaml` defaults to `ghcr.io/milouk/personal-portfolio-tracker:latest`.
Pin a specific version with `APP_TAG`:
```bash
APP_TAG=v1.2.3 docker compose up -d
```
### Upgrading
Whenever a new release is cut, the new image is on Docker Hub within minutes.
On your server:
```bash
docker compose pull && docker compose up -d
```
For unattended updates, drop in [Watchtower](https://containrrr.dev/watchtower/)
or run the above as a daily cron job.
### Building locally
If you've patched the source and want to run your local copy:
```bash
docker compose up -d --build # rebuilds from ./Dockerfile, ignores registry
```
### Passing environment variables
Compose looks at variables in this priority order, so you can mix and match:
1. **Shell** when invoking compose:
```bash
PORTFOLIO_API_TOKEN=xxx docker compose up -d
```
2. **`.env`** next to `compose.yaml` (auto-loaded by Docker Compose).
3. **`.env.local`** (loaded into the container via `env_file`, optional).
4. **`environment:`** block in `compose.yaml` (explicit overrides).
All vars from [`.env.example`](.env.example) work in any of those — NBG creds,
TR phone/PIN, SMTP, ntfy, `PORTFOLIO_API_TOKEN`. Anything left unset is treated
as empty (no defaults are baked into the image).
### One-time broker logins
```bash
# Trade Republic — interactive, sends SMS or push to your phone
docker compose run --rm app pytr login
# NBG — interactive, prompts for the Viber OTP
docker compose exec app npm run sync:nbg
```
Sessions are persisted in the named `pytr-home` volume and your local `./data`
directory, so subsequent syncs run without prompts (until the broker invalidates
the session, typically every few weeks).
### Periodic sync (host crontab)
```cron
0 8 * * * docker compose -f /path/to/compose.yaml exec -T app npm run sync:all
0 9 * * * docker compose -f /path/to/compose.yaml exec -T app npm run notify:calendar
```
### Headless OTP (no TTY)
When running without an interactive terminal, set `NBG_OTP_SOURCE=webhook`,
expose port 4848 in `compose.yaml`, then deliver the OTP from any phone:
```bash
curl -d 123456 http://your-server:4848/otp
```
## Privacy
- Hide-numbers toggle (eye icon, `⌘/Ctrl + .`) blurs every number on screen.
- All credentials and balances stay local — no cloud, no telemetry.
- `data/` is git-ignored by default.
## Demo
A static-export build with seeded dummy data lives in `demo/`:
```bash
npm run demo:dev # local preview on :3000
npm run demo:build # build for GitHub Pages → out/
```
## Stack
Next.js 16 · React 19 · Tailwind v4 · shadcn/ui · Recharts · Motion ·
Playwright · pytr
## License
Personal use. No warranties. PRs welcome.