Nhận bảng giá cổ phiếu cơ sở (HOSE, HNX, UPCOM) theo thời gian thực qua WebSocket.
Bạn chỉ cần mở một kết nối duy nhất — server sẽ tự động đẩy dữ liệu mỗi khi có thay đổi. Dữ liệu bao gồm:
Sổ lệnh mua/bán top 3 (bid/offer)
Giá khớp, khối lượng khớp
Giá trần, sàn, tham chiếu
Room khối ngoại
Chỉ số (VN-INDEX, VN30...)
Giao dịch thỏa thuận
Trang này dành cho cổ phiếu cơ sở (endpoint /stream/normal). Nếu bạn cần bảng giá phái sinh, xem trang 7.2 — endpoint, channel ID và data type đều khác.
{
"note": "Conceptual only — real auth is pipe-delimited: d|a|||[base64_token]",
"format": "d|a|||[BASE64_JWT]"
}
Kênh / Chủ đề
s|1 (bid info)Sổ lệnh mua top 3: giá và khối lượng.
s|2 (offer info)Sổ lệnh bán top 3: giá và khối lượng.
s|3 (foreign)Khối lượng mua/bán khối ngoại và room còn lại.
s|4 (base price)Giá trần, sàn, tham chiếu.
s|5 (matched price)Thông tin phiên: cao, thấp, trung bình, mở cửa.
s|6 (ticker match)Giao dịch khớp: giá, khối lượng, thay đổi.
Đã ngắt
// Nhấn Kết nối để bắt đầu luồng mô phỏng…
⚠️ Demo trên chỉ mang tính minh họa. Protocol thực tế trên wire là TEXT frame phân tách bằng pipe (|), KHÔNG phải JSON. Xem các phần bên dưới để biết chi tiết format thật.
⚠️ BẮT BUỘC: Phải disable auto-ping (RFC 6455 ping frame) của WebSocket library. Server TCBS chỉ chấp nhận heartbeat dạng text frame d|p|||. Nếu library tự gửi binary ping → server trả 1002 Protocol Error và ngắt kết nối.
Sau khi kết nối WebSocket thành công, bạn phải gửi auth message trước khi subscribe bất kỳ kênh nào. Token OpenAPI cần được base64 encode trước khi gửi.
Format auth message gửi tới server (xxx = base64 của token_openapi):
Sau khi auth thành công, bạn có 4 cách đăng ký dữ liệu tuỳ nhu cầu: theo mã, theo sàn, theo chỉ số, và theo thỏa thuận.
1. Theo mã cổ phiếu (tk)
Message subscribe theo mã gửi tới server:
d|s|tk|bp+bi+tm+mp+op+fe|<SYMBOLS>
Ví dụ: d|s|tk|bp+bi+tm+mp+op+fe|FPT,VCS,PNJ,PTB
Bảng channel codes tương ứng với channel ID mà server trả về:
Code
Ý nghĩa
Channel ID
bi
Sổ lệnh mua
s|1
op
Sổ lệnh bán
s|2
fe
Khối ngoại
s|3
bp
Giá trần-sàn-tham chiếu
s|4
mp
Thông tin phiên
s|5
tm
Giao dịch khớp
s|6
tmh
Lịch sử khớp trong ngày
s|22
2. Theo sàn (ro)
Message subscribe theo sàn gửi tới server:
d|s|ro|bp+bi+tm+mp+op+fe|<BOARDS>
Ví dụ: d|s|ro|bp+bi+tm+mp+op+fe|1,2 — server sẽ trả về các channel s|1, s|2, s|3, s|4, s|5, s|6 như trên.
3. Theo chỉ số (si)
Message subscribe theo chỉ số gửi tới server:
d|s|si|rt|<BOARDS>
Ví dụ: d|s|si|rt|1,2 — server trả về channel s|8 (rt / stock index).
Bảng mã sàn / chỉ số (dùng cho cả ro và si):
Mã
Sàn / Chỉ số
1
VN-INDEX
2
VN30-INDEX
3
HNX
4
HNX30-INDEX
5
UPCOM
7
Covered warrant
8
Derivative-VN30
9
Derivative-VGB5
4. Theo giao dịch thỏa thuận (pt)
Message subscribe thỏa thuận gửi tới server:
d|s|pt|ptm+ptb+pts|<FLOORS>
Ví dụ: d|s|pt|ptm+ptb+pts|1,2,3 — server trả về channel s|16 (pta) và s|17 (ptm). Mã sàn: 1-HOSE, 2-HNX, 3-UPCOM.
Huỷ đăng ký (Unsubscribe)
Để huỷ đăng ký, bạn dùng action u thay vì s. Cú pháp giống hoàn toàn lệnh subscribe.
Format chung của message unsubscribe gửi tới server:
d|u|<objectType>|<properties>|<object list>
Ví dụ
Huỷ theo mã — message gửi tới server:
d|u|tk|bp+bi+tm+mp+op+fe|TCB,VCB
Huỷ theo sàn — message gửi tới server:
d|u|ro|bp+bi+tm+mp+op+fe|1,2
Huỷ theo chỉ số — message gửi tới server:
d|u|si|rt|1,2
Huỷ giao dịch thỏa thuận — message gửi tới server:
d|u|pt|ptm+ptb+pts|1,2,3
Bạn có thể unsubscribe một phần mã. Ví dụ đang subscribe FPT,TCB,VCB mà muốn bỏ TCB → gửi d|u|tk|bp+bi+tm+mp+op+fe|TCB. Hai mã còn lại (FPT, VCB) vẫn tiếp tục nhận dữ liệu.
Lấy dữ liệu snapshot (Request)
Ngoài subscribe (nhận realtime liên tục), bạn có thể gửi lệnh request để lấy snapshot hiện tại một lần duy nhất mà không cần subscribe. Dùng action r.
Format message request gửi tới server (lưu ý phần properties để trống — 2 dấu pipe liên tiếp ||):
d|r|<objectType>||<object list>
Ví dụ
Request snapshot theo mã — message gửi tới server:
d|r|tk||TCB,VCB
Server trả về channel s|38 (Tickers snapshot) chứa toàn bộ thông tin hiện tại của các mã bạn yêu cầu.
Request snapshot theo sàn — message gửi tới server:
d|r|ro||2
Server trả về snapshot toàn bộ mã trong sàn VN30.
Request chỉ trả về 1 lần (one-shot). Nếu bạn cần dữ liệu liên tục, hãy dùng subscribe. Request hữu ích khi khởi tạo UI hoặc đồng bộ lại state sau reconnect.
Lịch sử khớp trong ngày — tmh (Ticker Match History)
Ngoài các channel cơ bản (bi, op, fe, bp, mp, tm), bạn có thể subscribe thêm tmh để nhận lịch sử khớp lệnh trong ngày (tick-by-tick).
Ví dụ subscribe có thêm tmh — message gửi tới server:
d|s|tk|bp+bi+tm+tmh+mp+op+fe|TCB,FPT
Khi subscribe tmh, server trả về channel s|22 (TickerMatchHis) chứa lịch sử khớp lệnh trong ngày cho mỗi mã.
⚠️ Channel ID hoàn toàn khác giữa 2 endpoint, và data type cũng khác (cổ phiếu trả string cho giá/khối lượng, phái sinh trả number). KHÔNG dùng chung parser cho cổ phiếu và phái sinh.
Sequence Diagram — Luồng hoàn chỉnh
Luồng kết nối WebSocket bảng giá cơ sở
Xử lý lỗi & Reconnect
Nguyên nhân disconnect
Nguyên nhân
Triệu chứng
Giải pháp
No heartbeat >15s
Close 1000
Check heartbeat task
Library sends RFC ping
Error 1002
Disable auto-ping
Token expired
d|0|{"error":...}
Refresh token, reconnect
Server maintenance
Connection refuse
Exponential backoff retry
Outside trading hours
Connect OK, no data
Normal — wait for session open
Chiến lược reconnect (khuyến nghị)
Dùng exponential backoff: delay = min(2^attempt, 30) giây. Tối đa 10 lần thử, sau đó dừng lại và alert.
Lần thử
Delay
1
1s
2
2s
3
4s
4
8s
5
16s
6+
30s
Ngoài giờ giao dịch
Phiên cơ sở: 9:00–11:30 (sáng) và 13:00–14:45 (chiều), có phiên ATC đến ~14:45–15:00 tuỳ sàn. Ngoài giờ thì bạn vẫn connect bình thường, heartbeat vẫn cần gửi, nhưng sẽ không có data message nào. Khi phiên sau mở, data tự chảy lại — không cần reconnect.
Code hoàn chỉnh
"""Minimal WebSocket cash price-board client for TCBS — Python 3.10+"""import asyncioimport base64import json# pip install websockets>=12.0from websockets.asyncio.client import connectWS_URL = "wss://openapi.tcbs.com.vn/ws/thesis/v1/stream/normal"TOKEN = "YOUR_OPENAPI_TOKEN_HERE"SYMBOLS = "FPT,VNM,HPG"async def main(): # 1. Connect — MUST disable auto-ping async with connect(WS_URL, ping_interval=None, ping_timeout=None) as ws: # 2. Auth — base64 encode token encoded_token = base64.b64encode(TOKEN.encode()).decode() await ws.send(f"d|a|||{encoded_token}") # 3. Wait for auth response: d|0|{"success":true,...} auth_response = await asyncio.wait_for(ws.recv(), timeout=5.0) print(f"Auth response: {auth_response}") if '"success":true' not in auth_response: print("Auth failed!") return # 4. Subscribe by symbol await ws.send(f"d|s|tk|bp+bi+tm+mp+op+fe|{SYMBOLS}") print(f"Subscribed to {SYMBOLS}") # 5. Start heartbeat background task async def heartbeat(): while True: await ws.send("d|p|||") await asyncio.sleep(2) asyncio.create_task(heartbeat()) # 6. Receive & parse messages while True: raw = await ws.recv() # Skip control messages if raw.startswith("d|"): continue # Parse data: s|[channel]|[json] if raw.startswith("s|"): parts = raw.split("|", 2) if len(parts) == 3: channel = parts[1] payload = json.loads(parts[2]) if channel == "6": print(f"[TRADE] {payload['symbol']} " f"Price={payload['matchPrice']} " f"Qty={payload['matchQtty']}") elif channel == "1": print(f"[BID] {payload['symbol']} " f"Best={payload['bidPrice01']} " f"Qty={payload['bidQtty01']}") elif channel == "2": print(f"[OFFER] {payload['symbol']} " f"Best={payload['offerPrice01']}") elif channel == "4": print(f"[BASE] {payload['symbol']} " f"Ref={payload['refPrice']}")if __name__ == "__main__": asyncio.run(main())
FAQ — Câu hỏi thường gặp
Q
Tại sao bị disconnect sau 15-20 giây?
A
99% là do library tự gửi RFC ping frame. Phải disable auto-ping và tự gửi d|p||| mỗi 2s.
Q
matchPrice là chuỗi (string) hay số (number)?
A
Bảng giá cơ sở trả string cho hầu hết trường giá và khối lượng (bi, op, fe, tm). Riêng bp, mp, rt trả number. Khuyến nghị: cast rõ ràng khi parse.
Q
Muốn nhận toàn bộ một sàn thay vì từng mã thì làm sao?
A
Dùng lệnh ro — ví dụ d|s|ro|bp+bi+tm+mp+op+fe|1,2 để nhận toàn bộ VN-INDEX và VN30-INDEX.
Q
Chỉ số (VN-INDEX...) lấy ở channel nào?
A
Subscribe bằng d|s|si|rt|1,2 và đọc channel s|8 (rt / stock index).
Q
Connect ngoài giờ giao dịch có được không?
A
Được. Connection sống bình thường (vẫn cần heartbeat), nhưng không có data cho đến khi phiên mở (9:00).
Q
Token expire thì sao?
A
Token hiệu lực tối đa 8 giờ. Server ngắt connection khi token hết hạn. Client cần detect disconnect → refresh token → reconnect.