How to Build a Real-Time Trading Dashboard with Socket.io and trade-data-generator
Building a trading UI is exciting — until you realize you need live market data to test it. Real data feeds cost thousands per month. Free APIs have rate limits. Hardcoded data looks broken on charts.
In this tutorial we'll build a fully working real-time trading dashboard in under 30 minutes using trade-data-generator and Socket.io — no API keys, no rate limits, no cost.
Here's what we're building:
- Live candlestick chart updating in real-time
- Order book depth with bids and asks
- Price ticker with 24h stats
- Multi-symbol support (crypto, equity, forex)
Prerequisites
- Node.js 14+
- Basic knowledge of Express and Socket.io
Setup
Create a new project:
mkdir trading-dashboard
cd trading-dashboard
npm init -y
npm install trade-data-generator socket.io express
Project structure
trading-dashboard/
server.js
public/
index.html
Step 1 — Set up the server
Create server.js:
const express = require('express')
const http = require('http')
const { Server } = require('socket.io')
const { MarketFeed } = require('trade-data-generator')
const app = express()
const server = http.createServer(app)
const io = new Server(server, { cors: { origin: '*' } })
app.use(express.static('public'))
// ── Setup market feed ──────────────────── //
const feed = new MarketFeed({
type: 'crypto',
interval: 1000, // 1 tick per second
candleIntervals: ['1m', '5m'], // track 1m and 5m candles
pairs: [
{ symbol: 'BTC/USDT', startPrice: 45000, volatility: 0.004, precision: 2 },
{ symbol: 'ETH/USDT', startPrice: 2800, volatility: 0.005, precision: 2 },
{ symbol: 'SOL/USDT', startPrice: 120, volatility: 0.008, precision: 3 },
]
})
// ── Pipe events to WebSocket clients ───── //
feed.on('tick', (data) => {
// Broadcast to all clients subscribed to this symbol
io.to(data.symbol).emit('ticker_update', data)
// Broadcast to mini ticker bar (all symbols)
io.emit('mini_ticker', data)
})
feed.on('candle', (data) => {
io.to(data.symbol).emit('candle_update', data)
})
feed.on('depth', (data) => {
io.to(data.symbol).emit('orderbook_update', data)
})
// ── Handle client connections ───────────── //
io.on('connection', (socket) => {
console.log('Client connected:', socket.id)
// Client subscribes to a pair
socket.on('subscribe', ({ symbol }) => {
// Leave any previous room
socket.rooms.forEach(room => {
if (room !== socket.id) socket.leave(room)
})
// Join new room
socket.join(symbol)
// Send current state immediately
const state = feed.getState(symbol)
if (state) socket.emit('snapshot', state)
console.log(`${socket.id} subscribed to ${symbol}`)
})
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id)
})
})
// ── Start ──────────────────────────────── //
feed.start()
server.listen(3001, () => {
console.log('Server running at http://localhost:3001')
})
Step 2 — Build the frontend
Create public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trading Dashboard</title>
<!-- Lightweight Charts by TradingView -->
<script src="https://unpkg.com/lightweight-charts@3.8.0/dist/lightweight-charts.standalone.production.js"></script>
<!-- Socket.io client -->
<script src="/socket.io/socket.io.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #040608; color: #e8edf5; font-family: sans-serif; }
.nav {
padding: 1rem 1.5rem;
border-bottom: 1px solid #1a1f2a;
display: flex;
align-items: center;
gap: 1rem;
}
.pair-btn {
padding: 0.4rem 1rem;
background: none;
border: 1px solid #1a1f2a;
border-radius: 6px;
color: #848e9c;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.pair-btn.active {
background: rgba(0,212,170,0.1);
border-color: #00d4aa;
color: #00d4aa;
}
.ticker {
padding: 1rem 1.5rem;
border-bottom: 1px solid #1a1f2a;
display: flex;
align-items: baseline;
gap: 1.5rem;
}
.ticker-price {
font-size: 2rem;
font-weight: 700;
font-family: monospace;
}
.ticker-change { font-size: 0.9rem; font-family: monospace; }
.up { color: #00d4aa; }
.down { color: #f6465d; }
.ticker-stat { font-size: 0.8rem; color: #5a6478; }
.ticker-stat span { color: #e8edf5; }
.main {
display: grid;
grid-template-columns: 1fr 250px;
height: calc(100vh - 130px);
}
#chart { width: 100%; height: 100%; }
.orderbook {
border-left: 1px solid #1a1f2a;
display: flex;
flex-direction: column;
overflow: hidden;
}
.ob-header {
padding: 0.75rem 1rem;
border-bottom: 1px solid #1a1f2a;
font-size: 0.75rem;
color: #5a6478;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.ob-labels {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0.4rem 1rem;
font-size: 0.7rem;
color: #5a6478;
border-bottom: 1px solid #1a1f2a;
}
.ob-labels span:last-child { text-align: right; }
.ob-row {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0.2rem 1rem;
font-family: monospace;
font-size: 0.75rem;
}
.ob-row .size { text-align: right; color: #5a6478; }
.ob-row.ask .price { color: #f6465d; }
.ob-row.bid .price { color: #00d4aa; }
.ob-spread {
padding: 0.4rem 1rem;
border-top: 1px solid #1a1f2a;
border-bottom: 1px solid #1a1f2a;
font-family: monospace;
font-size: 0.85rem;
text-align: center;
}
.ob-asks { display: flex; flex-direction: column-reverse; flex: 1; overflow: hidden; }
.ob-bids { flex: 1; overflow: hidden; }
</style>
</head>
<body>
<!-- Pair switcher -->
<div class="nav">
<strong style="color:#00d4aa; font-size:1rem;">TradeSim</strong>
<button class="pair-btn active" onclick="switchPair('BTC/USDT')">BTC/USDT</button>
<button class="pair-btn" onclick="switchPair('ETH/USDT')">ETH/USDT</button>
<button class="pair-btn" onclick="switchPair('SOL/USDT')">SOL/USDT</button>
</div>
<!-- Ticker -->
<div class="ticker">
<div class="ticker-price" id="price">—</div>
<div class="ticker-change up" id="change">—</div>
<div class="ticker-stat">24h High <span id="high">—</span></div>
<div class="ticker-stat">24h Low <span id="low">—</span></div>
<div class="ticker-stat">Volume <span id="vol">—</span></div>
</div>
<!-- Main layout -->
<div class="main">
<div id="chart"></div>
<div class="orderbook">
<div class="ob-header">Order Book</div>
<div class="ob-labels"><span>Price</span><span>Size</span></div>
<div class="ob-asks" id="ob-asks"></div>
<div class="ob-spread" id="spread-price">—</div>
<div class="ob-labels"><span>Price</span><span>Size</span></div>
<div class="ob-bids" id="ob-bids"></div>
</div>
</div>
<script>
// ── Chart setup ────────────────────── //
const chartEl = document.getElementById('chart')
const chart = LightweightCharts.createChart(chartEl, {
layout: { background: { color: '#040608' }, textColor: '#5a6478' },
grid: { vertLines: { color: '#0f1419' }, horzLines: { color: '#0f1419' } },
width: chartEl.clientWidth,
height: chartEl.clientHeight,
})
const candleSeries = chart.addCandlestickSeries({
upColor: '#00d4aa', downColor: '#f6465d',
borderUpColor: '#00d4aa', borderDownColor: '#f6465d',
wickUpColor: '#00d4aa', wickDownColor: '#f6465d',
})
window.addEventListener('resize', () => {
chart.applyOptions({ width: chartEl.clientWidth, height: chartEl.clientHeight })
})
// ── Socket setup ───────────────────── //
const socket = io()
let currentPair = 'BTC/USDT'
function switchPair(symbol) {
currentPair = symbol
// Update active button
document.querySelectorAll('.pair-btn').forEach(b => {
b.classList.toggle('active', b.textContent === symbol)
})
// Clear chart
candleSeries.setData([])
// Subscribe to new pair
socket.emit('subscribe', { symbol })
}
// Snapshot — load history on subscribe
socket.on('snapshot', (state) => {
if (state.candles && state.candles['1m']) {
candleSeries.setData(state.candles['1m'].map(c => ({
time: c.time, open: c.open, high: c.high,
low: c.low, close: c.close
})))
}
})
// Live price updates
socket.on('ticker_update', (data) => {
const p = data.price
const up = data.changePct >= 0
document.getElementById('price').textContent = p.toLocaleString()
document.getElementById('high').textContent = data.high24h.toLocaleString()
document.getElementById('low').textContent = data.low24h.toLocaleString()
document.getElementById('vol').textContent = data.volume.toLocaleString()
const chgEl = document.getElementById('change')
chgEl.textContent = (up ? '+' : '') + data.changePct.toFixed(2) + '%'
chgEl.className = 'ticker-change ' + (up ? 'up' : 'down')
document.getElementById('spread-price').textContent = p.toLocaleString()
document.getElementById('spread-price').className =
'ob-spread ' + (up ? 'up' : 'down')
})
// Candle updates
socket.on('candle_update', (data) => {
candleSeries.update({
time: data.openTime / 1000,
open: data.open, high: data.high,
low: data.low, close: data.close
})
})
// Order book updates
socket.on('orderbook_update', (data) => {
const asksEl = document.getElementById('ob-asks')
const bidsEl = document.getElementById('ob-bids')
asksEl.innerHTML = ''
;[...data.asks].reverse().forEach(a => {
const row = document.createElement('div')
row.className = 'ob-row ask'
row.innerHTML = `<span class="price">${a.price}</span><span class="size">${a.volume.toLocaleString()}</span>`
asksEl.appendChild(row)
})
bidsEl.innerHTML = ''
data.bids.forEach(b => {
const row = document.createElement('div')
row.className = 'ob-row bid'
row.innerHTML = `<span class="price">${b.price}</span><span class="size">${b.volume.toLocaleString()}</span>`
bidsEl.appendChild(row)
})
})
// Subscribe to default pair on connect
socket.on('connect', () => {
socket.emit('subscribe', { symbol: currentPair })
})
</script>
</body>
</html>
Step 3 — Run it
node server.js
Open http://localhost:3001 and you'll see a live trading dashboard with:
- Candlestick chart updating every second
- Order book with real bid/ask depth
- Switch between BTC/USDT, ETH/USDT, and SOL/USDT
How it works
MarketFeed (trade-data-generator)
↓ emits tick, candle, depth events
Server (Socket.io)
↓ broadcasts to subscribed clients
Browser (Lightweight Charts)
↓ renders chart and order book
The library handles all the market simulations. Socket.io handles the real-time transport. You own both — no external dependencies.
Adding equity and forex
Switch the market type and add market hours:
// Equity — respects NYSE hours
const feed = new MarketFeed({
type: 'equity',
marketHours: {
open: '09:30',
close: '16:00',
timezone: 'America/New_York',
},
pairs: [
{ symbol: 'AAPL', startPrice: 175.50, volatility: 0.002, precision: 2 },
{ symbol: 'GOOGL', startPrice: 142.30, volatility: 0.003, precision: 2 },
]
})
// Listen for market open/close
feed.on('open', () => console.log('NYSE opened'))
feed.on('closed', () => console.log('NYSE closed'))
// Forex — pip precision
const feed = new MarketFeed({
type: 'forex',
marketHours: {
open: '00:00', close: '23:59', timezone: 'UTC',
days: [1, 2, 3, 4, 5],
},
pairs: [
{ symbol: 'EUR/USD', startPrice: 1.08450, volatility: 0.0003, precision: 5 },
{ symbol: 'GBP/USD', startPrice: 1.26780, volatility: 0.0004, precision: 5 },
]
})
What's next
You now have a fully working real-time trading dashboard with zero external API dependencies. Some ideas to extend it:
- Add TradingView Lightweight Charts volume histogram
- Add a trade history panel using the
tickevent - Deploy to Vercel or Railway for a live demo
- Swap
trade-data-generatorfor a real exchange WebSocket when going to production — the server code stays identical
Top comments (0)