Skip to main content

Overview

The WebSocket API broadcasts three types of real-time updates: depth updates, ticker updates, and trade updates. All messages are sent as JSON-encoded strings.

Message Structure

All outgoing WebSocket messages follow this general structure:
{
  stream: string;      // Channel identifier
  data: {
    e: string;         // Event type: "depth", "ticker", or "trade"
    // ... event-specific fields
  }
}

Depth Updates

Depth updates contain order book bid and ask data for an event. These updates are sent whenever the order book changes due to new orders, cancellations, or trades.

Message Format

stream
string
required
Channel identifier, typically in format "event_orderbook_{eventId}"
data
object
required
Depth update data
data.e
string
required
Event type, always "depth"
data.b
[string, string][]
Array of bid levels [price, quantity]. Optional - only present when bids change.
data.a
[string, string][]
Array of ask levels [price, quantity]. Optional - only present when asks change.
data.id
number
required
Event ID for this depth update

Example Message

Depth Update
{
  "stream": "event_orderbook_3169798",
  "data": {
    "e": "depth",
    "b": [
      ["5.5", "100"],
      ["5.4", "250"],
      ["5.3", "180"]
    ],
    "a": [
      ["5.6", "150"],
      ["5.7", "200"],
      ["5.8", "120"]
    ],
    "id": 3169798
  }
}

Understanding Depth Data

Bids represent buy orders. Each entry is a tuple of [price, quantity]:
  • price: The price buyers are willing to pay (as string)
  • quantity: Total quantity available at that price (as string)
Bids are typically sorted from highest to lowest price.
Asks represent sell orders. Each entry is a tuple of [price, quantity]:
  • price: The price sellers are asking for (as string)
  • quantity: Total quantity available at that price (as string)
Asks are typically sorted from lowest to highest price.
Depth updates may contain only b (bids changed), only a (asks changed), or both. If a field is absent, it means that side of the order book hasn’t changed.

Handling Depth Updates

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.data.e === 'depth') {
    const { b: bids, a: asks, id: eventId } = message.data;
    
    // Update order book display
    if (bids) {
      updateBids(eventId, bids);
    }
    
    if (asks) {
      updateAsks(eventId, asks);
    }
  }
};

function updateBids(eventId, bids) {
  bids.forEach(([price, quantity]) => {
    console.log(`Event ${eventId} - Bid: ${quantity} @ ${price}`);
    // Update UI with bid data
  });
}

function updateAsks(eventId, asks) {
  asks.forEach(([price, quantity]) => {
    console.log(`Event ${eventId} - Ask: ${quantity} @ ${price}`);
    // Update UI with ask data
  });
}

Ticker Updates

Ticker updates provide aggregated price and volume statistics for an event.

Message Format

stream
string
required
Channel identifier for the ticker stream
data
object
required
Ticker data
data.e
string
required
Event type, always "ticker"
data.id
number
required
Event ID for this ticker update
data.c
string
Current/close price
data.h
string
High price in the period
data.l
string
Low price in the period
data.v
string
Volume traded
data.V
string
Quote volume traded
data.s
string
Symbol/market identifier

Example Message

Ticker Update
{
  "stream": "ticker_3169798",
  "data": {
    "e": "ticker",
    "id": 3169798,
    "c": "5.55",
    "h": "5.80",
    "l": "5.20",
    "v": "12500",
    "V": "68750",
    "s": "EVENT_3169798"
  }
}

Handling Ticker Updates

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.data.e === 'ticker') {
    const { c, h, l, v, id } = message.data;
    
    console.log(`Event ${id} Ticker:`);
    console.log(`  Current: ${c}`);
    console.log(`  High: ${h}`);
    console.log(`  Low: ${l}`);
    console.log(`  Volume: ${v}`);
    
    // Update ticker display in UI
    updateTickerDisplay(id, { current: c, high: h, low: l, volume: v });
  }
};

Trade Updates

Trade updates are broadcast when orders are matched and trades are executed.

Message Format

stream
string
required
Channel identifier for the trade stream
data
object
required
Trade data
data.e
string
required
Event type, always "trade"
data.t
string
required
Trade ID
data.m
boolean
required
Whether the buyer is the maker (true) or taker (false)
data.p
number
required
Trade price
data.q
string
required
Trade quantity
data.s
string
required
Symbol/market identifier

Example Message

Trade Update
{
  "stream": "trade_3169798",
  "data": {
    "e": "trade",
    "t": "TRD_550e8400-e29b-41d4-a716-446655440000",
    "m": true,
    "p": 5.55,
    "q": "100",
    "s": "EVENT_3169798"
  }
}

Understanding Trade Data

Unique identifier for the trade. Format: TRD_{uuid}
  • true: The buyer placed a limit order that was matched (maker)
  • false: The buyer placed a market order or aggressive limit order (taker)
Makers typically pay lower fees as they provide liquidity.
The price at which the trade was executed (as number).
The quantity of contracts traded (as string).

Handling Trade Updates

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  
  if (message.data.e === 'trade') {
    const { t: tradeId, m: isBuyerMaker, p: price, q: quantity, s: symbol } = message.data;
    
    console.log(`Trade Executed:`);
    console.log(`  ID: ${tradeId}`);
    console.log(`  Symbol: ${symbol}`);
    console.log(`  Price: ${price}`);
    console.log(`  Quantity: ${quantity}`);
    console.log(`  Buyer is Maker: ${isBuyerMaker}`);
    
    // Add trade to recent trades list
    addTradeToHistory({
      id: tradeId,
      price: price,
      quantity: parseFloat(quantity),
      isBuyerMaker: isBuyerMaker,
      timestamp: Date.now()
    });
  }
};

function addTradeToHistory(trade) {
  // Add to trade history and update UI
  console.log('Trade added to history:', trade);
}

Complete WebSocket Handler

Here’s a complete example that handles all message types:
import type { 
  DepthUpdateMessage, 
  TickerUpdateMessage, 
  TradeAddedMessage,
  WsMessage 
} from '@opinix/types';

class OpinixWebSocketClient {
  private ws: WebSocket;
  private subscriptions = new Set<string>();
  private orderBooks = new Map<number, any>();
  private tickers = new Map<number, any>();
  private trades = new Map<string, any[]>();
  
  constructor(url: string) {
    this.ws = new WebSocket(url);
    this.setupHandlers();
  }
  
  private setupHandlers() {
    this.ws.onopen = () => {
      console.log('✓ Connected to Opinix Trade WebSocket');
    };
    
    this.ws.onmessage = (event: MessageEvent) => {
      try {
        const message: WsMessage = JSON.parse(event.data);
        this.handleMessage(message);
      } catch (error) {
        console.error('Failed to parse WebSocket message:', error);
      }
    };
    
    this.ws.onerror = (error: Event) => {
      console.error('WebSocket error:', error);
    };
    
    this.ws.onclose = () => {
      console.log('✗ Disconnected from WebSocket');
      this.reconnect();
    };
  }
  
  private handleMessage(message: WsMessage) {
    const eventType = message.data?.e;
    
    switch (eventType) {
      case 'depth':
        this.handleDepth(message as DepthUpdateMessage);
        break;
      case 'ticker':
        this.handleTicker(message as TickerUpdateMessage);
        break;
      case 'trade':
        this.handleTrade(message as TradeAddedMessage);
        break;
      default:
        console.log('Unknown message type:', message);
    }
  }
  
  private handleDepth(message: DepthUpdateMessage) {
    const { b: bids, a: asks, id } = message.data;
    console.log(`📊 Depth update for event ${id}`);
    
    // Store or update order book
    let orderBook = this.orderBooks.get(id) || { bids: [], asks: [] };
    if (bids) orderBook.bids = bids;
    if (asks) orderBook.asks = asks;
    this.orderBooks.set(id, orderBook);
  }
  
  private handleTicker(message: TickerUpdateMessage) {
    const { id, c, h, l, v } = message.data;
    console.log(`📈 Ticker update for event ${id}: ${c}`);
    
    // Store ticker data
    this.tickers.set(id, { current: c, high: h, low: l, volume: v });
  }
  
  private handleTrade(message: TradeAddedMessage) {
    const { t: tradeId, p: price, q: quantity, s: symbol } = message.data;
    console.log(`🤝 Trade executed: ${quantity} @ ${price}`);
    
    // Store trade
    const eventId = symbol.split('_')[1] || symbol;
    let eventTrades = this.trades.get(eventId) || [];
    eventTrades.unshift({ id: tradeId, price, quantity, timestamp: Date.now() });
    this.trades.set(eventId, eventTrades.slice(0, 100)); // Keep last 100
  }
  
  private reconnect() {
    setTimeout(() => {
      console.log('Reconnecting...');
      this.ws = new WebSocket('ws://wss.opinix.trade');
      this.setupHandlers();
      
      // Resubscribe to previous subscriptions
      if (this.subscriptions.size > 0) {
        this.ws.onopen = () => {
          this.subscribe(Array.from(this.subscriptions));
        };
      }
    }, 3000);
  }
  
  subscribe(eventIds: string[]) {
    eventIds.forEach(id => this.subscriptions.add(id));
    
    this.ws.send(JSON.stringify({
      method: 'subscribe_orderbook',
      events: eventIds
    }));
    
    console.log(`📡 Subscribed to events: ${eventIds.join(', ')}`);
  }
  
  unsubscribe(eventIds: string[]) {
    eventIds.forEach(id => this.subscriptions.delete(id));
    
    this.ws.send(JSON.stringify({
      method: 'unsubscribe_orderbook',
      events: eventIds
    }));
    
    console.log(`📴 Unsubscribed from events: ${eventIds.join(', ')}`);
  }
  
  close() {
    this.ws.close();
  }
}

// Usage
const client = new OpinixWebSocketClient('ws://wss.opinix.trade');

// Subscribe to events when connected
client.subscribe(['3169798', '3169799']);

// Later, unsubscribe from specific events
// client.unsubscribe(['3169799']);

Type Definitions

Based on packages/types/src/index.ts:170-209:
export type TickerUpdateMessage = {
  stream: string;
  data: {
    c?: string;  // current/close price
    h?: string;  // high price
    l?: string;  // low price
    v?: string;  // volume
    V?: string;  // quote volume
    s?: string;  // symbol
    id: number;  // event ID
    e: "ticker";
  };
};

export type DepthUpdateMessage = {
  stream: string;
  data: {
    b?: [string, string][]; // bids [price, quantity]
    a?: [string, string][]; // asks [price, quantity]
    e: "depth";
  };
};

export type TradeAddedMessage = {
  stream: string;
  data: {
    e: "trade";
    t: string;   // trade ID
    m: boolean;  // is buyer maker
    p: number;   // price
    q: string;   // quantity
    s: string;   // symbol
  };
};

export type WsMessage =
  | TickerUpdateMessage
  | DepthUpdateMessage
  | TradeAddedMessage;

Implementation Notes

Source Code Reference:
  • Message type definitions: packages/types/src/index.ts:170-209
  • WebSocket types: services/wss/src/types/index.ts:18-28
  • Engine types: packages/types/src/index.ts:46-95

Message Broadcasting

WebSocket messages are broadcast via Redis pub/sub. When events occur in the matching engine:
  1. Engine publishes message to Redis channel
  2. WebSocket server receives message from Redis
  3. Server broadcasts to all subscribed clients

Price and Quantity Formats

Note the inconsistent type usage:
  • Depth updates: prices and quantities as strings (["5.5", "100"])
  • Trade updates: price as number, quantity as string
  • Always parse values before calculations

Next Steps