Choosing Your Dashboard Stack
The dashboard is what you interact with every day. It needs to be fast, responsive, and information-dense — showing the maximum amount of data with the minimum cognitive load.
| Framework | Language | Real-Time | Customization | Setup Time | Best For |
| Streamlit | Python | Polling (slow) | Medium | 30 min | Quick prototypes, data exploration |
| Dash (Plotly) | Python | WebSocket via callbacks | High | 2-4 hours | Interactive financial dashboards |
| Grafana | Config-based | Native (built-in) | Limited | 1 hour | Monitoring, time-series visualization |
| Custom HTML+ECharts | JS/HTML | Native WebSocket | Unlimited | 8-16 hours | Production trading terminal |
| Next.js + React | TypeScript | Native WebSocket | Unlimited | 16-32 hours | SaaS-quality multi-page app |
Our Recommendation: Express + ECharts
For a personal trading terminal, Express.js + static HTML + ECharts is the sweet spot. You get full control over every pixel, native WebSocket support, sub-100ms renders, and it's dead simple to deploy. Streamlit is great for prototyping, but its polling model and limited customization make it frustrating for daily use. Save React/Next.js for when you want to build a SaaS product for others.
Building the Watchlist View
The watchlist is your terminal's home screen. It shows every symbol in your universe at a glance: price, change, signal status, and key metrics. Here's the architecture:
JavaScript — Express Server with WebSocket
const express = require('express');
const { WebSocketServer } = require('ws');
const http = require('http');
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
app.use(express.static('public'));
// API routes
app.get('/api/watchlist', async (req, res) => {
const quotes = await cache.getWatchlistQuotes();
const signals = await db.getLatestSignals();
res.json({ quotes, signals });
});
app.get('/api/bars/:symbol', async (req, res) => {
const { symbol } = req.params;
const { period = '6M' } = req.query;
const bars = await db.getBars(symbol, period);
res.json(bars);
});
// WebSocket: push real-time updates
wss.on('connection', (ws) => {
console.log('Dashboard client connected');
// Subscribe to Redis pub/sub for live quotes
const sub = redis.duplicate();
sub.subscribe('quotes:live');
sub.on('message', (channel, data) => {
if (ws.readyState === ws.OPEN) {
ws.send(data);
}
});
ws.on('close', () => sub.unsubscribe());
});
server.listen(3000, () =>
console.log('Dashboard running on http://localhost:3000')
);
Market Heatmap
A heatmap gives you a 1-second snapshot of market health. Green = up, red = down, size = market cap. Here's a live example:
ECharts Candlestick with Indicator Overlays
The candlestick chart is the centerpiece of any trading terminal. ECharts' candlestick series gives you a professional-grade chart with zoom, pan, and multiple overlays. Here's a complete implementation:
JavaScript — ECharts Candlestick with EMA + Volume
function renderChart(containerId, data) {
const chart = echarts.init(document.getElementById(containerId));
// Calculate EMAs client-side
function ema(data, period) {
const k = 2 / (period + 1);
let result = [data[0]];
for (let i = 1; i < data.length; i++) {
result.push(data[i] * k + result[i-1] * (1 - k));
}
return result;
}
const closes = data.map(d => d.close);
const ema20 = ema(closes, 20);
const ema50 = ema(closes, 50);
chart.setOption({
grid: [
{ left: '8%', right: '3%', top: '8%', height: '55%' },
{ left: '8%', right: '3%', top: '70%', height: '18%' },
],
xAxis: [
{ type: 'category', data: data.map(d => d.date), gridIndex: 0 },
{ type: 'category', data: data.map(d => d.date), gridIndex: 1 },
],
yAxis: [
{ type: 'value', gridIndex: 0, scale: true },
{ type: 'value', gridIndex: 1, splitNumber: 2 },
],
dataZoom: [{ type: 'inside', xAxisIndex: [0,1], start: 60, end: 100 }],
series: [
{ type: 'candlestick',
data: data.map(d => [d.open, d.close, d.low, d.high]),
itemStyle: { color: '#10b981', color0: '#ef4444',
borderColor: '#059669', borderColor0: '#dc2626' },
xAxisIndex: 0, yAxisIndex: 0 },
{ type: 'line', data: ema20, name: 'EMA 20',
lineStyle: { color: '#2563eb', width: 1.5 }, symbol: 'none',
xAxisIndex: 0, yAxisIndex: 0 },
{ type: 'line', data: ema50, name: 'EMA 50',
lineStyle: { color: '#f59e0b', width: 1.5 }, symbol: 'none',
xAxisIndex: 0, yAxisIndex: 0 },
{ type: 'bar', data: data.map(d => d.volume), name: 'Volume',
itemStyle: { color: 'rgba(100,116,139,0.4)' },
xAxisIndex: 1, yAxisIndex: 1 },
]
});
}
Real-Time Data Flow
The magic of a live dashboard is server-push. Instead of the browser polling every second, the server pushes updates only when data changes. Here's the client-side WebSocket handler:
JavaScript — WebSocket Client with Auto-Reconnect
class LiveConnection {
constructor(url) {
this.url = url;
this.handlers = new Map();
this.reconnectDelay = 1000;
this.maxDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected to terminal');
this.reconnectDelay = 1000; // Reset backoff
document.getElementById('status').className = 'connected';
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
const handler = this.handlers.get(msg.type);
if (handler) handler(msg.data);
};
this.ws.onclose = () => {
document.getElementById('status').className = 'disconnected';
setTimeout(() => {
this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, this.maxDelay);
this.connect();
}, this.reconnectDelay);
};
}
on(type, handler) { this.handlers.set(type, handler); }
}
// Usage
const live = new LiveConnection('ws://localhost:3000');
live.on('quote', (data) => updateWatchlistRow(data));
live.on('signal', (data) => showSignalAlert(data));
live.on('news', (data) => appendNewsTicker(data));
Alerts via Telegram, Discord, Email
You can't watch the dashboard 24/7. The alert system monitors your signals and pushes notifications when something important happens. Here are the three channels you should set up:
Telegram Bot
Instant push notifications on your phone. Best for time-sensitive alerts (breakouts, stops hit). 4,096 char messages with Markdown.
Discord Webhook
Rich embeds with charts, tables, color-coded urgency. Best for morning briefings and daily scanner results. Searchable history.
Email (SMTP)
Weekly summaries, portfolio reports, compliance records. Not real-time, but permanent record. Use SendGrid or SES.
Python — Telegram Alert Service
import httpx
class TelegramAlert:
"""Send trading alerts via Telegram bot."""
def __init__(self, bot_token: str, chat_id: str):
self.base = f"https://api.telegram.org/bot{bot_token}"
self.chat_id = chat_id
async def send(self, message: str, urgent: bool = False):
"""Send a Markdown-formatted alert."""
prefix = "*ALERT*" if urgent else "Signal"
async with httpx.AsyncClient() as client:
await client.post(
f"{self.base}/sendMessage",
json={
"chat_id": self.chat_id,
"text": f"{prefix}: {message}",
"parse_mode": "Markdown",
"disable_notification": not urgent,
}
)
async def send_signal(self, signal: dict):
"""Format and send a trading signal alert."""
emoji = "BUY" if signal["direction"] == "long" else "SELL"
msg = (
f"*{emoji} {signal['symbol']}*\n"
f"Score: {signal['score']}/100 ({signal['grade']})\n"
f"Entry: ${signal['entry']:.2f}\n"
f"Stop: ${signal['stop']:.2f}\n"
f"Target: ${signal['target']:.2f}\n"
f"R/R: {signal['rr']:.1f}x"
)
await self.send(msg, urgent=signal["score"] >= 85)
Python — Discord Webhook with Rich Embeds
class DiscordAlert:
"""Send rich alert embeds via Discord webhook."""
def __init__(self, webhook_url: str):
self.webhook_url = webhook_url
async def send_scanner_results(self, results: list[dict]):
"""Send daily scanner results as Discord embed."""
fields = []
for r in results[:10]:
fields.append({
"name": f"{r['symbol']} — {r['grade']}",
"value": (
f"Entry: ${r['entry']:.2f} | "
f"Stop: ${r['stop']:.2f} | "
f"TP: ${r['target']:.2f}\n"
f"Score: {r['score']}/100 | "
f"R/R: {r['rr']:.1f}x"
),
"inline": False
})
embed = {
"title": "Daily Scanner — Top 10 Setups",
"color": 0x059669,
"fields": fields,
"footer": {"text": "Trading Terminal | Not financial advice"}
}
async with httpx.AsyncClient() as client:
await client.post(self.webhook_url, json={"embeds": [embed]})
Real-Time P&L and Exposure Tracking
The portfolio tracker shows your open positions with real-time P&L, exposure by sector/geography, and risk metrics. Here's what it should display:
Dashboard Design Principles
Three rules for effective trading dashboards: (1) Information density — show more data per pixel, less whitespace, no decorative elements. (2) Color = signal — green/red only for P&L; use blue/gray for neutral data. Never use color for decoration. (3) Hierarchy — the most important number (total P&L) should be the biggest thing on the screen. Secondary data (individual positions) smaller. Tertiary data (risk metrics) smallest.