Build Your Own Trading Terminal with AI — Part 4 of 6

Dashboard — Real-Time Visualization

Your terminal needs a face. In this part, we build a real-time dashboard with ECharts: watchlists, heatmaps, candlestick charts with indicator overlays, sector rotation views, and a multi-channel alert system via Telegram and Discord.

ECharts WebSocket Alerts Heatmaps
Build Your Own Trading Terminal with AI4/6
Framework ChoicesWatchlist & HeatmapCandlestick ChartsWebSocket UpdatesAlert SystemPortfolio TrackerKey Takeaways
Dashboard Framework Choices

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.

FrameworkLanguageReal-TimeCustomizationSetup TimeBest For
StreamlitPythonPolling (slow)Medium30 minQuick prototypes, data exploration
Dash (Plotly)PythonWebSocket via callbacksHigh2-4 hoursInteractive financial dashboards
GrafanaConfig-basedNative (built-in)Limited1 hourMonitoring, time-series visualization
Custom HTML+EChartsJS/HTMLNative WebSocketUnlimited8-16 hoursProduction trading terminal
Next.js + ReactTypeScriptNative WebSocketUnlimited16-32 hoursSaaS-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.

Watchlist & Market Heatmap

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:

Candlestick Charts with Overlays

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 }, ] }); }
WebSocket Live Updates

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));
Multi-Channel Alert System

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]})
Portfolio Tracker

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.

Key Takeaways

Key Takeaways — Part 4

Part 5 of 6
AI Layer — Claude API for Narrative Analysis

Build Your Own Trading Terminal with AI

Complete 6-part series — From architecture to production deployment

Back to Home
Build Your Own Trading Terminal with AI4/6