7.3. Thay đổi thông tin lệnh phái sinh (WebSocket)

Nhận thông báo real-time mỗi khi lệnh phái sinh của bạn thay đổi trạng thái — lệnh thường, lệnh điều kiện, và vị thế mở. Server đẩy cập nhật qua WebSocket ngay lập tức, bạn không cần polling. Mỗi loại lệnh có topic riêng để subscribe.
Endpoint: /ws/nesoi. Heartbeat là ping|1 mỗi 2 giây (timeout 45s). Format dữ liệu: message_proto|TOPIC|{JSON}.

Demo tương tác

Địa chỉ WSS
WSSwss://openapi.tcbs.com.vn/ws/nesoi
Payload kết nối / xác thực
{
  "note": "Real auth: authenticate|base64({jwt:token})",
  "format": "authenticate|BASE64_JWT_PAYLOAD"
}
Kênh / Chủ đề
  • DE_ORDERSổ lệnh thường phái sinh.
  • DE_CONDITIONAL_ORDERSổ lệnh điều kiện phái sinh.
  • DE_OPEN_POSITIONVị thế mở và lãi/lỗ.
Đã 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. Wire format thực tế là text frame dạng message_proto|TOPIC|{JSON}. Xem chi tiết bên dưới.

Kết nối

WSS/ws/nesoi
Địa chỉ đầy đủ của WebSocket endpoint:
wss://openapi.tcbs.com.vn/ws/nesoi
BẮT BUỘC: Bạn 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 ping|1.
Ngôn ngữThư việnDisable auto-ping
Pythonwebsocketsconnect(url, ping_interval=None, ping_timeout=None)
JavaScriptwsDefault off — do NOT rely on library ping

Xác thực (Authentication)

Sau khi kết nối thành công, bạn gửi message xác thực theo format sau: Client gửi đi — authenticate message:
authenticate|BASE64_PAYLOAD
Trong đó BASE64_PAYLOAD là base64 encode của chuỗi JSON chứa token OpenAPI của bạn:
{"jwt":"YOUR_OPENAPI_TOKEN"}
Ví dụ cụ thể:
Token OpenAPIigwfUq9M2IqWVhrFrYybEYKWxAxDbYzxz1RzOkp0L0S95lrMtrJDbMgFwy86r8Ir
Base64({"jwt":"<token>"})eyJqd3QiOiJpZ3dmVXE5TTJJcVdWaHJGcll5YkVZS1d4QXhEYll6eHoxUnpPa3AwTDBTOTVsck10ckpEYk1nRnd5ODZyOElyIn0=
Client gửi serverauthenticate|eyJqd3QiOiJpZ3dmVXE5TTJJcVdWaHJGcll5YkVZS1d4QXhEYll6eHoxUnpPa3AwTDBTOTVsck10ckpEYk1nRnd5ODZyOElyIn0=
Server trả về — đầu tiên là timeout config:
pingTimeout|45
Con số 45 nghĩa là nếu client không gửi heartbeat trong 45 giây, server sẽ ngắt kết nối. Server trả về — xác thực thành công:
authenticate|{"success":true,"error":null}
Bạn PHẢI đợi nhận response auth thành công trước khi gửi subscribe. Không gửi subscribe ngay sau authenticate.

Đăng ký kênh (Subscribe)

Sau khi auth thành công, bạn đăng ký từng topic riêng lẻ. Có 3 topic khả dụng:
TopicBase64Mô tả
DE_ORDEReyJ0b3BpYyI6IkRFX09SREVSIn0=Sổ lệnh thường
DE_CONDITIONAL_ORDEReyJ0b3BpYyI6IkRFX0NPTkRJVElPTkFMX09SREVSIn0=Sổ lệnh điều kiện
DE_OPEN_POSITIONeyJ0b3BpYyI6IkRFX09QRU5fUE9TSVRJT04ifQ==Vị thế mở
Client gửi đi — subscribe cho từng topic:
Subscribe theo topic
DE_ORDERsubscribe|eyJ0b3BpYyI6IkRFX09SREVSIn0=
DE_CONDITIONAL_ORDERsubscribe|eyJ0b3BpYyI6IkRFX0NPTkRJVElPTkFMX09SREVSIn0=
DE_OPEN_POSITIONsubscribe|eyJ0b3BpYyI6IkRFX09QRU5fUE9TSVRJT04ifQ==

Heartbeat — Giữ kết nối

Client PHẢI gửi heartbeat text frame mỗi 2 giây. Nếu server không nhận heartbeat trong 45 giây (giá trị pingTimeout) thì sẽ tự động ngắt kết nối. Client gửi đi — heartbeat frame:
ping|1
Heartbeat phải là TEXT frame, KHÔNG phải binary/ping frame theo RFC 6455. Tần suất: 1 message mỗi 2 giây.

Cấu trúc Message

Server đẩy về cho bạn 2 loại message — control (điều khiển) và data (dữ liệu lệnh):
PrefixLoạiMô tả
pingTimeout|NControlTimeout config (N seconds)
authenticate|{...}ControlAuth response
message_proto|TOPIC|{...}DataOrder/position update payload
Server trả về — data message có format như sau:
message_proto|<TOPIC>|{JSON_PAYLOAD}
Ví dụ thực tế — server đẩy về khi lệnh của bạn thay đổi trạng thái:
message_proto|DE_ORDER|{"subAccount":"738764A","orderNo":"41","pkOrderNo":"7861546","orderTime":"14:34:10","accountCode":"738764A","side":"B","symbol":"VN30F2110","volume":1,"showPrice":"MTL","status":"P","orderStatus":"2","channel":"D","group":"FU","isCancel":"1","isAmend":"1"}

Data Schema

DE_ORDER — Sổ lệnh thường

DE_ORDER — Lệnh phái sinh thường
TênKiểuBắt buộcMô tả
subAccountstringSố tài khoản phái sinh.
orderNostringSố hiệu lệnh.
pkOrderNostringPrimary key lệnh.
orderTimestringThời gian đặt lệnh.
accountCodestringMã tài khoản.
sidestringChiều lệnh: B = Mua, S = Bán.
symbolstringMã hợp đồng.
volumeint64Khối lượng đặt lệnh.
showPricestringGiá đặt (hoặc MTL = Market).
matchVolumeint64Khối lượng đã khớp.
matchPriceBQnumberGiá khớp bình quân.
statusstringTrạng thái lệnh (xem bảng tổ hợp dưới).
orderStatusstringMã trạng thái lệnh.
channelstringKênh đặt lệnh.
groupstringNhóm lệnh PS (FU = Futures).
cancelTimestringThời gian huỷ lệnh.
isCancelstringCó thể huỷ (0 = Không, 1 = Có).
isAmendstringCó thể sửa (0 = Không, 1 = Có).
infostringChi tiết từ chối (nếu bị reject).
maxPriceint64Không dùng, luôn là 0.
matchValuenumberGiá trị khớp.
quotestringTrạng thái: C=Hoàn tất, G=Chờ, Y=Khớp 1 phần.
autoTypestringLoại lệnh tự động (Arbitrage, SL/TP, hoặc rỗng).
productstringVới ABI = mã HĐ đối ứng; với SL/TP = chỉ số SL/TP.
orderTypestringLoại lệnh (N = Normal).
sourcestringSYS = hệ thống, USER = người dùng.
traderCodestringMã kênh đặt lệnh.

Bảng tổ hợp trạng thái DE_ORDER

statusquoteMô tả
PGChờ khớp trên gateway — có thể huỷ
PYLệnh đã vào sổ
PThông tin lệnh có sửa đổi
PMGLệnh đã khớp
PCYLệnh chờ xác nhận sửa
PCGĐã confirm sửa
PMWLệnh khớp 1 phần yêu cầu huỷ
PWYĐặt lệnh huỷ — chờ huỷ
PWXLệnh huỷ được confirm bởi sở
PMWXLệnh khớp 1 phần huỷ đã xác nhận

DE_CONDITIONAL_ORDER — Sổ lệnh điều kiện

DE_CONDITIONAL_ORDER — Lệnh điều kiện phái sinh
TênKiểuBắt buộcMô tả
sub_accountstringSố tài khoản PS.
order_nostringSố hiệu lệnh.
group_orderstringNhóm lệnh.
pk_order_nostringPrimary key lệnh.
account_codestringMã tài khoản.
sidestringB = Mua, S = Bán.
symbolstringMã hợp đồng.
volumeint64Khối lượng lệnh.
show_pricestringGiá đặt.
conditionstringĐiều kiện kích hoạt.
resultstring0 = Huỷ, 1 = Kích hoạt.
active_timestringThời gian kích hoạt.
send_timestringThời gian đặt lệnh.
cancel_timestringThời gian huỷ.
groupstringNhóm PS (FU).
channelstringKênh đặt lệnh.
so_pricenumberGiá stop.
order_typestringLoại lệnh.
from_timestringCó giá trị từ ngày.
exp_timestringĐến ngày.
statusstring1 = Kích hoạt, 2 = Đã huỷ, 3 = Từ chối.
detailsstringSố hiệu lệnh đã kích hoạt.
messagestringThông báo.
notesstringLoại chiến lược (SL/TP, Stop order, Arbitrage).
call_back_pointstringBiên trượt (trailing stop).
trailing_pricestringGiá trailing.
trigger_conditionstringĐiều kiện kích hoạt trailing.

DE_OPEN_POSITION — Vị thế mở

DE_OPEN_POSITION — Vị thế mở phái sinh
TênKiểuBắt buộcMô tả
sub_accountstringSố tài khoản PS.
symbolstringMã hợp đồng.
sidestringL = Long, S = Short.
accountstringSố tài khoản (duplicate).
last_pricenumberGiá hiện tại.
avg_remainnumberGiá trung bình phần còn lại.
im_valuenumberGiá trị ký quỹ ban đầu.
netnumberSố vị thế đang nắm giữ.
stoplossnumberGiá stop loss.
takeprofitnumberGiá take profit.
pc_remainnumber% lãi/lỗ vị thế mở.
vm_remainnumberGiá trị VM chưa đóng.
duedatestringNgày đáo hạn.
netoffvolint64Số cần net-off.
trigger_typestringGiá kích hoạt trailing.
call_back_pointnumberBiên trượt trailing.
trailing_pricenumberGiá lệnh trailing.

Sequence Diagram


Xử lý lỗi & Reconnect

Nguyên nhânTriệu chứngGiải pháp
No heartbeat >45sConnection closedEnsure ping|1 every 2s
Library sends RFC pingUnexpected disconnectDisable auto-ping
Token expiredAuth failsRefresh token, reconnect
Server maintenanceConnection refusedExponential backoff retry
Chiến lược reconnect: Dùng exponential backoff với công thức delay = min(2^attempt, 30) giây. Tối đa thử 10 lần, sau đó bạn nên alert để xử lý thủ công.

Code hoàn chỉnh

"""WebSocket client for TCBS derivative order updates — Python 3.10+"""

import asyncio
import base64
import json

from websockets.asyncio.client import connect


WS_URL = "wss://openapi.tcbs.com.vn/ws/nesoi"
TOKEN = "YOUR_OPENAPI_TOKEN_HERE"

TOPICS = ["DE_ORDER", "DE_CONDITIONAL_ORDER", "DE_OPEN_POSITION"]


async def main():
    async with connect(WS_URL, ping_interval=None, ping_timeout=None) as ws:

        # 1. Authenticate
        jwt_payload = json.dumps({"jwt": TOKEN})
        encoded = base64.b64encode(jwt_payload.encode()).decode()
        await ws.send(f"authenticate|{encoded}")

        # 2. Wait for auth response
        while True:
            msg = await asyncio.wait_for(ws.recv(), timeout=10.0)
            print(f"< {msg}")
            if msg.startswith("authenticate|"):
                payload = msg.split("|", 1)[1]
                result = json.loads(payload)
                if result.get("success"):
                    break
                else:
                    raise Exception(f"Auth failed: {result}")

        # 3. Subscribe to all topics
        for topic in TOPICS:
            sub_payload = base64.b64encode(json.dumps({"topic": topic}).encode()).decode()
            await ws.send(f"subscribe|{sub_payload}")
            print(f"Subscribed to {topic}")

        # 4. Heartbeat task
        async def heartbeat():
            while True:
                await ws.send("ping|1")
                await asyncio.sleep(2)

        asyncio.create_task(heartbeat())

        # 5. Receive updates
        while True:
            raw = await ws.recv()
            if raw.startswith("message_proto|"):
                parts = raw.split("|", 2)
                topic = parts[1]
                data = json.loads(parts[2])

                if topic == "DE_ORDER":
                    print(f"[ORDER] {data.get('symbol')} "
                          f"Side={data.get('side')} "
                          f"Status={data.get('status')} "
                          f"Vol={data.get('volume')}")
                elif topic == "DE_OPEN_POSITION":
                    print(f"[POSITION] {data.get('symbol')} "
                          f"Net={data.get('net')} "
                          f"PnL%={data.get('pcRemain')}")
                elif topic == "DE_CONDITIONAL_ORDER":
                    print(f"[COND] {data.get('symbol')} "
                          f"Status={data.get('status')} "
                          f"Type={data.get('notes')}")


if __name__ == "__main__":
    asyncio.run(main())

FAQ

Q

Endpoint Nesoi khác gì so với Aither (cổ phiếu)?

A

Nesoi dành riêng cho phái sinh, hỗ trợ 3 topic (DE_ORDER, DE_CONDITIONAL_ORDER, DE_OPEN_POSITION). Aither chỉ có 1 topic (STOCK_ORDER). Timeout của Nesoi là 45s (vs 7s của Aither).

Q

Trường statusquote kết hợp như thế nào?

A

status cho biết trạng thái luồng (P=Pending, PM=Matched, PC=Cancel, PW=Waiting cancel...), quote cho biết trạng thái chi tiết (G=Gateway/chờ, Y=Đã gửi sổ). Xem bảng tổ hợp ở trên.

Q

DE_OPEN_POSITION push khi nào?

A

Mỗi khi vị thế của bạn thay đổi (lệnh khớp, net-off) hoặc khi giá thị trường thay đổi đáng kể (cập nhật lãi/lỗ realtime).

Q

Có cần subscribe tất cả 3 topic không?

A

Không bắt buộc. Bạn chỉ cần subscribe topic nào mình quan tâm. Ví dụ chỉ theo dõi lệnh thường thì subscribe DE_ORDER là đủ.