跳到主要内容

WebSocket API

ZTDX 提供 WebSocket 接口用于实时推送市场数据和用户私有数据更新。相比轮询 REST API,WebSocket 具有更低的延迟和更高的效率。

连接信息

项目内容
WebSocket URLwss://ws.ztdx.io/ws
协议WebSocket (RFC 6455)
消息格式JSON
心跳间隔30 秒
重连策略指数退避(1s, 2s, 4s, 8s, ...最多30s)

连接与认证

公开数据流

公开数据(行情、K线、订单簿等)无需认证,直接连接即可订阅。

const ws = new WebSocket('wss://ws.ztdx.io/ws');

ws.onopen = () => {
console.log('WebSocket 已连接');

// 订阅 BTCUSDT 行情
ws.send(JSON.stringify({
action: 'subscribe',
channel: 'ticker',
symbol: 'BTCUSDT'
}));
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到消息:', data);
};

私有数据流

私有数据(订单更新、仓位变化、余额变化)需要先进行 WebSocket 认证。

认证流程

  1. 建立 WebSocket 连接
  2. 生成 EIP-712 签名(WebSocketAuth)
  3. 发送认证消息
  4. 等待认证成功
  5. 订阅私有数据流

EIP-712 WebSocketAuth 签名

{
"types": {
"WebSocketAuth": [
{ "name": "wallet", "type": "address" },
{ "name": "timestamp", "type": "uint256" }
]
},
"message": {
"wallet": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"timestamp": "1704067200"
}
}

前端认证示例

import { BrowserProvider } from 'ethers';

async function authenticateWebSocket(ws, address) {
const timestamp = Math.floor(Date.now() / 1000);

// 1. 生成 EIP-712 签名
const domain = {
name: "ZTDX",
version: "1",
chainId: 421614,
verifyingContract: vaultAddress
};

const types = {
WebSocketAuth: [
{ name: "wallet", type: "address" },
{ name: "timestamp", type: "uint256" }
]
};

const message = {
wallet: address.toLowerCase(),
timestamp: timestamp.toString()
};

const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const signature = await signer.signTypedData(domain, types, message);

// 2. 发送认证消息
ws.send(JSON.stringify({
action: 'auth',
address: address.toLowerCase(),
signature,
timestamp
}));

// 3. 等待认证响应
return new Promise((resolve, reject) => {
const handler = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'auth_response') {
if (data.success) {
console.log('WebSocket 认证成功');
ws.removeEventListener('message', handler);
resolve();
} else {
reject(new Error('认证失败: ' + data.error));
}
}
};
ws.addEventListener('message', handler);
});
}

// 使用示例
const ws = new WebSocket('wss://ws.ztdx.io/ws');

ws.onopen = async () => {
// 认证
await authenticateWebSocket(ws, userAddress);

// 订阅私有数据
ws.send(JSON.stringify({
action: 'subscribe',
channel: 'orders',
symbol: 'BTCUSDT'
}));
};

消息格式

发送消息(客户端 → 服务器)

订阅

{
"action": "subscribe",
"channel": "ticker",
"symbol": "BTCUSDT"
}

取消订阅

{
"action": "unsubscribe",
"channel": "ticker",
"symbol": "BTCUSDT"
}

心跳(Ping)

{
"action": "ping"
}

接收消息(服务器 → 客户端)

订阅成功确认

{
"type": "subscribed",
"channel": "ticker",
"symbol": "BTCUSDT",
"timestamp": 1704067200000
}

数据更新

{
"type": "ticker",
"channel": "ticker",
"symbol": "BTCUSDT",
"data": {
"last_price": "65000.00",
"high_24h": "66000.00",
"low_24h": "64000.00",
"volume_24h": "1234.56",
"change_24h": "2.5"
},
"timestamp": 1704067200000
}

心跳响应(Pong)

{
"type": "pong",
"timestamp": 1704067200000
}

错误消息

{
"type": "error",
"code": "INVALID_CHANNEL",
"message": "不支持的频道",
"timestamp": 1704067200000
}

公开数据流

1. Ticker(实时行情)

Channel: ticker

订阅消息:

{
"action": "subscribe",
"channel": "ticker",
"symbol": "BTCUSDT"
}

推送数据:

{
"type": "ticker",
"channel": "ticker",
"symbol": "BTCUSDT",
"data": {
"last_price": "65000.00",
"bid_price": "64995.00",
"ask_price": "65005.00",
"high_24h": "66000.00",
"low_24h": "64000.00",
"volume_24h": "1234.56",
"quote_volume_24h": "80125000.00",
"price_change_24h": "1500.00",
"price_change_percent_24h": "2.36",
"open_interest": "50000.00",
"funding_rate": "0.0001",
"next_funding_time": 1704067200
},
"timestamp": 1704067200000
}

更新频率: 实时(价格变化时)

2. Trades(最新成交)

Channel: trades

订阅消息:

{
"action": "subscribe",
"channel": "trades",
"symbol": "BTCUSDT"
}

推送数据:

{
"type": "trade",
"channel": "trades",
"symbol": "BTCUSDT",
"data": {
"id": "12345678",
"price": "65000.00",
"amount": "0.1",
"side": "buy",
"timestamp": 1704067200000
}
}

更新频率: 实时(每笔成交)

3. Orderbook(订单簿)

Channel: orderbook

订阅消息:

{
"action": "subscribe",
"channel": "orderbook",
"symbol": "BTCUSDT",
"depth": 20
}

推送数据:

全量快照(初次订阅):

{
"type": "orderbook:snapshot",
"channel": "orderbook",
"symbol": "BTCUSDT",
"data": {
"bids": [
["64995.00", "1.5"],
["64990.00", "2.3"]
],
"asks": [
["65005.00", "1.2"],
["65010.00", "3.1"]
]
},
"timestamp": 1704067200000
}

增量更新:

{
"type": "orderbook:update",
"channel": "orderbook",
"symbol": "BTCUSDT",
"data": {
"bids": [
["64995.00", "2.0"]
],
"asks": []
},
"timestamp": 1704067201000
}

更新频率: 实时(订单簿变化时)

增量更新说明
  • 价格为 "0" 表示该价位已清空
  • 客户端需要维护本地订单簿副本,应用增量更新

4. Klines(K线)

Channel: klines

订阅消息:

{
"action": "subscribe",
"channel": "klines",
"symbol": "BTCUSDT",
"period": "1m"
}

推送数据:

{
"type": "kline",
"channel": "klines",
"symbol": "BTCUSDT",
"period": "1m",
"data": {
"timestamp": 1704067200000,
"open": "65000.00",
"high": "65050.00",
"low": "64980.00",
"close": "65020.00",
"volume": "12.5",
"quote_volume": "812500.00",
"is_closed": false
}
}

支持的周期: 1m, 5m, 15m, 1h, 4h, D, W, M

更新频率: 实时(当前K线更新),is_closed: true 表示K线已收盘


私有数据流

1. Orders(订单更新)

Channel: orders
需要认证: ✅ 是

订阅消息:

{
"action": "subscribe",
"channel": "orders",
"symbol": "BTCUSDT"
}

推送数据:

{
"type": "order_update",
"channel": "orders",
"symbol": "BTCUSDT",
"data": {
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"user_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"symbol": "BTCUSDT",
"side": "buy",
"order_type": "limit",
"price": "65000.00",
"amount": "0.1",
"filled_amount": "0.05",
"remaining_amount": "0.05",
"status": "partially_filled",
"average_price": "64995.00",
"created_at": 1704067200000,
"updated_at": 1704067205000
},
"timestamp": 1704067205000
}

触发时机:

  • 订单创建
  • 订单部分成交
  • 订单完全成交
  • 订单取消
  • 订单拒绝

2. Positions(仓位更新)

Channel: positions
需要认证: ✅ 是

订阅消息:

{
"action": "subscribe",
"channel": "positions",
"symbol": "BTCUSDT"
}

推送数据:

{
"type": "position_update",
"channel": "positions",
"symbol": "BTCUSDT",
"data": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"user_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"symbol": "BTCUSDT",
"side": "long",
"size_in_tokens": "0.1",
"size_in_usd": "6500.00",
"entry_price": "65000.00",
"mark_price": "65200.00",
"collateral": "650.00",
"leverage": 10,
"unrealized_pnl": "20.00",
"liquidation_price": "60000.00",
"margin_rate": "0.10",
"status": "open"
},
"timestamp": 1704067205000
}

触发时机:

  • 开仓/加仓
  • 平仓/减仓
  • 标记价格变化(影响未实现盈亏)
  • 保证金调整

3. Balances(余额更新)

Channel: balances
需要认证: ✅ 是

订阅消息:

{
"action": "subscribe",
"channel": "balances"
}

推送数据:

{
"type": "balance_update",
"channel": "balances",
"data": {
"token": "USDT",
"available": "1000.50",
"frozen": "200.25",
"total": "1200.75",
"change": {
"available": "-100.00",
"frozen": "+100.00",
"reason": "order_created"
}
},
"timestamp": 1704067205000
}

触发时机:

  • 充值到账
  • 提现成功
  • 订单创建(保证金冻结)
  • 订单取消(保证金释放)
  • 交易成交
  • 资金费率结算

4. Trigger Orders(触发订单更新)

Channel: trigger_orders
需要认证: ✅ 是

订阅消息:

{
"action": "subscribe",
"channel": "trigger_orders",
"symbol": "BTCUSDT"
}

推送数据:

{
"type": "trigger_order_update",
"channel": "trigger_orders",
"symbol": "BTCUSDT",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440000",
"user_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"symbol": "BTCUSDT",
"side": "sell",
"trigger_type": "take_profit",
"trigger_price": "70000.00",
"order_type": "market",
"amount": "0.1",
"status": "triggered",
"created_at": 1704067200000,
"triggered_at": 1704070800000
},
"timestamp": 1704070800000
}

触发时机:

  • 触发订单创建
  • 触发条件满足(状态变为 triggered)
  • 触发订单取消

完整示例

React Hook 示例

import { useEffect, useRef, useState } from 'react';
import { BrowserProvider } from 'ethers';

function useZTDXWebSocket(channels = []) {
const ws = useRef(null);
const [connected, setConnected] = useState(false);
const [authenticated, setAuthenticated] = useState(false);
const [data, setData] = useState({});

useEffect(() => {
// 连接 WebSocket
ws.current = new WebSocket('wss://ws.ztdx.io/ws');

ws.current.onopen = async () => {
console.log('WebSocket 已连接');
setConnected(true);

// 如果需要私有数据,先认证
const needsAuth = channels.some(ch =>
['orders', 'positions', 'balances', 'trigger_orders'].includes(ch.channel)
);

if (needsAuth) {
try {
await authenticateWebSocket(ws.current, userAddress);
setAuthenticated(true);
} catch (error) {
console.error('认证失败:', error);
return;
}
}

// 订阅所有频道
channels.forEach(({ channel, symbol, ...params }) => {
ws.current.send(JSON.stringify({
action: 'subscribe',
channel,
symbol,
...params
}));
});
};

ws.current.onmessage = (event) => {
const message = JSON.parse(event.data);

// 处理不同类型的消息
switch (message.type) {
case 'ticker':
setData(prev => ({
...prev,
[`ticker_${message.symbol}`]: message.data
}));
break;

case 'trade':
setData(prev => ({
...prev,
[`trades_${message.symbol}`]: [
message.data,
...(prev[`trades_${message.symbol}`] || []).slice(0, 99)
]
}));
break;

case 'orderbook:snapshot':
case 'orderbook:update':
setData(prev => ({
...prev,
[`orderbook_${message.symbol}`]: message.data
}));
break;

case 'order_update':
setData(prev => ({
...prev,
orders: updateOrders(prev.orders || [], message.data)
}));
break;

case 'position_update':
setData(prev => ({
...prev,
positions: updatePositions(prev.positions || [], message.data)
}));
break;

case 'balance_update':
setData(prev => ({
...prev,
balances: message.data
}));
break;
}
};

ws.current.onclose = () => {
console.log('WebSocket 已断开');
setConnected(false);
setAuthenticated(false);

// 自动重连(指数退避)
setTimeout(() => {
console.log('尝试重连...');
// 重新初始化
}, 1000);
};

// 心跳
const pingInterval = setInterval(() => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({ action: 'ping' }));
}
}, 30000);

// 清理
return () => {
clearInterval(pingInterval);
ws.current?.close();
};
}, [channels]);

return { connected, authenticated, data };
}

// 使用
function TradingView() {
const { data } = useZTDXWebSocket([
{ channel: 'ticker', symbol: 'BTCUSDT' },
{ channel: 'orderbook', symbol: 'BTCUSDT', depth: 20 },
{ channel: 'trades', symbol: 'BTCUSDT' },
{ channel: 'orders', symbol: 'BTCUSDT' }
]);

return (
<div>
<h1>BTCUSDT</h1>
<p>价格: {data.ticker_BTCUSDT?.last_price}</p>
{/* ... */}
</div>
);
}

错误处理

常见错误

错误码描述解决方案
AUTH_REQUIRED需要认证先发送 auth 消息
AUTH_FAILED认证失败检查签名和时间戳
INVALID_CHANNEL无效频道检查频道名称
INVALID_SYMBOL无效交易对检查交易对名称
SUBSCRIPTION_LIMIT订阅数超限减少订阅数量(最多50个)
RATE_LIMIT消息频率过高降低发送频率

重连策略

class ZTDXWebSocket {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.connect();
}

connect() {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('已连接');
this.reconnectDelay = 1000; // 重置延迟
};

this.ws.onclose = () => {
console.log(`连接断开,${this.reconnectDelay}ms 后重连...`);

setTimeout(() => {
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
this.connect();
}, this.reconnectDelay);
};
}
}

最佳实践

1. 使用心跳保持连接

// 每 30 秒发送一次 ping
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ action: 'ping' }));
}
}, 30000);

2. 订阅多个symbol的相同频道

// ✅ 推荐
['BTCUSDT', 'ETHUSDT', 'SOLUSDT'].forEach(symbol => {
ws.send(JSON.stringify({
action: 'subscribe',
channel: 'ticker',
symbol
}));
});

3. 维护本地订单簿

let orderbook = { bids: {}, asks: {} };

ws.onmessage = (event) => {
const msg = JSON.parse(event.data);

if (msg.type === 'orderbook:snapshot') {
// 重置为快照
orderbook = {
bids: Object.fromEntries(msg.data.bids),
asks: Object.fromEntries(msg.data.asks)
};
} else if (msg.type === 'orderbook:update') {
// 应用增量更新
msg.data.bids?.forEach(([price, amount]) => {
if (amount === '0') {
delete orderbook.bids[price];
} else {
orderbook.bids[price] = amount;
}
});

msg.data.asks?.forEach(([price, amount]) => {
if (amount === '0') {
delete orderbook.asks[price];
} else {
orderbook.asks[price] = amount;
}
});
}
};

4. 处理网络异常

ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
// 不要在这里重连,等待 onclose 事件
};

ws.onclose = (event) => {
if (event.code === 1000) {
console.log('正常关闭');
} else {
console.log('异常断开,准备重连');
reconnect();
}
};

常见问题

Q: WebSocket 最多可以订阅多少个频道?

A: 每个连接最多订阅 50 个数据流。如果需要更多,请建立多个连接。

Q: 认证的签名有效期是多久?

A: WebSocket 认证签名的有效期为 5 分钟(与 REST API 一致)。

Q: 连接断开后会自动重连吗?

A: 服务器不会主动重连。客户端需要实现重连逻辑(建议使用指数退避策略)。

Q: 如何知道订阅是否成功?

A: 订阅成功后会收到 type: "subscribed" 的确认消息。

Q: 私有数据需要为每个symbol单独认证吗?

A: 不需要。只需认证一次,之后可以订阅任何symbol的私有数据。


相关文档