SBE Order Entry Integration
Overview
- Channel: Private MM WebSocket only (not available on public WS).
- Transport: WebSocket binary frames — each frame contains exactly one SBE message (no JSON).
- Encoding: SBE (Simple Binary Encoding), little-endian.
schemaId = 2,version = 1. - Purpose: High-performance, low-latency order entry — create, replace, and cancel orders individually or in batch.
- Compression: Disabled to avoid head-of-line blocking and CPU overhead.
Testnet
The estimated available date is 9 May, 2026. URL: wss://stream-testnet.bybit.com/v5/sbe/trade
SBE XML Template
Connection
Connection Lifecycle
- Open WebSocket connection.
- Send AuthReq (
templateId = 1). - Receive AuthResp (
templateId = 2) — proceed only ifretCode = 0. - Send order requests (CreateOrderReqV5, ReplaceOrderReqV5, CancelOrderReqV5, or batch variants).
- Receive the corresponding response for each request.
- Send PingReq (
templateId = 3) periodically; expect PongResp (templateId = 4) in return.
Heartbeat
- Send a PingReq every 10 s to keep the connection alive.
- If no data is received within 2 × heartbeat interval, reconnect and re-authenticate.
Reconnect Strategy
- Use exponential backoff with jitter.
- Re-authenticate immediately after reconnect before resuming order flow.
- Use
orderLinkIdfor client-side idempotency — query order status before resubmitting.
Authentication Flow
Send AuthReq
Signature: HMAC-SHA256 over "apiKey:expires", where expires is a future Unix timestamp in milliseconds.
import hashlib, hmac, struct, time
def generate_signature(api_key: str, api_secret: str, expires: int) -> str:
message = f"{api_key}:{expires}"
return hmac.new(
api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
Receive AuthResp
{
"header": {
"block_length": 132,
"template_id": 2,
"schema_id": 2,
"version": 1
},
"reqId": "req_00000000001",
"retCode": 0,
"connId": "d30fdpbboasp1pjbe7r0",
"retMsg": "OK"
}
retCode = 0— authentication succeeded.- Any non-zero
retCodeis a failure; readretMsgfor the cause.
Order Operations
Create Order
Send CreateOrderReqV5 (templateId = 5), receive CreateOrderRespV5 (templateId = 6).
priceandqtyuse Decimal64:value = mantissa × 10^exponent.orderLinkIdis a fixed 64-byte char field (null-padded); used for client-side deduplication.
CreateOrderRespV5 decode example:
{
"header": {
"block_length": 364,
"template_id": 6,
"schema_id": 2,
"version": 1
},
"respHeader": {
"reqId": "req_00000000002",
"connId": "d30fdpbboasp1pjbe7r0",
"traceId": "abc123def456789",
"timeNow": 1757497309814,
"inTime": 1757497309800,
"bapiLimit": 1000,
"bapiLimitStatus": 999,
"bapiLimitResetTimestamp": 1757497370000
},
"retCode": 0,
"result": {
"orderId": "1912284048591699456",
"orderLinkId": "cli_order_001"
},
"retMsg": "OK"
}
Replace Order
Send ReplaceOrderReqV5 (templateId = 7), receive ReplaceOrderRespV5 (templateId = 8).
- Identify the order to replace by
orderIdororderLinkId(at least one required). - Submit new
qtyand/orpriceas Decimal64.
Cancel Order
Send CancelOrderReqV5 (templateId = 9), receive CancelOrderRespV5 (templateId = 10).
- Identify the order to cancel by
orderIdororderLinkId.
Batch Operations
All batch requests embed an ApiRequestHeader at the top, followed by the category field, then a repeating group of order items. The group is prefixed by a groupSize16Encoding header (uint16 blockLength + uint16 numInGroup).
| Operation | Request Template ID | Response Template ID |
|---|---|---|
| Batch Create | 11 | 12 |
| Batch Replace | 13 | 14 |
| Batch Cancel | 15 | 16 |
Each response group item carries its own code and msg (varString8), plus orderId / orderLinkId for the acknowledged order. A top-level retMsg (varString8) follows all groups.
Error Handling
CommonErrResp (templateId = 17) is returned when the server cannot associate an error with a specific request message.
Fields: respHeader (ApiRespHeader), retCode (int32), retMsg (varString8).
SBE Message Structure
Message Header (8 bytes)
| Field | Type | Size (bytes) | Description |
|---|---|---|---|
| blockLength | uint16 | 2 | Fixed-body length |
| templateId | uint16 | 2 | Message type identifier |
| schemaId | uint16 | 2 | Fixed = 2 |
| version | uint16 | 2 | Fixed = 1 |
Composite Types
ApiRequestHeader (140 bytes)
Embedded at the start of every request message.
| Field | Type | Size (bytes) | Description |
|---|---|---|---|
| reqId | char[64] | 64 | Client request ID (optional; echoed in response) |
| timestamp | uint64 | 8 | Client timestamp (ms); must satisfy: server_time − recvWindow ≤ timestamp < server_time + 1000 |
| recvWindow | uint32 | 4 | Acceptable time window (ms); default 5000 |
| referer | char[64] | 64 | Broker / source identifier |
ApiRespHeader (232 bytes)
Embedded at the start of every response message.
| Field | Type | Size (bytes) | Description |
|---|---|---|---|
| reqId | char[64] | 64 | Echoed client request ID |
| connId | char[64] | 64 | Connection identifier |
| traceId | char[64] | 64 | Trace ID for diagnostics |
| timeNow | int64 | 8 | Server timestamp (ms) |
| inTime | int64 | 8 | Message ingress timestamp (ms) |
| bapiLimit | int64 | 8 | Total rate limit |
| bapiLimitStatus | int64 | 8 | Remaining rate limit tokens |
| bapiLimitResetTimestamp | int64 | 8 | Rate-limit reset timestamp (ms) |
CommonOrderRespData (128 bytes)
| Field | Type | Size (bytes) | Description |
|---|---|---|---|
| orderId | char[64] | 64 | Exchange-assigned order ID |
| orderLinkId | char[64] | 64 | Echoed client order ID |
Decimal64 (9 bytes)
| Field | Type | Size (bytes) | Description |
|---|---|---|---|
| exponent | int8 | 1 | Power of 10 |
| mantissa | int64 | 8 | Significand |
actual_value = mantissa × 10^exponent. Example: price 69000.00 → exponent=0, mantissa=69000; qty 0.01 → exponent=-2, mantissa=1.
Enumerations
| Enum | Values (uint8) |
|---|---|
CategoryType | 0=UNKNOWN, 1=SPOT, 2=LINEAR, 3=INVERSE, 4=OPTION, 254=NON_REPRESENTABLE |
SideType | 0=UNKNOWN, 1=BUY, 2=SELL, 254=NON_REPRESENTABLE |
OrderType | 0=UNKNOWN, 1=MARKET, 2=LIMIT, 254=NON_REPRESENTABLE |
TimeInForceType | 0=UNKNOWN, 1=GTC, 2=POST_ONLY, 3=IOC, 4=FOK, 5=RPI, 254=NON_REPRESENTABLE |
PositionIdxType | 0=ONE_WAY, 1=HEDGE_BUY, 2=HEDGE_SELL, 253=UNKNOWN, 254=NON_REPRESENTABLE |
MarketUnitType | 0=UNKNOWN, 1=BASE_COIN, 2=QUOTE_COIN, 254=NON_REPRESENTABLE |
SmpType | 0=UNKNOWN, 1=CANCEL_TAKER, 2=CANCEL_MAKER, 3=CANCEL_BOTH, 254=NON_REPRESENTABLE |
BoolEnum | 0=FALSE, 1=TRUE, 254=NON_REPRESENTABLE |
Message Field Tables
AuthReq (id=1, blockLength=200)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | reqId | char[64] | 64 | Client request ID |
| 2 | apiKey | char[64] | 64 | API Key (null-padded) |
| 3 | expires | uint64 | 8 | Expiry timestamp (ms); must be in future |
| 4 | signature | char[64] | 64 | HMAC-SHA256 over "apiKey:expires" |
AuthResp (id=2, blockLength=132)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | reqId | char[64] | 64 | Echoed request ID |
| 2 | retCode | int32 | 4 | 0 = OK |
| 3 | connId | char[64] | 64 | Connection identifier |
| 20 | retMsg | varString8 | variable | "OK" on success; error text otherwise |
PingReq (id=3, blockLength=8)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | timestamp | uint64 | 8 | Client timestamp (ms) |
PongResp (id=4, blockLength=16)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | timestamp | uint64 | 8 | Echoed client timestamp (ms) |
| 2 | pongTime | uint64 | 8 | Server pong timestamp (ms) |
CreateOrderReqV5 (id=5, blockLength=241)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | header | ApiRequestHeader | 140 | Request header |
| 2 | category | CategoryType | 1 | 1=SPOT, 2=LINEAR, 3=INVERSE, 4=OPTION |
| 3 | symbolId | int64 | 8 | Internal numeric symbol ID |
| 4 | side | SideType | 1 | 1=BUY, 2=SELL |
| 5 | orderType | OrderType | 1 | 1=MARKET, 2=LIMIT |
| 6 | qty | Decimal64 | 9 | Order quantity |
| 7 | price | Decimal64 | 9 | Order price; set mantissa=0 for MARKET orders |
| 8 | orderLinkId | char[64] | 64 | Client order ID (null-padded) |
| 9 | timeInForce | TimeInForceType | 1 | 1=GTC, 2=POST_ONLY, 3=IOC, 4=FOK, 5=RPI |
| 10 | positionIdx | PositionIdxType | 1 | 0=ONE_WAY, 1=HEDGE_BUY, 2=HEDGE_SELL |
| 11 | marketUnit | MarketUnitType | 1 | 1=BASE_COIN, 2=QUOTE_COIN |
| 12 | isLeverage | BoolEnum | 1 | 0=FALSE, 1=TRUE |
| 13 | reduceOnly | BoolEnum | 1 | 0=FALSE, 1=TRUE |
| 14 | closeOnTrigger | BoolEnum | 1 | 0=FALSE, 1=TRUE |
| 15 | mmp | BoolEnum | 1 | Market Maker Protection |
| 16 | smpType | SmpType | 1 | 0=UNKNOWN, 1=CANCEL_TAKER, 2=CANCEL_MAKER, 3=CANCEL_BOTH |
CreateOrderRespV5 (id=6, blockLength=364)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | respHeader | ApiRespHeader | 232 | Response header |
| 2 | retCode | int32 | 4 | 0 = accepted |
| 3 | result | CommonOrderRespData | 128 | Order identifiers |
| 20 | retMsg | varString8 | variable | "OK" on success |
ReplaceOrderReqV5 (id=7, blockLength=295)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | header | ApiRequestHeader | 140 | Request header |
| 2 | category | CategoryType | 1 | Product category |
| 3 | symbolId | int64 | 8 | Internal numeric symbol ID |
| 4 | orderId | char[64] | 64 | Order to replace (use orderId or orderLinkId) |
| 5 | orderLinkId | char[64] | 64 | Client order ID of the order to replace |
| 6 | qty | Decimal64 | 9 | New quantity |
| 7 | price | Decimal64 | 9 | New price |
ReplaceOrderRespV5 (id=8, blockLength=364)
Same layout as CreateOrderRespV5.
CancelOrderReqV5 (id=9, blockLength=277)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | header | ApiRequestHeader | 140 | Request header |
| 2 | category | CategoryType | 1 | Product category |
| 3 | symbolId | int64 | 8 | Internal numeric symbol ID |
| 4 | orderId | char[64] | 64 | Order to cancel (use orderId or orderLinkId) |
| 5 | orderLinkId | char[64] | 64 | Client order ID of the order to cancel |
CancelOrderRespV5 (id=10, blockLength=364)
Same layout as CreateOrderRespV5.
BatchCreateOrderReqV5 (id=11)
Fixed body (141 bytes): header (ApiRequestHeader, 140 bytes) + category (uint8, 1 byte).
Followed by repeating group request (groupSize16Encoding header + items):
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | symbolId | int64 | 8 | Internal numeric symbol ID |
| 2 | side | SideType | 1 | 1=BUY, 2=SELL |
| 3 | orderType | OrderType | 1 | 1=MARKET, 2=LIMIT |
| 4 | qty | Decimal64 | 9 | Order quantity |
| 5 | price | Decimal64 | 9 | Order price |
| 6 | orderLinkId | char[64] | 64 | Client order ID |
| 7 | timeInForce | TimeInForceType | 1 | 1=GTC, 2=POST_ONLY, 3=IOC, 4=FOK, 5=RPI |
| 8 | positionIdx | PositionIdxType | 1 | Position mode |
| 9 | marketUnit | MarketUnitType | 1 | 1=BASE_COIN, 2=QUOTE_COIN |
| 10 | isLeverage | BoolEnum | 1 | 0=FALSE, 1=TRUE |
| 11 | reduceOnly | BoolEnum | 1 | 0=FALSE, 1=TRUE |
| 12 | closeOnTrigger | BoolEnum | 1 | 0=FALSE, 1=TRUE |
| 13 | mmp | BoolEnum | 1 | Market Maker Protection |
| 14 | smpType | SmpType | 1 | 0=UNKNOWN, 1=CANCEL_TAKER, 2=CANCEL_MAKER, 3=CANCEL_BOTH |
Per-item blockLength = 100 bytes.
BatchCreateOrderRespV5 (id=12)
Fixed body (236 bytes): respHeader (232 bytes) + retCode (int32, 4 bytes).
Followed by repeating group list (per-item blockLength = 141 bytes):
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | code | int32 | 4 | Per-order result code |
| 2 | category | CategoryType | 1 | |
| 3 | symbolId | int64 | 8 | |
| 4 | orderId | char[64] | 64 | Exchange order ID |
| 5 | orderLinkId | char[64] | 64 | Client order ID |
| 20 | msg | varString8 | variable | Per-order message |
| 21 | createAt | varString8 | variable | Creation timestamp string |
Followed by top-level retMsg (varString8).
BatchReplaceOrderReqV5 (id=13)
Fixed body (141 bytes): same as BatchCreateOrderReqV5.
Repeating group request (per-item blockLength = 154 bytes):
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | symbolId | int64 | 8 | |
| 2 | orderId | char[64] | 64 | Order to replace |
| 3 | orderLinkId | char[64] | 64 | |
| 4 | qty | Decimal64 | 9 | New quantity |
| 5 | price | Decimal64 | 9 | New price |
BatchReplaceOrderRespV5 (id=14)
Fixed body (236 bytes). Repeating group list (per-item blockLength = 141 bytes, same fields as BatchCreateOrderRespV5 minus createAt). Followed by retMsg (varString8).
BatchCancelOrderReqV5 (id=15)
Fixed body (141 bytes). Repeating group request (per-item blockLength = 136 bytes):
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | symbolId | int64 | 8 | |
| 2 | orderId | char[64] | 64 | Order to cancel |
| 3 | orderLinkId | char[64] | 64 |
BatchCancelOrderRespV5 (id=16)
Same layout as BatchReplaceOrderRespV5.
CommonErrResp (id=17, blockLength=236)
| ID | Field | Type | Size (bytes) | Description |
|---|---|---|---|---|
| 1 | respHeader | ApiRespHeader | 232 | Response header |
| 2 | retCode | int32 | 4 | Error code |
| 20 | retMsg | varString8 | variable | Error description |
Integration Example
import hashlib
import hmac
import json
import logging
import struct
import threading
import time
from typing import Any, Dict, Optional, Tuple
import websocket
logging.basicConfig(
filename="logfile_order_entry.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
WS_URL = "wss://stream-testnet.bybit.com/v5/sbe/trade"
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
RECV_WINDOW = 5000
SCHEMA_ID = 2
VERSION = 1
# Template IDs
TMPL_AUTH_REQ = 1
TMPL_AUTH_RESP = 2
TMPL_PING_REQ = 3
TMPL_PONG_RESP = 4
TMPL_CREATE_REQ = 5
TMPL_CREATE_RESP = 6
TMPL_REPLACE_REQ = 7
TMPL_REPLACE_RESP = 8
TMPL_CANCEL_REQ = 9
TMPL_CANCEL_RESP = 10
TMPL_ERR_RESP = 17
# Enum values
CATEGORY_LINEAR = 2
SIDE_BUY = 1
SIDE_SELL = 2
ORDER_TYPE_LIMIT = 2
ORDER_TYPE_MARKET = 1
TIF_GTC = 1
POSITION_ONE_WAY = 0
MARKET_UNIT_BASE = 1
BOOL_FALSE = 0
BOOL_TRUE = 1
SMP_NONE = 0
# Struct formats (little-endian, no padding)
HDR_FMT = "<HHHH" # messageHeader: 8 bytes
HDR_SZ = struct.calcsize(HDR_FMT)
API_REQ_HDR_FMT = "<64sQI64s" # ApiRequestHeader: 140 bytes
API_REQ_HDR_SZ = struct.calcsize(API_REQ_HDR_FMT)
API_RESP_HDR_FMT = "<64s64s64sqqqqq" # ApiRespHeader: 232 bytes
API_RESP_HDR_SZ = struct.calcsize(API_RESP_HDR_FMT)
COMMON_RESP_FMT = "<64s64s" # CommonOrderRespData: 128 bytes
COMMON_RESP_SZ = struct.calcsize(COMMON_RESP_FMT)
DECIMAL64_FMT = "<bq" # Decimal64: 9 bytes
DECIMAL64_SZ = struct.calcsize(DECIMAL64_FMT)
_req_counter = 0
def _next_req_id() -> str:
global _req_counter
_req_counter += 1
return f"req_{_req_counter:012d}"
def _encode_sbe_header(block_length: int, template_id: int) -> bytes:
return struct.pack(HDR_FMT, block_length, template_id, SCHEMA_ID, VERSION)
def _parse_sbe_header(data: bytes) -> Dict[str, Any]:
bl, tid, sid, ver = struct.unpack_from(HDR_FMT, data, 0)
return {"block_length": bl, "template_id": tid, "schema_id": sid, "version": ver}
def _encode_str(s: str, length: int) -> bytes:
return s.encode("utf-8").ljust(length, b"\x00")[:length]
def _decode_str(b: bytes) -> str:
return b.rstrip(b"\x00").decode("utf-8")
def _encode_decimal64(mantissa: int, exponent: int) -> bytes:
"""Pack a Decimal64. value = mantissa × 10^exponent."""
return struct.pack(DECIMAL64_FMT, exponent, mantissa)
def _parse_varstring8(data: bytes, offset: int) -> Tuple[str, int]:
"""Parse a varString8: uint8 length prefix + UTF-8 data."""
(length,) = struct.unpack_from("<B", data, offset)
offset += 1
s = data[offset: offset + length].decode("utf-8")
offset += length
return s, offset
def _encode_api_req_header(req_id: str = "", referer: str = "") -> bytes:
ts = int(time.time() * 1000)
return struct.pack(
API_REQ_HDR_FMT,
_encode_str(req_id, 64),
ts,
RECV_WINDOW,
_encode_str(referer, 64),
)
def _parse_api_resp_header(data: bytes, offset: int) -> Tuple[Dict[str, Any], int]:
(req_id, conn_id, trace_id,
time_now, in_time,
bapi_limit, bapi_limit_status, bapi_limit_reset) = struct.unpack_from(
API_RESP_HDR_FMT, data, offset
)
offset += API_RESP_HDR_SZ
return {
"reqId": _decode_str(req_id),
"connId": _decode_str(conn_id),
"traceId": _decode_str(trace_id),
"timeNow": time_now,
"inTime": in_time,
"bapiLimit": bapi_limit,
"bapiLimitStatus": bapi_limit_status,
"bapiLimitResetTimestamp": bapi_limit_reset,
}, offset
# ----------------------------- Encoders -----------------------------
def encode_auth_req(api_key: str, api_secret: str) -> bytes:
req_id = _next_req_id()
expires = int(time.time() * 1000) + 10_000
message = f"{api_key}:{expires}"
signature = hmac.new(
api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
body = struct.pack(
"<64s64sQ64s",
_encode_str(req_id, 64),
_encode_str(api_key, 64),
expires,
_encode_str(signature, 64),
)
return _encode_sbe_header(200, TMPL_AUTH_REQ) + body
def encode_ping_req() -> bytes:
body = struct.pack("<Q", int(time.time() * 1000))
return _encode_sbe_header(8, TMPL_PING_REQ) + body
def encode_create_order(
category: int,
symbol_id: int,
side: int,
order_type: int,
qty_mantissa: int,
qty_exponent: int,
price_mantissa: int,
price_exponent: int,
order_link_id: str,
time_in_force: int = TIF_GTC,
position_idx: int = POSITION_ONE_WAY,
market_unit: int = MARKET_UNIT_BASE,
is_leverage: int = BOOL_FALSE,
reduce_only: int = BOOL_FALSE,
close_on_trigger: int = BOOL_FALSE,
mmp: int = BOOL_FALSE,
smp_type: int = SMP_NONE,
req_id: str = "",
referer: str = "",
) -> bytes:
body = (
_encode_api_req_header(req_id or _next_req_id(), referer) # 140 bytes
+ struct.pack("<Bq", category, symbol_id) # 9 bytes
+ struct.pack("<BB", side, order_type) # 2 bytes
+ _encode_decimal64(qty_mantissa, qty_exponent) # 9 bytes
+ _encode_decimal64(price_mantissa, price_exponent) # 9 bytes
+ _encode_str(order_link_id, 64) # 64 bytes
+ struct.pack("<BBBBBBBBB",
time_in_force, position_idx, market_unit,
is_leverage, reduce_only, close_on_trigger,
mmp, smp_type, 0) # 9 bytes (last byte pad to reach 241)
)
return _encode_sbe_header(241, TMPL_CREATE_REQ) + body
def encode_cancel_order(
category: int,
symbol_id: int,
order_id: str = "",
order_link_id: str = "",
req_id: str = "",
referer: str = "",
) -> bytes:
body = (
_encode_api_req_header(req_id or _next_req_id(), referer)
+ struct.pack("<Bq", category, symbol_id)
+ _encode_str(order_id, 64)
+ _encode_str(order_link_id, 64)
)
return _encode_sbe_header(277, TMPL_CANCEL_REQ) + body
# ----------------------------- Parsers -----------------------------
def parse_auth_resp(data: bytes) -> Dict[str, Any]:
hdr = _parse_sbe_header(data)
offset = HDR_SZ
req_id_b, ret_code, conn_id_b = struct.unpack_from("<64si64s", data, offset)
offset += 132
ret_msg, _ = _parse_varstring8(data, offset)
return {
"header": hdr,
"reqId": _decode_str(req_id_b),
"retCode": ret_code,
"connId": _decode_str(conn_id_b),
"retMsg": ret_msg,
}
def parse_order_resp(data: bytes) -> Dict[str, Any]:
"""Handles CreateOrderRespV5, ReplaceOrderRespV5, CancelOrderRespV5 (same layout)."""
hdr = _parse_sbe_header(data)
offset = HDR_SZ
resp_header, offset = _parse_api_resp_header(data, offset)
(ret_code,) = struct.unpack_from("<i", data, offset)
offset += 4
order_id_b, order_link_id_b = struct.unpack_from(COMMON_RESP_FMT, data, offset)
offset += COMMON_RESP_SZ
ret_msg, _ = _parse_varstring8(data, offset)
return {
"header": hdr,
"respHeader": resp_header,
"retCode": ret_code,
"result": {
"orderId": _decode_str(order_id_b),
"orderLinkId": _decode_str(order_link_id_b),
},
"retMsg": ret_msg,
}
def parse_pong_resp(data: bytes) -> Dict[str, Any]:
hdr = _parse_sbe_header(data)
ts, pong_time = struct.unpack_from("<QQ", data, HDR_SZ)
return {"header": hdr, "timestamp": ts, "pongTime": pong_time}
PARSERS = {
TMPL_AUTH_RESP: parse_auth_resp,
TMPL_CREATE_RESP: parse_order_resp,
TMPL_REPLACE_RESP: parse_order_resp,
TMPL_CANCEL_RESP: parse_order_resp,
TMPL_PONG_RESP: parse_pong_resp,
}
# ----------------------------- WebSocket handlers -----------------------------
def on_message(ws, message):
try:
if not isinstance(message, (bytes, bytearray)):
logging.warning("unexpected text frame: %r", message)
return
data = bytes(message)
hdr = _parse_sbe_header(data)
tid = hdr["template_id"]
parser = PARSERS.get(tid)
if parser is None:
logging.warning("unhandled templateId=%s", tid)
return
decoded = parser(data)
logging.info("templateId=%s %s", tid, decoded)
if tid == TMPL_AUTH_RESP:
print("auth:", decoded)
if decoded["retCode"] == 0:
# Send a sample limit buy order after successful auth
# qty=0.01 → mantissa=1, exponent=-2
# price=69000 → mantissa=69000, exponent=0
order = encode_create_order(
category=CATEGORY_LINEAR,
symbol_id=123456,
side=SIDE_BUY,
order_type=ORDER_TYPE_LIMIT,
qty_mantissa=1,
qty_exponent=-2,
price_mantissa=69000,
price_exponent=0,
order_link_id=_next_req_id(),
referer="my_broker",
)
ws.send(order)
else:
logging.error("auth failed retCode=%s retMsg=%s",
decoded["retCode"], decoded["retMsg"])
elif tid in (TMPL_CREATE_RESP, TMPL_REPLACE_RESP, TMPL_CANCEL_RESP):
print(
f"order resp templateId={tid} retCode={decoded['retCode']} "
f"orderId={decoded['result']['orderId']} "
f"orderLinkId={decoded['result']['orderLinkId']} "
f"retMsg={decoded['retMsg']}"
)
elif tid == TMPL_PONG_RESP:
print("pong:", decoded)
except Exception as e:
logging.exception("decode error: %s", e)
print("decode error:", e)
def on_error(ws, error):
print("WS error:", error)
logging.error("WS error: %s", error)
def on_close(ws, *_):
print("### connection closed ###")
logging.info("connection closed")
def on_open(ws):
print("opened")
ws.send(encode_auth_req(API_KEY, API_SECRET))
print("auth request sent")
threading.Thread(target=_ping_loop, args=(ws,), daemon=True).start()
def _ping_loop(ws):
while True:
try:
ws.send(encode_ping_req())
except Exception:
return
time.sleep(10)
def connWS():
ws = websocket.WebSocketApp(
WS_URL,
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close,
)
ws.run_forever(ping_interval=20, ping_timeout=10)
if __name__ == "__main__":
websocket.enableTrace(False)
connWS()
Limits & Errors
- Rate limits are surfaced in every response's
ApiRespHeader(bapiLimit,bapiLimitStatus,bapiLimitResetTimestamp). - Non-zero
retCodein any response indicates failure; readretMsg(varString8) for the diagnostic message. - CommonErrResp (
templateId = 17) is sent when the server cannot associate the error with a specific request. - In batch responses, each group item carries its own
codeandmsg— check per-item codes individually. - On socket close, reconnect and re-authenticate before resuming; use
orderLinkIdto detect duplicates.
Compatibility Notes
- Byte order: little-endian for all numeric primitives.
- Decimal64: packed as
int8 (exponent) + int64 (mantissa)= 9 bytes, no alignment padding.value = mantissa × 10^exponent. - BoolEnum: encoded as
uint8; valid values are 0 (FALSE) and 1 (TRUE). Value 254 signals a non-representable state. - Fixed-length strings: null-padded to declared length; strip trailing
\x00on decode. - varString8: prefixed by a 1-byte
uint8length; follows all fixed fields in the message body. - Repeating groups: prefixed by
groupSize16Encoding(uint16blockLength+ uint16numInGroup), before the group items. - Client clock must be NTP/PTP-synchronized; server rejects frames where the timestamp falls outside the
recvWindow.