'5m'],
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 }
]
});
// Route events to specific WebSocket rooms
marketSim.on('tick', (payload) => {
io.to(payload.symbol).emit('price_delta', payload);
io.emit('global_ticker', payload);
});
marketSim.on('candle', (payload) => {
io.to(payload.symbol).emit('candle_delta', payload);
});
marketSim.on('depth', (payload) => {
io.to(payload.symbol).emit('orderbook_delta', payload);
});
// Connection & subscription handler
io.on('connection', (client) => {
console.log(Session established: ${client.id});
client.on('join_market', ({ asset }) => {
// Clear previous subscriptions
client.rooms.forEach((room) => {
if (room !== client.id) client.leave(room);
});
client.join(asset);
// Push initial state to prevent blank UI
const initialState = marketSim.getState(asset);
if (initialState) client.emit('market_snapshot', initialState);
});
client.on('disconnect', () => {
console.log(Session terminated: ${client.id});
});
});
marketSim.start();
server.listen(3001, () => console.log('Simulation server active on port 3001'));
**Architecture Rationale:**
- **Socket.io Rooms**: Broadcasting to all connected clients wastes bandwidth and forces the frontend to filter irrelevant data. Rooms isolate traffic per asset, reducing payload size by ~70% in multi-symbol environments.
- **Snapshot + Delta Pattern**: Sending the full historical state on subscription eliminates the need for clients to request historical REST endpoints. Subsequent updates only transmit changes, minimizing network overhead.
- **Event Decoupling**: Separating `tick`, `candle`, and `depth` events allows the frontend to render components independently. The order book can update at 10Hz while candles aggregate at 1Hz without blocking each other.
### Step 2: Client-Side Rendering Pipeline
The frontend uses TradingView's Lightweight Charts for canvas-based candlestick rendering and a virtualized DOM approach for the order book. Socket.io handles reconnection and state synchronization automatically.
```typescript
// public/app.ts
import { io } from 'socket.io-client';
import { createChart, CandlestickSeries } from 'lightweight-charts';
const socket = io();
let activeAsset = 'BTC/USDT';
let chart: ReturnType<typeof createChart>;
let candleSeries: CandlestickSeries;
function initializeChart(container: HTMLElement) {
chart = createChart(container, {
layout: { background: { color: '#0a0e14' }, textColor: '#8a94a6' },
grid: { vertLines: { color: '#151a22' }, horzLines: { color: '#151a22' } },
width: container.clientWidth,
height: container.clientHeight,
timeScale: { timeVisible: true, secondsVisible: false }
});
candleSeries = chart.addCandlestickSeries({
upColor: '#00d4aa', downColor: '#f6465d',
borderUpColor: '#00d4aa', borderDownColor: '#f6465d',
wickUpColor: '#00d4aa', wickDownColor: '#f6465d'
});
window.addEventListener('resize', () => {
chart.applyOptions({ width: container.clientWidth, height: container.clientHeight });
});
}
function switchAsset(symbol: string) {
activeAsset = symbol;
candleSeries.setData([]);
socket.emit('join_market', { asset: symbol });
}
socket.on('market_snapshot', (state) => {
if (state.candles?.['1m']) {
candleSeries.setData(
state.candles['1m'].map((bar) => ({
time: bar.time,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close
}))
);
}
});
socket.on('candle_delta', (update) => {
candleSeries.update({
time: update.openTime / 1000,
open: update.open,
high: update.high,
low: update.low,
close: update.close
});
});
socket.on('price_delta', (data) => {
const priceEl = document.getElementById('live-price');
const changeEl = document.getElementById('price-change');
if (priceEl) priceEl.textContent = data.price.toLocaleString();
if (changeEl) {
const isPositive = data.changePct >= 0;
changeEl.textContent = `${isPositive ? '+' : ''}${data.changePct.toFixed(2)}%`;
changeEl.className = isPositive ? 'positive' : 'negative';
}
});
socket.on('orderbook_delta', (ladder) => {
const asksContainer = document.getElementById('asks-list');
const bidsContainer = document.getElementById('bids-list');
if (!asksContainer || !bidsContainer) return;
// Batch DOM updates to prevent layout thrashing
requestAnimationFrame(() => {
asksContainer.innerHTML = ladder.asks.slice(0, 10).reverse().map((a) =>
`<div class="row ask"><span>${a.price}</span><span>${a.volume}</span></div>`
).join('');
bidsContainer.innerHTML = ladder.bids.slice(0, 10).map((b) =>
`<div class="row bid"><span>${b.price}</span><span>${b.volume}</span></div>`
).join('');
});
});
socket.on('connect', () => {
socket.emit('join_market', { asset: activeAsset });
});
document.addEventListener('DOMContentLoaded', () => {
initializeChart(document.getElementById('chart-container') as HTMLElement);
socket.emit('join_market', { asset: activeAsset });
});
Architecture Rationale:
- Canvas Rendering: Lightweight Charts uses HTML5 Canvas instead of SVG or DOM nodes. This prevents memory leaks during high-frequency updates and maintains 60fps rendering even with 50,000+ data points.
requestAnimationFrame Batching: Order book updates can fire 10β20 times per second. Wrapping DOM mutations in requestAnimationFrame ensures the browser batches layout calculations, eliminating jank.
- Automatic Reconnection: Socket.io handles network drops gracefully. The
connect listener requests a fresh snapshot, ensuring the UI never renders stale or missing data after a reconnect.
Pitfall Guide
1. Room Accumulation on Disconnect
Explanation: Failing to clean up WebSocket rooms when clients disconnect causes the server to retain stale subscriptions. Over time, this increases memory usage and causes duplicate event routing.
Fix: Implement explicit leave logic in the disconnect handler and use Socket.io's built-in room management. Avoid manual tracking arrays; rely on socket.rooms.
2. Timestamp Drift Between Client and Server
Explanation: Client-side clocks are rarely synchronized with server time. Using local timestamps for candle alignment causes chart gaps or overlapping bars.
Fix: Always use server-generated Unix timestamps (milliseconds). Normalize on the client by dividing by 1000 for Lightweight Charts, which expects seconds.
3. DOM Thrashing on Order Book Updates
Explanation: Re-rendering the entire order book DOM tree on every tick forces the browser to recalculate layout and paint cycles, causing visible stutter.
Fix: Limit visible rows to 10β15, use requestAnimationFrame for batching, and consider virtual scrolling libraries if rendering 50+ levels.
4. Ignoring Backpressure During Volatility Spikes
Explanation: During high volatility, the simulation engine may emit events faster than the client can process them, leading to queue buildup and UI lag.
Fix: Use socket.volatile.emit() for non-critical updates like order book depth. Volatile emits drop packets if the client is busy, preserving chart and price ticker responsiveness.
5. Floating-Point Precision Loss
Explanation: JavaScript's native number type uses IEEE 754 doubles, which lose precision beyond 15β17 significant digits. Displaying raw numbers causes rounding errors in price/volume fields.
Fix: Keep raw numbers for calculations, but format for display using toFixed() or a decimal library. Never perform arithmetic on formatted strings.
6. Hardcoding Asset Class Logic
Explanation: Tying the simulation configuration to crypto-only parameters breaks when switching to equities or forex, which have different trading hours, volatility profiles, and precision rules.
Fix: Abstract market configuration into a type-safe interface. Use trade-data-generator's type field to switch between crypto, equity, and forex, and adjust volatility/precision per asset class.
7. Missing Initial State on Reconnect
Explanation: When a client reconnects after a network drop, it only receives delta events. Without a snapshot, the chart and order book render blank or partially loaded.
Fix: Always emit a market_snapshot event immediately after subscription. This guarantees the UI starts from a consistent state regardless of connection history.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal UI prototyping | Local simulation (trade-data-generator + Socket.io) | Zero cost, full control over volatility/latency, instant iteration | $0 |
| Pre-production load testing | Simulated stream with artificial latency injection | Validates rendering pipeline under realistic network conditions without API rate limits | $0 |
| Live trading environment | Production exchange WebSocket feed | Required for actual order execution and regulatory compliance | $1,200β$4,500/mo |
| Multi-asset dashboard | Room-based Socket.io routing + snapshot sync | Prevents bandwidth saturation when users switch between 10+ symbols | Infrastructure only |
Configuration Template
// config/market-simulation.ts
import { MarketFeedConfig } from 'trade-data-generator';
export const cryptoConfig: MarketFeedConfig = {
type: 'crypto',
interval: 1000,
candleIntervals: ['1m', '5m', '15m'],
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 }
]
};
export const equityConfig: MarketFeedConfig = {
type: 'equity',
interval: 2000,
candleIntervals: ['1m', '5m'],
marketHours: { open: '09:30', close: '16:00', timezone: 'America/New_York' },
pairs: [
{ symbol: 'AAPL', startPrice: 175, volatility: 0.002, precision: 2 },
{ symbol: 'TSLA', startPrice: 240, volatility: 0.006, precision: 2 }
]
};
export const forexConfig: MarketFeedConfig = {
type: 'forex',
interval: 500,
candleIntervals: ['1m', '5m'],
pairs: [
{ symbol: 'EUR/USD', startPrice: 1.0850, volatility: 0.0003, precision: 5 },
{ symbol: 'GBP/USD', startPrice: 1.2640, volatility: 0.0004, precision: 5 }
]
};
Quick Start Guide
- Initialize Project: Run
mkdir trading-sim && cd trading-sim && npm init -y
- Install Dependencies: Execute
npm install express socket.io trade-data-generator lightweight-charts socket.io-client typescript @types/node @types/express
- Create Server File: Add
server.ts with the simulation engine and Socket.io routing logic from the Core Solution section
- Create Client File: Add
public/app.ts with chart initialization, event listeners, and DOM rendering logic
- Launch: Run
npx ts-node server.ts, open http://localhost:3001, and verify real-time candlestick and order book updates