Trading dashboard FastAPI + Next.js — architektura mojego setupu
Mam wewnętrzny dashboard tradingowy: FastAPI backend + Next.js frontend, deployed jako Docker compose, dostępny przez Cloudflare tunnel z Zero Trust auth. Pokazuję strukturę i kluczowe decyzje.

Mój trading dashboard to wewnętrzna aplikacja webowa do monitorowania pozycji, alertów i analytics tradingowych. Stack: Python FastAPI backend + Next.js frontend + Postgres. Nazywam go "Kain" (zespół chce mieć imię). Pokazuję jak jest zbudowany.
Wysokopoziomowo
[Cloudflare tunnel + Zero Trust auth]
↓
[forge:3002 — Next.js frontend]
↓ /api/*
[forge:8000 — FastAPI backend]
↓
[Postgres (Proxmox VM)]
↓ async
[Trading exchanges API + scrapers]Frontend i backend lecą na forge PC (RTX 3070 Ti, bo niektóre analytics używają GPU dla ML inference). Postgres na hypervisorze. Kanał: HTTPS via tunnel, JWT auth.
Backend: FastAPI
Dlaczego Python: ekosystem ML/data jest niespotykanie bogaty. pandas, numpy, scikit-learn, native PyTorch. FastAPI dodaje async + auto-generowane OpenAPI.
# main.py struktura
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI(title="Kain Trading API")
class Position(BaseModel):
symbol: str
side: str # long/short
entry_price: float
current_price: float
pnl: float
@app.get("/positions", response_model=list[Position])
async def get_positions(user_id: int = Depends(verify_jwt)):
return await db.fetch_positions(user_id)Pydantic models = automatyczna validacja + auto-generowany TS dla frontu. Kontrakt JSON jest typed end-to-end.
Frontend: Next.js
Next.js 16 z App Router, server components dla data fetching, client components dla interactivity. Używam React Query do cachingu API responses.
// app/positions/page.tsx — server component
import { fetchPositions } from "@/lib/api";
export default async function PositionsPage() {
const positions = await fetchPositions();
return <PositionsTable initialData={positions} />;
}
// PositionsTable — client component z React Query
"use client";
import { useQuery } from "@tanstack/react-query";
export function PositionsTable({ initialData }) {
const { data } = useQuery({
queryKey: ["positions"],
queryFn: fetchPositions,
initialData,
refetchInterval: 5000, // 5s polling
});
return <Table data={data} />;
}Server component robi initial fetch (SSR), client component poll'uje update'y. SEO i interactivity razem.
Auth: JWT + Cloudflare Zero Trust
Dwie warstwy autoryzacji:
1. Cloudflare Zero Trust, pierwszy gate. Tylko zalogowany via Google na moim mailu może w ogóle dotrzeć do app.
2. JWT po Zero Trust, drugi gate. App ma multiple users (ja, partner od tradingu). JWT identyfikuje który user.
async def verify_jwt(authorization: str = Header(...)):
token = authorization.replace("Bearer ", "")
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return payload["user_id"]
except jwt.InvalidTokenError:
raise HTTPException(401, "Invalid token")Cloudflare daje pierwszą warstwę gratis. JWT to standardowy boilerplate.
Database: Postgres
Trzy główne tabele:
positions, open positions (~1000 rows zwykle)trades, executed trades (~500k rows historic)alerts, triggered alerts (~10k rows)
Indexy na user_id, symbol, timestamp. Query'y zwykle pod 50ms.
Kluczowy detail: Postgres nie jest na forge, tylko na hypervisorze (Proxmox VM). Forge może paść (GPU OOM przy ML inference) bez utraty danych.
# docker-compose.yml na forge
services:
backend:
build: ./backend
environment:
DATABASE_URL: postgres://kain:[email protected]:5432/kain
ports:
- "127.0.0.1:8000:8000"Real-time updates
Niektóre pozycje wymagają sub-sekundowych update'ów (gdy trade jest hot). WebSocket od backend do frontu:
# Backend FastAPI WebSocket
@app.websocket("/ws/positions")
async def positions_ws(websocket: WebSocket):
await websocket.accept()
async for update in pg_listen("positions_updates"):
await websocket.send_json(update)// Frontend
const ws = new WebSocket("wss://kain.kamilkaletka.dev/ws/positions");
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
queryClient.setQueryData(["positions"], (old) =>
old.map(p => p.id === update.id ? update : p)
);
};Postgres LISTEN/NOTIFY jako message bus. Trade execution wstawia rekord, trigger wysyła notify, backend forwarduje przez WebSocket.
Pułapki które mnie kosztowały
1. Python rebuild vs restart. Edytuję .py, robię docker compose restart backend, kod nadal stary. Trzeba up -d --build. Zapomnienie tego kosztowało mi godzinę debugowania "dlaczego mój fix nie działa".
2. SOCKS5 + internal DNS. Backend trafia do hostname proxmox-pg.local. SOCKS5 proxy na hoście próbuje to resolve'ować zewnętrznie i fail. Trzeba dodać proxmox-pg.local do NO_PROXY.
3. WebSocket przez Cloudflare tunnel. Działa, ale latency +30ms względem direct. Dla większości UI ok, dla trade execution alerts, w wąskim margin.
4. JSON serialization Decimal. Postgres zwraca Decimal, JSON nie obsługuje natywnie. Custom encoder w FastAPI:
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)Inaczej każde pobranie pozycji = error.
Stats
Dashboard żyje od ~14 miesięcy. Numbers:
- ~500k zarejestrowanych trade'ów
- ~12k uniqueych pozycji historic
- Średni response time API: 35ms
- Uptime: 99.7% (downtime głównie przy update'ach Postgres)
Nie jest to system handlowy z 10k req/s. To wewnętrzny dashboard, gdzie ja i partner siedzimy z otwartą zakładką. Architektura odpowiada skali.
FastAPI + Next.js to świetna kombinacja dla wewnętrznych narzędzi. Type safety end-to-end, ML/data eco po stronie Python, modern UX po stronie React. Cloudflare tunnel i Zero Trust dają enterprise-grade auth bez infra. Setup zajmuje weekend, działa latami.