Production-grade multi-task ML platform for financial markets: direction baseline + volatility forecasting + meta-labeling signal quality + cross-sectional stock ranking. Full-stack (FastAPI + React), containerized, observable, deployable to Kubernetes.
Live: Frontend quant-ai-ui.vercel.app · Backend quant-ai-qzrg.onrender.com · API docs
⚠️ Research & education platform — not investment advice. Direction probabilities are deliberately treated as a baseline; the interesting ML is the volatility forecasting and meta-labeling stack layered on top.
First visit? The live demo runs on free tiers — the first API call can take ~30s while the backend wakes (the UI tells you). A 4-step tour walks you through Screener → Dashboard → Portfolio → Leaderboard on first load.
Dashboard — one screen per ticker: live price + candles, AI direction prediction, volatility gauge, SHAP "why", AI-written summary.

Portfolio — your whole watchlist scored in one call: bullish/neutral/bearish split + per-ticker probability.

Leaderboard — honest model scoreboard: active models per task with primary metric + live hit-rate from the prediction log.

👀 Visitor — 2 minutes, zero setup
- Open the live demo — a 4-step tour points the way
- Screener — market overview, pick a ticker
- Dashboard — one screen: price chart, 5-day direction probability, volatility gauge, SHAP "why", AI-written summary
- Portfolio — your whole watchlist scored bullish / neutral / bearish in one call
- Paper Trading — place a virtual order; an optional meta-label gate blocks low-quality signals and sizes the position (half-Kelly)
- Leaderboard — come back later: every prediction was logged, and gets scored against reality once its horizon passes
🔬 Operator — the ML loop behind it
- Seed data —
python -m scripts.backfill_prices(10 tickers × 2 years of daily OHLCV → Postgres) - Train — pick a target (direction / volatility / meta-label), one of 6 model types, feature groups; Optuna multi-objective search optional
- Promote — the winner becomes the serving model (registry + artifact store)
- Serve & log — every
/predict*call writes a row toprediction_log - Score honestly — accuracy resolves automatically once each prediction's horizon passes; ablation quantifies what each feature group really contributes
- Iterate — the Leaderboard shows which models survive contact with live data
- Predicts stock direction with 6 model types (logistic, random forest, XGBoost, LightGBM, CatBoost, ensemble voting/stacking) — kept as a baseline.
- Forecasts realized volatility with regression head sharing the same 6-model architecture (MAE / RMSE / MAPE / QLIKE / R²); dynamic-vol-aware barriers feed the meta-labeling stage.
- Scores signal reliability (López de Prado Ch.3) — primary signal (4 rule strategies OR the direction model) → triple-barrier with vol-scaled TP/SL → meta-classifier predicts "is this signal trade-worthy?". Purged K-Fold CV with embargo (Ch.7).
- Ranks the cross-section — recasts "predict up/down" as "rank relative strength": each day labels the S&P 500's top-30% forward return as the strong group, z-scores every factor within that day's cross-section (leakage-free, scaler-less — same transform at train and serve), and trains a learning-to-rank model on a Phase-B-selected factor set (28 candidates → IC + correlation de-dup → 12). Honest result on 527 names: Rank IC ≈ 0.05 / precision@30% ≈ 0.37 — a real edge where single-stock direction has none (AUC 0.53). Survivorship-corrected point-in-time universe (reconstructed from S&P 500 change logs).
GET /predict/rankingserves today's Top-N strength board. - Backtests the selection —
GET /backtest/rankingruns an out-of-sample Top-N portfolio backtest: every 5 days (= the label horizon, so returns are non-overlapping) score the cross-section, equal-weight the top decile, hold to the next rebalance, charge turnover-based transaction costs, and chain the realized returns into an equity curve vs an equal-weight-universe benchmark (isolates selection skill). On the model's held-out test split the top-decile selection beat the benchmark net of costs — but the window is short (~16 rebalances), so the/rankingpage's equity card ships with a "directional read, not a robust Sharpe" disclaimer. (OOS boundary = the persistedtest_start_date, i.e. after fit and tuning — leak-free even for a hyperparameter-searched model.) - Gates Paper Trading orders — opt-in meta-score threshold + half-Kelly sizing before any order is placed.
- Tracks live accuracy — every
/predict*and/api/signal-scorecall writes a row toprediction_log(Supabase).GET /models/{id}/accuracylazily resolves rows past their horizon by fetching market closes, returns 30-day hit-rate / realized R / vol MAE. - Quantifies feature contribution —
POST /api/ablation/runtrains 6 models (3 targets × 2 feature sets) with default params and returns a delta matrix. Frontend/ablationrenders the heatmap; honest reporting when sentiment doesn't help — absent metrics render asn/a, never a fake0.0, and mock-provider sentiment columns are labeled ℹ️ in the UI. - Optimizes hyperparameters multi-objectively (NSGA-II via Optuna) and strategy parameters (TPE).
- Backtests both ML models and rule-based strategies with transaction costs and position sizing.
- Explains predictions via SHAP feature importance and vector search across historical cases.
- Paper-trades live with WebSocket price feed, order book, portfolio tracking.
- Observable: Prometheus metrics, Grafana dashboard, Kafka event stream for per-ticker real-time stats.
- Handles time-series properly — no look-ahead bias, deterministic train/val/test splits by date, Purged K-Fold for event-indexed meta-labels.
| Page | Route | What you can do |
|---|---|---|
| Screener | /screener |
10 hot tickers with real Supabase prices, sort by change% or volume, click-through to Dashboard |
| Strength Ranking | /ranking |
Cross-sectional Top-N strong stocks scored by the xs_strong model — rank, ticker, strength bar, score, percentile; "relative rank, not a price target" disclaimer; out-of-sample backtest card (equity curve vs equal-weight universe, net of costs, with an honest short-window caveat) |
| Dashboard | /dashboard?ticker=AAPL |
Lightweight Charts K-line + volume, 5-day prediction, Volatility Gauge + 7d signal-quality sparkline, SHAP explain panel, model selector dialog |
| Portfolio | /portfolio |
Whole watchlist scored in one call via the portfolio agent — bullish/neutral/bearish distribution bar, per-ticker P(up), add/remove tickers, one-click into Dashboard |
| Signal Console | /signal-console |
Watchlist × 4-strategy matrix of meta-models with AUC + E[R] cells; click a cell to preview latest reliability score, expected R, recommended action and half-Kelly sizing; one-click "Train meta" CTA fires POST /api/meta-label/train |
| Leaderboard | /leaderboard |
3 tabs (direction / volatility / meta-label), each a sortable table of active models with primary metric + live 30-day hit rate from prediction_log |
| Ablation | /ablation |
Run 3 targets × 2 feature sets (ta_basic vs ta_basic + sentiment) with default params, render delta matrix heatmap, summary identifies which target sentiment helps most |
| Training | /training |
Train any of 6 model types, Auto-Optimize (Optuna), 3-tab layout (Train / Runs / Models promote) |
| Strategy | /strategy |
4 strategies (MA cross, RSI, Bollinger, Sentiment) with schema-driven params, signal viz, backtest, Optimize params, Meta-Label Coverage badge showing per-strategy meta-model count + best AUC |
| Trading | /trading |
Paper-trade with market/limit orders, live WebSocket prices (Zustand store), portfolio P&L, order book; opt-in meta-label gate (model dropdown + threshold slider + score preview) blocks low-quality signals and sizes orders by half-Kelly |
| Explain | /explain |
SHAP top features + similar historical cases via vector search, graceful fallback when optional deps missing |
Frontend stack: React 19 + Vite + Tailwind v3 + Tremor (charts/KPI) + shadcn/ui (Radix primitives) + Lightweight Charts v4 + TanStack Query v5 + Zustand + react-hook-form + zod + Geist fonts + Vitest. Page-level code splitting via React.lazy() keeps first-screen JS under 340KB.
| Piece | Status |
|---|---|
| Market prices | Real — Supabase price store, daily candles |
| Live accuracy | Real — every /predict* call writes to prediction_log; GET /models/{id}/accuracy resolves outcomes once the horizon passes |
| Training & CV | Real — chronological splits (no look-ahead), Purged K-Fold + embargo for event-indexed meta-labels |
| Cross-sectional ranking | Real — Rank IC ~0.05 / precision@30% ~0.37 on 527 S&P names, survivorship-corrected point-in-time universe; an honest mid-tier edge, reported not inflated (sells the tool, not the signal) |
| News sentiment | Mock provider in this build — labeled ℹ️ in the Ablation UI; its deltas measure pipeline wiring, not news alpha |
| Paper trading | Virtual money only |
| Hosting | Free tiers (Render + Vercel + Supabase) — first request after idle takes ~30s to wake; the UI says so instead of looking broken |
| Category | Representative endpoints |
|---|---|
| Health & Observability | /health, /health/ready, /metrics (Prometheus) |
| Market Data | /data/market, /data/sentiment, /data/news |
| ML Training | /train, /runs, /runs/{id}, /runs/{id}/reproduce |
| Models | /models?label_type=&ticker=, /models/{id}, /models/{id}/promote, /models/types |
| Prediction | /predict (GET legacy + POST), POST /predict/volatility, GET /predict/ranking?top_n= (cross-sectional Top-N strength board) |
| Meta-Labeling | POST /api/meta-label/train (event-indexed labels via triple-barrier + Purged K-Fold), POST /api/signal-score (3-mode inference: explicit signal / auto-trigger / fallback), GET /api/meta-label/coverage?strategy= (per-strategy coverage + best AUC) |
| Live Accuracy + Ablation | GET /models/{id}/accuracy?window_days=30 (lazy-resolves prediction_log rows past horizon, returns hit-rate / realized R / vol MAE), POST /api/ablation/run (synchronous 3-target × N-feature-set trainer with delta matrix + summary) |
| Backtest | /backtest, /backtest/report |
| Features | /features/groups, /features/groups/{name} |
| Strategies | /api/strategies, /api/strategies/{name}/signals, /api/strategies/{name}/backtest |
| Paper Trading | /api/trading/orders (now accepts optional meta_model_id + score_threshold for opt-in meta gate), /api/trading/portfolio, /api/trading/trades, /api/trading/ws/prices |
| Hyperparameter Optimization | /api/optimize/model, /api/optimize/strategy, /api/optimize/runs |
| Explainability | /explain, /search |
| News | /news/{ticker}, /news/{ticker}/sentiment-summary, /news/{ticker}/similar-days |
| RAG | /rag/answer, /rag/search, /rag/index |
| Agents | /agents/technical (returns 8 model-metadata fields), /agents/summary |
| Serverless Functions | /api/functions, /api/functions/{name}/invoke |
Full OpenAPI docs: https://quant-ai-qzrg.onrender.com/docs
| Layer | Technology |
|---|---|
| Backend API | FastAPI (Python 3.11) |
| ML | scikit-learn, XGBoost, LightGBM, CatBoost, Optuna, SHAP |
| Database | PostgreSQL (Supabase) |
| Cache | Redis (fallback to in-memory) |
| Message broker | Kafka (aiokafka) / Redis / in-memory (pluggable) |
| Job queue | SQS / Redis / in-memory (pluggable) |
| Artifact storage | Local / Supabase / S3 (pluggable) |
| Observability | Prometheus metrics (/metrics) + Grafana dashboard |
| Frontend UI kit | Tremor + shadcn/ui (built on Radix) + Lightweight Charts v4 |
| Frontend | React 19, Vite, Tailwind CSS v3, React Router v7 |
| Frontend state | TanStack Query v5 (server state) + Zustand (WebSocket live store) |
| Frontend forms | react-hook-form + zod |
| Frontend fonts | Geist + Geist Mono (@font-face woff2) |
| Frontend tests | Vitest + @testing-library/react (90 tests: pages, features, api hooks) |
| Container | Docker (multi-stage, separate api + consumer images) |
| Orchestration | Kubernetes (manifests in k8s/) + Horizontal Pod Autoscaler |
| CI | GitHub Actions (unit + contract + frontend test + docker build + post-deploy health check + keep-alive cron) |
| Deploy | Render (backend) + Vercel (frontend) + Supabase (prices + news + RLS) |
┌─────────────────────────────────────────────────────────────────────────┐
│ React 19 + Tremor + shadcn/ui + Lightweight Charts (Vercel CDN) │
│ Screener · Dashboard · Training · Strategy · Trading · Explain │
│ lazy-loaded pages · TanStack Query cache · Zustand WS live store │
└───────────────────────────────┬─────────────────────────────────────────┘
│ HTTPS / WebSocket
┌───────────────────────────────▼─────────────────────────────────────────┐
│ FastAPI Backend (Render) │
│ REST + WebSocket + /metrics │ rate-limit, CORS, request context │
└───────────────────────────────┬─────────────────────────────────────────┘
┌────────────────────┼─────────────────────┬────────────────┐
▼ ▼ ▼ ▼
┌────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌─────────┐
│ Services │ │ ModelFactory │ │ Strategy Registry│ │ Agents │
│ · Training │ │ 6 model types │ │ 4 strategies │ │ (LLM) │
│ · Predict │ │ · logistic │ │ · MA cross │ └─────────┘
│ · Backtest │ │ · random forest │ │ · RSI │
│ · Explain │ │ · xgboost │ │ · Bollinger │
│ · Optimization │ │ · lightgbm │ │ · Sentiment │
│ · News │ │ · catboost │ └──────────────────┘
│ · Search │ │ · ensemble │
└────────┬───────┘ └──────────────────┘
│
┌───────┼──────────┬──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌──────┐┌───────┐┌──────────┐┌──────────────┐┌──────────────┐
│Redis ││ Kafka ││Postgres ││ ArtifactStore││ Functions │
│cache ││events ││(Supabase)││(local/S3) ││ (serverless) │
└──────┘└───────┘└──────────┘└──────────────┘└──────────────┘
Distributed layer (k8s/ for local Minikube demo):
· quant-ai-api (2 replicas + HPA 2-5)
· quant-ai-consumer (subscribes prediction_events, exposes /stats/{ticker})
· prometheus + grafana (pre-provisioned 6-panel dashboard)
git clone https://github.com/jigangz/quant-ai.git
cd quant-ai
cp .env.example .env
# Edit .env with DATABASE_URL (Supabase or local Postgres)
docker-compose up --build
# API → http://localhost:8000
# Consumer → http://localhost:8001
# Prometheus → http://localhost:9090
# Grafana → http://localhost:3000 (admin/admin)python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
# Configure .env at minimum: DATABASE_URL
uvicorn app.main:app --reloadcd quant-ai-ui
npm install
VITE_API_BASE=http://localhost:8000 npm run dev
# Opens at http://localhost:5173minikube start --cpus=4 --memory=6g
minikube addons enable metrics-server
eval $(minikube docker-env)
docker build -t quant-ai:latest --target production .
docker build -t quant-ai-consumer:latest -f Dockerfile.consumer .
cp k8s/secret.example.yaml k8s/secret.yaml # fill in values
kubectl create configmap grafana-dashboards -n quant-ai \
--from-file=observability/dashboards/quant-ai.json \
--dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f k8s/
minikube service -n quant-ai api # open API
minikube service -n quant-ai grafana # open dashboardFull runbook: k8s/README.md
Time-series needs special handling — random shuffling leaks future data into training.
# WRONG — future leaks into train
X_train, X_test = train_test_split(X, shuffle=True)
# RIGHT — chronological split
# 2020-01 → 2023-06 : train (70%)
# 2023-07 → 2023-09 : validation (15%)
# 2023-10 → 2024-01 : test (15%)The DatasetBuilder enforces this via SplitConfig with train_ratio/val_ratio. Stacking ensembles use KFold(shuffle=False) to preserve ordering in OOF predictions.
The frontend is designed to run on real market data — one idempotent script seeds it:
| Component | Detail |
|---|---|
| Source | yfinance (free, no API key needed) |
| Loader | scripts/backfill_prices.py — idempotent (ON CONFLICT DO NOTHING) |
| Target | Supabase prices table (10 tickers × 2 years of daily candles) |
| Tickers | AAPL, MSFT, GOOGL, AMZN, NVDA, TSLA, META, JPM, V, WMT |
| RLS | service_role full access + public read via anon key |
| Schema | scripts/create_prices_table.sql (declarative, programmatic creation via SQLAlchemy) |
Re-run any time without duplicates:
python -m scripts.backfill_pricesFrontend first-screen load went from ~4 seconds (cold Render + 710KB JS + full 500KB payload) down to <1.5s (warm Render + 335KB JS + 8KB payload):
| Fix | Technique | Impact |
|---|---|---|
| Render cold-start | .github/workflows/keepalive.yml cron */10 * * * * pings /health |
no more 30s cold-start on free tier |
| First-screen JS | React.lazy() + <Suspense> per page, 6 route-level chunks |
710KB → 335KB (-53%) |
| Screener payload | useMarket(ticker, lookback=5) — only need last 2 closes for %change |
500KB → 8KB (-98%) per screener load |
See quant-ai-ui/src/api/queries.js (normalizeMarket + useScreenerTickers) and quant-ai-ui/src/app/router.jsx (lazy routes).
# Backend — 472 tests (unit + integration + contract)
pytest tests/
# Frontend — 90 tests
cd quant-ai-ui && npx vitest run
# Ruff lint
ruff check app/ --ignore F401,F841,E501,F541,E402
# Frontend build check
cd quant-ai-ui && npm run buildCI runs all of the above plus Docker build + post-deploy health check on every push to main.
Everything below actually runs — not just aspirational — with manifests, metrics, and a Grafana dashboard:
| Component | Role | Lives in |
|---|---|---|
| K8s Deployments | API (2+ replicas) and consumer as separate pods | k8s/deployment-*.yaml |
| Horizontal Pod Autoscaler | Auto-scale api 2-5 pods on CPU > 70% | k8s/hpa-api.yaml |
| Liveness / readiness probes | Health-based pod restarts + traffic gating | k8s/deployment-api.yaml |
| Kafka prediction event stream | /predict fires events → consumer aggregates per-ticker stats |
app/services/prediction_event_publisher.py + app/workers/events_consumer.py |
| Prometheus metrics | Auto HTTP metrics + 3 custom ML metrics (PREDICT_TOTAL, PREDICT_CONFIDENCE, MODEL_INFERENCE_SECONDS) |
app/core/metrics.py |
| Grafana dashboard | 6 panels (RPS, p95 latency, predictions/min, confidence heatmap, inference time, pod count) | observability/dashboards/quant-ai.json |
CAP analysis and production scale-up plan in docs/architecture/distributed.md.
Key environment variables (see .env.example):
| Variable | Default | Description |
|---|---|---|
ENV |
dev |
Environment (dev/test/prod) |
DATABASE_URL |
— | PostgreSQL connection string |
REDIS_URL |
— | Redis URL (empty → in-memory) |
CACHE_BACKEND |
memory |
memory or redis |
BROKER_BACKEND |
redis |
kafka, redis, or memory |
QUEUE_BACKEND |
redis |
sqs, redis, or memory |
STORAGE_BACKEND |
local |
local, supabase, or s3 |
KAFKA_BOOTSTRAP_SERVERS |
localhost:9092 |
Kafka brokers |
DEFAULT_MODEL_TYPE |
logistic |
Default ML model |
quant-ai/
├── app/ # FastAPI backend
│ ├── api/ # REST route handlers
│ ├── services/ # Business logic
│ ├── ml/ # Models, features, dataset
│ │ └── hyperparam/ # Optuna multi-objective + strategy optimizers
│ ├── strategies/ # Trading strategies (MA, RSI, Bollinger, Sentiment)
│ ├── workers/ # Kafka events consumer
│ ├── infra/ # Broker, queue, cache abstractions
│ ├── db/ # Repos (prices, news, models, optimization runs)
│ └── core/ # Settings, logging, metrics
├── quant-ai-ui/ # React frontend
├── k8s/ # Kubernetes manifests (Minikube-ready)
├── observability/ # Prometheus + Grafana configs + dashboard
├── docs/ # Architecture + specs + implementation plans
├── scripts/ # Helper scripts
├── tests/ # 472 backend tests (unit + integration + contract)
├── docker-compose.yml # Full local stack
├── Dockerfile # Multi-stage API image
├── Dockerfile.consumer # Events consumer image
└── requirements.txt
MIT