17. Trading WebSocket

Đặt/hủy lệnh và truy vấn vị thế, sổ lệnh qua Trading WebSocket

Lưu ý: Trading WebSocket import trực tiếp từ ssi_sdk (trading_ws, async_trading_ws, TradingWSError...). Các facade Trading / Stream không tự kết nối tới nó, bạn gọi trading_ws(auth) một cách tường minh.

Nâng cấp lên SDK 3.3.2: module ssi_sdk.experimental đã bị xóa, không có alias. Import cũ from ssi_sdk.experimental import ... sẽ báo ModuleNotFoundError; đổi thành from ssi_sdk import ....

Cảnh báo: sample này gửi lệnh thật (LO mua tại giá sàn, xa thị trường, rồi hủy ngay) và yêu cầu gõ YES trước khi gửi. Kiểm tra kỹ tài khoản, mã, giá, khối lượng trước khi chạy.

Mục tiêu

Đặt/hủy lệnh và truy vấn vị thế, sổ lệnh qua Trading WebSocket (/ws/v3/trading) thay vì REST, dùng cùng token và private key với REST. Tương ứng Sample 17 trong repo tutorials (SDK v3.3.2). Cần OTP khi authenticate.

Luồng xử lý

Auth (OTP) → Master data (giá sàn) → trading_ws(auth) → subscribe_order_events → place_order → cancel_order → query_position / query_order_book → close
  1. Kết nối trading_ws(auth) (async: await async_trading_ws(auth))
  2. Gắn client.on_event để nhận sự kiện lệnh (frame có channel, topic, data)
  3. subscribe_order_events(account) → place_order (LO mua) → nhận ack trạng thái PD → cancel_order
  4. query_position (có dòng tổng hợp khi query_summary=True) và query_order_book
  5. Đọc client.rate_limit; xử lý TradingWSError (401/403/429/5xx); luôn close() socket

Các mẫu trong repo

Ngôn ngữFile
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

Trang này minh họa bằng Python. Repo: ssi-fastconnect-v3-tutorials.

Chuẩn bị

  • Token có các scope tradingws:order:post, tradingws:order:delete, tradingws:position:get, tradingws:orderBook:get. Thiếu scope, server trả 403.
  • Credentials và số tài khoản (equity_account) trong config.json, xem Tutorial 1. Sample repo đọc cấu hình qua config.py; ở đây dùng placeholder để code gọn.
  • auth_helper.py (token cache, Tutorial 1) và OTP cho lần đăng nhập đầu.
  • helpers.py trong repo: confirm_real_orders, mask_account, require_account, enum_value.

Sample Code — Sync

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

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):
    # Chỉ giữ loại sự kiện và trạng thái, không giữ topic/số tài khoản
    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 lệnh LO mua {SYMBOL} qua trading WebSocket")

with Auth(config) as auth:
    ensure_auth(auth, otp="<your_otp>")

    # Giá sàn qua REST để đặt lệnh nằm chờ, xa thị trường
    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"Không có giá sàn của {SYMBOL}, dừng để an toàn.")
    floor_price = Decimal(str(master.floor))

    # Bước 1: Kết nối
    try:
        ws = trading_ws(auth)  # tùy chọn: respect_rate_limit, max_rate_limit_wait_ms, request_timeout_ms...
    except (TradingWSError, WebSocketError) as e:
        raise SystemExit(f"Không kết nối được trading WS (kiểm tra scope tradingws:* và URL trading WS): {type(e).__name__}: {e}")

    try:
        # Bước 2: Nhận sự kiện lệnh
        ws.client.on_event = on_event
        ws.subscribe_order_events(ACCOUNT_NO)
        print(f"Đã subscribe sự kiện lệnh tài khoản {mask_account(ACCOUNT_NO)}")

        # Bước 3: Đặt lệnh rồi hủy
        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 ký tự
            )
            print(f"Ack đặt lệnh: orderId={ack.order_id} status={enum_value(ack.status)} (PD = đang chờ xử lý)")
            show_rate_limit(ws, "sau place_order")

            time.sleep(2)  # chờ sự kiện lệnh về on_event
            cancel = ws.cancel_order(account_no=ACCOUNT_NO, order_id=ack.order_id)
            print(f"Ack hủy lệnh: status={enum_value(cancel.status)}")
            time.sleep(2)
        except ValidationError as e:
            print(f"Dữ liệu không hợp lệ (chưa gửi gì): {e}")
        except TradingWSError as e:
            # status: 401 token hết hạn | 403 thiếu scope | 429 hết quota | 5xx lỗi server
            print(f"Server từ chối lệnh: status={e.status} code={e.error_code} msg={e.server_message}")
            if e.status == 429:
                print("  Hết quota: chờ resetMs trong client.rate_limit rồi mới gửi tiếp.")

        # Bước 4: Truy vấn vị thế và sổ lệnh
        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, "sau truy vấn")
        except TradingWSError as e:
            print(f"Truy vấn bị từ chối: status={e.status} code={e.error_code} msg={e.server_message}")

        print(f"Tổng số sự kiện nhận được: {len(events)}")
        ws.unsubscribe_all_events()
    finally:
        ws.close()  # luôn đóng socket

Sample Code — Async

Async dùng cùng luồng; chỉ khác AsyncAuth / AsyncData, await async_trading_ws(auth) và await cho các lời gọi trên ws.

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 lệnh LO mua {SYMBOL} qua 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"Không có giá sàn của {SYMBOL}, dừng để an toàn.")
        floor_price = Decimal(str(master.floor))

        try:
            ws = await async_trading_ws(auth)
        except (TradingWSError, WebSocketError) as e:
            raise SystemExit(f"Không kết nối được trading WS (kiểm tra scope tradingws:* và URL trading WS): {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 ký tự
                )
                print(f"Ack đặt lệnh: orderId={ack.order_id} status={enum_value(ack.status)}")

                await asyncio.sleep(2)  # chờ sự kiện lệnh về on_event
                cancel = await ws.cancel_order(account_no=ACCOUNT_NO, order_id=ack.order_id)
                print(f"Ack hủy lệnh: status={enum_value(cancel.status)}")
                await asyncio.sleep(2)
            except ValidationError as e:
                print(f"Dữ liệu không hợp lệ (chưa gửi gì): {e}")
            except TradingWSError as e:
                print(f"Server từ chối lệnh: 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"Truy vấn bị từ chối: status={e.status} code={e.error_code}")

            await ws.unsubscribe_all_events()
        finally:
            await ws.close()  # luôn đóng socket

asyncio.run(main())

Các thao tác

MethodMô tả
subscribe_order_events(account_no) / unsubscribe_all_events()Nhận / ngừng nhận sự kiện lệnh qua client.on_event
place_order(...)Đặt lệnh; trả về ack trạng thái PD
cancel_order(account_no, order_id)Hủy lệnh; trả về ack
query_position(account_no, query_summary=True)Truy vấn vị thế, kèm dòng tổng hợp khi query_summary=True
query_order_book(account_no, symbol=..., page_size=...)Truy vấn sổ lệnh

Lưu ý

  • Xem thêm: nếu cần đặt/hủy lệnh qua REST hoặc nhận sự kiện qua stream, xem Đặt lệnh Limit, Hủy lệnh và WebSocket Trading.
  • Ack chỉ là xác nhận đã nhận: place_order trả về ack trạng thái PD (đang chờ xử lý). Kết quả thật đến qua sự kiện lệnh (on_event), không phải qua ack.
  • Lỗi TradingWSError: status 401 là token hết hạn, 403 là thiếu scope, 429 là hết quota, 5xx là lỗi server; đọc thêm error_code và server_message.
  • Rate limit: SDK tự chờ khi quota về 0 (tùy chọn respect_rate_limit, tối đa max_rate_limit_wait_ms). Quota theo sample: 50 lệnh mỗi cửa sổ cho lệnh, 100 cho truy vấn. Đọc client.rate_limit (limit, remaining, resetMs) để biết còn bao nhiêu.
  • on_event phải ngắn và không in nguyên frame: topic của frame chứa số tài khoản. Sample chỉ giữ eventType và orderStatus.
  • client_request_id (tối đa 20 ký tự) là khóa idempotency của lệnh.
  • Luôn close() socket trong finally.
  • Đọc vị thế và sức mua qua REST: Vị thế, PPMMR & Max mua/bán. Phân loại lỗi chung của SDK: Xử lý lỗi & Rate limit.

Trên trang này