- Frontend: React + Vite app (
/frontend) for holdings entry, charting, and portfolio metrics. - Backend: Node.js + Express API in TypeScript (
/backend) for validation, data aggregation, and caching. - Database: SQLite for holdings, validation cache, latest quotes, and price history.
- Market Data: Provider abstraction with a primary provider and fallback provider. Cached results stored in SQLite.
Data flow:
- UI submits a holding → API validates ticker + market → API stores holding → API fetches/returns cached quote.
- UI requests performance → API loads holdings → API fetches cached history → API computes daily portfolio values.
Tables:
portfolios:id,name,base_currency,created_atholdings:id,portfolio_id,ticker,market,buy_date,buy_price,quantity,created_atmarket_symbols:ticker,market,name,currency,exchange,provider,last_verified_atquote_cache:ticker,market,price,currency,as_of,source,fetched_at,expires_atprice_history:ticker,market,interval,price,currency,date,source,fetched_at
Key indexes:
holdings_portfolio_idxonholdings(portfolio_id)holdings_ticker_market_idxonholdings(ticker, market)market_symbols_lookup_idxonmarket_symbols(ticker, market)quote_cache_lookup_idxonquote_cache(ticker, market, expires_at)price_history_lookup_idxonprice_history(ticker, market, interval, date)
Scaling to multi-portfolio:
holdings.portfolio_idalready supports multiple portfolios. Add CRUD for portfolios and passportfolioIdin requests.- Quote and history caches are shared across portfolios to minimize provider calls.
Base URL: http://localhost:4000
GET /api/exchanges
Response:
{
"exchanges": [
{ "code": "NASDAQ", "label": "NASDAQ (US)", "assetType": "equity" },
{ "code": "XLON", "label": "London Stock Exchange (XLON)", "assetType": "equity" },
{ "code": "BINANCE", "label": "Binance (Crypto)", "assetType": "crypto" }
]
}GET /api/health
Response:
{ "status": "ok" }POST /api/validate
Request:
{ "ticker": "AAPL", "market": "NASDAQ" }Response:
{ "valid": true, "source": "TWELVE_DATA", "symbol": { "ticker": "AAPL", "market": "NASDAQ" } }GET /api/holdings
Response:
{
"holdings": [
{
"id": 1,
"ticker": "AAPL",
"market": "NASDAQ",
"company_name": "Apple Inc.",
"buy_price": 120,
"quantity": 2,
"latest_quote": { "price": 185.1, "as_of": "2024-01-02", "source": "TWELVE_DATA", "cached": true },
"market_value": 370.2,
"unrealized_pnl": 130.2
}
]
}POST /api/holdings
Request:
{
"ticker": "AAPL",
"market": "NASDAQ",
"buy_date": "2024-01-01",
"buy_price": 120,
"quantity": 2
}Response:
{ "holding": { "id": 1, "ticker": "AAPL", "market": "NASDAQ" } }DELETE /api/holdings/:id
Response:
{ "deleted": true }GET /api/portfolio/performance?from=2024-01-01&to=2024-02-01
Response:
{
"from": "2024-01-01",
"to": "2024-02-01",
"series": [
{ "date": "2024-01-01", "value": 10000 },
{ "date": "2024-01-02", "value": 10120 }
]
}POST /api/refresh
Request:
{ "from": "2024-01-01", "to": "2024-02-01", "currency": "PLN" }Response:
{ "refreshed": true }- Check
market_symbolscache for a recent validation entry. - If missing/expired, call provider
searchSymbolwithtickerandmarket(exchange). - If found, cache the result for
VALIDATION_TTL_DAYSand allow holding creation. - If not found, return
400with validation error.
Focused on US, London, EU, plus Binance for BTC:
- US:
NASDAQ,NYSE,AMEX - UK:
XLON(London Stock Exchange) - EU:
XETR,XPAR,XAMS,XBRU,XMIL,XMAD,XLIS - Crypto:
BINANCE(useBTC/USDT, orBTCwhich defaults toBTC/USDT)
- Uses Chart.js via
react-chartjs-2. - API returns daily series; frontend renders a smooth line with filled area.
- Missing dates are handled server-side by carrying forward last known prices per holding.
- Currency selector lets you switch base currency (PLN/USD/EUR/GBP) using cached FX rates.
- Primary: Twelve Data (global coverage, real-time/near real-time where supported). Also used for company names.
- Fallback: Stooq (free daily prices for US/UK/PL/DE equities).
- Provider chain is configured in
backend/src/providers/index.tsand used by caching services.
If you prefer yfinance, swap in a provider that scrapes Yahoo Finance data. See "Provider Tradeoffs" below.
- Twelve Data: strong global coverage and multiple asset classes, but API key required and rate limits on free tiers.
- Stooq: free and keyless, but daily-only; best for US/UK/PL/DE equities.
- yfinance: unofficial scraping library, inconsistent real-time behavior, and subject to throttling/changes.
Prerequisites: Node.js 18+ (for built-in fetch).
- Backend:
cd backendnpm installcp .env.example .envand setTWELVE_DATA_API_KEY- Note:
.envfiles are gitignored to avoid publishing secrets. npm run dev- Optional type check:
npm run typecheck
- Frontend:
cd frontendnpm installcp .env.example .envand adjustVITE_API_URLif needednpm run dev
- Open
http://localhost:5173in the browser.
Optional:
- Set
VITE_API_URLinfrontend/.envif the backend runs on another host/port. - QA smoke test (Playwright):
cd frontendnpm run test:e2e -- --headed
Prerequisites: Docker Desktop or Docker Engine with Compose.
- Export your Twelve Data API key (or create a
.envfile in repo root):export TWELVE_DATA_API_KEY=your_key_here
- Build and start:
docker compose up --build
- Open:
- Frontend:
http://localhost:5173 - Backend health:
http://localhost:4000/api/health
- Frontend:
Use the provided script to import open positions from the OPEN POSITION sheet in account exports.
cd backend
npm install
npx tsx scripts/import-xlsx.ts --dry-run
npx tsx scripts/import-xlsx.tsOptions:
--portfolio=1(default: 1)--dir=.(default: repo root)