17. Trading WebSocket
Place/cancel orders and query positions and the order book over the Trading WebSocket
Note: The Trading WebSocket is imported directly from
ssi_sdk(trading_ws,async_trading_ws,TradingWSError...). TheTrading/Streamfacades never connect to it; you calltrading_ws(auth)explicitly.Upgrading to SDK 3.3.2: the
ssi_sdk.experimentalmodule was removed, with no alias. The oldfrom ssi_sdk.experimental import ...raisesModuleNotFoundError; change it tofrom ssi_sdk import ....
Warning: this sample sends a real order (buy LO at the floor price, far from the market, cancelled right away) and asks you to type
YESbefore sending. Double-check the account, symbol, price and quantity before running.
Objective
Place/cancel orders and query positions and the order book over the Trading WebSocket (/ws/v3/trading) instead of REST, using the same token and private key as REST. Matches Sample 17 in the tutorials repo (SDK v3.3.2). Requires OTP during authentication.
Flow
Auth (OTP) → Master data (floor price) → trading_ws(auth) → subscribe_order_events → place_order → cancel_order → query_position / query_order_book → close- Connect with
trading_ws(auth)(async:await async_trading_ws(auth)) - Set
client.on_eventto receive order events (frames withchannel,topic,data) subscribe_order_events(account)→place_order(buy LO) → receive thePDstatus ack →cancel_orderquery_position(includes a summary row whenquery_summary=True) andquery_order_book- Read
client.rate_limit; handleTradingWSError(401/403/429/5xx); alwaysclose()the socket
Samples in the repo
| Language | File |
|---|---|
| Python | python/sample_17_trading_ws.py, python/sample_17_trading_ws_async.py |
| Node.js | node/sample_17_trading_ws.js |
| .NET | dotnet/Sample17TradingWs.cs |
| Go | go/sample_17_trading_ws.go |
This page uses Python for illustration. Repo: ssi-fastconnect-v3-tutorials.
Prerequisites
- A token with the scopes
tradingws:order:post,tradingws:order:delete,tradingws:position:get,tradingws:orderBook:get. Missing scopes make the server return 403. - 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,mask_account,require_account,enum_value.
Sample Code — Sync
The code below is condensed from the repo sample and keeps the same flow.
import time
import uuid
from decimal import Decimal
from ssi_sdk import Auth, Data, Config, TradingWSError, trading_ws
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import ValidationError, WebSocketError
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
events: list[dict] = []
def on_event(frame: dict):
# Keep only the event type and status, not the topic/account number
data = frame.get("data") if isinstance(frame.get("data"), dict) else {}
events.append({"eventType": data.get("eventType"), "status": data.get("orderStatus")})
print(f" [EVENT] {data.get('eventType')} | status={data.get('orderStatus')}")
def show_rate_limit(ws, label: str):
rl = ws.client.rate_limit
print(f" [RATE] {label}: limit={rl.get('limit')} remaining={rl.get('remaining')} resetMs={rl.get('resetMs')}")
require_account(ACCOUNT_NO)
confirm_real_orders(f"1 LO buy order for {SYMBOL} over the trading WebSocket")
with Auth(config) as auth:
ensure_auth(auth, otp="<your_otp>")
# Floor price via REST so the 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))
# Step 1: Connect
try:
ws = trading_ws(auth) # options: respect_rate_limit, max_rate_limit_wait_ms, request_timeout_ms...
except (TradingWSError, WebSocketError) as e:
raise SystemExit(f"Cannot connect to trading WS (check the tradingws:* scopes and the trading WS URL): {type(e).__name__}: {e}")
try:
# Step 2: Receive order events
ws.client.on_event = on_event
ws.subscribe_order_events(ACCOUNT_NO)
print(f"Subscribed to order events of account {mask_account(ACCOUNT_NO)}")
# Step 3: Place an order, then cancel it
try:
ack = ws.place_order(
account_no=ACCOUNT_NO,
symbol=SYMBOL,
side=OrderSide.BUY,
quantity=QUANTITY,
price=floor_price,
order_type=OrderType.LO,
client_request_id=f"ws{uuid.uuid4().hex[:12]}", # <= 20 chars
)
print(f"Place ack: orderId={ack.order_id} status={enum_value(ack.status)} (PD = pending)")
show_rate_limit(ws, "after place_order")
time.sleep(2) # wait for the order event to reach on_event
cancel = ws.cancel_order(account_no=ACCOUNT_NO, order_id=ack.order_id)
print(f"Cancel ack: status={enum_value(cancel.status)}")
time.sleep(2)
except ValidationError as e:
print(f"Invalid data (nothing sent): {e}")
except TradingWSError as e:
# status: 401 token expired | 403 missing scope | 429 quota exhausted | 5xx server error
print(f"Server rejected the order: status={e.status} code={e.error_code} msg={e.server_message}")
if e.status == 429:
print(" Quota exhausted: wait for resetMs in client.rate_limit before sending again.")
# Step 4: Query positions and the order book
try:
pos = ws.query_position(ACCOUNT_NO, query_summary=True)
print(f"query_position: status={pos.status}")
book = ws.query_order_book(ACCOUNT_NO, symbol=SYMBOL, page_size=10)
print(f"query_order_book: status={book.status}")
show_rate_limit(ws, "after queries")
except TradingWSError as e:
print(f"Query rejected: status={e.status} code={e.error_code} msg={e.server_message}")
print(f"Total events received: {len(events)}")
ws.unsubscribe_all_events()
finally:
ws.close() # always close the socketSample Code — Async
The async version follows the same flow; only AsyncAuth / AsyncData, await async_trading_ws(auth) and await on calls to ws differ.
import asyncio
import uuid
from decimal import Decimal
from ssi_sdk import AsyncAuth, AsyncData, Config, TradingWSError, async_trading_ws
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import ValidationError, WebSocketError
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"
def on_event(frame: dict):
data = frame.get("data") if isinstance(frame.get("data"), dict) else {}
print(f" [EVENT] {data.get('eventType')} | status={data.get('orderStatus')}")
async def main():
require_account(ACCOUNT_NO)
confirm_real_orders(f"1 LO buy order for {SYMBOL} over the trading WebSocket")
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))
try:
ws = await async_trading_ws(auth)
except (TradingWSError, WebSocketError) as e:
raise SystemExit(f"Cannot connect to trading WS (check the tradingws:* scopes and the trading WS URL): {type(e).__name__}: {e}")
try:
ws.client.on_event = on_event
await ws.subscribe_order_events(ACCOUNT_NO)
try:
ack = await ws.place_order(
account_no=ACCOUNT_NO, symbol=SYMBOL, side=OrderSide.BUY,
quantity=100, price=floor_price, order_type=OrderType.LO,
client_request_id=f"ws{uuid.uuid4().hex[:12]}", # <= 20 chars
)
print(f"Place ack: orderId={ack.order_id} status={enum_value(ack.status)}")
await asyncio.sleep(2) # wait for the order event to reach on_event
cancel = await ws.cancel_order(account_no=ACCOUNT_NO, order_id=ack.order_id)
print(f"Cancel ack: status={enum_value(cancel.status)}")
await asyncio.sleep(2)
except ValidationError as e:
print(f"Invalid data (nothing sent): {e}")
except TradingWSError as e:
print(f"Server rejected the order: status={e.status} code={e.error_code}")
try:
pos = await ws.query_position(ACCOUNT_NO, query_summary=True)
book = await ws.query_order_book(ACCOUNT_NO, symbol=SYMBOL, page_size=10)
print(f"query_position: {pos.status} | query_order_book: {book.status}")
except TradingWSError as e:
print(f"Query rejected: status={e.status} code={e.error_code}")
await ws.unsubscribe_all_events()
finally:
await ws.close() # always close the socket
asyncio.run(main())Operations
| Method | Description |
|---|---|
subscribe_order_events(account_no) / unsubscribe_all_events() | Start / stop receiving order events via client.on_event |
place_order(...) | Place an order; returns an ack with status PD |
cancel_order(account_no, order_id) | Cancel an order; returns an ack |
query_position(account_no, query_summary=True) | Query positions, with a summary row when query_summary=True |
query_order_book(account_no, symbol=..., page_size=...) | Query the order book |
Notes
- See also: to place/cancel orders over REST or receive events over the stream, see Limit Order, Cancel Order and WebSocket Trading.
- The ack only confirms receipt:
place_orderreturns an ack with statusPD(pending). The real result arrives through order events (on_event), not through the ack. TradingWSError:status401 means the token expired, 403 a missing scope, 429 an exhausted quota, 5xx a server error; also readerror_codeandserver_message.- Rate limit: the SDK waits automatically when the quota reaches 0 (option
respect_rate_limit, up tomax_rate_limit_wait_ms). Quotas per the sample: 50 orders per window for orders, 100 for queries. Readclient.rate_limit(limit,remaining,resetMs) to see what is left. - Keep
on_eventshort and never print the whole frame: the frametopiccontains the account number. The sample only keepseventTypeandorderStatus. client_request_id(up to 20 characters) is the idempotency key of the order.- Always
close()the socket infinally. - Read positions and buying power over REST: Positions, PPMMR & Max Buy/Sell. General SDK error classification: Error Handling & Rate Limits.