Moonbase Docs

Trading Operations

Create and cancel orders via WebSocket

Overview

The WebSocket API allows you to create and cancel orders directly through the WebSocket connection. This provides lower latency compared to REST API calls and enables you to receive immediate order updates through the orders channel.

Authentication Required: You must authenticate before performing trading operations. See Authentication for details.

Create Order

Create a new order using the create_order operation.

Request Format

{
  "op": "create_order",
  "request_id": "my-request-123",
  "data": {
    "type": "limit",
    "side": "buy",
    "product_id": "BTC-VND",
    "price": "3100000000",
    "size": "0.1",
    "time_in_force": "GTC",
    "post_only": false,
    "client_order_id": "my-order-456"
  }
}

Parameters

FieldTypeRequiredDescription
opstringYesMust be "create_order"
request_idstringNoUnique identifier for this request (for tracking responses)
dataobjectYesOrder parameters

Order Data Fields

FieldTypeRequiredDescription
typestringYesOrder type: "limit" or "market"
sidestringYesOrder side: "buy" or "sell"
product_idstringYesTrading pair (e.g., "BTC-VND")
pricestringFor limitOrder price (required for limit orders)
sizestringConditionalOrder size in base currency
quote_sizestringConditionalOrder size in quote currency (for market buy)
exact_quote_sizestringConditionalExact quote size (for market buy)
time_in_forcestringNo"GTC", "IOC", or "FOK" (default: "GTC")
post_onlybooleanNoPost-only flag (default: false)
stpstringNoSelf-trade prevention: "DC", "CO", "CN", "CB"
client_order_idstringNoYour custom order identifier
stop_trigger_pricestringNoStop order trigger price
expired_atintegerNoOrder expiration timestamp (nanoseconds)
scheduled_atintegerNoScheduled order activation time (nanoseconds)
waitbooleanNoWait for order confirmation (default: false)

Time In Force Options

  • GTC (Good-Till-Cancelled) - Remains active until filled or cancelled
  • IOC (Immediate-Or-Cancel) - Fills immediately or cancels unfilled portion
  • FOK (Fill-Or-Kill) - Fills completely or cancels entirely

Self-Trade Prevention (STP)

  • DC (Decrease and Cancel) - Reduce size of both orders
  • CO (Cancel Oldest) - Cancel the older order
  • CN (Cancel Newest) - Cancel the newer order
  • CB (Cancel Both) - Cancel both orders

Response

Success

If the order is successfully submitted, you'll receive updates through the orders channel:

{
  "channel": "orders",
  "type": "update",
  "data": [{
    "id": "0x01234...",
    "client_order_id": "my-order-456",
    "status": "pending",
    ...
  }]
}

Error

If there's an error creating the order:

{
  "channel": "orders",
  "request_id": "my-request-123",
  "type": "error",
  "message": "insufficient balance",
  "code": 400,
  "data": {
    "type": "limit",
    "side": "buy",
    "product_id": "BTC-VND",
    "price": "3100000000",
    "size": "0.1"
  }
}

Example: Create Limit Order

// Create a limit buy order
ws.send(JSON.stringify({
  op: 'create_order',
  request_id: `req_${Date.now()}`,
  data: {
    type: 'limit',
    side: 'buy',
    product_id: 'BTC-VND',
    price: '3100000000',
    size: '0.1',
    time_in_force: 'GTC',
    post_only: true,
    client_order_id: `order_${Date.now()}`
  }
}));

Example: Create Market Order

// Market buy using quote size
ws.send(JSON.stringify({
  op: 'create_order',
  request_id: `req_${Date.now()}`,
  data: {
    type: 'market',
    side: 'buy',
    product_id: 'BTC-VND',
    quote_size: '100000000',  // 100M VND
    time_in_force: 'IOC'
  }
}));

// Market sell using size
ws.send(JSON.stringify({
  op: 'create_order',
  request_id: `req_${Date.now()}`,
  data: {
    type: 'market',
    side: 'sell',
    product_id: 'BTC-VND',
    size: '0.05',  // 0.05 BTC
    time_in_force: 'IOC'
  }
}));

Cancel Order

Cancel an existing order using the cancel_order operation.

Request Format

{
  "op": "cancel_order",
  "request_id": "cancel-req-123",
  "data": {
    "order_id": "0x01234..."
  }
}

Or cancel by client order ID:

{
  "op": "cancel_order",
  "request_id": "cancel-req-123",
  "data": {
    "client_order_id": "my-order-456"
  }
}

Parameters

FieldTypeRequiredDescription
opstringYesMust be "cancel_order"
request_idstringNoUnique identifier for this request
dataobjectYesCancel parameters

Cancel Data Fields

FieldTypeRequiredDescription
order_idstringConditionalOrder ID to cancel
client_order_idstringConditionalClient order ID to cancel

You must provide either order_id or client_order_id, but not both.

Response

Success

If the cancel request is successful, you'll receive an update through the orders channel:

{
  "channel": "orders",
  "type": "update",
  "data": [{
    "id": "0x01234...",
    "status": "cancelled",
    "cancel_requested_at": "1698765440000000000",
    ...
  }]
}

Error

If there's an error cancelling the order:

{
  "channel": "orders",
  "request_id": "cancel-req-123",
  "type": "error",
  "message": "order not found",
  "code": 404,
  "data": {
    "order_id": "0x01234..."
  }
}

Example: Cancel by Order ID

ws.send(JSON.stringify({
  op: 'cancel_order',
  request_id: `cancel_${Date.now()}`,
  data: {
    order_id: '0x01234567890abcdef'
  }
}));

Example: Cancel by Client Order ID

ws.send(JSON.stringify({
  op: 'cancel_order',
  request_id: `cancel_${Date.now()}`,
  data: {
    client_order_id: 'my-order-456'
  }
}));

Complete Trading Example

const WebSocket = require('ws');
const crypto = require('crypto');

class WebSocketTrader {
  constructor(apiKey, apiSecret) {
    this.apiKey = apiKey;
    this.apiSecret = apiSecret;
    this.ws = null;
    this.authenticated = false;
    this.orderCallbacks = new Map();
  }

  async connect() {
    this.ws = new WebSocket('wss://ws.dev.mbhq.net/ws');

    this.ws.on('open', () => {
      console.log('Connected to WebSocket');
      this.authenticate();
    });

    this.ws.on('message', (data) => {
      this.handleMessage(JSON.parse(data));
    });

    this.ws.on('error', (error) => {
      console.error('WebSocket error:', error);
    });

    this.ws.on('close', () => {
      console.log('WebSocket closed');
      this.authenticated = false;
    });
  }

  authenticate() {
    const timestamp = Math.floor(Date.now() / 1000);
    const message = `${this.apiKey},${timestamp}`;
    const signature = crypto
      .createHmac('sha256', this.apiSecret)
      .update(message)
      .digest('hex');

    this.ws.send(JSON.stringify({
      op: 'auth',
      data: {
        key: this.apiKey,
        timestamp: timestamp,
        signature: signature
      }
    }));
  }

  handleMessage(message) {
    // Handle authentication
    if (message.channel === 'auth' && message.type === 'authenticated') {
      console.log('Authenticated successfully');
      this.authenticated = true;
      this.subscribeToOrders();
      return;
    }

    // Handle order updates
    if (message.channel === 'orders') {
      if (message.type === 'error') {
        console.error('Order error:', message.message);
        const callback = this.orderCallbacks.get(message.request_id);
        if (callback) {
          callback(new Error(message.message), null);
          this.orderCallbacks.delete(message.request_id);
        }
      } else if (message.type === 'update') {
        message.data.forEach(order => {
          console.log(`Order ${order.id}: ${order.status}`);
          if (order.client_order_id) {
            const callback = this.orderCallbacks.get(order.client_order_id);
            if (callback) {
              callback(null, order);
              if (order.status === 'filled' || order.status === 'cancelled') {
                this.orderCallbacks.delete(order.client_order_id);
              }
            }
          }
        });
      }
    }
  }

  subscribeToOrders() {
    this.ws.send(JSON.stringify({
      op: 'sub',
      channel: 'orders'
    }));
  }

  createOrder(orderData, callback) {
    if (!this.authenticated) {
      return callback(new Error('Not authenticated'), null);
    }

    const requestId = `req_${Date.now()}_${Math.random()}`;
    const clientOrderId = orderData.client_order_id || requestId;

    this.orderCallbacks.set(clientOrderId, callback);

    this.ws.send(JSON.stringify({
      op: 'create_order',
      request_id: requestId,
      data: {
        ...orderData,
        client_order_id: clientOrderId
      }
    }));

    // Timeout after 30 seconds
    setTimeout(() => {
      if (this.orderCallbacks.has(clientOrderId)) {
        this.orderCallbacks.delete(clientOrderId);
        callback(new Error('Order timeout'), null);
      }
    }, 30000);
  }

  cancelOrder(orderIdOrClientId, callback) {
    if (!this.authenticated) {
      return callback(new Error('Not authenticated'), null);
    }

    const requestId = `cancel_${Date.now()}_${Math.random()}`;
    const isOrderId = orderIdOrClientId.startsWith('0x');

    this.ws.send(JSON.stringify({
      op: 'cancel_order',
      request_id: requestId,
      data: isOrderId
        ? { order_id: orderIdOrClientId }
        : { client_order_id: orderIdOrClientId }
    }));

    // Set up callback for cancel confirmation
    this.orderCallbacks.set(orderIdOrClientId, callback);
  }
}

// Usage
const trader = new WebSocketTrader('your_api_key', 'your_api_secret');

trader.connect();

// Wait for authentication, then create an order
setTimeout(() => {
  trader.createOrder({
    type: 'limit',
    side: 'buy',
    product_id: 'BTC-VND',
    price: '3100000000',
    size: '0.1',
    post_only: true
  }, (error, order) => {
    if (error) {
      console.error('Order creation failed:', error);
    } else {
      console.log('Order created:', order);

      // Cancel the order after 5 seconds
      setTimeout(() => {
        trader.cancelOrder(order.id, (err, cancelledOrder) => {
          if (err) {
            console.error('Cancel failed:', err);
          } else {
            console.log('Order cancelled:', cancelledOrder);
          }
        });
      }, 5000);
    }
  });
}, 2000);

Error Codes

Common error codes returned by trading operations:

CodeDescription
400Bad request (invalid parameters)
401Unauthorized (authentication required or failed)
403Forbidden (read-only API key, trading not allowed)
404Not found (order not found)
409Conflict (insufficient balance, position limit)
500Internal server error

Common Error Messages

Create Order Errors

  • "insufficient balance" - Not enough funds for the order
  • "invalid product" - Product ID not supported
  • "invalid price" - Price is invalid or out of range
  • "invalid size" - Size is invalid or below minimum
  • "post only would match" - Post-only order would match immediately
  • "read-only api key" - API key doesn't have write permissions

Cancel Order Errors

  • "order not found" - Order ID or client order ID not found
  • "order already done" - Order is already filled or cancelled
  • "missing order id, client order id" - Neither ID provided

Best Practices

  1. Use Request IDs - Track requests with unique request_id values
  2. Use Client Order IDs - Track your orders with client_order_id
  3. Subscribe to Orders - Always subscribe to orders channel to receive updates
  4. Handle Errors - Implement proper error handling for all operations
  5. Validate Before Sending - Check balances and parameters before creating orders
  6. Implement Timeouts - Set timeouts for order operations
  7. Use Post-Only for Makers - Set post_only: true for maker orders
  8. Check Read-Only Keys - Ensure API key has write permissions

Advantages Over REST API

  1. Lower Latency - No need to establish new HTTP connections
  2. Immediate Updates - Receive order updates through the same connection
  3. Connection Reuse - Single WebSocket for both trading and data feeds
  4. Reduced Overhead - Less protocol overhead compared to HTTP
  5. Bidirectional - Send and receive data simultaneously