Skip to main content

Overview

Order update streams provide real-time notifications for all order-related events including submissions, fills, cancellations, and rejections. These updates are essential for maintaining accurate order state and implementing trading algorithms.
Order update streams require authentication and an explicit subscription to the orders channel.
Related Events: When an order fills, you may receive additional account events (position and balance updates) regardless of your subscriptions. This ensures you don’t miss account-wide changes triggered by order execution.

Update Format

{
  "type": "order",
  "data": {
    "orderId": "01981061-787c-7fd0-b707-18fa252f0d0b",
    "accountId": "f2268d92-b3d8-48b1-8541-90dc250cd7e9", 
    "symbol": "GOLD-BTC",
    "status": "partiallyfilled",
    "remainingQty": "0.50",
    "executedQty": "0.50", 
    "avgPx": "50125.50",
    "fillPx": "50125.50",
    "filledFees": "0.00002506",
    "side": "buy",
    "orderType": "limit",
    "orderSize": "1.00",
    "limitPrice": "50200.00",
    "clientOrderId": "my-order-123",
    "entryPositionId": "pos-uuid-entry",
    "exitPositionId": null,
    "reason": null,
    "timestamp": 1752621480060801000
  }
}
data
object
Order update details

Order Status Values

pendingsubmit: Order created but not yet submitted to matching enginewaitingtrigger: Order waiting for trigger condition (stop/take profit orders)submitted: Order submitted to matching engine and waiting for execution
partiallyfilled: Order partially executed, remainder still activefilled: Order completely executedpartiallyfilledcancelled: Order partially filled then cancelled
cancelled: Order cancelled by user or systemrejected: Order rejected (insufficient balance, invalid parameters, etc.)inactive: Order is inactive due to account restrictionspendingcancel: Order cancellation requested but not yet confirmed

Key Field Usage

fillPx: The price of the most recent fill. Useful for tracking individual fill prices, especially for orders that fill at multiple price levels.filledFees: Total fees accumulated from all fills. Essential for calculating net proceeds and tracking trading costs.avgPx vs fillPx: avgPx shows the volume-weighted average price across all fills, while fillPx shows the price of the latest individual fill.
clientOrderId: Your custom identifier for the order. Useful for matching order updates to your internal order management system.entryPositionId/exitPositionId: Links orders to specific positions. entryPositionId is set when opening a position, exitPositionId when closing.orderType: Shows whether the order was limit, market, stop, etc. Helpful for strategy analysis.
reason: Only present for rejected orders. Provides specific rejection reasons like “insufficient balance”, “invalid price”, etc.side: Explicitly shows buy or sell to avoid ambiguity.orderSize: The original order size, useful for calculating fill percentages and remaining quantities.

Implementation Examples

JavaScript Order Manager

class OrderManager {
  constructor(apiKey, apiSecret) {
    this.apiKey = apiKey;
    this.apiSecret = apiSecret;
    this.orders = new Map(); // orderId -> order data
    this.ordersBySymbol = new Map(); // symbol -> Set of orderIds
    this.ws = null;
    this.listeners = [];
  }

  connect() {
    const signature = this.generateSignature('GET:/ws');

    this.ws = new WebSocket('wss://ws.roxom.com/ws', [], {
      headers: {
        'X-API-Key': this.apiKey,
        'X-API-Signature': signature
      }
    });

    this.ws.onopen = () => {
      console.log('🔐 Connected to order updates stream');
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.type === 'order') {
        this.handleOrderUpdate(data.data);
      }
    };

    this.ws.onclose = () => {
      console.log('❌ Order stream disconnected');
      this.reconnect();
    };
  }

  generateSignature(timestamp) {
    const crypto = require('crypto');
    return crypto
      .createHmac('sha256', this.apiSecret)
      .update(timestamp)
      .digest('base64');
  }

  handleOrderUpdate(orderData) {
    const orderId = orderData.orderId;
    const status = orderData.status;
    const symbol = orderData.symbol;

    // Update order tracking
    this.updateOrderTracking(orderData);

    console.log(`📋 Order Update: ${orderId} (${symbol}) -> ${status}`);

    // Handle different order statuses
    switch (status) {
      case 'submitted':
        this.onOrderSubmitted(orderData);
        break;
      case 'partiallyfilled':
        this.onOrderPartiallyFilled(orderData);
        break;
      case 'filled':
        this.onOrderFilled(orderData);
        break;
      case 'cancelled':
        this.onOrderCancelled(orderData);
        break;
      case 'rejected':
        this.onOrderRejected(orderData);
        break;
    }

    // Notify listeners
    this.notifyListeners('orderUpdate', orderData);
  }

  updateOrderTracking(orderData) {
    const orderId = orderData.orderId;
    const symbol = orderData.symbol;

    // Update main order record
    this.orders.set(orderId, {
      ...this.orders.get(orderId),
      ...orderData,
      lastUpdated: Date.now()
    });

    // Update symbol-based index
    if (!this.ordersBySymbol.has(symbol)) {
      this.ordersBySymbol.set(symbol, new Set());
    }
    this.ordersBySymbol.get(symbol).add(orderId);

    // Remove from tracking if order is final
    if (this.isFinalStatus(orderData.status)) {
      this.cleanupOrder(orderId, symbol);
    }
  }

  isFinalStatus(status) {
    return ['filled', 'cancelled', 'rejected', 'partiallyfilledcancelled'].includes(status);
  }

  cleanupOrder(orderId, symbol) {
    // Keep order data but mark as completed
    const order = this.orders.get(orderId);
    if (order) {
      order.isCompleted = true;
      order.completedAt = Date.now();
    }

    // Remove from active symbol tracking
    const symbolOrders = this.ordersBySymbol.get(symbol);
    if (symbolOrders) {
      symbolOrders.delete(orderId);
      if (symbolOrders.size === 0) {
        this.ordersBySymbol.delete(symbol);
      }
    }
  }

  onOrderSubmitted(order) {
    console.log(`✅ Order ${order.orderId} submitted to market`);
    this.updateOrderUI(order.orderId, 'pending');
    
    // Track submission metrics
    this.trackMetric('orderSubmitted', order);
  }

  onOrderPartiallyFilled(order) {
    const fillPercent = this.calculateFillPercent(order);
    const fillAmount = parseFloat(order.executedQty);
    const avgPrice = parseFloat(order.avgPx);
    const lastFillPrice = parseFloat(order.fillPx);
    const totalFees = parseFloat(order.filledFees);
    const clientOrderId = order.clientOrderId;
    
    console.log(`🟡 Order ${order.orderId}${clientOrderId ? ` (${clientOrderId})` : ''} ${fillPercent.toFixed(1)}% filled`);
    console.log(`   - ${fillAmount} at avg price ${avgPrice} (last fill: ${lastFillPrice})`);
    console.log(`   - Total fees: ${totalFees} BTC`);
    
    this.updateOrderUI(order.orderId, 'partial', fillPercent);
    this.trackMetric('orderPartialFill', order);
    
    // Calculate unrealized value and net proceeds
    const filledValue = fillAmount * avgPrice;
    const netProceeds = filledValue - totalFees;
    console.log(`💰 Filled value: $${filledValue.toFixed(2)}, Net: $${netProceeds.toFixed(2)}`);
  }

  onOrderFilled(order) {
    const totalFilled = parseFloat(order.executedQty);
    const avgPrice = parseFloat(order.avgPx);
    const lastFillPrice = parseFloat(order.fillPx);
    const totalFees = parseFloat(order.filledFees);
    const clientOrderId = order.clientOrderId;
    const orderType = order.orderType;
    const totalValue = totalFilled * avgPrice;
    const netProceeds = totalValue - totalFees;
    
    console.log(`🟢 Order ${order.orderId}${clientOrderId ? ` (${clientOrderId})` : ''} completely filled`);
    console.log(`   - ${totalFilled} ${order.symbol} at avg price ${avgPrice} (last fill: ${lastFillPrice})`);
    console.log(`   - Order type: ${orderType}, Side: ${order.side}`);
    console.log(`   - Total value: $${totalValue.toFixed(2)}, Fees: ${totalFees} BTC, Net: $${netProceeds.toFixed(2)}`);
    
    this.updateOrderUI(order.orderId, 'filled');
    this.trackMetric('orderFilled', order);
    
    // Trigger post-fill logic with enhanced data
    this.onTradingComplete(order);
  }

  onOrderCancelled(order) {
    const remainingQty = parseFloat(order.remainingQty);
    
    console.log(`🔴 Order ${order.orderId} cancelled - ${remainingQty} remaining`);
    
    this.updateOrderUI(order.orderId, 'cancelled');
    this.trackMetric('orderCancelled', order);
  }

  onOrderRejected(order) {
    const clientOrderId = order.clientOrderId;
    const rejectionReason = order.reason || 'Unknown reason';
    
    console.log(`❌ Order ${order.orderId}${clientOrderId ? ` (${clientOrderId})` : ''} rejected`);
    console.log(`   - Reason: ${rejectionReason}`);
    console.log(`   - Order details: ${order.orderSize} ${order.symbol} ${order.side} ${order.orderType}`);
    
    this.updateOrderUI(order.orderId, 'rejected');
    this.trackMetric('orderRejected', order);
    
    // Handle rejection analysis with reason
    this.analyzeRejection(order, rejectionReason);
  }

  calculateFillPercent(order) {
    const executed = parseFloat(order.executedQty);
    const remaining = parseFloat(order.remainingQty);
    const total = executed + remaining;
    
    return total > 0 ? (executed / total) * 100 : 0;
  }

  updateOrderUI(orderId, status, progress = null) {
    // Update DOM elements if available
    const element = document.getElementById(`order-${orderId}`);
    if (element) {
      element.className = `order-status-${status}`;
      
      if (progress !== null) {
        const progressBar = element.querySelector('.progress-bar');
        if (progressBar) {
          progressBar.style.width = `${progress}%`;
        }
      }
      
      const statusElement = element.querySelector('.status');
      if (statusElement) {
        statusElement.textContent = status.toUpperCase();
      }
    }
  }

  onTradingComplete(order) {
    console.log(`🎯 Trade completed: ${order.executedQty} ${order.symbol} at ${order.avgPx}`);
    
    // Trigger post-trade workflows
    this.notifyListeners('tradeCompleted', order);
    
    // Could trigger:
    // - Portfolio rebalancing
    // - Risk management checks  
    // - Profit/loss calculations
    // - Next order in strategy sequence
    // - Notification systems
  }

  analyzeRejection(order) {
    console.log(`🔍 Analyzing rejection for order ${order.orderId}`);
    
    // Common rejection analysis
    // - Check account balance
    // - Validate order parameters
    // - Check market hours
    // - Verify symbol availability
    
    this.notifyListeners('orderRejected', order);
  }

  trackMetric(eventType, order) {
    // Implement metrics tracking
    const metric = {
      timestamp: Date.now(),
      event: eventType,
      orderId: order.orderId,
      symbol: order.symbol,
      status: order.status
    };
    
    // Send to analytics system
    this.sendMetric(metric);
  }

  // Query methods
  getOrder(orderId) {
    return this.orders.get(orderId);
  }

  getOrdersBySymbol(symbol) {
    const orderIds = this.ordersBySymbol.get(symbol) || new Set();
    return Array.from(orderIds).map(id => this.orders.get(id)).filter(Boolean);
  }

  getActiveOrders() {
    return Array.from(this.orders.values()).filter(order => 
      !order.isCompleted && 
      ['submitted', 'partiallyfilled', 'pendingsubmit'].includes(order.status)
    );
  }

  getCompletedOrders(limit = 100) {
    return Array.from(this.orders.values())
      .filter(order => order.isCompleted)
      .sort((a, b) => b.completedAt - a.completedAt)
      .slice(0, limit);
  }

  // Event management
  addListener(callback) {
    this.listeners.push(callback);
  }

  notifyListeners(eventType, data) {
    this.listeners.forEach(callback => {
      try {
        callback(eventType, data);
      } catch (error) {
        console.error('Listener error:', error);
      }
    });
  }

  reconnect() {
    setTimeout(() => {
      console.log('🔄 Reconnecting to order stream...');
      this.connect();
    }, 5000);
  }

  sendMetric(metric) {
    // Implement metric sending logic
    console.log('📊 Metric:', metric);
  }
}

// Usage
const orderManager = new OrderManager(
  process.env.ROXOM_API_KEY,
  process.env.ROXOM_API_SECRET
);

// Add event listeners
orderManager.addListener((eventType, data) => {
  switch (eventType) {
    case 'orderUpdate':
      console.log(`Order ${data.orderId} updated: ${data.status}`);
      break;
    case 'tradeCompleted':
      console.log(`Trade completed: ${data.symbol} ${data.executedQty} @ ${data.avgPx}`);
      break;
    case 'orderRejected':
      console.log(`Order rejected: ${data.orderId}`);
      // Implement rejection handling logic
      break;
  }
});

orderManager.connect();

Order Lifecycle Tracking

Track complete order lifecycle with state transitions:
class OrderLifecycleTracker {
  constructor() {
    this.lifecycles = new Map(); // orderId -> lifecycle events
  }

  trackOrderEvent(orderData) {
    const orderId = orderData.orderId;
    
    if (!this.lifecycles.has(orderId)) {
      this.lifecycles.set(orderId, {
        orderId,
        symbol: orderData.symbol,
        events: [],
        startTime: Date.now(),
        endTime: null,
        totalDuration: null
      });
    }

    const lifecycle = this.lifecycles.get(orderId);
    
    // Add event to lifecycle
    lifecycle.events.push({
      status: orderData.status,
      timestamp: orderData.timestamp,
      executedQty: orderData.executedQty,
      remainingQty: orderData.remainingQty,
      avgPx: orderData.avgPx,
      recordedAt: Date.now()
    });

    // Mark completion if final status
    if (this.isFinalStatus(orderData.status)) {
      lifecycle.endTime = Date.now();
      lifecycle.totalDuration = lifecycle.endTime - lifecycle.startTime;
      
      this.analyzeLifecycle(lifecycle);
    }
  }

  analyzeLifecycle(lifecycle) {
    const events = lifecycle.events;
    const duration = lifecycle.totalDuration;
    
    console.log(`📈 Order Lifecycle Analysis - ${lifecycle.orderId}:`);
    console.log(`  Duration: ${duration}ms`);
    console.log(`  Events: ${events.length}`);
    console.log(`  Final Status: ${events[events.length - 1].status}`);
    
    // Calculate time to first fill
    const firstFill = events.find(e => e.status === 'partiallyfilled' || e.status === 'filled');
    if (firstFill) {
      const timeToFill = firstFill.recordedAt - lifecycle.startTime;
      console.log(`  Time to First Fill: ${timeToFill}ms`);
    }
    
    // Calculate fill efficiency
    if (events.some(e => e.status === 'filled')) {
      const fillEvents = events.filter(e => 
        e.status === 'partiallyfilled' || e.status === 'filled'
      );
      console.log(`  Fill Events: ${fillEvents.length}`);
    }
  }

  getLifecycle(orderId) {
    return this.lifecycles.get(orderId);
  }

  getAverageExecutionTime() {
    const completedLifecycles = Array.from(this.lifecycles.values())
      .filter(lc => lc.totalDuration !== null);
      
    if (completedLifecycles.length === 0) return 0;
    
    const totalDuration = completedLifecycles.reduce((sum, lc) => sum + lc.totalDuration, 0);
    return totalDuration / completedLifecycles.length;
  }

  isFinalStatus(status) {
    return ['filled', 'cancelled', 'rejected', 'partiallyfilledcancelled'].includes(status);
  }
}

Use Cases

Use order updates to trigger next steps in complex trading strategies:
function handleAlgorithmicTrade(orderData) {
  if (orderData.status === 'filled') {
    // Order completed, execute next strategy step
    executeNextStrategyStep(orderData);
  } else if (orderData.status === 'partiallyfilled') {
    // Partial fill, may need to adjust remaining order
    adjustRemainingOrder(orderData);
  } else if (orderData.status === 'rejected') {
    // Handle rejection in strategy
    handleStrategyRejection(orderData);
  }
}
Monitor order fills for risk limit enforcement:
function enforceRiskLimits(orderData) {
  if (orderData.status === 'filled' || orderData.status === 'partiallyfilled') {
    const position = calculateNewPosition(orderData);
    
    if (exceedsRiskLimits(position)) {
      console.log('🚨 Risk limit exceeded, reducing position');
      placeOffsetOrder(orderData);
    }
  }
}
Update trading interface with real-time order status:
function updateOrderUI(orderData) {
  const row = document.getElementById(`order-${orderData.orderId}`);
  if (row) {
    row.querySelector('.status').textContent = orderData.status;
    row.querySelector('.filled').textContent = orderData.executedQty;
    row.querySelector('.remaining').textContent = orderData.remainingQty;
    
    if (orderData.avgPx) {
      row.querySelector('.avg-price').textContent = orderData.avgPx;
    }
  }
}

Error Handling

Handle reconnection and state synchronization:
class RobustOrderManager extends OrderManager {
  onReconnected() {
    // Refresh order state after reconnection
    this.requestOrderSummary();
  }
  
  async requestOrderSummary() {
    try {
      const response = await fetch('/api/orders/active', {
        headers: { 'Authorization': `Bearer ${this.apiKey}` }
      });
      const orders = await response.json();
      
      // Sync local state with server state
      this.syncOrderState(orders);
    } catch (error) {
      console.error('Failed to sync order state:', error);
    }
  }
}
Handle potential duplicate order updates:
function handleOrderUpdate(orderData) {
  const lastUpdate = this.getLastUpdate(orderData.orderId);
  
  // Check if this is a duplicate update
  if (lastUpdate && lastUpdate.timestamp >= orderData.timestamp) {
    console.log('🔄 Ignoring duplicate order update');
    return;
  }
  
  // Process the update
  this.processOrderUpdate(orderData);
}

Next Steps

I