16. Advanced WebSocket Streaming
Subscribe by board and index, real-time index data, connection events, and subscription management
Objective
Use the advanced streaming features of the SDK beyond WebSocket Market Data and WebSocket Trading: subscribe by board (exchange) and index, receive real-time index data, track connection events (connected / reconnected / disconnected / failed), and list and cancel subscriptions. Matches Sample 16 in the tutorials repo (SDK v3.3.2).
Market data does not need OTP. The order/portfolio channels (on_order, on_order_match, on_portfolio) require OTP and an account in the configuration; without an account, the sample only runs the market data part.
Flow
Client ↔ Streaming API ↔ Market Data + Trading Event- Register callbacks before
connect()so you do not miss theconnectedevent connect()and readis_connected,reconnect_count,last_error- Subscribe: symbols, board, index, index ticks, index summary, session flag, candles, and orders/positions if an account is configured
wait(timeout_ms=...)to listen; callbacks only count messageslist_subscription()to see which subscriptions the server holdsunsubscribe_*piece by piece, thenunsubscribe_all()
Samples in the repo
| Language | File |
|---|---|
| Python | python/sample_16_streaming_advanced.py, python/sample_16_streaming_advanced_async.py |
| Node.js | node/sample_16_streaming_advanced.js |
| .NET | dotnet/Sample16StreamingAdvanced.cs |
| Go | go/sample_16_streaming_advanced.go |
This page uses Python for illustration. Repo: ssi-fastconnect-v3-tutorials.
Prerequisites
- Credentials in
config.json(Tutorial 1); addequity_accountif you want order/portfolio events. The repo samples load the config throughconfig.py; placeholders are used here to keep the code short. auth_helper.py(token cache, Tutorial 1). OTP is only needed when an account is configured.helpers.pyfrom the repo:enum_value(safe display string for enum fields) andmask_account(masks the account number when printing).
Subscriptions
| Method | Data received |
|---|---|
subscribe_trades(symbols) / subscribe_quotes(symbols) | Trades / bid-ask quotes by symbol (aliases of subscribe_symbol_trade / subscribe_symbol_quote) |
subscribe_board(boards) | All trade/quote/room data of a whole exchange, e.g. Board.HNX. Very high volume, possibly thousands of messages per second |
subscribe_index(indexes) | Data of the index constituents, e.g. VN30 |
subscribe_index_ticks(indexes) | Ticks of the index itself, e.g. VNINDEX (IndexTickMessage) |
subscribe_index_summary(indexes) | Aggregated index figures (IndexSummaryMessage) |
subscribe_session_flag() | ATO/LO/ATC session flag (MarketFlagMessage, alias of subscribe_market_flag) |
subscribe_candles(symbols, interval) | Real-time candles; interval supports only tick, 1m, 5m (no stream for 1w/1M) |
subscribe_orders(account_no) | Order status and order matches (OrderStatusMessage, OrderMatchMessage) |
subscribe_positions(account_no) | Portfolio (PortfolioMessage); events only for derivative accounts |
Callbacks
| Callback | Receives |
|---|---|
on_connection | Connection events: state is connected, reconnected, disconnected or failed, plus reconnect_count and error |
on_data / on_trading | Every data / trading message (as in tutorials 10 and 11) |
on_market_flag | MarketFlagMessage |
on_index_summary | IndexSummaryMessage |
on_order | OrderStatusMessage |
on_order_match | OrderMatchMessage |
on_portfolio | PortfolioMessage |
Sample Code — Sync
The code below is condensed from the repo sample and keeps the same flow.
from collections import Counter
from ssi_sdk import Auth, Stream, Config
from ssi_sdk.enums import Board
from ssi_sdk.models.streaming import (
IndexSummaryMessage, MarketFlagMessage,
OrderMatchMessage, OrderStatusMessage, PortfolioMessage,
)
from auth_helper import ensure_auth
from helpers import enum_value, mask_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>" # leave empty if you only need market data
LISTEN_MS = 30_000 # listen for 30 seconds, then clean up
counts: Counter = Counter()
# Callbacks run on the SDK's own thread: keep them SHORT, just count/record
def on_connection(event):
# state: connected | reconnected | disconnected | failed
err = f" | error: {type(event.error).__name__}" if event.error else ""
print(f" [CONN] {event.state} (reconnect_count={event.reconnect_count}){err}")
def on_data(msg):
counts[type(msg).__name__] += 1
def on_market_flag(msg: MarketFlagMessage):
print(f" [FLAG] {msg.board} | {msg.flag} | {msg.trading_time}")
def on_index_summary(msg: IndexSummaryMessage):
counts["IndexSummary"] += 1
if counts["IndexSummary"] == 1: # print only the first one to see the structure
print(f" [INDEX-SUMMARY] {msg.index} | fields: {len(msg.data)}")
def on_order(msg: OrderStatusMessage):
print(
f" [ORDER] {msg.symbol} {enum_value(msg.side)} {enum_value(msg.order_type)} "
f"| {enum_value(msg.status)} | filled {msg.filled_quantity}/{msg.quantity}"
)
def on_order_match(msg: OrderMatchMessage):
print(f" [MATCH] {msg.symbol} {enum_value(msg.side)} | match price {msg.match_price} | qty {msg.match_qty}")
def on_portfolio(msg: PortfolioMessage):
print(f" [PORTFOLIO] {msg.symbol} | long={msg.long_qty} short={msg.short_qty} net={msg.net}")
has_account = bool(ACCOUNT_NO) and not ACCOUNT_NO.startswith("<")
with Auth(config) as auth:
# Market data needs no OTP; the trading channel (orders/portfolio) does
ensure_auth(auth, otp="<your_otp>") if has_account else ensure_auth(auth)
with Stream(auth) as stream:
ws = stream.streaming
# Step 1: Register callbacks BEFORE connect so the "connected" event is not missed
ws.on_connection = on_connection
ws.on_data = on_data
ws.on_market_flag = on_market_flag
ws.on_index_summary = on_index_summary
ws.on_order = on_order
ws.on_order_match = on_order_match
ws.on_portfolio = on_portfolio
# Step 2: Connect and check the state
ws.connect()
print(f"is_connected={ws.is_connected} reconnect_count={ws.reconnect_count} last_error={ws.last_error}")
# Step 3: Subscribe
ws.subscribe_trades(["SSI", "HPG"])
ws.subscribe_quotes(["SSI"])
ws.subscribe_board([Board.HNX]) # all trade/quote/room of HNX (a lot!)
ws.subscribe_index(["VN30"]) # data of the VN30 CONSTITUENTS
ws.subscribe_index_ticks(["VNINDEX"]) # ticks of the INDEX ITSELF (IndexTickMessage)
ws.subscribe_index_summary(["VN30"]) # IndexSummaryMessage
ws.subscribe_session_flag() # ATO/LO/ATC session flag
ws.subscribe_candles(["SSI"], "1m") # 1-minute candles; tick/1m/5m only
if has_account:
print(f"Subscribing to orders/portfolio of account {mask_account(ACCOUNT_NO)}")
ws.subscribe_orders(ACCOUNT_NO) # order status + matches
ws.subscribe_positions(ACCOUNT_NO) # portfolio (events for derivatives only)
# Step 4: Listen
print(f"Listening for {LISTEN_MS // 1000} seconds...")
try:
ws.wait(timeout_ms=LISTEN_MS)
except KeyboardInterrupt:
print("Interrupted...")
print("Messages received by type:")
for name, n in counts.most_common():
print(f" {name:<22} {n}")
# Step 5: See which subscriptions the server holds
subs = ws.list_subscription() # {"trading": [...], "data": [...]}
print(f"list_subscription: data={len(subs['data'])} topics, trading={len(subs['trading'])} topics")
# Step 6: Cancel piece by piece, then cancel everything
ws.unsubscribe_board([Board.HNX]) # heaviest board: cancel first
ws.unsubscribe_index_ticks(["VNINDEX"])
removed = ws.unsubscribe_all() # asks the server, then cancels whatever is left
print(f"unsubscribe_all removed: data={len(removed['data'])}, trading={len(removed['trading'])}")
print(f"Done: is_connected={ws.is_connected} reconnect_count={ws.reconnect_count}")Sample Code — Async
The async version follows the same flow and the same callbacks; only AsyncAuth / AsyncStream and await on connect, subscribe_*, wait, list_subscription and unsubscribe_* differ. Callbacks are still plain functions and run in the SDK's task, so keep them short too.
import asyncio
from collections import Counter
from ssi_sdk import AsyncAuth, AsyncStream, Config
from ssi_sdk.enums import Board
from auth_helper import ensure_auth_async
config = Config(...)
LISTEN_MS = 30_000
counts: Counter = Counter()
def on_connection(event):
print(f" [CONN] {event.state} (reconnect_count={event.reconnect_count})")
def on_data(msg):
counts[type(msg).__name__] += 1
async def main():
async with AsyncAuth(config) as auth:
await ensure_auth_async(auth) # market data only, no OTP needed
async with AsyncStream(auth) as stream:
ws = stream.streaming
ws.on_connection = on_connection # register BEFORE connect
ws.on_data = on_data
await ws.connect()
print(f"is_connected={ws.is_connected} reconnect_count={ws.reconnect_count}")
await ws.subscribe_trades(["SSI", "HPG"])
await ws.subscribe_board([Board.HNX])
await ws.subscribe_index(["VN30"])
await ws.subscribe_index_ticks(["VNINDEX"])
await ws.subscribe_index_summary(["VN30"])
try:
await ws.wait(timeout_ms=LISTEN_MS)
except KeyboardInterrupt:
pass
subs = await ws.list_subscription()
print(f"data={len(subs['data'])} topics, trading={len(subs['trading'])} topics")
await ws.unsubscribe_board([Board.HNX])
await ws.unsubscribe_index_ticks(["VNINDEX"])
await ws.unsubscribe_all()
asyncio.run(main())Notes
subscribe_indexandsubscribe_index_ticksare different:subscribe_index(["VN30"])delivers data of the VN30 constituents; for ticks of the index itself (e.g.VNINDEX) usesubscribe_index_ticks, and for aggregated figures usesubscribe_index_summary.subscribe_boardis heavy: one exchange can produce thousands of messages per second. That is why the sample only counts messages by type and prints a summary instead of printing each message. Cancel the board before the lighter subscriptions.- Keep callbacks short: callbacks run on the SDK's thread (sync) or task (async). Only count/record, and let the main flow do the processing.
- Register callbacks before
connect()so you do not miss theconnectedevent. After a dropped connection the SDK reconnects on its own and reportsreconnected; checkreconnect_countandlast_errorwhen debugging. wait(timeout_ms=...)is in milliseconds (SDK 3.3.0 and later). The oldtimeout=in seconds has been removed and raises an error.on_portfolioonly fires for derivative accounts.on_order/on_order_match/on_portfolioneed OTP and an account in the configuration.- Do not print the account number to logs; use
mask_accountas in the sample. - Classifying connection and API errors: Error Handling & Rate Limits.