Building a Trading Dashboard with HTML, CSS & JavaScript
This tutorial guides you through building a real-time trading dashboard for DeepBook using Surflux APIs and Server-Sent Events. You'll create a professional interface that displays live order books, price charts, market depth, and real-time trade execution.
What You'll Build
A complete trading dashboard featuring:
- Real-time candlestick price charts with multiple timeframes
- Live order book with depth visualization
- Recent trade history with color-coded buy/sell indicators
- Market depth chart showing cumulative bid/ask liquidity
- Server-Sent Events for instant data updates
Prerequisites
- Basic HTML, CSS, and JavaScript knowledge
- Understanding of REST APIs
- Familiarity with trading concepts (order books, candlestick charts)
- A Surflux API key
This tutorial demonstrates a proof of concept using direct frontend API calls. For production applications, proxy all Surflux API requests through your backend to protect your API key and add authentication, rate limiting, and caching.
Architecture Overview
The dashboard connects to multiple Surflux endpoints:
REST APIs (for initial data):
/deepbook/get_pools— Pool metadata and trading parameters/deepbook/{poolName}/ohlcv/{timeframe}— Historical candlestick data/deepbook/{poolName}/order-book-depth— Current order book snapshot/deepbook/{poolName}/trades— Recent trade history
SSE Stream (for real-time updates):
/deepbook/{poolName}/live-trades— Live order book and trade events
Project Setup
Create a new HTML file and include the required libraries:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepBook Trading Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lightweight-charts.standalone.production.js"></script>
</head>
<body>
<!-- Dashboard content goes here -->
</body>
</html>
We use Tailwind CSS for styling and Lightweight Charts for the price chart visualization.
Fetching Pool Information
First, retrieve pool metadata to get decimals and trading parameters:
// Surflux API endpoints
const API_URL = 'https://api.surflux.dev'; // REST API for data queries
const STREAM_URL = 'https://flux.surflux.dev'; // SSE for real-time updates
let poolData = null;
let baseDecimals = 9;
let quoteDecimals = 6;
async function apiRequest(endpoint, params = {}) {
const url = new URL(API_URL + endpoint);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
const response = await fetch(url);
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response.json();
}
async function fetchPoolInfo(apiKey, poolName) {
const pools = await apiRequest('/deepbook/get_pools', { 'api-key': apiKey });
poolData = pools.find(p => p.pool_name === poolName);
if (!poolData) {
throw new Error(`Pool ${poolName} not found`);
}
baseDecimals = poolData.base_asset_decimals;
quoteDecimals = poolData.quote_asset_decimals;
console.log(`Loaded pool: ${poolData.pool_name}`);
console.log(`Base: ${poolData.base_asset_symbol} (${baseDecimals} decimals)`);
console.log(`Quote: ${poolData.quote_asset_symbol} (${quoteDecimals} decimals)`);
}
The decimals are crucial for converting between on-chain values (smallest units) and human-readable amounts.
Loading Historical Price Data
Fetch OHLCV (candlestick) data for the price chart:
let chart = null;
let candlestickSeries = null;
let currentTimeframe = '1m';
async function fetchOHLCV(apiKey, poolName, timeframe) {
const to = Math.floor(Date.now() / 1000);
const from = to - 24 * 60 * 60; // Last 24 hours
const data = await apiRequest(`/deepbook/${poolName}/ohlcv/${timeframe}`, {
'api-key': apiKey,
from,
to,
limit: 500
});
const candleData = data.map(candle => ({
time: Math.floor(new Date(candle.timestamp).getTime() / 1000),
open: parseFloat(candle.open) / Math.pow(10, quoteDecimals),
high: parseFloat(candle.high) / Math.pow(10, quoteDecimals),
low: parseFloat(candle.low) / Math.pow(10, quoteDecimals),
close: parseFloat(candle.close) / Math.pow(10, quoteDecimals),
})).reverse();
candlestickSeries.setData(candleData);
chart.timeScale().fitContent();
if (candleData.length > 0) {
const latest = candleData[candleData.length - 1];
const first = candleData[0]?.close || latest.close;
updatePrice(latest.close, first);
}
}
function updatePrice(currentPrice, previousPrice) {
const priceElement = document.getElementById('currentPrice');
const changeElement = document.getElementById('priceChange');
priceElement.textContent = currentPrice.toFixed(4);
const change = ((currentPrice - previousPrice) / previousPrice) * 100;
const isPositive = change >= 0;
priceElement.className = `text-lg lg:text-xl font-semibold ${isPositive ? 'text-green' : 'text-red'}`;
changeElement.textContent = `${isPositive ? '+' : ''}${change.toFixed(2)}%`;
changeElement.className = `text-sm px-2 py-1 rounded ${isPositive ? 'bg-green/10 text-green' : 'bg-red/10 text-red'}`;
}
Available timeframes: 1m, 5m, 15m, 1h, 4h, 1d
Initializing the Price Chart
Set up Lightweight Charts with a candlestick series:
function initCharts() {
const chartOptions = {
layout: {
background: { color: '#13171D' },
textColor: '#848E9C'
},
grid: {
vertLines: { color: '#1E2329' },
horzLines: { color: '#1E2329' }
},
timeScale: {
timeVisible: true,
borderColor: '#1E2329'
},
rightPriceScale: { borderColor: '#1E2329' },
handleScroll: {
mouseWheel: true,
pressedMouseMove: true
}
};
const chartContainer = document.getElementById('chart');
chart = LightweightCharts.createChart(chartContainer, {
...chartOptions,
width: chartContainer.offsetWidth,
height: chartContainer.offsetHeight || 400
});
candlestickSeries = chart.addCandlestickSeries({
upColor: '#0ECB81',
downColor: '#F6465D',
borderVisible: false,
wickUpColor: '#0ECB81',
wickDownColor: '#F6465D',
});
window.addEventListener('resize', () => {
const container = document.getElementById('chart');
chart.applyOptions({
width: container.offsetWidth,
height: container.offsetHeight || 400
});
});
}
Loading Order Book Data
Fetch the current order book snapshot:
async function fetchOrderBook(apiKey, poolName) {
const data = await apiRequest(`/deepbook/${poolName}/order-book-depth`, {
'api-key': apiKey,
limit: 15
});
updateOrderBook(data);
}
function updateOrderBook(orderBook) {
if (!orderBook.asks || !orderBook.bids) return;
const maxAmount = Math.max(
...orderBook.asks.map(a => parseFloat(a.total_quantity)),
...orderBook.bids.map(b => parseFloat(b.total_quantity))
);
const createOrderRow = (order, isBid) => {
const price = parseFloat(order.price) / Math.pow(10, quoteDecimals);
const amount = parseFloat(order.total_quantity) / Math.pow(10, baseDecimals);
const total = price * amount;
const depth = (parseFloat(order.total_quantity) / maxAmount) * 100;
const colorClass = isBid ? 'text-green' : 'text-red';
const bgClass = isBid ? 'bg-green/10' : 'bg-red/10';
return `
<div class="relative grid grid-cols-3 gap-2 px-2 py-1 text-xs font-mono">
<div class="absolute inset-y-0 right-0 ${bgClass} opacity-60" style="width: ${depth}%"></div>
<div class="relative ${colorClass}">${price.toFixed(4)}</div>
<div class="relative text-right">${amount.toFixed(2)}</div>
<div class="relative text-right">${total.toFixed(2)}</div>
</div>
`;
};
const headerHtml = `
<div class="grid grid-cols-3 gap-2 px-2 py-1 text-text-secondary text-xs uppercase border-b border-gray-border">
<div>Price</div>
<div class="text-right">Amount</div>
<div class="text-right">Total</div>
</div>
`;
const asksHtml = headerHtml + orderBook.asks.slice(0, 15).reverse()
.map(ask => createOrderRow(ask, false)).join('');
const bidsHtml = headerHtml + orderBook.bids.slice(0, 15)
.map(bid => createOrderRow(bid, true)).join('');
document.getElementById('asksList').innerHTML = asksHtml;
document.getElementById('bidsList').innerHTML = bidsHtml;
// Calculate and display spread
if (orderBook.asks.length > 0 && orderBook.bids.length > 0) {
const bestAsk = parseFloat(orderBook.asks[0].price) / Math.pow(10, quoteDecimals);
const bestBid = parseFloat(orderBook.bids[0].price) / Math.pow(10, quoteDecimals);
const spread = bestAsk - bestBid;
const spreadPercent = (spread / bestAsk) * 100;
document.getElementById('spreadRow').textContent =
`${spread.toFixed(4)} (${spreadPercent.toFixed(2)}%)`;
}
updateDepthChart(orderBook);
}
Each order row includes:
- Price: Execution price in quote asset
- Amount: Total quantity at this price level in base asset
- Total: Price × Amount in quote asset
- Depth bar: Visual representation of liquidity
Loading Recent Trades
Fetch initial trade history:
let lastPrice = null;
async function fetchInitialTrades(apiKey, poolName) {
const data = await apiRequest(`/deepbook/${poolName}/trades`, {
'api-key': apiKey
});
document.getElementById('tradesList').innerHTML = '';
// Display trades in reverse order (newest first)
data.reverse().forEach(trade => addTrade(trade));
}
function addTrade(trade) {
const price = parseFloat(trade.price) / Math.pow(10, quoteDecimals);
const amount = parseFloat(trade.base_quantity) / Math.pow(10, baseDecimals);
const time = new Date(parseInt(trade.checkpoint_timestamp_ms)).toLocaleTimeString();
const isBuy = trade.taker_is_bid;
const priceClass = isBuy ? 'text-green' : 'text-red';
const tradeHtml = `
<div class="grid grid-cols-3 gap-2 px-2 py-1.5 text-xs font-mono border-b border-gray-border/50 hover:bg-gray-border/30">
<div class="${priceClass}">${price.toFixed(4)}</div>
<div class="text-right">${amount.toFixed(2)}</div>
<div class="text-right text-text-secondary text-xs">${time}</div>
</div>
`;
const tradesList = document.getElementById('tradesList');
tradesList.insertAdjacentHTML('afterbegin', tradeHtml);
// Keep only last 50 trades
while (tradesList.children.length > 50) {
tradesList.removeChild(tradesList.lastChild);
}
updatePrice(price, lastPrice || price);
lastPrice = price;
}
Connecting to Live Updates
Establish an SSE connection for real-time data:
let eventSource = null;
function connectLiveTrades(apiKey, poolName) {
// Close existing connection
if (eventSource) {
eventSource.close();
}
const url = new URL(`${STREAM_URL}/deepbook/${poolName}/live-trades`);
url.searchParams.append('api-key', apiKey);
eventSource = new EventSource(url);
eventSource.onopen = () => {
console.log('SSE connection established');
document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-green';
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'deepbook_order_book_depth' && data.data?.bids) {
updateOrderBook(data.data);
} else if (data.type === 'deepbook_live_trades') {
addTrade(data.data);
}
} catch (error) {
console.error('SSE parse error:', error);
}
};
eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-text-secondary';
};
}
The SSE stream delivers two event types:
deepbook_order_book_depth— Order book updatesdeepbook_live_trades— New trade executions
Market Depth Visualization
Create a cumulative depth chart:
function updateDepthChart(orderBook) {
if (!orderBook.bids || !orderBook.asks ||
orderBook.bids.length === 0 || orderBook.asks.length === 0) {
return;
}
const barsContainer = document.getElementById('depthBars');
const xAxisLabels = document.getElementById('xAxisLabels');
const yAxisLabels = document.getElementById('yAxisLabels');
barsContainer.innerHTML = '';
xAxisLabels.innerHTML = '';
yAxisLabels.innerHTML = '';
const maxLevels = 10;
const bids = orderBook.bids.slice(0, maxLevels);
const asks = orderBook.asks.slice(0, maxLevels);
// Calculate cumulative amounts
let bidCumulative = 0;
let askCumulative = 0;
const bidAmounts = bids.map(bid => {
const amount = parseFloat(bid.total_quantity) / Math.pow(10, baseDecimals);
bidCumulative += amount;
return bidCumulative;
});
const askAmounts = asks.map(ask => {
const amount = parseFloat(ask.total_quantity) / Math.pow(10, baseDecimals);
askCumulative += amount;
return askCumulative;
});
const maxAmount = Math.max(
bidAmounts[bidAmounts.length - 1] || 0,
askAmounts[askAmounts.length - 1] || 0
);
// Create Y-axis labels (amounts)
for (let i = 4; i >= 0; i--) {
const label = document.createElement('div');
const value = (maxAmount * (i + 1)) / 5;
label.textContent = value > 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(0);
label.className = 'text-right';
yAxisLabels.appendChild(label);
}
const allPrices = [];
// Create bid bars (left side, reversed)
for (let i = bids.length - 1; i >= 0; i--) {
const price = parseFloat(bids[i].price) / Math.pow(10, quoteDecimals);
allPrices.push(price);
const height = (bidAmounts[i] / maxAmount) * 100;
const bar = document.createElement('div');
bar.className = 'flex-1 bg-green/40 hover:bg-green/60 transition-colors cursor-pointer';
bar.style.height = `${height}%`;
bar.title = `Price: ${price.toFixed(4)}, Amount: ${bidAmounts[i].toFixed(2)}`;
barsContainer.appendChild(bar);
}
// Create ask bars (right side)
for (let i = 0; i < asks.length; i++) {
const price = parseFloat(asks[i].price) / Math.pow(10, quoteDecimals);
allPrices.push(price);
const height = (askAmounts[i] / maxAmount) * 100;
const bar = document.createElement('div');
bar.className = 'flex-1 bg-red/40 hover:bg-red/60 transition-colors cursor-pointer';
bar.style.height = `${height}%`;
bar.title = `Price: ${price.toFixed(4)}, Amount: ${askAmounts[i].toFixed(2)}`;
barsContainer.appendChild(bar);
}
// Create X-axis labels (prices)
const minPrice = Math.min(...allPrices);
const maxPrice = Math.max(...allPrices);
const midPrice = (minPrice + maxPrice) / 2;
const strategicPrices = [
minPrice,
(minPrice + midPrice) / 2,
midPrice,
(midPrice + maxPrice) / 2,
maxPrice
];
strategicPrices.forEach(price => {
const label = document.createElement('div');
label.textContent = price.toFixed(4);
label.className = 'text-center';
xAxisLabels.appendChild(label);
});
document.getElementById('bidTotal').textContent = bidCumulative.toFixed(2);
document.getElementById('askTotal').textContent = askCumulative.toFixed(2);
}
Connecting Everything
Main initialization function:
async function connect() {
const poolName = document.getElementById('poolName').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
if (!poolName || !apiKey) {
alert('Please enter pool name and API key');
return;
}
const btn = document.getElementById('connectBtn');
btn.disabled = true;
btn.textContent = 'Connecting...';
try {
// Initialize chart first
if (!chart) {
initCharts();
}
// Load initial data
await fetchPoolInfo(apiKey, poolName);
await fetchOHLCV(apiKey, poolName, currentTimeframe);
await fetchOrderBook(apiKey, poolName);
await fetchInitialTrades(apiKey, poolName);
// Connect to live stream
connectLiveTrades(apiKey, poolName);
document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-green';
btn.textContent = 'Connected';
} catch (error) {
alert(`Connection failed: ${error.message}`);
btn.disabled = false;
btn.textContent = 'Connect';
document.getElementById('statusIndicator').className = 'w-2 h-2 rounded-full bg-red';
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
initCharts();
});
Testing the Dashboard
Local Development
Serve the HTML file using a local server:
npx serve .
Open http://localhost:3000 in your browser.
Connection Steps
- Enter a pool name (e.g., "SUI_USDC")
- Enter your Surflux API key
- Click "Connect"
- Watch the dashboard populate with live data
Verification Checklist
- Price chart displays historical candlesticks
- Order book shows bids and asks with depth bars
- Recent trades appear in the list
- New trades stream in real-time
- Order book updates automatically
- Timeframe buttons switch chart periods
- Market depth chart visualizes liquidity
Production Considerations
Security
Never expose API keys in production. Implement a backend proxy:
// Frontend calls your backend
const response = await fetch('/api/deepbook/pools');
// Backend proxies to Surflux
app.get('/api/deepbook/pools', async (req, res) => {
// Add authentication, rate limiting, caching
const response = await fetch(
'https://api.surflux.dev/deepbook/get_pools?api-key=' + SURFLUX_API_KEY
);
res.json(await response.json());
});
Performance
- Cache pool metadata — Pool configurations rarely change
- Throttle DOM updates — Use requestAnimationFrame for smooth rendering
- Implement virtual scrolling — For large trade lists
- Debounce chart updates — Avoid excessive redraws
Error Handling
eventSource.onerror = (error) => {
console.error('SSE error:', error);
// Attempt reconnection after 5 seconds
setTimeout(() => {
console.log('Reconnecting...');
connectLiveTrades(apiKey, poolName);
}, 5000);
};
Monitoring
Track key metrics:
- SSE connection uptime
- API response latency
- Event processing rate
- Client-side errors
Next Steps
- Add order placement functionality
- Implement user authentication
- Create mobile-responsive layout
- Add technical indicators (RSI, MACD, Bollinger Bands)
- Build portfolio tracking
- Integrate wallet connections for trading
Related Documentation
- Live Trades — SSE event structure and field reference
- Order Book Depth — Order book event details
- Get Pools — Pool metadata endpoint
- OHLCV Data — Candlestick data endpoint
- Consuming SSE Events — SSE basics
- Building a Trading Dashboard with React & TypeScript — Upgrade to React