A full‑stack app to track, analyze, and visualize investment portfolios. It supports order entry (buy/sell), FIFO-based realized/unrealized gains, live pricing via yfinance, and a responsive dashboard.
- End‑to‑end product: Flask API + React UI with charts, tables, and a modern theme (MUI + Nivo + Chart.js).
- Real market data: yfinance integration with validation and ticker bootstrapping from Russell constituents.
- Correct PnL: FIFO lot‑matching for realized gains; open‑lot reconstruction for unrealized gains; rolling average cost for positions.
- Clean domain model: Users, Holdings, Transactions, Stocks, Snapshots; SQLite by default, switchable via
DATABASE_URL. - Safe trade flow: balance checks on buys, position checks on sells, and server‑side validation.
- Backend: Python, Flask, SQLAlchemy, Flask‑CORS, yfinance
- Frontend: React 18, React Router, Redux Toolkit, MUI, Nivo, Chart.js
- Data: SQLite by default; Russell constituents CSV to seed stocks; live prices from yfinance
backend/exposes a REST API with blueprints:/user,/transaction,/holding,/data.gui/is a React SPA that consumes the API and renders dashboard views (pie/line charts, tables, sidebar/topbar).app/models: SQLAlchemy ORM entities (User, Holding, Transaction, Stock, Snapshot).app/services: business logic (orders, holdings, users, pricing, and FIFO calculations).app/ressources/russell.csv: initial stock universe; used to populate thestocktable on demand.
Package diagram
Deployment diagram
This project implements FIFO for both realized and unrealized gains. In plain terms:
- Buy creates a lot: quantity at a given buy price, appended to a queue per ticker.
- Sell consumes the oldest lots first (FIFO). Gains are computed per matched quantity.
- Realized gains: when you sell, each sell unit is matched to the oldest unconsumed buy lot; gain = (sell_price − buy_price) × matched_qty. Totals are summed across sales. Implementation: see
UserService.get_realized_gains(per‑tickerdeque, pop/adjust as you match). - Unrealized gains: rebuild remaining open lots via all buys and sells in chronological order, then value open lots at current market price and subtract their historical cost. Implementation: see
UserService.get_unrealized_gains(per‑ticker FIFO queues of remaining lots, valued withget_holdings_current_prices). - Average cost on the holding: maintained on net‑long positions using weighted average of buy transactions only (sells don’t change
avg_price). SeeHoldingService.calculate_avg_price.
Edge cases handled:
- Oversell prevention: server rejects a sell if holding quantity is insufficient.
- Insufficient cash prevention: server rejects a buy if cash balance is inadequate.
- Ticker validation: server queries yfinance; unknown/invalid tickers are rejected.
- Python 3.10+
- Node.js 16+ and npm
- Create and activate a virtual environment
cd backend
python3 -m venv venv
source venv/bin/activate- Install dependencies
pip install -r requirements.txt- Configure env (optional)
- Create
.envinbackend/to override defaults:DATABASE_URL(e.g.,sqlite:///app.dborpostgresql://…)- You may also add
SECRET_KEYif you enable it inapp/config.py.
- Run the API
python run.pyThis starts Flask on port 5000 with CORS enabled for the SPA.
- Seed stocks (optional but recommended)
curl -s http://localhost:5000/data/ | jq '.'The first call loads backend/app/ressources/russell.csv into the stock table.
- Install and start
cd gui
npm install
npm startThis runs the SPA on port 3000. The UI includes a dashboard, charts (line/pie), and order/portfolio views.
- Average cost (
avg_price) updates only on net‑buy; sells don’t change it (common brokerage convention). - SQLite is default; for production use Postgres/MySQL by setting
DATABASE_URL. - yfinance returns latest close; intraday or delayed data nuances may apply.
MIT


