关于 orderly-websocket-streaming
面向使用 Orderly Network 构建实时界面的开发者,介绍 WebSocket 认证、公共与私有数据订阅及连接维护,涵盖行情和账户流。
主要功能
- 01订阅公共市场数据
- 02认证私有账户数据流
- 03处理心跳与重连
- 04使用 React SDK 流式 Hooks
- 05检查流错误与消息序列
适用场景
- 01展示实时订单簿和成交数据
- 02跟踪订单执行与持仓变化
- 03监控余额和清算警告
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ orderlynetwork/ skills --skill orderly-websocket-streaming安装后在 Claude Code 中输入/orderly-websocket-streaming确认已加载。
详细介绍
Orderly Network:WebSocket 流式传输
本技能介绍如何在 Orderly Network 上建立 WebSocket 连接、进行身份验证并订阅实时数据流。
适用场景
- 构建实时交易界面
- 流式传输订单簿数据
- 接收成交通知
- 监控持仓更新
前置条件
- Orderly 账户 ID
- 用于签名的 Ed25519 私钥
- WebSocket 客户端库(原生 WebSocket、socket.io 等)
WebSocket 端点
| 环境 | URL |
|---|---|
| 主网公共端点 | wss://ws.orderly.org/ws/stream |
| 主网私有端点 | wss://ws-private-evm.orderly.org/v2/ws/private/stream/{account_id} |
| 测试网公共端点 | wss://testnet-ws.orderly.org/ws/stream |
| 测试网私有端点 | wss://testnet-ws-private-evm.orderly.org/v2/ws/private/stream/{account_id} |
连接流程
1. Connect to WebSocket endpoint
2. Authenticate (for private streams)
3. Subscribe to topics
4. Receive real-time messages
5. Handle reconnection/heartbeat
公共数据流(无需身份验证)
连接并订阅
const ws = new WebSocket('wss://ws.orderly.org/ws/stream');
ws.onopen = () => {
// Subscribe to orderbook
ws.send(
JSON.stringify({
id: 'sub_ob_1',
event: 'subscribe',
topic: 'PERP_ETH_USDC@orderbook',
})
);
// Subscribe to trades
ws.send(
JSON.stringify({
id: 'sub_trades_1',
event: 'subscribe',
topic: 'PERP_ETH_USDC@trade',
})
);
// Subscribe to 24h ticker
ws.send(
JSON.stringify({
id: 'sub_ticker_1',
event: 'subscribe',
topic: 'PERP_ETH_USDC@24hrTicker',
})
);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
可用的公共主题
| 主题 | 说明 |
|---|---|
{symbol}@orderbook | 完整订单簿快照 |
{symbol}@orderbookupdate | 增量更新 |
{symbol}@trade | 成交记录 |
{symbol}@kline_{interval} | K 线/蜡烛图数据 |
{symbol}@markprice | 标记价格更新 |
{symbol}@indexprice | 指数价格更新 |
{symbol}@24hrTicker | 24 小时统计数据 |
markprice | 所有交易对的标记价格 |
indexprice | 所有交易对的指数价格 |
订单簿数据流
interface OrderbookMessage {
topic: string;
ts: number; // Timestamp
data: {
asks: [string, string][]; // [price, quantity]
bids: [string, string][];
prevTs: number; // Previous timestamp for sequencing
};
}
// Subscribe
{ "event": "subscribe", "topic": "PERP_ETH_USDC@orderbookupdate" }
// Maintain local orderbook
let orderbook = { asks: [], bids: [] };
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.topic?.includes('orderbookupdate')) {
// Apply incremental updates
for (const [price, qty] of msg.data.asks) {
updateLevel(orderbook.asks, price, qty);
}
for (const [price, qty] of msg.data.bids) {
updateLevel(orderbook.bids, price, qty);
}
}
};
function updateLevel(levels: [string, string][], price: string, qty: string) {
const idx = levels.findIndex(l => l[0] === price);
if (qty === '0') {
// Remove level
if (idx !== -1) levels.splice(idx, 1);
} else {
// Update or insert
if (idx !== -1) {
levels[idx] = [price, qty];
} else {
levels.push([price, qty]);
// Sort (asks ascending, bids descending)
levels.sort((a, b) => parseFloat(a[0]) - parseFloat(b[0]));
}
}
}
K 线数据流
// Available intervals: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
ws.send(JSON.stringify({
id: 'sub_kline',
event: 'subscribe',
topic: 'PERP_ETH_USDC@kline_1m',
}));
// Message format
{
"topic": "PERP_ETH_USDC@kline_1m",
"ts": 1699123456789,
"data": {
"t": 1699123440000, // Kline start time
"o": "3000.00", // Open
"h": "3010.00", // High
"l": "2995.00", // Low
"c": "3005.00", // Close
"v": "123.45", // Volume
"qv": "370350.00" // Quote volume
}
}
私有数据流(需要身份验证)
身份验证
import { signAsync } from '@noble/ed25519';
const accountId = 'your_account_id';
const privateKey = new Uint8Array(32); // Your Ed25519 private key
const ws = new WebSocket(`wss://ws-private-evm.orderly.org/v2/ws/private/stream/${accountId}`);
ws.onopen = async () => {
const timestamp = Date.now();
const message = timestamp.toString();
const signature = await signAsync(new TextEncoder().encode(message), privateKey);
// Encode as base64url (browser & Node.js compatible)
const base64 = btoa(String.fromCharCode(...signature));
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// Send authentication
ws.send(
JSON.stringify({
id: 'auth_1',
event: 'auth',
params: {
orderly_key: `ed25519:${publicKeyBase58}`,
sign: base64url,
timestamp: timestamp,
},
})
);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'auth' && data.success) {
console.log('Authenticated successfully');
// Now subscribe to private topics
subscribePrivateTopics();
}
};
私有主题
| 主题 | 说明 |
|---|---|
executionreport | 订单状态更新 |
position | 持仓变更 |
balance | 余额更新 |
liquidationsaccount | 强平警告 |
wallet | 钱包/存款更新 |
account | 账户更新 |
settle | 结算更新 |
notifications | 通知 |
成交报告数据流
function subscribePrivateTopics() {
// Subscribe to order updates
ws.send(
JSON.stringify({
id: 'sub_exec',
event: 'subscribe',
topic: 'executionreport',
// Optional: filter by symbol
params: { symbol: 'PERP_ETH_USDC' },
})
);
}
// Message format
interface ExecutionReport {
topic: 'executionreport';
ts: number;
data: {
orderId: string;
clientOrderId?: string;
symbol: string;
side: 'BUY' | 'SELL';
orderType: string;
status: 'NEW' | 'PARTIAL_FILLED' | 'FILLED' | 'CANCELLED' | 'REJECTED';
price: string;
quantity: string;
executedQty: string;
avgPrice?: string;
leavesQty: string;
lastExecutedPrice?: string;
lastExecutedQty?: string;
fee?: string;
feeToken?: string;
reason?: string;
};
}
持仓数据流
ws.send(JSON.stringify({
id: 'sub_position',
event: 'subscribe',
topic: 'position',
}));
// Message format
{
"topic": "position",
"ts": 1699123456789,
"data": {
"symbol": "PERP_ETH_USDC",
"positionQty": 0.5,
"averageOpenPrice": 3000,
"markPrice": 3100,
"unrealizedPnl": 50,
"leverage": 10,
"liqPrice": 2700,
"mmr": 0.01,
"imr": 0.02
}
}
余额数据流
ws.send(JSON.stringify({
id: 'sub_balance',
event: 'subscribe',
topic: 'balance',
}));
// Message format
{
"topic": "balance",
"ts": 1699123456789,
"data": {
"token": "USDC",
"total": "10000.00",
"free": "8000.00",
"locked": "2000.00",
"unsettledPnl": "500.00"
}
}
React SDK:内置流式传输
Orderly SDK 会自动处理 WebSocket 连接:
import {
useOrderbookStream,
usePositionStream,
useOrderStream,
useCollateral,
useHoldingStream
} from '@orderly.network/hooks';
import { OrderStatus } from '@orderly.network/types';
function TradingInterface({ symbol }: { symbol: string }) {
// Orderbook - automatically connects to WebSocket
const { asks, bids } = useOrderbookStream(symbol);
// Positions - real-time updates (returns a 3-tuple)
const [positions] = usePositionStream();
// Orders - real-time updates
const [orders] = useOrderStream({ status: OrderStatus.INCOMPLETE });
// Balance / collateral and real-time holdings
const { freeCollateral } = useCollateral({ dp: 2 });
const holdings = useHoldingStream();
return (
<div>
<OrderbookWidget asks={asks} bids={bids} />
<PositionsTable positions={positions?.rows ?? []} />
<OrdersTable orders={orders ?? []} />
<BalanceDisplay freeCollateral={freeCollateral} holdings={holdings} />
</div>
);
}
没有
useBalancehook。请使用useCollateral(账户价值/可用抵押品)或useHoldingStream(按代币提供的持仓数据流)。usePositionStream会返回一个 3 元组[positions, calc, status]——请将其解构为数组,而非对象。
连接管理
心跳
// Orderly sends ping, you respond with pong
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'ping') {
ws.send(JSON.stringify({ event: 'pong' }));
}
};
// Or use native WebSocket ping/pong
ws.onopen = () => {
// Some clients need explicit ping
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ event: 'ping' }));
}
}, 30000);
};
重新连接
class OrderlyWebSocket {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private subscriptions: string[] = [];
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.authenticate();
this.resubscribe();
};
this.ws.onclose = () => {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(
() => {
this.reconnectAttempts++;
this.connect();
},
Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)
);
}
};
}
private resubscribe() {
for (const topic of this.subscriptions) {
this.ws?.send(
JSON.stringify({
event: 'subscribe',
topic,
})
);
}
}
subscribe(topic: string) {
this.subscriptions.push(topic);
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(
JSON.stringify({
event: 'subscribe',
topic,
})
);
}
}
}
错误处理
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.success === false) {
console.error('WebSocket error:', data.errorMsg);
switch (data.errorMsg) {
case 'invalid symbol':
// Handle invalid symbol
break;
case 'authentication failed':
// Re-authenticate
break;
case 'rate limit exceeded':
// Slow down subscription requests
break;
}
}
};
// Error response format
{
"id": "sub_ob_1",
"event": "subscribe",
"success": false,
"errorMsg": "invalid symbol"
}
速率限制
| 操作 | 限制 |
|---|---|
| 每个连接的订阅数 | 100 |
| 每秒消息数 | 50 |
| 每个 IP 的连接数 | 10 |
常见问题
“身份验证失败”错误
- 检查时间戳是否为当前时间(而非未来或过去的时间)
- 验证签名编码(base64url)
- 确保 orderly_key 格式为
ed25519:{publicKey}
连接频繁断开
- 实现心跳/pong
- 检查网络稳定性
- 使用重新连接逻辑
消息缺失
- 检查订阅是否成功
- 验证主题名称格式
- 监控错误消息
订单簿不同步
- 使用
prevTs检测遗漏的消息 - 重新订阅以获取最新快照
- 实现序列验证
Related Skills
- orderly-api-authentication - Ed25519 signing details
- orderly-trading-orders - Order management
- orderly-positions-tpsl - Position monitoring
- orderly-sdk-react-hooks - SDK streaming hooks
评论
支持 Markdown