# SBE Public Trade Integration Guide

## Overview

| Field            | Description                                                                                                                                                                                                    |
|:-----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Topic            | `publicTrade`                                                                                                                                                                                                  |
| TemplateId       | `1003`                                                                                                                                                                                                         |
| Format           | SBE binary frame (opcode = 2), little-endian                                                                                                                                                                   |
| Units            | Timestamps in microseconds (µs), but only accurate to milliseconds. Append `000` to a millisecond timestamp to get the microsecond format. E.g., millisecond timestamp 1726233600001 corresponds to µs timestamp 1726233600001000 |
| Content          | Public platform trade records (batch push, a single frame may contain multiple trades)                                                                                                                         |
| Update Frequency | Real-time                                                                                                                                                                                                      |

---

## Connection

- **WebSocket URL**: `wss://ws.bitget.com/v3/ws/public/sbe`
- **Heartbeat**: Send text frame `"ping"` every 30 seconds, server responds with `"pong"`
- **Message Protocol**: JSON text frames during subscription phase; SBE binary frames during market data push phase, distinguished by WebSocket opCode (opCode=1 for text, opCode=2 for binary)

---

## Subscription Flow 

### 1. Send Subscription Request

```json
{
  "op": "subscribe",
  "args": [
    {
      "instType": "usdt-futures",
      "topic": "publicTrade",
      "symbol": "BTCUSDT"
    }
  ]
}
```

**Parameter Description:**

| Parameter | Type   | Description                                                                          |
|:----------|:-------|:-------------------------------------------------------------------------------------|
| instType  | string | Product type: `spot` <br/> `usdt-futures`  <br/> `usdc-futures` <br/> `coin-futures` |
| topic     | string | Fixed value: `publicTrade`                                                           |
| symbol    | string | Symbol name, e.g. `BTCUSDT`, `ETHUSDT`                                               |

### 2. Subscription Confirmation

```json
{
  "event": "subscribe",
  "arg": {
    "instType": "usdt-futures",
    "topic": "publicTrade",
    "symbol": "BTCUSDT"
  }
}
```

### 3. Receiving Data

After subscription confirmation, SBE binary frames are pushed in real-time whenever a trade occurs. A single frame may contain multiple trades (batch).

### 4. Unsubscribe

```json
{
  "op": "unsubscribe",
  "args": [
    {
      "instType": "usdt-futures",
      "topic": "publicTrade",
      "symbol": "BTCUSDT"
    }
  ]
}
```

---

## SBE Message Structure

### Price/Size Calculation Formula

```
actual_value = mantissa × 10^exponent
```

**Example**: mantissa = 123456, exponent = -4, represents 12.3456 (actual_value = mantissa × 10 ^ exponent)

### Common Message Header (8 bytes)

All SBE messages must include a fixed 8-byte header for parsing and identifying subsequent data.

| Field       | Type   | Length (Byte) | Description                                     |
|:------------|:-------|:--------------|:------------------------------------------------|
| blockLength | uint16 | 2             | Root block length                               |
| templateId  | uint16 | 2             | Channel unique identifier, fixed value = `1003` |
| schemaId    | uint16 | 2             | Schema ID                                       |
| version     | uint16 | 2             | Schema version                                  |

### Message Field Definitions

**Root block**

| id  | Field         | Type      | Description  |
|:----|:--------------|:----------|:-------------|
| -   | messageHeader | Composite | Fixed header |
| 1   | priceExponent | int8      | Price exponent |
| 2   | sizeExponent  | int8      | Size exponent  |
| 100 | padding       | uint8  | Padding bytes  |

**Group: `trades` (id=200)**

| id  | Field  | Type   | Description                                                                                                                                                                                                            |
|:----|:-------|:-------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1   | ts     | uint64 | Matching engine timestamp<br/> µs timestamp, but only accurate to milliseconds. Append `000` to a millisecond timestamp to get the microsecond format. E.g., millisecond timestamp 1726233600001 → µs 1726233600001000 |
| 2   | execId | uint64 | Execution ID                                                                                                                                                                                                           |
| 3   | price  | int64  | Trade price mantissa                                                                                                                                                                                                   |
| 4   | size   | int64  | Trade size mantissa                                                                                                                                                                                                    |
| 5   | side     | uint8  | Trade direction: Buy / Sell                                                                                                                                                                                          |
| 6   | isRPI    | uint8 (BooleanType) | Retail Price Improvement flag: `T` / `F` (since schema version 4)                                                                                                                                      |
| 7   | sts      | uint64 | Stream service push timestamp in microseconds                                                                                                                                                                        |
| 8   | category | uint8  | Product line: `spot` / `usdt-futures` / `coin-futures` / `usdc-futures`                                                                                                                                              |
| 50  | padding  | uint8  | Padding bytes                                                                                                                                                                                                        |

**Data**

| id  | Field  | Type         | Description               |
|:----|:-------|:-------------|:--------------------------|
| 300 | symbol | varString[8] | Symbol name, UTF-8 format |

### Binary Layout Overview

```javascript
┌─────────────┬─────────────┬──────────────────────────────────────────┬──────────┐
│ Header (8B) │ Root (8B)   │ Trades: GrpHdr(4B) + N×40B               │ Symbol   │
└─────────────┴─────────────┴──────────────────────────────────────────┴──────────┘
```

**Single trade message size**: 8 + 8 + 4 + 40 + 1 + len(symbol) = approximately 69 bytes  
**N trade message size**: 8 + 8 + 4 + 40×N + 1 + len(symbol)

---

## Trade Direction (side)

| Value | Name | Description               |
|:------|:-----|:--------------------------|
| 0     | Buy  | Active buy (Taker buyer)  |
| 1     | Sell | Active sell (Taker seller)|

---

## Decoding Example

### Raw Binary (single trade, hex)

```javascript
08 00 EB 03 01 00 04 00  <- header: blockLength=8, templateId=1003, schemaId=1, version=4
FE                       <- priceExponent = -2
FC                       <- sizeExponent  = -4
00 00 00 00 00 00        <- padding6
28 00 01 00              <- trades group: entryBlockLength=40, numInGroup=1
02 80 C6 A7 86 3E 06 00  <- trades[0].ts       (uint64 LE)
0F 27 00 00 00 00 00 00  <- trades[0].execId   = 9999
52 39 64 00 00 00 00 00  <- trades[0].price    = 6566738
88 13 00 00 00 00 00 00  <- trades[0].size     = 5000
00                       <- trades[0].side    = 0 (Buy)
00 00 00 00 00 00 00     <- padding7
07 42 54 43 55 53 44 54  <- symbol: length=7, "BTCUSDT"
```

### Decoded JSON

```javascript
{
  "header": {
    "block_length": 8,
    "template_id": 1003,
    "schema_id": 1,
    "version": 4
  },
  "price_exponent": -2,
  "size_exponent": -4,
  "trades": [
    {
      "ts": 1700000000000002,
      "exec_id": 9999,
      "price": "65667.38",
      "size": "0.5000",
      "side": "Buy",
      "isRPI": "F",
      "sts": 1700000000001002,
      "category": 1
    }
  ],
  "symbol": "BTCUSDT"
}
```

### Batch Trade Decoded JSON (multiple trades)

```javascript
{
  "header": {
    "block_length": 8,
    "template_id": 1003,
    "schema_id": 1,
    "version": 4
  },
  "price_exponent": -2,
  "size_exponent": -4,
  "trades": [
    {
      "ts": 1700000000000002,
      "exec_id": 10001,
      "price": "65665.78",
      "size": "0.5000",
      "side": "Buy",
      "isRPI": "F",
      "sts": 1700000000001002,
      "category": 1
    },
    {
      "ts": 1700000000000003,
      "exec_id": 10002,
      "price": "65666.00",
      "size": "1.2000",
      "side": "Sell",
      "isRPI": "T",
      "sts": 1700000000001003,
      "category": 1
    },
    {
      "ts": 1700000000000004,
      "exec_id": 10003,
      "price": "65664.50",
      "size": "0.3000",
      "side": "Buy",
      "isRPI": "F",
      "sts": 1700000000001004,
      "category": 1
    }
  ],
  "symbol": "BTCUSDT"
}
```

---

## Python Integration Example

```javascript

"""Bitget publicTrade SBE WebSocket subscription example"""
import asyncio
import json
import struct
from decimal import Decimal
import websockets

WS_URL    = "wss://ws.bitget.com/v3/ws/public/sbe"
INST_TYPE = "usdt-futures"
SYMBOL    = "BTCUSDT"
TOPIC     = "publicTrade"

SIDE_MAP = {0: "Buy", 1: "Sell"}
BOOLEAN_MAP = {0: "F", 1: "T"}


def decode_trade(data: bytes) -> dict:
    """Decode Trade (templateId=1003) SBE frame"""
    block_length, template_id, schema_id, version = struct.unpack_from('<HHHH', data, 0)
    assert template_id == 1003, f"unexpected templateId: {template_id}"

    offset    = 8
    base      = offset
    price_exp, = struct.unpack_from('<b', data, offset); offset += 1
    size_exp,  = struct.unpack_from('<b', data, offset); offset += 1

    # Skip padding, based on blockLength
    offset = base + block_length

    # trades group
    entry_bl, num = struct.unpack_from('<HH', data, offset); offset += 4
    has_is_rpi = version >= 4  # sinceVersion=4: entry carries the isRPI field (entryBlockLength stays fixed due to padding, so gate on schema version instead)

    to_dec = lambda m, e: Decimal(m) * Decimal(10) ** e

    trades = []
    for _ in range(num):
        entry_start = offset
        ts,       = struct.unpack_from('<Q', data, offset); offset += 8
        exec_id,  = struct.unpack_from('<Q', data, offset); offset += 8
        price,    = struct.unpack_from('<q', data, offset); offset += 8
        size,     = struct.unpack_from('<q', data, offset); offset += 8
        side_raw,  = struct.unpack_from('<B', data, offset); offset += 1
        if has_is_rpi:
            is_rpi_raw, = struct.unpack_from('<B', data, offset); offset += 1
        sts,       = struct.unpack_from('<Q', data, offset); offset += 8
        category,  = struct.unpack_from('<B', data, offset); offset += 1
        # Skip padding, based on entry_bl
        offset = entry_start + entry_bl
        trade = {
            "ts":       ts,
            "exec_id":  exec_id,
            "price":    str(to_dec(price, price_exp)),
            "size":     str(to_dec(size,  size_exp)),
            "side":     SIDE_MAP.get(side_raw, str(side_raw)),
        }
        if has_is_rpi:
            trade["isRPI"] = BOOLEAN_MAP.get(is_rpi_raw, str(is_rpi_raw))
        trade["sts"] = sts
        trade["category"] = category
        trades.append(trade)

    # symbol (varString8)
    sym_len, = struct.unpack_from('<B', data, offset); offset += 1
    symbol = data[offset:offset + sym_len].decode('utf-8')

    return {
        "price_exponent": price_exp,
        "size_exponent":  size_exp,
        "trades": trades,
        "symbol": symbol,
    }


async def main():
    async with websockets.connect(WS_URL) as ws:
        # Subscribe
        await ws.send(json.dumps({
            "op": "subscribe",
            "args": [{"instType": INST_TYPE, "topic": TOPIC, "symbol": SYMBOL}]
        }))
        print(f"[SUB] {INST_TYPE} {TOPIC} {SYMBOL}")

        # Heartbeat
        async def ping_loop():
            while True:
                await asyncio.sleep(20)
                await ws.send("ping")
                print("[PING] sent")

        asyncio.create_task(ping_loop())

        async for message in ws:
            if isinstance(message, bytes):
                try:
                    msg = decode_trade(message)
                    print(f"\n[Trade] {msg['symbol']}  ({len(msg['trades'])} trades)")
                    for t in msg['trades']:
                        ts_ms = t['ts'] // 1000
                        print(f"  [{t['side']:4s}] price={t['price']}  size={t['size']}  "
                              f"ts={ts_ms}ms  execId={t['exec_id']}")
                except Exception as e:
                    print(f"[ERROR] {e}  raw={message.hex()}")
            else:
                if message == "pong":
                    print("[PONG] received")
                else:
                    print(f"[TEXT] {message}")


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