# DataGate WebSocket API

## 认证

先签发一次性 ticket：

```http
POST /api/ws-ticket
X-API-Key: <key>
```

然后连接：

```text
wss://feed.batx.ai/ws?ticket=<ticket>
```

只有 `schema` 精确为 `batx-market-ticket-request/v1` 的 JSON object 才进入加法式
typed exchange。空 body、无法解析的 body，以及其他 legacy body 继续被忽略，返回与
既有版本完全相同的 legacy ticket；上述 `/ws` 行为不变。BATX 浏览器行情请求为：

```json
{"schema":"batx-market-ticket-request/v1","subject":"<opaque-subject>","audience":"batx-market-feed/v1"}
```

`subject` 必须是调用方服务端生成的 opaque identity，不能是浏览器提交的 uid。响应为：

```json
{"code":1,"schema":"batx-market-ticket/v1","protocol":"batx-market-feed/v1","ticket":"...","expires_ts":1788220860000,"audience":"batx-market-feed/v1","jti":"...","scope_id":"<opaque-subject>","websocket_path":"/api/cell/v2/market/ws"}
```

该 ticket 只允许在指定 audience 使用一次；过期、重放、错误 audience，以及拿旧
legacy ticket 连接新路径都会返回 401。失败的消费尝试也会使 ticket 失效。

## Legacy Feed protocol v2

连接首帧：

```json
{"code":1,"msg":"Connected Successfully","protocol":2}
```

订阅和退订：

```json
{"ac":"subscribe","params":"BTC_USDT_BINANCE","types":"order,trade"}
{"ac":"unsubscribe","params":"BTC_USDT_BINANCE","types":"order,trade"}
```

快照和心跳：

```json
{"ac":"snapshot","params":"BTC_USDT_BINANCE","types":"order,trade"}
{"ac":"ping","params":"client timestamp"}
```

`types` 支持 `order`、`trade`；兼容别名为 `depth -> order`、`tick -> trade`、
`quote -> trade`。

Kline：

```json
{"action":"subscribe","channel":"kline","symbols":["AAPL_NASDAQ"],"tf":"4h"}
{"action":"unsubscribe","channel":"kline","symbols":["AAPL_NASDAQ"],"tf":"4h"}
```

自 2026-09-21 起 kline 订阅不再预载最近历史 K 线；K 线自订阅起由实时成交
增量生成，历史读取请使用 NAS/SQL 产品。

Kline 事件包含 `tf`、epoch-ms `d/ts`、IANA `timezone`、RFC 3339 `time`、
`trading_day`、`trading_date`、`o/h/l/c/v/chg`、`forming` 和 `final`。`d` 与 `ts`
表示同一桶起始 instant，`trading_date` 是 `trading_day` 的兼容别名。

Cell V2 WebSocket 的公开 frame 见 [Cell V2 Public API](cell-v2-api.md)。

## BATX market protocol v1

连接 `wss://feed.batx.ai/api/cell/v2/market/ws?ticket=<typed-ticket>`。首帧固定为
`batx-market-connected/v1`，包含 `protocol:"batx-market-feed/v1"`、进程级
`stream_id`、票据绑定的 `scope_id`、epoch-ms `server_time_ms`、RFC 3339
`server_time` 与 `ticket_expires_at_ms`。客户端必须先核对 protocol 和 scope，不能把
上一用户或未知协议的 frame 写入行情状态。

订阅只发送 BATX canonical symbol，不发送客户端猜测的 provider symbol：

```json
{
  "schema":"batx-market-subscribe/v1",
  "protocol":"batx-market-feed/v1",
  "type":"subscribe",
  "request_id":"request-1",
  "stream_id":"prior-stream-id",
  "interests":[
    {"canonical_symbol":"AAPL_NASDAQ","mode":"quote","after_sequences":{"quote":41}},
    {"canonical_symbol":"0700_HK","mode":"book","after_sequences":{"quote":9,"book":7}}
  ]
}
```

`stream_id` 和 `after_sequences` 都可省略；省略 cursor 会返回当前 snapshot。`mode` 仅为
`quote|book`，book 同时保留 quote。ACK 使用 `batx-market-subscribe-ack/v1`，其
`accepted[]` 返回 `canonical_symbol/mode/provider/provider_symbol`，`rejected[]`
返回 `canonical_symbol/mode/code`，provider 映射只认该服务端 ACK。后续 frame 的
`provider/provider_symbol` 必须与该 symbol 最近一次 accepted ACK 完全一致；目录映射
变化时先以 gap 结束旧 replay，再由新 ACK 与 snapshot 收敛。每个映射使用独立的
sequence/history epoch，旧映射高水位不参与新映射去重；客户端不得自行转换。

snapshot、event 与 status 共同包含：

- `stream_id/scope_id/canonical_symbol/provider/provider_symbol/channel`；
- 权威 `sequence` 和 `previous_sequence`；
- epoch-ms `event_time_ms`、RFC 3339 `event_time` 和市场 IANA `timezone`；
- 闭集状态对：正常事件为 `live/null`，无新成交的有序状态为 `live/no-update`，
  源 gap 为 `delayed/source-gap`，源不可用为 `unavailable/source-unavailable`；
- quote 的 `data.price/change?/change_percent?`，或 book 的
  `data.bids/asks[{price,quantity}]`。纯状态 frame 的 `data` 为 `null`。

同一 `stream_id + canonical_symbol + channel` 只按 sequence 递增消费。断线后以先前
stream/cursor 重订阅会重放保留窗口内的 frame；窗口不足、服务重启或客户端队列溢出时，
服务发送 `batx-market-gap/v1`，其 `reason` 为
`stream-restarted|replay-window-miss|client-overflow`，`recovery` 固定为
`snapshot-resubscribe`。客户端必须保留已确认值为 stale、触发有界 snapshot/REST 恢复，
不得补造价格或本地推算 sequence。

`live/no-update` 只推进 sequence 和 freshness，不是 gap，也不产生价格。客户端必须严格
校验所有 frame 字段；服务端拒绝 control 时返回固定字段的
`batx-market-protocol-error/v1`，未知字段或未知 code 必须 fail closed。

心跳为 `batx-market-ping/v1` / `batx-market-pong/v1`。退订为
`batx-market-unsubscribe/v1`，携带 `request_id` 与 `canonical_symbols`。
