跳至主要内容

Fast Order Response SBE

僅限 MMWS

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

概述

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

本頻道以速度和效率為核心設計。詳細推送觸發邏輯,請參閱下方推送邏輯章節。

上線時間表

產品測試網主網
現貨
期貨(U本位 & 幣本位)
期權

連接

環境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固定 = 2

消息體

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僅當現貨訂單類型為市價單且 marketUnit=quoteCoin 時有效,其他情況為 0(需應用 valueExponent
11creationTimeint64訂單在 Fast Order 頻道的創建時間戳(微秒)
12updatedTimeint64撮合時間戳(微秒)
13seqint64跨序列 ID
14symbolIDint32交易對 ID
15liquidityint81=taker,2=maker(成交時),否則為 0。版本 1 新增欄位
  • 對於盤前永續合約,僅支持連續交易階段
  • 16amendFlagint80=不是改單請求觸發的推送,1=是改單請求觸發的推送。版本 2 新增欄位(7月21日主網)
    17fillQtyint64訂單已成交數量尾數(需應用 sizeExponent)。版本 2 新增欄位(7月21日主網)
  • 對於盤前永續合約,僅支持連續交易階段
  • 對於taker有多筆成交時, 該字段給的是多筆成交的累計的成交數量
  • 18fillPriceint64訂單成交價格尾數(需應用 priceExponent)。版本 2 新增欄位(7月21日主網)
  • 對於盤前永續合約,僅支持連續交易階段
  • 對於taker有多筆成交時, 該字段給的是多筆成交的最後一筆成交價格
  • 19originalQtyint64訂單數量尾數(需應用 sizeExponent)。版本 2 新增欄位(7月21日主網)
    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="2" 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="Active only when spot order type is market and marketUnit=quoteCoin; 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"/>
    <!-- Liquidity -->
    <field id="15" name="liquidity" type="int8" sinceVersion="1" description="1=taker, 2=maker when trade, otherwise 0"/>
    <!-- Version 2 add new fields-->
    <field id="16" name="amendFlag" type="int8" sinceVersion="2" description="0=not amended, 1=amended"/>
    <field id="17" name="fillQty" type="int64" sinceVersion="2" description="order Filled quantity mantissa"/>
    <field id="18" name="fillPrice" type="int64" sinceVersion="2" description="order fill price mantissa"/>
    <field id="19" name="originalQty" type="int64" sinceVersion="2" description="order originalorder quantity mantissa"/>
    <!-- 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"`
    Liquidity int8 `json:"liquidity"`

    AmendFlag int8 `json:"amendFlag,omitempty"`
    FillQtyMantissa int64 `json:"fillQtyMantissa,omitempty"`
    FillPriceMantissa int64 `json:"fillPriceMantissa,omitempty"`
    OriginalQtyMantissa int64 `json:"originalQtyMantissa,omitempty"`

    OrderID string `json:"orderId"`
    OrderLinkID string `json:"orderLinkId"`

    Price float64 `json:"price"`
    LeavesQty float64 `json:"leavesQty"`
    LeavesValue float64 `json:"leavesValue"`
    FillQty float64 `json:"fillQty,omitempty"`
    FillPrice float64 `json:"fillPrice,omitempty"`
    OriginalQty float64 `json:"originalQty,omitempty"`

    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

    bodyStart := off // fixed block begins here; var-length fields follow at bodyStart+blockLen

    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
    }
    // field 15 liquidity (sinceVersion=1) — present when blockLen covers it
    if off-bodyStart < int(blockLen) {
    if resp.Liquidity, err = readI8(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 16 amendFlag (sinceVersion=2)
    if off-bodyStart < int(blockLen) {
    if resp.AmendFlag, err = readI8(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 17 fillQty (sinceVersion=2)
    if off-bodyStart+8 <= int(blockLen) {
    if resp.FillQtyMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 18 fillPrice (sinceVersion=2)
    if off-bodyStart+8 <= int(blockLen) {
    if resp.FillPriceMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    }
    // field 19 originalQty (sinceVersion=2)
    if off-bodyStart+8 <= int(blockLen) {
    if resp.OriginalQtyMantissa, err = readI64LE(payload, &off); err != nil {
    return nil, err
    }
    }
    // skip any future fixed fields we don't know about
    off = bodyStart + int(blockLen)
    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.FillQty = applyExp(resp.FillQtyMantissa, resp.SizeExponent)
    resp.FillPrice = applyExp(resp.FillPriceMantissa, resp.PriceExponent)
    resp.OriginalQty = applyExp(resp.OriginalQtyMantissa, resp.SizeExponent)
    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)
    }
    }