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
YESbefore 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)- Get the floor price from master data and build a list of
BatchPlaceOrderItem - Check the order count does not exceed
Config.max_batch_orders(default 20) - Call
place_batch_orders(items)and handle the result of each order (success,error_code) - Call
cancel_batch_orders(items)for the orders that were placed successfully (BatchCancelOrderItem)
Samples in the repo
| Language | File |
|---|---|
| Python | python/sample_14_batch_orders.py, python/sample_14_batch_orders_async.py |
| Node.js | node/sample_14_batch_orders.js |
| .NET | dotnet/Sample14BatchOrders.cs |
| Go | go/sample_14_batch_orders.go |
This page uses Python for illustration. Repo: ssi-fastconnect-v3-tutorials.
Prerequisites
- Credentials and account number (
equity_account) inconfig.json, see Tutorial 1. The repo samples load the config throughconfig.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.pyfrom the repo:confirm_real_orders(warns and requires typingYES),mask_account(masks the account number when printing),require_account(stops if no account is configured) andenum_value(safe display string for enum fields).
Sample Code — Sync
The code below is condensed from the repo sample and keeps the same flow.
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.
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
| Field | Meaning |
|---|---|
response.batch_request_id | ID of the whole batch |
r.success | True if this order was accepted |
r.order_id, r.status | Order ID and initial status when success |
r.error_code, r.error_message | Reason when this order was rejected |
r.client_request_id | The key you set, used to match each order in the request to its result |
r.client_cancel_id | Key of the cancel request (result of cancel_batch_orders) |
Notes
- The whole batch is validated first: one invalid order makes the entire batch fail (
ValidationErrororAPIError). Only when the batch is accepted do you get a separate result per order, so always iterateresponse.resultsinstead 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-retryPOST/DELETErequests; the original orders may already be on the exchange, so check the order book before resending. - Cancel: each
BatchCancelOrderItemmust have exactly one oforder_idorclient_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.