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
  1. Đăng ký callback trước khi connect() để không lỡ sự kiện connected
  2. connect() và đọc is_connected, reconnect_count, last_error
  3. 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
  4. wait(timeout_ms=...) để lắng nghe; callback chỉ đếm message
  5. list_subscription() để xem server đang giữ những subscription nào
  6. unsubscribe_* từng phần rồi unsubscribe_all()

Các mẫu trong repo

Ngôn ngữFile
Pythonpython/sample_16_streaming_advanced.py, python/sample_16_streaming_advanced_async.py
Node.jsnode/sample_16_streaming_advanced.js
.NETdotnet/Sample16StreamingAdvanced.cs
Gogo/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êm equity_account nếu muốn nhận sự kiện lệnh/danh mục. Sample repo đọc cấu hình qua config.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.py trong 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

MethodDữ 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

CallbackNhận
on_connectionSự kiện kết nối: state là connected, reconnected, disconnected hoặc failed, kèm reconnect_count và error
on_data / on_tradingMọi message dữ liệu / giao dịch (như các tutorial 10, 11)
on_market_flagMarketFlagMessage
on_index_summaryIndexSummaryMessage
on_orderOrderStatusMessage
on_order_matchOrderMatchMessage
on_portfolioPortfolioMessage

Sample Code — Sync

Code dưới đây rút gọn từ sample trong repo, giữ nguyên luồng.

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

python/sample_16_streaming_advanced_async.py
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_index và subscribe_index_ticks khá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ùng subscribe_index_ticks, và muốn số liệu tổng hợp dùng subscribe_index_summary.
  • subscribe_board rấ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ện connected. Sau khi mất kết nối, SDK tự reconnect và báo reconnected; kiểm tra reconnect_count và last_error khi 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_portfolio chỉ có sự kiện với tài khoản phái sinh. on_order / on_order_match / on_portfolio cầ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_account như trong sample.
  • Phân loại lỗi kết nối và lỗi API: Xử lý lỗi & Rate limit.

Trên trang này