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();