14. Batch Orders

Place and cancel multiple orders in one request and handle per-order results

Objective

Place and cancel multiple orders in one request signed with RSA (POST /order/batch and DELETE /order/batch), then read the result of each order. Matches Sample 14 in the tutorials repo (SDK v3.3.2). Requires OTP during authentication.

Warning: this sample sends real orders on the configured account. To stay safe, every order is a buy LO at the floor price (far from the market, it just rests in the book) with a small quantity, and the script asks you to type YES before sending. Still, double-check the account, symbol, price and quantity before running, and try UAT first.

Flow

Market Data (floor price) → Build batch → Trading API (place batch) → Per-order results → Trading API (cancel batch)
  1. Get the floor price from master data and build a list of BatchPlaceOrderItem
  2. Check the order count does not exceed Config.max_batch_orders (default 20)
  3. Call place_batch_orders(items) and handle the result of each order (success, error_code)
  4. Call cancel_batch_orders(items) for the orders that were placed successfully (BatchCancelOrderItem)

Samples in the repo

LanguageFile
Pythonpython/sample_14_batch_orders.py, python/sample_14_batch_orders_async.py
Node.jsnode/sample_14_batch_orders.js
.NETdotnet/Sample14BatchOrders.cs
Gogo/sample_14_batch_orders.go

This page uses Python for illustration. Repo: ssi-fastconnect-v3-tutorials.

Prerequisites

  • Credentials and account number (equity_account) in config.json, see Tutorial 1. The repo samples load the config through config.py; placeholders are used here to keep the code short.
  • auth_helper.py (token cache, Tutorial 1) and an OTP for the first login.
  • helpers.py from the repo: confirm_real_orders (warns and requires typing YES), mask_account (masks the account number when printing), require_account (stops if no account is configured) and enum_value (safe display string for enum fields).

Sample Code — Sync

The code below is condensed from the repo sample and keeps the same flow.

python/sample_14_batch_orders.py
import uuid
from decimal import Decimal

from ssi_sdk import Auth, Data, Trading, Config
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import APIError, DuplicateRequestError, ValidationError
from ssi_sdk.models import BatchCancelOrderItem, BatchPlaceOrderItem
from auth_helper import ensure_auth
from helpers import confirm_real_orders, enum_value, mask_account, require_account

config = Config(
    client_id="<your_client_id>",
    api_key="<your_api_key>",
    api_secret="<your_api_secret>",
    private_key="<your_private_key>",
)
ACCOUNT_NO = "<your_account_no>"
SYMBOL = "SSI"
QUANTITY = 100
NUM_ORDERS = 3

require_account(ACCOUNT_NO)
confirm_real_orders(f"{NUM_ORDERS} LO buy orders for {SYMBOL} (batch)")

with Auth(config) as auth:
    ensure_auth(auth, otp="<your_otp>")

    # Step 1: Use the floor price (order rests in the book, far from the market)
    with Data(auth) as data:
        master = next((m for m in data.market_data.get_master_data() if m.symbol == SYMBOL), None)
    if master is None or master.floor <= 0:
        raise SystemExit(f"No floor price for {SYMBOL}, stopping to stay safe.")
    floor_price = Decimal(str(master.floor))

    with Trading(auth) as trading:
        # Step 2: Build the batch, each order has its own client_request_id
        batch_tag = uuid.uuid4().hex[:8]
        place_items: list[BatchPlaceOrderItem] = [
            {
                "account_no": ACCOUNT_NO,
                "symbol": SYMBOL,
                "side": OrderSide.BUY,
                "quantity": QUANTITY,
                "price": floor_price,
                "order_type": OrderType.LO,
                "client_request_id": f"bt{batch_tag}{i:02d}",  # <= 20 chars
            }
            for i in range(1, NUM_ORDERS + 1)
        ]
        if len(place_items) > config.max_batch_orders:
            raise SystemExit(f"At most {config.max_batch_orders} orders / batch (Config.max_batch_orders).")

        # Step 3: Place the batch
        print(f"place_batch_orders: {len(place_items)} orders, account {mask_account(ACCOUNT_NO)}")
        try:
            response = trading.trading.place_batch_orders(place_items)
        except ValidationError as e:
            raise SystemExit(f"Invalid batch (nothing sent): {e}")
        except DuplicateRequestError as e:
            raise SystemExit(f"Batch id already used today (409), check the order book before resending: {e}")
        except APIError as e:
            raise SystemExit(f"Server rejected the batch: code={e.code} status={e.status_code} msg={e.message}")

        print(f"Batch ID: {response.batch_request_id}")
        placed_ok: list[str] = []
        for r in response.results:
            if r.success:
                placed_ok.append(r.order_id)
                print(f"  OK   {r.client_request_id} -> orderId={r.order_id} status={enum_value(r.status)}")
            else:
                print(f"  FAIL {r.client_request_id} -> code={r.error_code} msg={r.error_message}")

        # Step 4: Cancel the orders that were just placed
        if not placed_ok:
            print("No order was placed successfully, skipping cancel.")
        else:
            # Each item must have EXACTLY ONE of order_id / client_request_id
            cancel_items: list[BatchCancelOrderItem] = [
                {"account_no": ACCOUNT_NO, "order_id": oid} for oid in placed_ok
            ]
            print(f"cancel_batch_orders: {len(cancel_items)} orders")
            try:
                cancel_resp = trading.trading.cancel_batch_orders(cancel_items)
            except (ValidationError, DuplicateRequestError, APIError) as e:
                raise SystemExit(f"Batch cancel failed: {type(e).__name__}: {e}")
            for r in cancel_resp.results:
                state = "OK  " if r.success else "FAIL"
                print(
                    f"  {state} orderId={r.order_id} cancelId={r.client_cancel_id} "
                    f"status={enum_value(r.status)} {r.error_code} {r.error_message}".rstrip()
                )

Sample Code — Async

The async version follows the same flow; only AsyncAuth / AsyncData / AsyncTrading and await differ.

python/sample_14_batch_orders_async.py
import asyncio
import uuid
from decimal import Decimal

from ssi_sdk import AsyncAuth, AsyncData, AsyncTrading, Config
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import APIError, DuplicateRequestError, ValidationError
from auth_helper import ensure_auth_async
from helpers import confirm_real_orders, enum_value, require_account

config = Config(...)
ACCOUNT_NO = "<your_account_no>"
SYMBOL = "SSI"

async def main():
    require_account(ACCOUNT_NO)
    confirm_real_orders(f"3 LO buy orders for {SYMBOL} (batch)")

    async with AsyncAuth(config) as auth:
        await ensure_auth_async(auth, otp="<your_otp>")

        async with AsyncData(auth) as data:
            master_data = await data.market_data.get_master_data()
        master = next((m for m in master_data if m.symbol == SYMBOL), None)
        if master is None or master.floor <= 0:
            raise SystemExit(f"No floor price for {SYMBOL}, stopping to stay safe.")
        floor_price = Decimal(str(master.floor))

        async with AsyncTrading(auth) as trading:
            batch_tag = uuid.uuid4().hex[:8]
            place_items = [
                {
                    "account_no": ACCOUNT_NO, "symbol": SYMBOL, "side": OrderSide.BUY,
                    "quantity": 100, "price": floor_price, "order_type": OrderType.LO,
                    "client_request_id": f"bt{batch_tag}{i:02d}",  # <= 20 chars
                }
                for i in range(1, 4)
            ]
            try:
                response = await trading.trading.place_batch_orders(place_items)
            except (ValidationError, DuplicateRequestError, APIError) as e:
                raise SystemExit(f"Batch failed: {type(e).__name__}: {e}")

            placed_ok = [r.order_id for r in response.results if r.success]
            for r in response.results:
                print(f"  {'OK' if r.success else 'FAIL'} {r.client_request_id} {enum_value(r.status)}")

            if placed_ok:
                cancel_items = [{"account_no": ACCOUNT_NO, "order_id": oid} for oid in placed_ok]
                cancel_resp = await trading.trading.cancel_batch_orders(cancel_items)
                for r in cancel_resp.results:
                    print(f"  {'OK' if r.success else 'FAIL'} orderId={r.order_id}")

asyncio.run(main())

Per-order results

FieldMeaning
response.batch_request_idID of the whole batch
r.successTrue if this order was accepted
r.order_id, r.statusOrder ID and initial status when success
r.error_code, r.error_messageReason when this order was rejected
r.client_request_idThe key you set, used to match each order in the request to its result
r.client_cancel_idKey of the cancel request (result of cancel_batch_orders)

Notes

  • The whole batch is validated first: one invalid order makes the entire batch fail (ValidationError or APIError). Only when the batch is accepted do you get a separate result per order, so always iterate response.results instead of just checking "no exception was raised".
  • Idempotency: client_request_id (up to 20 characters) is the idempotency key of each order. Set it yourself so you can look up and cancel orders later.
  • Resending a batch with an id already used today returns DuplicateRequestError (HTTP 409). The SDK does not auto-retry POST/DELETE requests; the original orders may already be on the exchange, so check the order book before resending.
  • Cancel: each BatchCancelOrderItem must have exactly one of order_id or client_request_id.
  • Order count limit: the sample checks Config.max_batch_orders (default 20) before calling. For API-side rules (maximum orders per request, replay protection, ...) see trade-order-batch-place and trade-order-batch-cancel.
  • Track the status of the orders you just placed with Order Status or WebSocket Trading.
  • How to classify errors and retry safely: Error Handling & Rate Limits. Check buying power and positions before placing: Positions, PPMMR & Max Buy/Sell.

On this page