Live market data and ML price forecasts, with the API keys where they belong — on the server.
StockLab is a stock and crypto dashboard that streams live quotes, charts historical prices, and forecasts short-term price movement with a real (if modest) ML model. It exists mostly as a study in doing the boring parts properly: no keys in the browser, upstream calls cached against rate limits, and synthetic data that is always labelled as synthetic.
⚠️ Forecasts are educational estimates produced by a simple model. Not financial advice.
frontend/ React 18 + TypeScript + Vite + Tailwind + Recharts → deploys to Vercel
backend/ FastAPI (Python) + Postgres → deploys to Render
The original version called Alpha Vantage / Finnhub / CoinGecko straight from the browser, which published the API keys to anyone who opened devtools and burned the free tier's rate limit in a few refreshes. The backend fixes both: it holds the keys, proxies the requests, and caches the responses. The frontend has no upstream credentials at all, and no client-side fallback data — if a provider fails, the backend returns clearly-labelled synthetic data rather than letting the UI invent prices that look live.
| Endpoint | Does |
|---|---|
GET /api/stocks |
Quotes for a default watchlist |
GET /api/quote?symbol= |
Single quote |
GET /api/search?q= |
Symbol search |
GET /api/crypto |
Crypto prices (CoinGecko) |
GET /api/history?symbol=&days= |
Daily OHLC history, 1–365 days |
GET /api/predict?symbol=&horizon= |
Price forecast, 1–30 days, with confidence bands |
GET /api/health |
Health check (used by Render) |
Interactive API docs are at /docs on any running backend.
The database schema in backend/app/models.py defines tables for
paper trading (Holding, Trade), lesson progress (LessonProgress), and
forecast accuracy evaluation (PriceBar, ForecastRun, ForecastPoint). The paper-trading
and lesson tables are created at startup but no endpoints read or write them yet — the
schema is groundwork, not a working feature. The evaluation tables are written by the
backfill/backtest scripts and read by /api/predict — see
Seeding the database. Likewise the AI tutor and indicator
explainers are planned, not implemented; ANTHROPIC_API_KEY is currently unused.
The forecast model is a scikit-learn baseline. Prophet and LSTM are deliberately not installed — they carry heavy build dependencies, and the baseline is what's honest to ship.
Backend:
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # works without keys — you'll get labelled demo data
uvicorn app.main:app --reload --port 8000Frontend (in a second terminal):
cd frontend
npm install
npm run dev # Vite proxies /api → http://localhost:8000Open the printed Vite URL. It runs without any API keys; every value sourced from fallback data is badged as demo in the UI.
/api/predict reports measured accuracy — looked up from scored backtests, never derived
from the fit. On an empty database there is nothing to look up, so accuracy_for() returns
None and the endpoint reports no accuracy at all. That's deliberate: inventing a confidence
number is the failure this replaced. Everything else works fine unseeded.
To populate it, run both scripts from backend/ with the venv active:
# 1. Real daily bars via yfinance → the PriceBar corpus. Idempotent: re-running
# only inserts dates not already stored.
python -m scripts.backfill --period 10y
# 2. Walk-forward backtest → scored ForecastPoints. Wipes prior backtest runs
# and recomputes, so re-running is safe. Prints an accuracy table when done.
python -m scripts.backtest --horizon 30 --stride 5Both default to the eight symbols in POPULAR_STOCKS (AAPL, GOOGL, MSFT, TSLA, AMZN, NVDA,
META, NFLX); pass --symbols AAPL,MSFT to narrow. The backtest fits models at many origins
across every symbol, so it takes a while.
To seed a deployed database, point DATABASE_URL at it first. Render's free tier has no
shell, so run this from your machine using the External Database URL (the Internal one
only resolves inside Render's network):
DATABASE_URL="postgres://...render.com/stocklab" python -m scripts.backfill --period 10y
DATABASE_URL="postgres://...render.com/stocklab" python -m scripts.backtestThe postgres:// scheme Render hands out is normalised for SQLAlchemy automatically.
| Variable | Purpose |
|---|---|
ALPHA_VANTAGE_API_KEY |
Stock quotes & daily history. Omit for demo data. |
FINNHUB_API_KEY |
Backup stock quotes |
ANTHROPIC_API_KEY |
Reserved for the planned AI tutor; unused today |
ANTHROPIC_MODEL |
Optional; defaults to claude-opus-4-8 |
DATABASE_URL |
Defaults to sqlite:///./stocklab.db; Postgres in production |
CORS_ORIGINS |
Comma-separated allowed origins |
CORS_ORIGIN_REGEX |
Optional regex, for Vercel preview URLs |
The frontend takes one variable, VITE_API_BASE — the backend's public origin. Leave it
unset in dev (Vite's proxy handles it). It must never hold an upstream provider key.
The two halves deploy separately: Vercel serves the static frontend, Render runs the API and Postgres. GitHub Pages can't host this — it serves static files only, and there'd be no API or database behind it.
render.yaml is a blueprint that provisions both the web service and a free
Postgres instance, and wires DATABASE_URL between them automatically.
- Render Dashboard → New → Blueprint → select this repo.
- When prompted, fill in
ALPHA_VANTAGE_API_KEY,FINNHUB_API_KEY,ANTHROPIC_API_KEY(blank is fine), andCORS_ORIGINS. These are markedsync: falseso they're entered in the dashboard, never committed. - Deploy, then note the service URL:
https://stocklab-api.onrender.com.
Tables are created on startup via init_db(), so there's no migration step. Postgres is used
rather than SQLite because Render's filesystem is ephemeral — a SQLite file would be silently
erased on every deploy.
On Render's free tier the API sleeps after ~15 minutes idle, so the first request after a quiet spell takes ~30s to wake it. The free Postgres instance expires after 90 days.
- Vercel → Add New → Project → import this repo.
- Set Root Directory to
frontend. (frontend/vercel.jsonsupplies the build settings and the SPA rewrite.) - Add an environment variable:
VITE_API_BASE= your Render URL from step 1. - Deploy.
VITE_API_BASEis baked in at build time, not read at runtime. If it's missing the build still succeeds, but the app calls its own Vercel origin and every request 404s — so set it before deploying, and redeploy (not just restart) after changing it.
Back on Render, set CORS_ORIGINS to your Vercel production URL (e.g.
https://stocklab.vercel.app). Preview deploys are already covered by the
CORS_ORIGIN_REGEX default in the blueprint. Without this the browser blocks every API call
while the backend looks perfectly healthy — it's the failure that wastes the most time here.
MIT — use it for learning or your portfolio.
Built by Ashok Gaire