Smart Balance — WebSocket Notifications
Subscribe to Smart Balance portfolio execution updates over WebSocket. The same connection also carries full open-orders snapshots.
Authentication: HMAC apiKey signature via URL query string at handshake time. See Authentication.
Channels
Smart Balance clients subscribe to two channels:
| Name | Requires accountId | Description |
|---|---|---|
| trade-news | false | Mandatory — rich portfolio lifecycle events (multiple MessageTypes) |
| order.change.any | true | Recommended — full open-orders snapshot, pushed on every order change for the subscribed accountId |
The WS endpoint also serves other channels (
paradigm-news,block-rfq-news,block.trade,brokerage.trade,algo.change.any) that cover unrelated business domains and are outside the scope of this document.
Subscribe
Request
{
"id": "sub-<uuid>",
"op": "subscribe",
"accountId": 10003443,
"data": [
{"channel": "trade-news"},
{"channel": "order.change.any"}
]
}
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | true | Client-generated request ID, echoed back in the response |
| op | string | true | subscribe / un-subscribe / heartbeat |
| accountId | long | conditional | Required when subscribing order.change.any; omit when subscribing only trade-news |
| data | array | true | Array of {channel} objects |
Response
{
"id": "sub-<uuid>",
"op": "subscribe",
"code": 0,
"data": [
{"channel": "trade-news", "success": true, "errorMsg": null},
{"channel": "order.change.any", "success": true, "errorMsg": null}
]
}
Each channel is acknowledged individually. On failure (for example
accountIdmissing) the per-channelsuccessisfalsewitherrorMsgpopulated.
Unsubscribe
{
"id": "unsub-<uuid>",
"op": "un-subscribe",
"data": [
{"channel": "trade-news"}
]
}
Same shape as subscribe;
opset toun-subscribe.
Heartbeat
Request
{"id": "hb-1", "op": "heartbeat"}
Response
{"id": "hb-1", "op": "heartbeat", "code": 0}
Server heartbeat timeout is 3 minutes. Client SHOULD send a heartbeat every 10–30 seconds. Missing three consecutive heartbeats causes the server to terminate the connection.
Push Envelope
Every push frame is JSON. trade-news and order.change.any share the same top-level envelope shape but different data semantics.
{
"id": "<uuid>",
"channel": "<channel-name>",
"accountId": "<accountId>", // only present for order.change.any
"data": { /* channel-specific */ }
}
Channel: trade-news
Rich event stream. Each push carries a type discriminator (MessageType) plus an inner data payload.
{
"id": "<uuid>",
"channel": "trade-news",
"data": {
"type": "<MessageType>",
"data": { /* MessageType-specific payload */ },
"timestamp": "<millis>"
}
}
Message Type Summary
| Name | Type | Description |
|---|---|---|
| PORTFOLIO_ACTIVE | string | Portfolio starts running; full PortfolioWsDTO payload |
| PORTFOLIO_FILLED | string | Portfolio fully filled; full PortfolioWsDTO + per-leg fill summary |
| PORTFOLIO_CANCELLED | string | Portfolio cancelled; full PortfolioWsDTO |
| PORTFOLIO_CANCELLING | string | Cancel request accepted, in flight |
| LEG_ACTIVE | string | Leg starts accepting fills |
| LEG_FILLED | string | Leg fully filled; LegWsDTO + fill summary |
| LEG_PENDING_PRICE | string | Leg dynamic price refreshed (high-frequency, lightweight payload) |
| TRADE | string | Fill report |
Message — PORTFOLIO_ACTIVE
{
"id": "1b0688d9-4d6c-4ad4-88ef-5c1f999c6e68",
"channel": "trade-news",
"data": {
"type": "PORTFOLIO_ACTIVE",
"data": {
"uid": 13554298,
"nickname": "",
"accountId": 10003443,
"accountName": "rfq-test-01",
"exchange": "DERIBIT",
"portfolioId": "144115188955249274",
"submitTimestamp": "1776668666388",
"portfolioStatus": 1,
"balanceTrade": true,
"icebergTrade": false,
"parts": 3,
"eachLimitMs": 15000,
"totalLimitMs": 0,
"totalEndAction": 2,
"totalMs": 45000,
"fees": [],
"legs": [
{
"legId": "216172783206681210",
"instrument": "BTC-25DEC26-40000-P",
"instrumentType": "OPTION",
"quoteCurrency": "BTC",
"kind": 7,
"side": 1,
"type": 1,
"placeQuantity": "1.5",
"quantityIn": "BTC",
"placePrice": "0.6",
"placePriceIn": "IV",
"placePriceLabel": 2,
"placePriceOffset": "0",
"placeDynamicMs": 30000,
"priceSnapshot": [
{"placePriceIn": "USD", "placePrice": "2144.89", "price": "2144.89", "priceIn": "USD"},
{"placePriceIn": "BTC", "placePrice": "0.031", "price": "0.031", "priceIn": "BTC"},
{"placePriceIn": "IV", "placePrice": "0.6", "price": "0.6", "priceIn": "IV"}
],
"legStatus": 1,
"remainingQuantity": "1.5"
}
]
},
"timestamp": "1776668666715"
}
}
PORTFOLIO_ACTIVE Payload
Top-level fields in
data.data.
| Name | Type | Description |
|---|---|---|
| uid | long | User ID |
| accountId | long | Account ID |
| accountName | string | Account display name |
| exchange | string | DERIBIT / OKX / BYBIT / ... |
| portfolioId | string | Portfolio ID (stringified long) |
| submitTimestamp | string | Submit time in ms |
| portfolioStatus | int | 1=ACTIVE, 2=FILLED, 3=CANCELLED, 4=CANCELLING, 6=PENDING |
| balanceTrade | boolean | true when portfolioType is BALANCE |
| icebergTrade | boolean | true when portfolioType is ICEBERG |
| parts / eachLimitMs / totalLimitMs / totalEndAction / totalMs | — | Balance strategy parameters |
| fees | array | Aggregate fees (empty while still running) |
| legs | array | Array of Leg objects (see below) |
legs[].Legsub-fields (push shape).
| Name | Type | Description |
|---|---|---|
| legId | string | Leg ID (stringified long) |
| kind | int | 1=SPOT, 3=U_PERPETUAL, 5=C_PERPETUAL, 7=C_OPTION, ... (see Enum Reference) |
| side | int | 1=BUY, 2=SELL |
| type | int | 1=LIMIT, 2=MARKET |
| legStatus | int | 1=ACTIVE/PENDING, 2=FILLED, 3=CANCELLED, 4=CANCELLING |
| placePriceLabel | int | 1=MID, 2=MARK, 3=MODEL, 4=BID_ASK |
| placePriceIn | string | USD / USDT / BTC / IV / ... |
| priceSnapshot | array | [{placePriceIn, placePrice, price, priceIn}, ...] |
| priority | boolean | true if this leg is the priority (leading) leg that drives portfolio pacing; others follow by ratio. Default false |
| hedge | boolean | true if this leg is an auto-derived hedge leg (non-option leg in a mixed option+linear portfolio). Default false |
Message — LEG_ACTIVE
{
"id": "aac64beb-341a-4955-be02-690c3ebde4d9",
"channel": "trade-news",
"data": {
"type": "LEG_ACTIVE",
"data": {
"placePriceLabel": 2,
"placePriceOffset": "0",
"accountId": 10003443,
"accountName": "rfq-test-01",
"exchange": "DERIBIT",
"legId": "216172783206681210",
"portfolioId": "144115188955249274",
"instrument": "BTC-25DEC26-40000-P",
"instrumentType": "OPTION",
"baseCurrency": "BTC",
"quoteCurrency": "BTC",
"kind": 7,
"side": 1,
"type": 1,
"legStatus": 1,
"placeQuantity": "1.5",
"quantityIn": "BTC",
"placePrice": "0.6",
"placePriceIn": "IV",
"priceSnapshot": [
{"placePriceIn": "USD", "placePrice": "2144.89", "price": "2144.89", "priceIn": "USD"},
{"placePriceIn": "BTC", "placePrice": "0.031", "price": "0.031", "priceIn": "BTC"},
{"placePriceIn": "IV", "placePrice": "0.6", "price": "0.6", "priceIn": "IV"}
],
"priceCurrency": "BTC",
"remainingQuantity": "1.5"
},
"timestamp": "1776668667839"
}
}
Message — TRADE
{
"id": "764e4291-cb28-4d67-83c7-d8c860bc66f8",
"channel": "trade-news",
"data": {
"type": "TRADE",
"data": {
"accountId": 10003443,
"accountName": "rfq-test-01",
"exchange": "DERIBIT",
"portfolioType": 1,
"portfolioId": "144115188955249274",
"legId": "216172783206681210",
"orderId": "288231502551859205",
"instrument": "BTC-25DEC26-40000-P",
"instrumentType": "OPTION",
"side": 1,
"orderType": "LIMIT",
"orderPriceIn": "IV",
"placeQuantity": "1.5",
"filledQuantity": "0.5",
"filledAvgPrice": "0.155",
"remainingQuantity": "1",
"quantity": "0.5",
"price": "0.155",
"fee": "0.00015",
"role": "TAKER",
"indexPrice": "74783.2",
"markPrice": "0.226986",
"underlyingPrice": "76229.74",
"iv": "1.4947",
"forCcy": "BTC",
"domCcy": "BTC",
"markCcy": "BTC",
"notionalCcy": "BTC",
"defaultPvCcy": "BTC",
"mode": 2,
"type": 1,
"priceCurrency": "BTC",
"priceIn": "IV",
"quantityIn": "BTC"
},
"timestamp": "1776668668119"
}
}
TRADE Payload
| Name | Type | Description |
|---|---|---|
| portfolioType | int | PortfolioMode code (int here, not the string used in order.change.any): 1=COMMON, 2=BALANCE, 4=TWAP, 5=BLOCK_RFQ, 6=ICEBERG |
| orderType | string | LIMIT / MARKET |
| placeQuantity | string | Full order placed amount |
| filledQuantity | string | Accumulated filled on this order |
| remainingQuantity | string | Order-level remainder |
| quantity | string | This fill's delta amount |
| price | string | This fill's price |
| filledAvgPrice | string | Weighted average fill price across all fills on this order |
| fee | string | Fee for this fill (in fee currency) |
| role | string | MAKER / TAKER |
| indexPrice | string | Market snapshot at fill time |
| markPrice | string | Market snapshot at fill time |
| underlyingPrice | string | Option only — underlying mark |
| iv | string | Option only — implied volatility |
| mode | int | PortfolioMode: 1=COMMON_COMBO, 2=BALANCE_COMBO, ... |
| type | int | 1=LIMIT / 2=MARKET |
A single market order can emit multiple TRADE events (one per fill slice).
quantityis per-fill delta;filledQuantityaccumulates.
Message — LEG_FILLED
{
"id": "908b389e-e31a-492e-b9bf-1bc12cc81528",
"channel": "trade-news",
"data": {
"type": "LEG_FILLED",
"data": {
"placePriceLabel": 2,
"placePriceOffset": "0",
"accountId": 10003443,
"accountName": "rfq-test-01",
"exchange": "DERIBIT",
"legId": "216172783206681210",
"portfolioId": "144115188955249274",
"instrument": "BTC-25DEC26-40000-P",
"instrumentType": "OPTION",
"baseCurrency": "BTC",
"quoteCurrency": "BTC",
"kind": 7,
"side": 1,
"type": 1,
"legStatus": 1,
"placeQuantity": "1.5",
"quantityIn": "BTC",
"placePrice": "0.6",
"placePriceIn": "IV",
"priceSnapshot": [ /* ... */ ],
"pendingPrice": "0.2265",
"priceCurrency": "BTC",
"filledQuantity": "1.5",
"filledAvgPrice": "0.155",
"remainingQuantity": "0",
"fee": "0.00045",
"feeCurrency": "BTC"
},
"timestamp": "1776668673990"
}
}
Extra fields versus
LEG_ACTIVE:pendingPrice,filledQuantity,filledAvgPrice,fee,feeCurrency.
Message — LEG_PENDING_PRICE (lightweight)
{
"id": "dee9afd4-e796-49a2-9cc9-c68ae394b146",
"channel": "trade-news",
"data": {
"type": "LEG_PENDING_PRICE",
"data": {
"accountId": 10003443,
"legId": "216172783206681210",
"portfolioId": "144115188955249274"
},
"timestamp": "1776668673990"
}
}
This is a "price dirty" hint — fires on every reprice tick with a deliberately minimal payload. The client should re-read
order.change.anyorGET /infoto obtain the fresh price state.
Message — PORTFOLIO_FILLED
{
"id": "fad1a292-93bf-4f83-854e-396c0b695123",
"channel": "trade-news",
"data": {
"type": "PORTFOLIO_FILLED",
"data": {
"uid": 13554298,
"nickname": "",
"accountId": 10003443,
"accountName": "rfq-test-01",
"exchange": "DERIBIT",
"portfolioId": "144115188955249274",
"submitTimestamp": "1776668666388",
"filledTimestamp": "1776668676325",
"portfolioStatus": 2,
"balanceTrade": true,
"icebergTrade": false,
"parts": 3,
"eachLimitMs": 15000,
"totalLimitMs": 0,
"totalEndAction": 2,
"totalMs": 45000,
"fees": [
{"fee": "0.00053717", "feeCurrency": "BTC"}
],
"legs": [
{
"legId": "216172783206681210",
"instrument": "BTC-25DEC26-40000-P",
"instrumentType": "OPTION",
"kind": 7,
"side": 1,
"type": 1,
"placeQuantity": "1.5",
"placePrice": "0.6",
"placePriceIn": "IV",
"placePriceLabel": 2,
"priceSnapshot": [ /* ... */ ],
"legStatus": 2,
"priceCurrency": "BTC",
"filledQuantity": "1.5",
"filledAvgPrice": "0.155",
"remainingQuantity": "0",
"fee": "0.00045",
"feeCurrency": "BTC"
}
]
},
"timestamp": "1776668676526"
}
}
Extra fields versus
PORTFOLIO_ACTIVE:filledTimestamp, aggregatefees[], and per-legfilledQuantity/filledAvgPrice/fee/feeCurrency.
Channel: order.change.any
Full open-orders snapshot for the subscribed accountId. Pushed whenever any open order changes.
{
"id": "30d62c6c-8ab9-4c9f-b16b-b4dd392cc710",
"channel": "order.change.any",
"accountId": "10003443",
"data": [
{
"uid": "13554298",
"priceLabel": 2,
"priceOffset": 0,
"legPlaceQuantity": 0.5,
"legFilledQuantity": 0,
"portfolioId": "144116212076266501",
"portfolioType": "BALANCE",
"legId": "216173806114194437",
"exchange": "DERIBIT",
"orderId": "288231400156115973",
"exOrderId": "89509828570",
"placeTimestamp": "1774345200637",
"instrument": "BTC-27MAR26-71000-C",
"instrumentType": "OPTION",
"baseCurrency": "BTC",
"quoteCurrency": "BTC",
"side": 2,
"type": 1,
"orderStatus": 1,
"placeQuantity": 0.1,
"quantityIn": "BTC",
"placePrice": 53.5,
"placePriceIn": "IV",
"pendingPrice": 0.02,
"priceCurrency": "BTC",
"filledQuantity": 0,
"cancelledQuantity": 0,
"remainingQuantity": 0.1,
"timeInForce": 1
}
]
}
Order Fields
| Name | Type | Description |
|---|---|---|
| uid | string | User ID |
| portfolioId | string | Portfolio ID |
| legId | string | Leg ID |
| orderId | string | Signalplus order ID |
| exOrderId | string | Exchange-side order ID |
| portfolioType | string | BALANCE / COMMON / etc. (string here, differs from the int form used in TRADE payload) |
| placeTimestamp | string | Order place time in ms |
| side | int | 1=BUY / 2=SELL |
| type | int | 1=LIMIT / 2=MARKET |
| orderStatus | int | 1=ACTIVE/PENDING, 2=FILLED, 3=CANCELLED, 4=CANCELLING |
| cancelType | int | Present only when cancelled: 1=by system, 2=by user, 5=by no dynamic price |
| cancelledReason | string | Human-readable cancel reason |
| placeQuantity | decimal | Placed quantity |
| filledQuantity | decimal | Filled quantity |
| cancelledQuantity | decimal | Cancelled quantity |
| remainingQuantity | decimal | Unfilled quantity |
| priceLabel | int | 1=MID / 2=MARK / 3=MODEL / 4=BID_ASK |
| priceOffset | decimal | Offset from dynamic reference |
| placePrice | decimal | Placed price |
| pendingPrice | decimal | Current reprice target |
| placePriceIn | string | USD / USDT / BTC / IV / ... |
| priceCurrency | string | Price currency |
| timeInForce | int | 1=GTC / 2=IOC / 3=FOK / 4=GTD |
| legPlaceQuantity | decimal | Leg-level roll-up across orders in the same leg |
| legFilledQuantity | decimal | Leg-level filled roll-up |
Semantics: this channel always pushes the full open-orders set for the accountId (not delta). On reconnect, the first push after re-subscribe serves as the fresh snapshot. The client should rebuild UI state from the payload rather than patching.
Reconnect Guidelines
| Topic | Recommendation |
|---|---|
| Heartbeat | Send {"op":"heartbeat"} every 10–30 seconds. Server timeout is 3 minutes |
| Disconnect | Reconnect after 1–3 s backoff. All subscriptions are lost (connection-scoped); the client must re-subscribe |
| State reconciliation | After re-subscribing to order.change.any, the first push is the fresh snapshot — no need to call /info separately |
| Authoritative state | For authoritative state, call GET /portfolios/info after each reconnect; WS push is best-effort |