diff --git a/README.md b/README.md index 133d9f7..b6c552d 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,32 @@ # BTC Portfolio Tracker -A full-stack Bitcoin portfolio tracker with live EUR pricing, purchase history, and interactive charts. +A full-stack Bitcoin portfolio tracker with live EUR pricing, purchase/sell history, candlestick charts, and price predictions. --- ## Features -- **Live BTC price** — fetched from CoinGecko in EUR -- **Purchase tracking** — log BTC buys with amount (EUR) and price per BTC -- **Portfolio stats** — total invested, current value, profit/loss -- **Interactive charts** — portfolio value over time and 1-year BTC price history -- **Edit & delete** — manage purchases with inline editing +- **Live BTC price** — fetched from CoinGecko in EUR, with cached fallback and stale warning +- **Purchase tracking** — log BTC buys with amount (EUR), price per BTC, and a custom date +- **Sell tracking** — log BTC sells with BTC amount, price per BTC, and a custom date +- **Portfolio stats** — total invested, current value, profit/loss, net BTC held +- **Price prediction** — enter a target BTC price to see projected portfolio value and P&L +- **Interactive charts** — portfolio value over time and BTC candlestick chart (OHLC stored locally) +- **Edit & delete** — inline editing and deletion for both purchases and sells +- **Admin panel** — admin users can create, list, and delete accounts - **JWT authentication** — secure per-user portfolios --- ## Tech Stack -| Layer | Technology | -|----------|-------------------------------------| -| Frontend | React 18, Chart.js, dark theme | -| Backend | FastAPI, SQLAlchemy, SQLite | -| Auth | JWT (HS256, 24h expiry) + bcrypt | -| Pricing | CoinGecko API (EUR) | -| Deploy | Docker + Docker Compose | +| Layer | Technology | +|----------|-----------------------------------------| +| Frontend | React 18, Chart.js, dark theme | +| Backend | FastAPI, SQLAlchemy, SQLite | +| Auth | JWT (HS256, 24h expiry) + bcrypt | +| Pricing | CoinGecko API (EUR) | +| Deploy | Docker + Docker Compose + nginx | --- @@ -65,42 +68,55 @@ btc-portfolio/ ├── backend/ │ └── app/ │ ├── main.py # FastAPI app + CORS -│ ├── models.py # User & Purchase ORM models +│ ├── models.py # User, Purchase, Sell, OHLCCandle ORM models │ ├── auth.py # JWT + bcrypt │ ├── dependencies.py # Auth dependency injection │ ├── routes/ │ │ ├── users.py # POST /register, POST /login │ │ ├── purchases.py # CRUD /purchases +│ │ ├── sells.py # CRUD /sells │ │ ├── stats.py # GET /stats -│ │ └── history.py # GET /history (365-day BTC prices) +│ │ ├── history.py # GET /history (365-day BTC prices) +│ │ ├── candles.py # GET /candles (OHLC data + purchases overlay) +│ │ └── admin.py # GET/POST/DELETE /admin/users │ └── services/ │ └── btc.py # CoinGecko integration └── frontend/ └── src/ ├── App.js # Routing ├── pages/ - │ └── Dashboard.js # Main view + │ └── Dashboard.js # Main view: stats, predictions, charts, tables └── components/ - ├── AddPurchase.js # Purchase form - ├── PurchaseList.js # Purchase table (edit/delete) - ├── PortfolioChart.js # Invested vs current value - └── BTCHistoryChart.js # 1-year BTC price history + ├── AddPurchase.js # Purchase form (amount EUR, price, date) + ├── PurchaseList.js # Purchase table (inline edit/delete) + ├── AddSell.js # Sell form (BTC amount, price, date) + ├── SellList.js # Sell table (inline edit/delete) + ├── PortfolioChart.js # Invested vs current value over time + └── BTCCandlestickChart.js # OHLC candlestick chart with purchase markers ``` --- ## API Endpoints -| Method | Endpoint | Description | Auth | -|--------|-------------------|------------------------------|------| -| POST | `/register` | Create account | No | -| POST | `/login` | Get JWT token | No | -| GET | `/purchases` | List user purchases | Yes | -| POST | `/purchases` | Add a purchase | Yes | -| PUT | `/purchases/{id}` | Edit a purchase | Yes | -| DELETE | `/purchases/{id}` | Delete a purchase | Yes | -| GET | `/stats` | Portfolio stats (P&L) | Yes | -| GET | `/history` | 365-day BTC price history | Yes | +| Method | Endpoint | Description | Auth | +|--------|------------------------|------------------------------------|-------| +| POST | `/register` | Create account | No | +| POST | `/login` | Get JWT token | No | +| GET | `/purchases` | List user purchases | Yes | +| POST | `/purchases` | Add a purchase | Yes | +| PUT | `/purchases/{id}` | Edit a purchase | Yes | +| DELETE | `/purchases/{id}` | Delete a purchase | Yes | +| GET | `/sells` | List user sells | Yes | +| POST | `/sells` | Add a sell | Yes | +| PUT | `/sells/{sell_id}` | Edit a sell | Yes | +| DELETE | `/sells/{sell_id}` | Delete a sell | Yes | +| GET | `/stats` | Portfolio stats (P&L, net BTC) | Yes | +| GET | `/history` | 365-day BTC price history | Yes | +| GET | `/candles` | OHLC candles + purchase overlay | Yes | +| GET | `/admin/users` | List all users (admin only) | Admin | +| POST | `/admin/users` | Create a user (admin only) | Admin | +| DELETE | `/admin/users/{id}` | Delete a user (admin only) | Admin | --- @@ -108,15 +124,19 @@ btc-portfolio/ SQLite, stored at `/app/data/btc_portfolio.db` (persisted via Docker volume). -| Table | Columns | -|-------------|------------------------------------------------------| -| `users` | id, username (unique), password (bcrypt hash) | -| `purchases` | id, amount_eur, price_eur, created_at, user_id (FK) | +| Table | Columns | +|----------------|---------------------------------------------------------------| +| `users` | id, username (unique), password (bcrypt hash), is_admin | +| `purchases` | id, amount_eur, price_eur, created_at, user_id (FK) | +| `sells` | id, btc_amount, price_eur, created_at, user_id (FK) | +| `ohlc_candles` | id, date (unique, YYYY-MM-DD), open, high, low, close | --- ## Notes - The `SECRET_KEY` in `auth.py` is hardcoded — use an environment variable in production. -- CoinGecko requests are unauthenticated; failures return `0.0` gracefully. +- CoinGecko requests are unauthenticated; failures fall back to the last cached price with a UI warning. +- OHLC candle data is fetched from CoinGecko and stored locally to reduce API calls. - CORS is restricted to `localhost:3000` by default. +- The frontend is served via nginx in the Docker production setup. diff --git a/btc-portfolio/backend/app/routes/candles.py b/btc-portfolio/backend/app/routes/candles.py index b286be4..3339f17 100644 --- a/btc-portfolio/backend/app/routes/candles.py +++ b/btc-portfolio/backend/app/routes/candles.py @@ -1,9 +1,12 @@ from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session +from datetime import datetime, timezone from ..database import get_db from .. import models from ..dependencies import get_current_user +from ..services.candles import refresh_latest_candles +from ..services.btc import get_btc_price_eur router = APIRouter() @@ -14,6 +17,9 @@ def get_candles( db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user), ): + # Refresh candles on every request (no-op if DB is already current) + refresh_latest_candles(db) + query = db.query(models.OHLCCandle).order_by(models.OHLCCandle.date.asc()) if days != "all": @@ -38,6 +44,26 @@ def get_candles( for c in candles_db ] + # Patch today's candle with the live price so it tracks intraday movement + today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") + live_price, _ = get_btc_price_eur() + if live_price and candles: + last = candles[-1] + if last["date"] == today: + last["close"] = round(live_price, 2) + last["high"] = round(max(last["high"], live_price), 2) + last["low"] = round(min(last["low"], live_price), 2) + elif last["date"] < today: + # No candle for today yet — create a synthetic one from live price + prev_close = last["close"] + candles.append({ + "date": today, + "open": prev_close, + "high": round(max(prev_close, live_price), 2), + "low": round(min(prev_close, live_price), 2), + "close": round(live_price, 2), + }) + purchases_db = db.query(models.Purchase).filter( models.Purchase.user_id == current_user.id ).all() diff --git a/btc-portfolio/frontend/src/components/BTCCandlestickChart.js b/btc-portfolio/frontend/src/components/BTCCandlestickChart.js index 94e5946..5282862 100644 --- a/btc-portfolio/frontend/src/components/BTCCandlestickChart.js +++ b/btc-portfolio/frontend/src/components/BTCCandlestickChart.js @@ -39,7 +39,7 @@ const btnStyle = { marginLeft: '0.5rem', }; -export default function BTCCandlestickChart({ candles, purchases, stats, fullscreen, onToggleFullscreen }) { +export default function BTCCandlestickChart({ candles, purchases, stats, fullscreen, onToggleFullscreen, livePrice }) { const containerRef = useRef(null); const chartRef = useRef(null); const candleSeriesRef = useRef(null); @@ -167,7 +167,14 @@ export default function BTCCandlestickChart({ candles, purchases, stats, fullscr return (