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...). The Trading / Stream facades never connect to it; you call trading_ws(auth) explicitly.

Upgrading to SDK 3.3.2: the ssi_sdk.experimental module was removed, with no alias. The old from ssi_sdk.experimental import ... raises ModuleNotFoundError; change it to from 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 YES before 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
  1. Connect with trading_ws(auth) (async: await async_trading_ws(auth))
  2. Set client.on_event to receive order events (frames with channel, topic, data)
  3. subscribe_order_events(account) → place_order (buy LO) → receive the PD status ack → cancel_order
  4. query_position (includes a summary row when query_summary=True) and query_order_book
  5. Read client.rate_limit; handle TradingWSError (401/403/429/5xx); always close() the socket

Samples in the repo

LanguageFile
Pythonpython/sample_17_trading_ws.py, python/sample_17_trading_ws_async.py
Node.jsnode/sample_17_trading_ws.js
.NETdotnet/Sample17TradingWs.cs
Gogo/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) 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, mask_account, require_account, enum_value.

Sample Code — Sync

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

python/sample_17_trading_ws.py
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 socket

Sample Code — Async

The async version follows the same flow; only AsyncAuth / AsyncData, await async_trading_ws(auth) and await on calls to ws differ.

python/sample_17_trading_ws_async.py
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

MethodDescription
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_order returns an ack with status PD (pending). The real result arrives through order events (on_event), not through the ack.
  • TradingWSError: status 401 means the token expired, 403 a missing scope, 429 an exhausted quota, 5xx a server error; also read error_code and server_message.
  • Rate limit: the SDK waits automatically when the quota reaches 0 (option respect_rate_limit, up to max_rate_limit_wait_ms). Quotas per the sample: 50 orders per window for orders, 100 for queries. Read client.rate_limit (limit, remaining, resetMs) to see what is left.
  • Keep on_event short and never print the whole frame: the frame topic contains the account number. The sample only keeps eventType and orderStatus.
  • client_request_id (up to 20 characters) is the idempotency key of the order.
  • Always close() the socket in finally.
  • Read positions and buying power over REST: Positions, PPMMR & Max Buy/Sell. General SDK error classification: Error Handling & Rate Limits.

On this page