DEV Community

Love Pareek
Love Pareek

Posted on

How to build a real-time trading dashboard with Socket.io and trade-data-generator

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
Enter fullscreen mode Exit fullscreen mode

Project structure

trading-dashboard/
  server.js
  public/
    index.html
Enter fullscreen mode Exit fullscreen mode

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')
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Step 3 — Run it

node server.js
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode
// 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 },
  ]
})
Enter fullscreen mode Exit fullscreen mode

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 tick event
  • Deploy to Vercel or Railway for a live demo
  • Swap trade-data-generator for a real exchange WebSocket when going to production — the server code stays identical

Links

Top comments (0)