Time & Sales (Trade Tape) Development
Time & Sales (T&S) is a continuous stream of all executed trades in real-time: time, price, volume, direction. Professional traders read T&S like a market pulse — they see buyer and seller aggression, large blocks, sequence of bid/ask hits.
Trade Tape Structure
interface Trade {
id: string;
timestamp: number; // unix milliseconds
price: number;
quantity: number;
side: 'buy' | 'sell'; // aggressive side
value: number; // price * quantity in USD
isLargeTrade: boolean; // above significant volume threshold
}
Each T&S row contains:
- Time: HH:MM:SS.mmm (with milliseconds for professional platforms)
- Price: with direction highlight
- Volume: in base asset
- Side: Buy (green) / Sell (red)
Backend: Aggregation and Streaming
type TimeAndSalesHub struct {
trades chan Trade
clients map[string]map[*WSClient]bool // pair -> clients
mu sync.RWMutex
recentBuf map[string]*RingBuffer // stores last N trades for new connections
}
type RingBuffer struct {
items []Trade
head int
size int
mu sync.Mutex
}
func (rb *RingBuffer) Add(trade Trade) {
rb.mu.Lock()
defer rb.mu.Unlock()
rb.items[rb.head%rb.size] = trade
rb.head++
}
func (rb *RingBuffer) GetAll() []Trade {
rb.mu.Lock()
defer rb.mu.Unlock()
result := make([]Trade, 0, rb.size)
start := rb.head - rb.size
if start < 0 { start = 0 }
for i := start; i < rb.head; i++ {
result = append(result, rb.items[i%rb.size])
}
return result
}
func (hub *TimeAndSalesHub) OnTrade(trade Trade) {
// Save in ring buffer
hub.recentBuf[trade.Pair].Add(trade)
// Broadcast to all subscribers
hub.mu.RLock()
defer hub.mu.RUnlock()
data, _ := json.Marshal(trade)
for client := range hub.clients[trade.Pair] {
select {
case client.send <- data:
default:
go client.close()
}
}
}
// New client gets last 100 trades immediately
func (hub *TimeAndSalesHub) OnClientConnect(client *WSClient, pair string) {
hub.mu.Lock()
hub.clients[pair][client] = true
hub.mu.Unlock()
// Send history
recent := hub.recentBuf[pair].GetAll()
for _, trade := range recent {
data, _ := json.Marshal(trade)
client.send <- data
}
}
Frontend: High-performance Rendering
T&S updates very frequently — on BTC/USDT up to 10–20 trades per second during active periods. Standard React list will lag.
import { useRef, useEffect, useCallback } from 'react';
const MAX_ROWS = 200; // max rows in tape
function TimeAndSalesList({ pair }: { pair: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const tradesRef = useRef<Trade[]>([]);
const wsRef = useRef<WebSocket | null>(null);
// Direct DOM manipulation for hot path — no React re-render
const appendTrade = useCallback((trade: Trade) => {
const container = containerRef.current;
if (!container) return;
// Create new row
const row = document.createElement('div');
row.className = `trade-row ${trade.side} ${trade.isLargeTrade ? 'large' : ''}`;
const time = new Date(trade.timestamp);
const timeStr = `${time.getHours().toString().padStart(2,'0')}:` +
`${time.getMinutes().toString().padStart(2,'0')}:` +
`${time.getSeconds().toString().padStart(2,'0')}`;
row.innerHTML = `
<span class="time">${timeStr}</span>
<span class="price">${formatPrice(trade.price)}</span>
<span class="qty">${formatQuantity(trade.quantity)}</span>
<span class="value">$${formatVolume(trade.value)}</span>
`;
// Insert at top (new trades on top)
container.insertBefore(row, container.firstChild);
// Remove extra rows at bottom
while (container.children.length > MAX_ROWS) {
container.removeChild(container.lastChild!);
}
// Flash animation for large trades
if (trade.isLargeTrade) {
row.classList.add('flash');
setTimeout(() => row.classList.remove('flash'), 500);
}
}, []);
useEffect(() => {
wsRef.current = new WebSocket(`wss://api.exchange.com/ws`);
wsRef.current.send(JSON.stringify({ op: 'subscribe', channel: `trades.${pair}` }));
wsRef.current.onmessage = (e) => {
const trade = JSON.parse(e.data);
appendTrade(trade);
};
return () => wsRef.current?.close();
}, [pair, appendTrade]);
return (
<div className="time-and-sales">
<div className="ts-header">
<span>Time</span>
<span>Price</span>
<span>Size</span>
<span>Value</span>
</div>
<div ref={containerRef} className="ts-body" />
</div>
);
}
Filtering and Highlighting
interface TSFilters {
minValue: number; // show only trades >= $X
side: 'all' | 'buy' | 'sell';
highlightLargeThreshold: number; // threshold for highlighting large blocks
}
// CSS classes for styling
const styles = `
.trade-row {
display: grid;
grid-template-columns: 80px 100px 80px 80px;
padding: 1px 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
border-bottom: 1px solid #1e2030;
transition: background-color 0.2s;
}
.trade-row.buy .price { color: #00B15E; }
.trade-row.sell .price { color: #E84242; }
.trade-row.large {
background: rgba(255, 215, 0, 0.05);
font-weight: 600;
}
.trade-row.large.buy { background: rgba(0, 177, 94, 0.15); }
.trade-row.large.sell { background: rgba(232, 66, 66, 0.15); }
@keyframes flash-green {
0% { background-color: rgba(0, 177, 94, 0.4); }
100% { background-color: transparent; }
}
.trade-row.flash { animation: flash-green 0.5s ease-out; }
`;
Time-based Aggregation
For less dense markets — combine trades over short interval (100–500 ms):
class TradeAggregator {
private buffer: Trade[] = [];
private flushInterval: number = 100; // ms
private onFlush: (aggregated: AggregatedTrade[]) => void;
add(trade: Trade) {
this.buffer.push(trade);
}
private flush() {
if (this.buffer.length === 0) return;
// Group by price (tick) and side
const groups = new Map<string, AggregatedTrade>();
for (const trade of this.buffer) {
const key = `${trade.price}:${trade.side}`;
const existing = groups.get(key);
if (existing) {
existing.quantity += trade.quantity;
existing.value += trade.value;
existing.count++;
} else {
groups.set(key, { ...trade, count: 1 });
}
}
this.onFlush([...groups.values()].sort((a, b) => b.timestamp - a.timestamp));
this.buffer = [];
}
}
Trade Flow Statistics
Useful to show aggregated stats next to the tape for last N seconds:
function TradeFlowStats({ trades }: { trades: Trade[] }) {
const stats = useMemo(() => {
const last60s = trades.filter(t => Date.now() - t.timestamp < 60000);
const buyVolume = last60s.filter(t => t.side === 'buy')
.reduce((sum, t) => sum + t.value, 0);
const sellVolume = last60s.filter(t => t.side === 'sell')
.reduce((sum, t) => sum + t.value, 0);
return {
buyVolume,
sellVolume,
delta: buyVolume - sellVolume,
buyPercent: buyVolume / (buyVolume + sellVolume) * 100,
};
}, [trades]);
return (
<div className="flow-stats">
<span className="green">Buy: ${formatVolume(stats.buyVolume)}</span>
<FlowBar buyPct={stats.buyPercent} />
<span className="red">Sell: ${formatVolume(stats.sellVolume)}</span>
<span className={stats.delta > 0 ? 'green' : 'red'}>
Δ {stats.delta > 0 ? '+' : ''}${formatVolume(Math.abs(stats.delta))}
</span>
</div>
);
}
Development of Time & Sales tape with real-time WebSocket updates, filtering, large block highlighting and flow statistics: 3–4 weeks.







