16. WebSocket nâng cao
Đăng ký theo sàn và chỉ số (board, index), dữ liệu chỉ số thời gian thực, sự kiện kết nối, quản lý subscription
Mục tiêu
Dùng các tính năng streaming nâng cao của SDK ngoài WebSocket Market Data và WebSocket Trading: đăng ký theo sàn (board) và chỉ số (index), nhận dữ liệu chỉ số thời gian thực, theo dõi sự kiện kết nối (connected / reconnected / disconnected / failed), xem và hủy subscription. Tương ứng Sample 16 trong repo tutorials (SDK v3.3.2).
Dữ liệu thị trường không cần OTP. Các kênh lệnh/danh mục (on_order, on_order_match, on_portfolio) cần OTP và tài khoản trong cấu hình; nếu không có tài khoản, sample chỉ chạy phần dữ liệu thị trường.
Luồng xử lý
Client ↔ Streaming API ↔ Market Data + Trading Event- Đăng ký callback trước khi
connect()để không lỡ sự kiệnconnected connect()và đọcis_connected,reconnect_count,last_error- Subscribe: symbol, board, index, tick chỉ số, tổng hợp chỉ số, cờ phiên, nến, và lệnh/danh mục nếu có tài khoản
wait(timeout_ms=...)để lắng nghe; callback chỉ đếm messagelist_subscription()để xem server đang giữ những subscription nàounsubscribe_*từng phần rồiunsubscribe_all()
Các mẫu trong repo
| Ngôn ngữ | 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 |
Trang này minh họa bằng Python. Repo: ssi-fastconnect-v3-tutorials.
Chuẩn bị
- Cấu hình credentials trong
config.json(Tutorial 1); thêmequity_accountnếu muốn nhận sự kiện lệnh/danh mục. Sample repo đọc cấu hình quaconfig.py; ở đây dùng placeholder để code gọn. auth_helper.py(token cache, Tutorial 1). OTP chỉ cần khi có tài khoản.helpers.pytrong repo:enum_value(lấy chuỗi hiển thị an toàn cho trường enum) vàmask_account(che số tài khoản khi in).
Đăng ký dữ liệu
| Method | Dữ liệu nhận được |
|---|---|
subscribe_trades(symbols) / subscribe_quotes(symbols) | Giá khớp / bảng giá bid-ask theo mã (alias của subscribe_symbol_trade / subscribe_symbol_quote) |
subscribe_board(boards) | Toàn bộ trade/quote/room của cả sàn, ví dụ Board.HNX. Rất nhiều message, có thể hàng nghìn mỗi giây |
subscribe_index(indexes) | Dữ liệu của các mã thành phần của chỉ số, ví dụ VN30 |
subscribe_index_ticks(indexes) | Tick của chính chỉ số, ví dụ VNINDEX (IndexTickMessage) |
subscribe_index_summary(indexes) | Số liệu tổng hợp của chỉ số (IndexSummaryMessage) |
subscribe_session_flag() | Cờ phiên ATO/LO/ATC (MarketFlagMessage, alias của subscribe_market_flag) |
subscribe_candles(symbols, interval) | Nến real-time; interval chỉ hỗ trợ tick, 1m, 5m (không có stream cho 1w/1M) |
subscribe_orders(account_no) | Trạng thái lệnh và khớp lệnh (OrderStatusMessage, OrderMatchMessage) |
subscribe_positions(account_no) | Danh mục (PortfolioMessage); chỉ có sự kiện với tài khoản phái sinh |
Callback
| Callback | Nhận |
|---|---|
on_connection | Sự kiện kết nối: state là connected, reconnected, disconnected hoặc failed, kèm reconnect_count và error |
on_data / on_trading | Mọi message dữ liệu / giao dịch (như các tutorial 10, 11) |
on_market_flag | MarketFlagMessage |
on_index_summary | IndexSummaryMessage |
on_order | OrderStatusMessage |
on_order_match | OrderMatchMessage |
on_portfolio | PortfolioMessage |
Sample Code — Sync
Code dưới đây rút gọn từ sample trong repo, giữ nguyên luồng.
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>" # để trống nếu chỉ cần dữ liệu thị trường
LISTEN_MS = 30_000 # nghe 30 giây rồi dọn dẹp
counts: Counter = Counter()
# Callback chạy trên thread riêng của SDK: giữ NGẮN, chỉ đếm/ghi nhận
def on_connection(event):
# state: connected | reconnected | disconnected | failed
err = f" | lỗi: {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: # chỉ in lần đầu để xem cấu trúc
print(f" [INDEX-SUMMARY] {msg.index} | số trường: {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)} | khớp {msg.filled_quantity}/{msg.quantity}"
)
def on_order_match(msg: OrderMatchMessage):
print(f" [MATCH] {msg.symbol} {enum_value(msg.side)} | giá khớp {msg.match_price} | KL {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:
# Dữ liệu thị trường không cần OTP; kênh trading (lệnh/danh mục) cần OTP
ensure_auth(auth, otp="<your_otp>") if has_account else ensure_auth(auth)
with Stream(auth) as stream:
ws = stream.streaming
# Bước 1: Đăng ký callback TRƯỚC khi connect để không lỡ sự kiện "connected"
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
# Bước 2: Kết nối và xem trạng thái
ws.connect()
print(f"is_connected={ws.is_connected} reconnect_count={ws.reconnect_count} last_error={ws.last_error}")
# Bước 3: Subscribe
ws.subscribe_trades(["SSI", "HPG"])
ws.subscribe_quotes(["SSI"])
ws.subscribe_board([Board.HNX]) # toàn bộ trade/quote/room của sàn HNX (nhiều!)
ws.subscribe_index(["VN30"]) # dữ liệu các MÃ THÀNH PHẦN của VN30
ws.subscribe_index_ticks(["VNINDEX"]) # tick của CHÍNH chỉ số (IndexTickMessage)
ws.subscribe_index_summary(["VN30"]) # IndexSummaryMessage
ws.subscribe_session_flag() # cờ phiên ATO/LO/ATC
ws.subscribe_candles(["SSI"], "1m") # nến 1 phút; chỉ tick/1m/5m
if has_account:
print(f"Subscribe lệnh/danh mục của tài khoản {mask_account(ACCOUNT_NO)}")
ws.subscribe_orders(ACCOUNT_NO) # trạng thái lệnh + khớp lệnh
ws.subscribe_positions(ACCOUNT_NO) # danh mục (chỉ phái sinh có event)
# Bước 4: Lắng nghe
print(f"Đang lắng nghe {LISTEN_MS // 1000} giây...")
try:
ws.wait(timeout_ms=LISTEN_MS)
except KeyboardInterrupt:
print("Ngắt theo yêu cầu...")
print("Số message nhận được theo loại:")
for name, n in counts.most_common():
print(f" {name:<22} {n}")
# Bước 5: Xem server đang giữ những subscription nào
subs = ws.list_subscription() # {"trading": [...], "data": [...]}
print(f"list_subscription: data={len(subs['data'])} topic, trading={len(subs['trading'])} topic")
# Bước 6: Hủy từng phần rồi hủy tất cả
ws.unsubscribe_board([Board.HNX]) # board nặng nhất: hủy trước
ws.unsubscribe_index_ticks(["VNINDEX"])
removed = ws.unsubscribe_all() # hỏi server rồi hủy đúng những gì còn lại
print(f"unsubscribe_all đã gỡ: data={len(removed['data'])}, trading={len(removed['trading'])}")
print(f"Kết thúc: is_connected={ws.is_connected} reconnect_count={ws.reconnect_count}")Sample Code — Async
Async dùng cùng luồng và cùng bộ callback; chỉ khác AsyncAuth / AsyncStream và await cho các lời gọi connect, subscribe_*, wait, list_subscription, unsubscribe_*. Callback vẫn là hàm thường và chạy trong task của SDK nên cũng phải giữ ngắn.
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) # chỉ dữ liệu thị trường, không cần OTP
async with AsyncStream(auth) as stream:
ws = stream.streaming
ws.on_connection = on_connection # đăng ký TRƯỚC 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'])} topic, trading={len(subs['trading'])} topic")
await ws.unsubscribe_board([Board.HNX])
await ws.unsubscribe_index_ticks(["VNINDEX"])
await ws.unsubscribe_all()
asyncio.run(main())Lưu ý
subscribe_indexvàsubscribe_index_tickskhác nhau:subscribe_index(["VN30"])nhận dữ liệu các mã thành phần của VN30; muốn tick của chính chỉ số (ví dụVNINDEX) dùngsubscribe_index_ticks, và muốn số liệu tổng hợp dùngsubscribe_index_summary.subscribe_boardrất nặng: một sàn có thể tạo hàng nghìn message mỗi giây. Vì vậy sample chỉ đếm message theo loại và in tổng kết, không in từng message. Hủy board trước khi hủy các subscription nhẹ hơn.- Callback phải ngắn: callback chạy trên thread (sync) hoặc task (async) của SDK. Chỉ đếm/ghi nhận, để luồng chính xử lý.
- Đăng ký callback trước
connect()để không bỏ lỡ sự kiệnconnected. Sau khi mất kết nối, SDK tự reconnect và báoreconnected; kiểm trareconnect_countvàlast_errorkhi cần debug. wait(timeout_ms=...)tính bằng mili-giây (SDK 3.3.0 trở lên). Tên cũtimeout=tính bằng giây đã bị xóa và báo lỗi.on_portfoliochỉ có sự kiện với tài khoản phái sinh.on_order/on_order_match/on_portfoliocần OTP và tài khoản trong cấu hình.- Không in số tài khoản ra log; dùng
mask_accountnhư trong sample. - Phân loại lỗi kết nối và lỗi API: Xử lý lỗi & Rate limit.