跳至主要内容

Fast Order Response SBE

僅限 MMWS

此頻道可透過您的專屬 Market Maker WebSocket(MMWS) 主機訪問,標準 WebSocket 端點無法使用。

概述

Fast Order SBE 頻道透過 Market Maker WebSocket(MMWS)為高頻交易(HFT)客戶提供超低延遲的訂單推送。它直接從撮合引擎推送 SBE(Simple Binary Encoding)二進位編碼訊息,用於下單、改單和撤單的確認回執。

本頻道以速度和效率為核心設計,僅推送用戶主動操作觸發的事件,不包含成交通知、系統撤單等被動事件。

上線時間表

產品測試網主網
現貨2026年5月27日2026年6月23日
期貨(線性 & 反向)2026年6月9日2026年7月9日
期權2026年6月16日2026年7月21日

連接

環境URL
測試網wss://stream-testnet.bybits.org/v5/private-sbe
主網wss://<your-dedicated-MMWS-host>.bybit-aws.com/v5/private-sbe
  • SBE 訊息以二進位幀opcode = 2)發送。
  • 控制幀(鑒權、ping/pong、訂閱/取消訂閱)使用標準 Bybit V5 API JSON 格式

鑒權

建立連接後必須立即進行鑒權。

鑒權請求

{
"req_id": "10001",
"op": "auth",
"args": [
"api_key",
1662350400000,
"signature"
]
}
欄位說明
req_id可選的客戶端標識符
args[1]時間戳,必須大於當前時間
args[2]使用 Bybit API 簽名算法 生成

鑒權成功響應

{
"success": true,
"ret_msg": "",
"op": "auth",
"conn_id": "cejreaspqfh3sjdnldmg-p"
}

心跳

發送 Ping

{"req_id": "100001", "op": "ping"}

接收 Pong

{
"success": true,
"ret_msg": "pong",
"conn_id": "465772b1-7630-4fdc-a492-e003e6f0f260",
"req_id": "100001",
"op": "ping"
}

訂閱

可用 Topic

Topic說明
order.sbe.resp.spot現貨快速訂單響應
order.sbe.resp.linear線性合約(USDT/USDC)快速訂單響應
order.sbe.resp.inverse反向合約快速訂單響應
order.sbe.resp.option期權快速訂單響應

訂閱示例

{
"op": "subscribe",
"args": ["order.sbe.resp.linear", "order.sbe.resp.spot", "order.sbe.resp.option"]
}

訂閱確認

{
"success": true,
"ret_msg": "",
"conn_id": "d30fdpbboasp1pjbe7r0",
"req_id": "abc123",
"op": "subscribe"
}

推送邏輯

當用戶主動發起操作(下單、改單、撤單)時,fast.resp.order 訊息將主動推送給客戶端。

信息

頻道重啟或重新訂閱後,推送從最新的撮合事件開始——核心是速度,不支持補發歷史數據。

場景 / 事件是否推送備註
Maker 訂單新建(已接受 / ack)✅ 是所有由客戶端主動發起的操作(下單 / 改單 / 撤單 / 拒絕)。
Maker 訂單成交 / 部分成交✅ 是所有由客戶端主動發起的操作(下單 / 改單 / 撤單 / 拒絕)。
Taker 訂單(主動方)✅ 是所有由客戶端主動發起的操作(下單 / 改單 / 撤單 / 拒絕)。
COT(CloseOnTrigger)訂單✅ 是(針對觸發後訂單)觸發的 COT 訂單行為類似新的 Taker 訂單;若開反向倉位,orderLinkId=""
RO / ReduceOnly 訂單✅ 是正常推送;若因保證金不足或倉位限制被拒絕,rejectReason 將有值。
條件單 / TP-SL 觸發訂單✅ 是條件觸發且訂單變為活躍後推送,orderLinkId="" (為空)。
DCP(斷線全部保護)✅ 是DCP 在斷線時強制撤單時推送。
SMP 撤 Taker / 同時撤兩方(自成交保護)✅ 是Taker / Maker 兩側撤單均會推送。
SMP 撤 Maker✅ 是Taker / Maker 兩側撤單均會推送。
MMP(做市商保護)✅ 是MMP 觸發的撤單也會在 Fast Order 頻道推送。
下架 / 合約到期 / 期權交割❌ 否系統主動平倉,不推送 Fast Order。
訂單被拒(撮合 / 驗證拒絕)✅ 是立即推送,附帶 rejectReason
改單成功 / 拒絕✅ 是主動改單的 ack / 拒絕均推送。
撤單成功 / 拒絕✅ 是主動撤單的 ack / 拒絕均推送。

与标准 WebSocket 订单频道的差异

下表列出了 Fast Order SBE 频道(order.sbe.resp.*)与标准私有 WebSocket 订单频道在行为上的差异。

场景Fast Order 频道标准 WS 订单频道说明
条件单下改撤(触发前)❌ 无推送✅ 有推送条件单在触发前不会发送到撮合引擎,撮合引擎无法获取触发前的订单状态。
TP/SL 订单下改撤(触发前)❌ 无推送✅ 有推送原因同上。
Trailing Stop 订单下改撤(触发前)❌ 无推送✅ 有推送原因同上。
仓位强平订单❌ 无推送✅ 有推送注:强平触发的撤单两者推送一致。
合约下架撤单❌ 无推送✅ 有推送下架撤单由内部处理,不发送到撮合引擎。
改单/撤单请求到达前订单已完全成交orderStatus=RejectedorderStatus=Filled例如 qty=10,改成 12,但 10 手已成交 — Fast Order 返回 Rejected,标准 WS 返回 Filled
正常下改撤 — leavesValue 字段无值(0有值Fast Order 对非现货市价按金额买单(non-spot-market-buy-order-by-value)的 leavesValue 始终返回 0
盘前集合竞价阶段 — 撤单被拒orderStatus=RejectedorderStatus=New盘前集合竞价阶段不允许撤单,Fast Order 返回 Rejected,标准 WS 返回 New

OrderLinkId 各版本行為說明

場景2026 測試網 / 主網備註
主動新建訂單(用戶發起)✅ 有值客戶端發起的下單包含用戶的 orderLinkId
改單 / 撤單(用戶發起)✅ 有值客戶端發起的下單包含用戶的 orderLinkId
Maker→Taker 轉變(如價格改單穿越盤口)✅ 有值客戶端發起的下單包含用戶的 orderLinkId
主動新建條件單(用戶發起)✅ 有值客戶端發起的下單包含用戶的 orderLinkId
倉位設置止盈止損訂單❌ 為空系統創建,無 orderLinkId

消息結構(SBE)

templateId = 21000FastOrderResp

消息頭(8 字節)

欄位類型大小(字節)說明
blockLengthuint162消息體長度
templateIduint162固定 = 21000
schemaIduint162固定 = 1
versionuint162固定 = 0

消息體

ID欄位類型說明
1categoryuint81=現貨, 2=線性, 3=反向, 4=期權
2sideuint81=買, 2=賣
3orderStatusuint8訂單狀態枚舉。0=Others, 4=PartiallyFilledAndCancelled, 5=Rejected, 6=New, 7=Cancelled, 8=PartiallyFilled, 9=Filled
4priceExponentint8價格小數位數。price = mantissa / 10^priceExponent
5sizeExponentint8數量小數位數
6valueExponentint8金額小數位數
7rejectReasonuint16無拒絕時為 0,詳見 rejectReason 映射
8priceint64價格尾數(需應用 priceExponent
9leavesQtyint64剩餘數量尾數(需應用 sizeExponent
10leavesValueint64僅現貨市價買單有效,其他情況為 0(需應用 valueExponent
11creationTimeint64訂單在 Fast Order 頻道的創建時間戳(微秒)
12updatedTimeint64撮合時間戳(微秒)
13seqint64跨序列 ID
14symbolIDint32交易對 ID
100orderIdvarString8訂單 ID(UUID)
101orderLinkIdvarString8可選;用戶主動操作時存在

rejectReason 映射

代碼名稱
0EC_NoError
1EC_Others
2EC_UnknownMessageType
3EC_MissingClOrdID
4EC_MissingOrigClOrdID
5EC_ClOrdIDOrigClOrdIDAreTheSame
6EC_DuplicatedClOrdID
7EC_OrigClOrdIDDoesNotExist
8EC_TooLateToCancel
9EC_UnknownOrderType
10EC_UnknownSide
11EC_UnknownTimeInForce
12EC_WronglyRouted
13EC_MarketOrderPriceIsNotZero
14EC_LimitOrderInvalidPrice
15EC_NoEnoughQtyToFill
16EC_NoImmediateQtyToFill
17EC_QtyCannotBeZero
18EC_PerCancelRequest
19EC_MarketOrderCannotBePostOnly
20EC_PostOnlyWillTakeLiquidity
21EC_CancelReplaceOrder
22EC_InvalidSymbolStatus
23EC_MarketOrderNoSupportTIF
24EC_ReachMaxTradeNum
25EC_InvalidPriceScale
26EC_BitIndexInvalid
27EC_StopBySelfMatch
28EC_BySelfMatch
29EC_InvalidSmpType
30EC_CancelByMMP
31EC_InCallAuctionStatus
34EC_InvalidUserType
35EC_InvalidMirrorOid
36EC_InvalidMirrorUid
37EC_SymbolNotExist
38EC_CancelNoActiveOrders
39EC_MissingUID
100EC_EcInvalidQty
101EC_InvalidAmount
102EC_LoadOrderCancel
103EC_CancelForNoFullFill
104EC_MarketQuoteNoSuppSell
105EC_DisorderOrderID
106EC_InvalidBaseValue
107EC_LoadOrderCanMatch
108EC_SecurityStatusFail
110EC_ReachRiskPriceLimit
111EC_OrderNotExist
112EC_CancelByOrderValueZero
113EC_CancelByMatchValueZero
200EC_ReachMarketPriceLimit

SBE XML 模板(Fast Order Response)

<?xml version="1.0" encoding="UTF-8"?>
<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
xmlns:mbx="https://bybit-exchange.github.io/docs/v5/intro"
package="order.fast.sbe"
id="1"
version="0"
semanticVersion="1.0.0"
description="Bybit fast order response SBE schema"
byteOrder="littleEndian"
headerType="messageHeader">
<types>
<composite name="messageHeader" description="Template ID and length of message root">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
<composite name="varString8" description="Variable length UTF-8 string">
<type name="length" primitiveType="uint8"/>
<type name="varData" length="0" primitiveType="uint8" semanticType="String" characterEncoding="UTF-8"/>
</composite>
</types>
<!-- Fast order response: active place/cancel/amend acknowledgements -->
<sbe:message name="FastOrderResp" id="21000">
<!-- Routing / classification -->
<field id="1" name="category" type="uint8" description="1=spot, 2=linear, 3=inverse, 4=option"/>
<!-- Side / status / rejection -->
<field id="2" name="side" type="uint8" description="1=Buy, 2=Sell"/>
<field id="3" name="orderStatus" type="uint8" description="Order state enum"/>
<!-- Price / size (mantissas) with exponents -->
<field id="4" name="priceExponent" type="int8" description="Decimal places for price"/>
<field id="5" name="sizeExponent" type="int8" description="Decimal places for size"/>
<field id="6" name="valueExponent" type="int8" description="Decimal places for value"/>
<field id="7" name="rejectReason" type="uint16" description="0 if N/A"/>
<field id="8" name="price" type="int64" mbx:exponent="priceExponent" description="Price mantissa"/>
<field id="9" name="leavesQty" type="int64" mbx:exponent="sizeExponent" description="Remaining quantity mantissa"/>
<field id="10" name="leavesValue" type="int64" mbx:exponent="valueExponent" description="Spot market buy only; otherwise 0"/>
<!-- Timing -->
<field id="11" name="creationTime" type="int64" description="Order creation timestamp in Fast order channel(microseconds)"/>
<field id="12" name="updatedTime" type="int64" description="Matching timestamp (microseconds)"/>
<field id="13" name="seq" type="int64" description="Cross sequence ID"/>
<!-- SymbolID -->
<field id="14" name="symbolID" type="int32" description="Symbol ID"/>
<!-- Order identifiers -->
<data id="100" name="orderId" type="varString8" description="Order ID"/>
<data id="101" name="orderLinkId" type="varString8" description="Optional; present for user-initiated orders"/>
</sbe:message>
</sbe:messageSchema>

代碼示例

package main

import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"math"
"os"
"os/signal"
"time"

"github.com/gorilla/websocket"
)

// ---------- Config ----------

const (
MMWSURLTestnetBybits = "wss://stream-testnet.bybits.org/v5/private-sbe"
MMWSURLTestnetBybit = "wss://stream-testnet.bybit.com/v5/private-sbe"
MMWSURLMainnet = "wss://stream.bybit.com/v5/private-sbe"
)

// TODO: 填入您的真實密鑰
const (
APIKey = "YOUR_API_KEY"
APISecret = "YOUR_API_SECRET"
)

var subTopics = []string{
"order.sbe.resp.spot",
}

// ---------- SBE helpers ----------

func readU8(buf []byte, off *int) (uint8, error) {
if *off+1 > len(buf) {
return 0, fmt.Errorf("readU8: out of range")
}
v := buf[*off]
*off++
return v, nil
}

func readI8(buf []byte, off *int) (int8, error) {
if *off+1 > len(buf) {
return 0, fmt.Errorf("readI8: out of range")
}
v := int8(buf[*off])
*off++
return v, nil
}

func readU16LE(buf []byte, off *int) (uint16, error) {
if *off+2 > len(buf) {
return 0, fmt.Errorf("readU16LE: out of range")
}
v := binary.LittleEndian.Uint16(buf[*off : *off+2])
*off += 2
return v, nil
}

func readI32LE(buf []byte, off *int) (int32, error) {
if *off+4 > len(buf) {
return 0, fmt.Errorf("readI32LE: out of range")
}
v := int32(binary.LittleEndian.Uint32(buf[*off : *off+4]))
*off += 4
return v, nil
}

func readI64LE(buf []byte, off *int) (int64, error) {
if *off+8 > len(buf) {
return 0, fmt.Errorf("readI64LE: out of range")
}
v := int64(binary.LittleEndian.Uint64(buf[*off : *off+8]))
*off += 8
return v, nil
}

func readVarString8(buf []byte, off *int) (string, error) {
if *off+1 > len(buf) {
return "", fmt.Errorf("readVarString8: no length byte")
}
ln := int(buf[*off])
*off++
if ln == 0 {
return "", nil
}
if *off+ln > len(buf) {
return "", fmt.Errorf("readVarString8: length out of range")
}
s := string(buf[*off : *off+ln])
*off += ln
return s, nil
}

func applyExp(mantissa int64, exp int8) float64 {
e := int(exp)
if e >= 0 {
return float64(mantissa) / math.Pow10(e)
}
return float64(mantissa) * math.Pow10(-e)
}

// ---------- Fast Order SBE decode ----------

type FastOrderSBEResp struct {
SBEHeader struct {
BlockLength uint16 `json:"blockLength"`
TemplateID uint16 `json:"templateId"`
SchemaID uint16 `json:"schemaId"`
Version uint16 `json:"version"`
} `json:"_sbe_header"`

Category uint8 `json:"category"`
Side uint8 `json:"side"`
OrderStatus uint8 `json:"orderStatus"`
PriceExponent int8 `json:"priceExponent"`
SizeExponent int8 `json:"sizeExponent"`
ValExponent int8 `json:"valueExponent"`
RejectReason uint16 `json:"rejectReason"`

PriceMantissa int64 `json:"priceMantissa"`
LeavesQtyMantissa int64 `json:"leavesQtyMantissa"`
LeavesValueMantissa int64 `json:"leavesValueMantissa"`

CreationTime int64 `json:"creationTime"`
UpdatedTime int64 `json:"updatedTime"`
Seq int64 `json:"seq"`

SymbolID int32 `json:"symbolID"`
OrderID string `json:"orderId"`
OrderLinkID string `json:"orderLinkId"`

Price float64 `json:"price"`
LeavesQty float64 `json:"leavesQty"`
LeavesValue float64 `json:"leavesValue"`

RawOffsetEnd int `json:"_raw_offset_end"`
}

func decodeFastOrderResp(payload []byte, debug bool) (*FastOrderSBEResp, error) {
if len(payload) < 8 {
return nil, fmt.Errorf("payload too short for SBE header")
}
off := 0
blockLen := binary.LittleEndian.Uint16(payload[off : off+2])
templateID := binary.LittleEndian.Uint16(payload[off+2 : off+4])
schemaID := binary.LittleEndian.Uint16(payload[off+4 : off+6])
version := binary.LittleEndian.Uint16(payload[off+6 : off+8])
off += 8

if debug {
log.Printf("HEADER: block_len=%d, template_id=%d, schema_id=%d, version=%d",
blockLen, templateID, schemaID, version)
}

if templateID != 21000 {
return nil, fmt.Errorf("unexpected templateId: %d", templateID)
}

var err error
resp := &FastOrderSBEResp{}
resp.SBEHeader.BlockLength = blockLen
resp.SBEHeader.TemplateID = templateID
resp.SBEHeader.SchemaID = schemaID
resp.SBEHeader.Version = version

if resp.Category, err = readU8(payload, &off); err != nil {
return nil, err
}
if resp.Side, err = readU8(payload, &off); err != nil {
return nil, err
}
if resp.OrderStatus, err = readU8(payload, &off); err != nil {
return nil, err
}
if resp.PriceExponent, err = readI8(payload, &off); err != nil {
return nil, err
}
if resp.SizeExponent, err = readI8(payload, &off); err != nil {
return nil, err
}
if resp.ValExponent, err = readI8(payload, &off); err != nil {
return nil, err
}
if resp.RejectReason, err = readU16LE(payload, &off); err != nil {
return nil, err
}
if resp.PriceMantissa, err = readI64LE(payload, &off); err != nil {
return nil, err
}
if resp.LeavesQtyMantissa, err = readI64LE(payload, &off); err != nil {
return nil, err
}
if resp.LeavesValueMantissa, err = readI64LE(payload, &off); err != nil {
return nil, err
}
if resp.CreationTime, err = readI64LE(payload, &off); err != nil {
return nil, err
}
if resp.UpdatedTime, err = readI64LE(payload, &off); err != nil {
return nil, err
}
if resp.Seq, err = readI64LE(payload, &off); err != nil {
return nil, err
}
if resp.SymbolID, err = readI32LE(payload, &off); err != nil {
return nil, err
}
if resp.OrderID, err = readVarString8(payload, &off); err != nil {
return nil, err
}
if resp.OrderLinkID, err = readVarString8(payload, &off); err != nil {
return nil, err
}

resp.Price = applyExp(resp.PriceMantissa, resp.PriceExponent)
resp.LeavesQty = applyExp(resp.LeavesQtyMantissa, resp.SizeExponent)
resp.LeavesValue = applyExp(resp.LeavesValueMantissa, resp.ValExponent)
resp.RawOffsetEnd = off

return resp, nil
}

// ---------- WebSocket helpers ----------

func sendJSON(conn *websocket.Conn, v any) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, data)
}

func signAuth(secret, value string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(value))
return hex.EncodeToString(h.Sum(nil))
}

func heartbeat(ctx context.Context, conn *websocket.Conn) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
reqID := fmt.Sprintf("%d", time.Now().UnixMilli())
err := sendJSON(conn, map[string]any{
"req_id": reqID,
"op": "ping",
})
if err != nil {
log.Printf("[heartbeat] error sending ping: %v", err)
return
}
}
}
}

// ---------- Main run ----------

func run(ctx context.Context, url string) error {
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
EnableCompression: false,
}

conn, _, err := dialer.Dial(url, nil)
if err != nil {
return fmt.Errorf("dial error: %w", err)
}
defer conn.Close()
log.Printf("Connected to %s", url)

expires := (time.Now().Unix() + 10000) * 1000
val := fmt.Sprintf("GET/realtime%d", expires)
sig := signAuth(APISecret, val)

authMsg := map[string]any{
"req_id": "10001",
"op": "auth",
"args": []any{APIKey, expires, sig},
}
if err := sendJSON(conn, authMsg); err != nil {
return fmt.Errorf("send auth error: %w", err)
}

if _, msg, err := conn.ReadMessage(); err != nil {
return fmt.Errorf("read auth ack error: %w", err)
} else {
log.Printf("auth-ack: %s", string(msg))
}

subMsg := map[string]any{
"op": "subscribe",
"args": subTopics,
}
if err := sendJSON(conn, subMsg); err != nil {
return fmt.Errorf("send subscribe error: %w", err)
}

hbCtx, hbCancel := context.WithCancel(ctx)
defer hbCancel()
go heartbeat(hbCtx, conn)

for {
select {
case <-ctx.Done():
log.Printf("context canceled, exit read loop")
return nil
default:
}

mt, data, err := conn.ReadMessage()
if err != nil {
return fmt.Errorf("read message error: %w", err)
}

switch mt {
case websocket.BinaryMessage:
resp, err := decodeFastOrderResp(data, false)
if err != nil {
log.Printf("binary decode error: %v", err)
} else {
j, _ := json.Marshal(resp)
log.Printf("FAST_ORDER_SBE: %s", string(j))
}
case websocket.TextMessage:
var obj map[string]any
if err := json.Unmarshal(data, &obj); err != nil {
log.Printf("text-nonjson: %s", string(data))
continue
}
if op, ok := obj["op"].(string); ok && op == "pong" {
continue
}
j, _ := json.Marshal(obj)
log.Printf("control: %s", string(j))
default:
log.Printf("unknown message type %d", mt)
}
}
}

// ---------- Entry ----------

func main() {
url := flag.String("url", MMWSURLTestnetBybits, "WebSocket URL")
flag.Parse()

if APIKey == "YOUR_API_KEY" || APISecret == "YOUR_API_SECRET" {
log.Println("⚠️ Please set APIKey and APISecret in the source before running.")
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

if err := run(ctx, *url); err != nil {
log.Fatalf("run error: %v", err)
}
}