Order Updates
Stream real-time order submissions, partial fills, completions, cancellations, and rejections via WebSocket.
Overview
Order update streams provide real-time notifications for all order-related events including submissions, fills, cancellations, and rejections. Use these updates to maintain accurate order state and implement trading algorithms.
Update format
Response
{
"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
}
}dataobjectOrder update details
Order status values
Active Statuses
pendingsubmit: Order created but not yet submitted to matching engine
waitingtrigger: Order waiting for trigger condition (stop/take profit orders)
submitted: Order submitted to matching engine and waiting for execution
Execution Statuses
partiallyfilled: Order partially executed, remainder still active
filled: Order completely executed
partiallyfilledcancelled: Order partially filled then cancelled
Final Statuses
cancelled: Order cancelled by user or system
rejected: Order rejected (insufficient balance, invalid parameters, etc.)
inactive: Order is inactive due to account restrictions
pendingcancel: Order cancellation requested but not yet confirmed
Key field usage
Execution Tracking Fields
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.
Order Identification
clientOrderId: Your custom identifier for the order. Useful for matching order updates to your internal order management system.
entryPositionId and 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.
Error Handling
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
Algorithmic Trading
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);
}
}Risk Management
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);
}
}
}Order Management UI
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
Connection Loss
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);
}
}
}Duplicate Updates
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
- Learn about Balance Updates for account balance tracking
- Explore Position Updates for position monitoring
- Review Connection Authentication for secure access
Last modified on