> For the complete documentation index, see [llms.txt](https://docscn.jkidata.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docscn.jkidata.com/websocket-xie-yi-shuo-ming/3.-ding-yue-hang-qing.md).

# 📊 3. 订阅行情

WebSocket 连接成功后，客户端可以按产品或按市场订阅实时行情。

支持：

* 订阅单个产品。
* 一次订阅多个产品。
* 同时订阅不同市场的产品。
* 订阅单个市场。
* 一次订阅多个市场。
* 在同一条连接中同时维护产品订阅和市场订阅。

### 订阅前置条件

发送订阅指令前，必须完成以下步骤：

```
建立 WebSocket 连接
        ↓
收到 connect: ok
        ↓
等待至少5秒
        ↓
发送订阅指令
```

连接成功消息：

```
{
  "type": "connect",
  "msg": "ok"
}
```

不要在 WebSocket 的 `open` 事件触发后立即订阅。

如果需要发送多条独立指令，指令之间也建议保留至少5秒间隔。优先使用批量订阅格式，避免短时间内连续发送大量指令。

### 指令格式

订阅指令使用纯文本格式，不是 JSON。

产品订阅：

```
/symbol/交易所:产品代码
```

市场订阅：

```
/exchange/交易所代码
```

多个订阅项之间使用英文逗号 `,` 分隔，不要添加空格。

### 3.1 订阅单个产品

订阅 JM 市场的 BTCUSDT：

```
/symbol/JM:BTCUSDT
```

格式说明：

| 内容   | 示例         | 说明         |
| ---- | ---------- | ---------- |
| 指令类型 | `/symbol/` | 表示按产品订阅    |
| 交易所  | `JM`       | 产品所属市场或交易所 |
| 分隔符  | `:`        | 分隔交易所与产品代码 |
| 产品代码 | `BTCUSDT`  | 需要订阅的产品    |

完整格式：

```
/symbol/JM:BTCUSDT
```

### 单产品订阅成功

服务端返回：

```
{
  "msg": [
    {
      "type": "symbol",
      "res": true,
      "message": "[ JM:BTCUSDT ] symbol subscription success."
    }
  ],
  "type": "bind"
}
```

字段说明：

| 字段              | 类型      | 说明                       |
| --------------- | ------- | ------------------------ |
| `type`          | String  | 外层消息类型，订阅结果为 `bind`      |
| `msg`           | Array   | 本次订阅中每个产品的处理结果           |
| `msg[].type`    | String  | 订阅类型，产品订阅为 `symbol`      |
| `msg[].res`     | Boolean | `true` 表示成功，`false` 表示失败 |
| `msg[].message` | String  | 订阅结果说明                   |

### 3.2 一次订阅多个产品

多个产品使用英文逗号分隔。

订阅 JM 市场的 BTCUSDT 和 ETHUSDT：

```
/symbol/JM:BTCUSDT,JM:ETHUSDT
```

每个产品都必须使用完整格式：

```
交易所:产品代码
```

正确：

```
/symbol/JM:BTCUSDT,JM:ETHUSDT
```

错误：

```
/symbol/JM:BTCUSDT,ETHUSDT
```

错误：

```
/symbol/JM:BTCUSDT, JM:ETHUSDT
```

### 多产品订阅成功

服务端对每个产品分别返回结果：

```
{
  "msg": [
    {
      "type": "symbol",
      "res": true,
      "message": "[ JM:BTCUSDT ] symbol subscription success."
    },
    {
      "type": "symbol",
      "res": true,
      "message": "[ JM:ETHUSDT ] symbol subscription success."
    }
  ],
  "type": "bind"
}
```

客户端必须遍历 `msg` 数组，逐项检查 `res`。

### 3.3 同时订阅不同市场的产品

产品订阅项包含交易所代码，因此同一个指令可以包含不同市场的产品。

例如：

```
/symbol/JM:BTCUSDT,NASDAQ:AAPL
```

该指令表示：

| 交易所      | 产品        |
| -------- | --------- |
| `JM`     | `BTCUSDT` |
| `NASDAQ` | `AAPL`    |

适合只关注少量指定产品、但产品分布在不同市场的场景。

### 3.4 建议订阅数量

服务端支持批量订阅多个产品。

为了避免客户端一次发送过多内容，建议：

* 单次订阅不超过50个产品。
* 超过50个产品时分批发送。
* 每个产品都使用 `交易所:产品代码` 格式。
* 分批指令之间至少间隔5秒。
* 不要在短时间内重复提交相同的订阅。
* 需要某个市场大量产品时，优先使用市场订阅。

> 50个是客户端建议值，不是服务端固定的技术边界。

### 3.5 订阅单个市场

订阅 JM 市场：

```
/exchange/JM
```

格式说明：

| 内容    | 示例           | 说明      |
| ----- | ------------ | ------- |
| 指令类型  | `/exchange/` | 表示按市场订阅 |
| 交易所代码 | `JM`         | 需要订阅的市场 |

市场订阅成功后，服务端会推送该市场中的实时行情数据。

### 单市场订阅成功

```
{
  "msg": [
    {
      "type": "exchange",
      "res": true,
      "message": "[ JM ] exchange subscription success."
    }
  ],
  "type": "bind"
}
```

### 3.6 一次订阅多个市场

多个市场代码使用英文逗号分隔：

```
/exchange/JM,NASDAQ
```

验证时 JM 和 NASDAQ 均返回订阅成功：

```
{
  "msg": [
    {
      "type": "exchange",
      "res": true,
      "message": "[ JM ] exchange subscription success."
    },
    {
      "type": "exchange",
      "res": true,
      "message": "[ NASDAQ ] exchange subscription success."
    }
  ],
  "type": "bind"
}
```

多个市场之间不要添加空格。

正确：

```
/exchange/JM,NASDAQ
```

不推荐：

```
/exchange/JM, NASDAQ
```

### 产品订阅与市场订阅的选择

| 使用场景        | 推荐方式  |
| ----------- | ----- |
| 只关注一个产品     | 单产品订阅 |
| 关注少量指定产品    | 多产品订阅 |
| 产品来自不同市场    | 多产品订阅 |
| 需要一个市场的大量产品 | 单市场订阅 |
| 需要多个市场的全部行情 | 多市场订阅 |

不要在已经订阅整个市场后，再重复订阅该市场中的大量单个产品，否则可能接收到重复行情。

### 3.7 行情数据返回

订阅成功后，行情数据通过 JSON 数组推送。

单条行情示例：

```
[
  {
    "msg": {
      "e": "JM",
      "s": "BTCUSDT",
      "p": "77727.8",
      "utc": "1788000499"
    },
    "type": "quote"
  }
]
```

一个 WebSocket 消息中可能同时包含多条行情：

```
[
  {
    "msg": {
      "e": "JM",
      "s": "BTCUSDT",
      "p": "77727.8"
    },
    "type": "quote"
  },
  {
    "msg": {
      "e": "JM",
      "s": "ETHUSDT",
      "p": "2436.49"
    },
    "type": "quote"
  }
]
```

客户端必须遍历数组，不能把整条消息当作单个行情对象。

### 混合数据类型

同一个数组中可能包含不同类型的数据：

```
[
  {
    "type": "quote",
    "msg": {}
  },
  {
    "type": "bars",
    "msg": {}
  }
]
```

客户端应逐项检查 `type`：

```
for (const item of messages) {
  switch (item.type) {
    case "quote":
      handleQuote(item.msg);
      break;

    case "bars":
      handleBars(item.msg);
      break;

    default:
      console.log("其他消息：", item);
  }
}
```

### 订阅结果与行情可能交错

市场订阅成功后，服务端可能立即开始推送行情。

因此：

* `bind` 结果与行情数据可能在很短时间内连续到达。
* 不要阻塞 WebSocket 消息接收线程。
* 每条消息都应独立解析。
* 状态消息按对象处理。
* 行情消息按数组处理。
* 不要假设收到 `bind` 后才会收到第一条行情。

### 3.8 错误产品

订阅不存在的产品：

```
/symbol/JM:INVALID_SYMBOL
```

服务端保持连接，并返回：

```
{
  "msg": [
    {
      "type": "symbol",
      "res": false,
      "message": "The symbol subscription [ JM:INVALID_SYMBOL ] does not exist. Please verify the symbol or subscribe first."
    }
  ],
  "type": "bind"
}
```

客户端应记录失败产品，不要反复发送相同的错误订阅。

### 3.9 错误市场

订阅不存在的市场：

```
/exchange/INVALID_MARKET
```

服务端保持连接，并返回：

```
{
  "msg": [
    {
      "type": "exchange",
      "res": false,
      "message": "The exchange subscription [ INVALID_MARKET ] does not exist. Please verify the exchange or subscribe first."
    }
  ],
  "type": "bind"
}
```

错误市场或产品通常不会导致整个 WebSocket 连接断开，其他有效订阅可以继续工作。

### 部分成功

批量订阅时，成功和失败结果可能同时存在：

```
{
  "msg": [
    {
      "type": "symbol",
      "res": true,
      "message": "[ JM:BTCUSDT ] symbol subscription success."
    },
    {
      "type": "symbol",
      "res": false,
      "message": "The symbol subscription [ JM:INVALID_SYMBOL ] does not exist."
    }
  ],
  "type": "bind"
}
```

不要因为其中一个产品失败，就将整批订阅标记为失败。

正确处理方式：

```
遍历 msg 数组
        ↓
res = true：加入已订阅列表
        ↓
res = false：加入失败列表
```

### JavaScript订阅示例

以下示例假设连接已经成功，并已等待至少5秒。

#### 订阅多个产品

```
socket.send(
  "/symbol/JM:BTCUSDT,JM:ETHUSDT"
);
```

#### 订阅多个市场

```
socket.send(
  "/exchange/JM,NASDAQ"
);
```

#### 处理订阅结果

```
function handleBind(data) {
  for (const result of data.msg) {
    if (result.res) {
      console.log(
        "订阅成功：",
        result.message
      );
    } else {
      console.error(
        "订阅失败：",
        result.message
      );
    }
  }
}
```

#### 处理全部消息

```
socket.addEventListener("message", (event) => {
  const data = JSON.parse(event.data);

  if (
    !Array.isArray(data) &&
    data.type === "bind"
  ) {
    handleBind(data);
    return;
  }

  if (Array.isArray(data)) {
    for (const item of data) {
      if (item.type === "quote") {
        handleQuote(item.msg);
      } else if (item.type === "bars") {
        handleBars(item.msg);
      }
    }
  }
});
```

### Python订阅示例

以下示例假设连接已经成功，并已等待至少5秒。

#### 订阅多个产品

```
ws.send(
    "/symbol/JM:BTCUSDT,JM:ETHUSDT"
)
```

#### 订阅多个市场

```
ws.send(
    "/exchange/JM,NASDAQ"
)
```

#### 处理订阅结果

```
def handle_bind(data):
    for result in data["msg"]:
        if result["res"]:
            print(
                "订阅成功：",
                result["message"],
            )
        else:
            print(
                "订阅失败：",
                result["message"],
            )
```

#### 处理全部消息

```
import json


def on_message(ws, message):
    data = json.loads(message)

    if (
        isinstance(data, dict)
        and data.get("type") == "bind"
    ):
        handle_bind(data)
        return

    if isinstance(data, list):
        for item in data:
            message_type = item.get("type")
            message_data = item.get("msg")

            if message_type == "quote":
                handle_quote(message_data)

            elif message_type == "bars":
                handle_bars(message_data)
```

### 订阅状态管理

客户端建议分别维护：

```
pendingSubscriptions
activeSubscriptions
failedSubscriptions
```

处理规则：

* 指令已发送但未收到结果：`pending`。
* `res: true`：移动到 `active`。
* `res: false`：移动到 `failed`。
* 连接断开：清除当前活动状态。
* 重连成功后：等待5秒，再恢复有效订阅。

### 使用建议

* 收到 `connect: ok` 后至少等待5秒再订阅。
* 产品格式必须为 `交易所:产品代码`。
* 多个订阅项使用英文逗号分隔。
* 单次建议不超过50个产品。
* 超过建议数量时分批发送，批次之间至少间隔5秒。
* 批量订阅必须逐项检查 `res`。
* 市场订阅可能产生大量实时数据。
* 行情推送为 JSON 数组，一次可能包含多条数据。
* 按 `type` 分别处理 `quote`、`bars` 和其他消息。
* 不要在消息回调中执行耗时的同步操作。
* 取消已有订阅请查看下一页“取消订阅”。
