WebSocket API
ZTDX 提供 WebSocket 接口用于实时推送市场数据和用户私有数据更新。相比轮询 REST API,WebSocket 具有更低的延迟和更高的效率。
连接信息
| 项目 | 内容 |
|---|---|
| WebSocket URL | wss://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 认证。
认证流程
- 建立 WebSocket 连接
- 生成 EIP-712 签名(WebSocketAuth)
- 发送认证消息
- 等待认证成功
- 订阅私有数据流
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的私有数据。