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 facadeTrading/Streamkhông tự kết nối tới nó, bạn gọitrading_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áoModuleNotFoundError; đổi thànhfrom 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õ
YEStrướ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- Kết nối
trading_ws(auth)(async:await async_trading_ws(auth)) - Gắn
client.on_eventđể nhận sự kiện lệnh (frame cóchannel,topic,data) subscribe_order_events(account)→place_order(LO mua) → nhận ack trạng tháiPD→cancel_orderquery_position(có dòng tổng hợp khiquery_summary=True) vàquery_order_book- Đọc
client.rate_limit; xử lýTradingWSError(401/403/429/5xx); luônclose()socket
Các mẫu trong repo
| Ngôn ngữ | File |
|---|---|
| Python | python/sample_17_trading_ws.py, python/sample_17_trading_ws_async.py |
| Node.js | node/sample_17_trading_ws.js |
| .NET | dotnet/Sample17TradingWs.cs |
| Go | go/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) trongconfig.json, xem Tutorial 1. Sample repo đọc cấu hình quaconfig.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.pytrong 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.
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 socketSample 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.
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
| Method | Mô 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_ordertrả về ack trạng tháiPD(đ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:status401 là token hết hạn, 403 là thiếu scope, 429 là hết quota, 5xx là lỗi server; đọc thêmerror_codevàserver_message. - Rate limit: SDK tự chờ khi quota về 0 (tùy chọn
respect_rate_limit, tối đamax_rate_limit_wait_ms). Quota theo sample: 50 lệnh mỗi cửa sổ cho lệnh, 100 cho truy vấn. Đọcclient.rate_limit(limit,remaining,resetMs) để biết còn bao nhiêu. on_eventphải ngắn và không in nguyên frame:topiccủa frame chứa số tài khoản. Sample chỉ giữeventTypevàorderStatus.client_request_id(tối đa 20 ký tự) là khóa idempotency của lệnh.- Luôn
close()socket trongfinally. - Đọ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.