649 lines
12 KiB
Markdown
649 lines
12 KiB
Markdown
# WebSocket Testing Guide - OrbiQuant Trading Platform
|
|
|
|
## Overview
|
|
|
|
The OrbiQuant backend now supports real-time market data via WebSocket, powered by direct integration with Binance WebSocket streams. This guide explains how to test and use the WebSocket API.
|
|
|
|
## WebSocket Endpoint
|
|
|
|
```
|
|
ws://localhost:3000/ws
|
|
```
|
|
|
|
For production:
|
|
```
|
|
wss://your-domain.com/ws
|
|
```
|
|
|
|
## Authentication (Optional)
|
|
|
|
To access private channels (portfolio, orders, user-specific data), include a JWT token in the query string:
|
|
|
|
```
|
|
ws://localhost:3000/ws?token=YOUR_JWT_TOKEN
|
|
```
|
|
|
|
Public market data channels do not require authentication.
|
|
|
|
## Message Format
|
|
|
|
### Client to Server
|
|
|
|
All messages must be valid JSON with a `type` field:
|
|
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["price:BTCUSDT"]
|
|
}
|
|
```
|
|
|
|
### Server to Client
|
|
|
|
Server responses include a `type`, optional `channel`, and `timestamp`:
|
|
|
|
```json
|
|
{
|
|
"type": "price",
|
|
"channel": "price:BTCUSDT",
|
|
"data": {
|
|
"symbol": "BTCUSDT",
|
|
"price": 97523.45,
|
|
"change24h": 2345.67,
|
|
"changePercent24h": 2.47,
|
|
"high24h": 98500.00,
|
|
"low24h": 95000.00,
|
|
"volume24h": 12345.67,
|
|
"timestamp": 1701806400000
|
|
},
|
|
"timestamp": "2024-12-06T12:00:00.000Z"
|
|
}
|
|
```
|
|
|
|
## Available Channels
|
|
|
|
### 1. Price Updates (`price:<symbol>`)
|
|
|
|
Real-time price updates for a specific symbol (via Binance ticker stream).
|
|
|
|
**Subscribe:**
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["price:BTCUSDT"]
|
|
}
|
|
```
|
|
|
|
**Data received:**
|
|
```json
|
|
{
|
|
"type": "price",
|
|
"channel": "price:BTCUSDT",
|
|
"data": {
|
|
"symbol": "BTCUSDT",
|
|
"price": 97523.45,
|
|
"change24h": 2345.67,
|
|
"changePercent24h": 2.47,
|
|
"high24h": 98500.00,
|
|
"low24h": 95000.00,
|
|
"volume24h": 12345.67,
|
|
"timestamp": 1701806400000
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Ticker Updates (`ticker:<symbol>`)
|
|
|
|
Full 24h ticker statistics (bid, ask, volume, etc.).
|
|
|
|
**Subscribe:**
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["ticker:ETHUSDT"]
|
|
}
|
|
```
|
|
|
|
**Data received:**
|
|
```json
|
|
{
|
|
"type": "ticker",
|
|
"channel": "ticker:ETHUSDT",
|
|
"data": {
|
|
"symbol": "ETHUSDT",
|
|
"price": 3650.00,
|
|
"bid": 3649.50,
|
|
"ask": 3650.50,
|
|
"volume": 123456.78,
|
|
"change": 125.50,
|
|
"changePercent": 3.56,
|
|
"high": 3700.00,
|
|
"low": 3500.00,
|
|
"open": 3524.50,
|
|
"previousClose": 3524.50,
|
|
"timestamp": "2024-12-06T12:00:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Klines/Candlesticks (`klines:<symbol>:<interval>`)
|
|
|
|
Real-time candlestick data at specified intervals.
|
|
|
|
**Intervals:** `1m`, `3m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `6h`, `12h`, `1d`, `3d`, `1w`, `1M`
|
|
|
|
**Subscribe:**
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["klines:BTCUSDT:1m"]
|
|
}
|
|
```
|
|
|
|
**Data received:**
|
|
```json
|
|
{
|
|
"type": "kline",
|
|
"channel": "klines:BTCUSDT:1m",
|
|
"data": {
|
|
"symbol": "BTCUSDT",
|
|
"interval": "1m",
|
|
"time": 1701806400000,
|
|
"open": 97500.00,
|
|
"high": 97600.00,
|
|
"low": 97400.00,
|
|
"close": 97523.45,
|
|
"volume": 123.45,
|
|
"isFinal": false,
|
|
"timestamp": "2024-12-06T12:00:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Note:** `isFinal: true` indicates the candle is closed and will not change.
|
|
|
|
### 4. Trades (`trades:<symbol>`)
|
|
|
|
Individual trade executions as they happen.
|
|
|
|
**Subscribe:**
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["trades:BTCUSDT"]
|
|
}
|
|
```
|
|
|
|
**Data received:**
|
|
```json
|
|
{
|
|
"type": "trade",
|
|
"channel": "trades:BTCUSDT",
|
|
"data": {
|
|
"symbol": "BTCUSDT",
|
|
"price": 97523.45,
|
|
"quantity": 0.5,
|
|
"side": "buy",
|
|
"timestamp": "2024-12-06T12:00:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Depth/Order Book (`depth:<symbol>`)
|
|
|
|
Order book depth updates (top 10 levels by default).
|
|
|
|
**Subscribe:**
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["depth:BTCUSDT"]
|
|
}
|
|
```
|
|
|
|
**Data received:**
|
|
```json
|
|
{
|
|
"type": "depth",
|
|
"channel": "depth:BTCUSDT",
|
|
"data": {
|
|
"symbol": "BTCUSDT",
|
|
"bids": [
|
|
[97520.00, 1.5],
|
|
[97519.00, 2.3]
|
|
],
|
|
"asks": [
|
|
[97521.00, 0.8],
|
|
[97522.00, 1.2]
|
|
],
|
|
"timestamp": "2024-12-06T12:00:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. ML Signals (`signals:<symbol>`)
|
|
|
|
AI-powered trading signals (requires backend ML service).
|
|
|
|
**Subscribe:**
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["signals:BTCUSDT"]
|
|
}
|
|
```
|
|
|
|
## Client Messages
|
|
|
|
### Subscribe
|
|
|
|
```json
|
|
{
|
|
"type": "subscribe",
|
|
"channels": ["price:BTCUSDT", "ticker:ETHUSDT", "klines:SOLUSDT:5m"]
|
|
}
|
|
```
|
|
|
|
### Unsubscribe
|
|
|
|
```json
|
|
{
|
|
"type": "unsubscribe",
|
|
"channels": ["price:BTCUSDT"]
|
|
}
|
|
```
|
|
|
|
### Ping (Keepalive)
|
|
|
|
```json
|
|
{
|
|
"type": "ping"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"type": "pong",
|
|
"timestamp": "2024-12-06T12:00:00.000Z"
|
|
}
|
|
```
|
|
|
|
## Testing Tools
|
|
|
|
### 1. Using `wscat` (Node.js)
|
|
|
|
Install:
|
|
```bash
|
|
npm install -g wscat
|
|
```
|
|
|
|
Connect:
|
|
```bash
|
|
wscat -c ws://localhost:3000/ws
|
|
```
|
|
|
|
Send messages:
|
|
```
|
|
> {"type":"subscribe","channels":["price:BTCUSDT"]}
|
|
< {"type":"subscribed","channel":"price:BTCUSDT","timestamp":"..."}
|
|
< {"type":"price","channel":"price:BTCUSDT","data":{...}}
|
|
```
|
|
|
|
### 2. Using `websocat` (Rust)
|
|
|
|
Install:
|
|
```bash
|
|
# macOS
|
|
brew install websocat
|
|
|
|
# Linux
|
|
cargo install websocat
|
|
```
|
|
|
|
Connect:
|
|
```bash
|
|
websocat ws://localhost:3000/ws
|
|
```
|
|
|
|
### 3. Using Browser JavaScript
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>WebSocket Test</title>
|
|
</head>
|
|
<body>
|
|
<h1>OrbiQuant WebSocket Test</h1>
|
|
<div id="output"></div>
|
|
|
|
<script>
|
|
const ws = new WebSocket('ws://localhost:3000/ws');
|
|
const output = document.getElementById('output');
|
|
|
|
ws.onopen = () => {
|
|
console.log('Connected to WebSocket');
|
|
output.innerHTML += '<p>Connected!</p>';
|
|
|
|
// Subscribe to price updates
|
|
ws.send(JSON.stringify({
|
|
type: 'subscribe',
|
|
channels: ['price:BTCUSDT', 'klines:ETHUSDT:1m']
|
|
}));
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
console.log('Received:', msg);
|
|
output.innerHTML += `<p>${msg.type}: ${JSON.stringify(msg.data || {})}</p>`;
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
output.innerHTML += '<p>Error occurred</p>';
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
console.log('Disconnected');
|
|
output.innerHTML += '<p>Disconnected</p>';
|
|
};
|
|
|
|
// Send ping every 30 seconds
|
|
setInterval(() => {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
}
|
|
}, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### 4. Using Python
|
|
|
|
```python
|
|
import asyncio
|
|
import websockets
|
|
import json
|
|
|
|
async def test_websocket():
|
|
uri = "ws://localhost:3000/ws"
|
|
async with websockets.connect(uri) as websocket:
|
|
# Subscribe to channels
|
|
await websocket.send(json.dumps({
|
|
"type": "subscribe",
|
|
"channels": ["price:BTCUSDT", "klines:BTCUSDT:1m"]
|
|
}))
|
|
|
|
# Listen for messages
|
|
while True:
|
|
message = await websocket.recv()
|
|
data = json.loads(message)
|
|
print(f"Received: {data['type']}")
|
|
if 'data' in data:
|
|
print(f" Data: {data['data']}")
|
|
|
|
asyncio.run(test_websocket())
|
|
```
|
|
|
|
## WebSocket Stats Endpoint
|
|
|
|
Check WebSocket server statistics:
|
|
|
|
```bash
|
|
curl http://localhost:3000/api/v1/ws/stats
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"connectedClients": 5,
|
|
"activeChannels": ["price:BTCUSDT", "klines:ETHUSDT:1m"],
|
|
"quoteStreams": 0,
|
|
"signalStreams": 2,
|
|
"binanceStreams": 3,
|
|
"binanceActiveStreams": ["btcusdt@ticker", "ethusdt@kline_1m"],
|
|
"priceCache": 2
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Symbols
|
|
|
|
- `BTCUSDT` - Bitcoin/USDT
|
|
- `ETHUSDT` - Ethereum/USDT
|
|
- `BNBUSDT` - Binance Coin/USDT
|
|
- `SOLUSDT` - Solana/USDT
|
|
- `XRPUSDT` - Ripple/USDT
|
|
- `DOGEUSDT` - Dogecoin/USDT
|
|
- `ADAUSDT` - Cardano/USDT
|
|
- `AVAXUSDT` - Avalanche/USDT
|
|
|
|
## Error Handling
|
|
|
|
### Connection Errors
|
|
|
|
If connection fails, check:
|
|
1. Backend server is running on port 3000
|
|
2. WebSocket path is `/ws`
|
|
3. No firewall blocking the connection
|
|
|
|
### Subscription Errors
|
|
|
|
```json
|
|
{
|
|
"type": "error",
|
|
"channel": "price:INVALID",
|
|
"data": {
|
|
"message": "Failed to fetch quote for INVALID"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Authentication Errors
|
|
|
|
For private channels without valid token:
|
|
```json
|
|
{
|
|
"type": "error",
|
|
"channel": "portfolio:user123",
|
|
"data": {
|
|
"message": "Authentication required for this channel"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Heartbeat**: Send ping every 30 seconds to keep connection alive
|
|
2. **Reconnection**: Implement exponential backoff for reconnections
|
|
3. **Subscription Limit**: Don't subscribe to too many symbols at once (max 50 per client)
|
|
4. **Clean Disconnect**: Unsubscribe before closing connection
|
|
5. **Error Handling**: Always handle `error` messages from server
|
|
|
|
## Example: Complete Trading Dashboard
|
|
|
|
```javascript
|
|
class TradingDashboard {
|
|
constructor() {
|
|
this.ws = null;
|
|
this.subscriptions = new Set();
|
|
this.reconnectAttempts = 0;
|
|
this.maxReconnectAttempts = 5;
|
|
}
|
|
|
|
connect() {
|
|
this.ws = new WebSocket('ws://localhost:3000/ws');
|
|
|
|
this.ws.onopen = () => {
|
|
console.log('Connected to trading server');
|
|
this.reconnectAttempts = 0;
|
|
this.resubscribe();
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
this.handleMessage(msg);
|
|
};
|
|
|
|
this.ws.onclose = () => {
|
|
console.log('Disconnected from server');
|
|
this.attemptReconnect();
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
};
|
|
|
|
// Heartbeat
|
|
setInterval(() => this.ping(), 30000);
|
|
}
|
|
|
|
subscribe(channel) {
|
|
this.subscriptions.add(channel);
|
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify({
|
|
type: 'subscribe',
|
|
channels: [channel]
|
|
}));
|
|
}
|
|
}
|
|
|
|
unsubscribe(channel) {
|
|
this.subscriptions.delete(channel);
|
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify({
|
|
type: 'unsubscribe',
|
|
channels: [channel]
|
|
}));
|
|
}
|
|
}
|
|
|
|
resubscribe() {
|
|
if (this.subscriptions.size > 0) {
|
|
this.ws.send(JSON.stringify({
|
|
type: 'subscribe',
|
|
channels: Array.from(this.subscriptions)
|
|
}));
|
|
}
|
|
}
|
|
|
|
ping() {
|
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
}
|
|
}
|
|
|
|
attemptReconnect() {
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
console.error('Max reconnect attempts reached');
|
|
return;
|
|
}
|
|
|
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
this.reconnectAttempts++;
|
|
|
|
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
setTimeout(() => this.connect(), delay);
|
|
}
|
|
|
|
handleMessage(msg) {
|
|
switch (msg.type) {
|
|
case 'connected':
|
|
console.log('Server says: connected', msg.data);
|
|
break;
|
|
case 'price':
|
|
this.updatePrice(msg.data);
|
|
break;
|
|
case 'kline':
|
|
this.updateChart(msg.data);
|
|
break;
|
|
case 'ticker':
|
|
this.updateTicker(msg.data);
|
|
break;
|
|
case 'trade':
|
|
this.addTrade(msg.data);
|
|
break;
|
|
case 'error':
|
|
console.error('Server error:', msg.data);
|
|
break;
|
|
case 'pong':
|
|
// Heartbeat acknowledged
|
|
break;
|
|
default:
|
|
console.log('Unknown message type:', msg.type);
|
|
}
|
|
}
|
|
|
|
updatePrice(data) {
|
|
console.log(`Price update: ${data.symbol} = $${data.price}`);
|
|
// Update UI
|
|
}
|
|
|
|
updateChart(data) {
|
|
console.log(`Kline update: ${data.symbol} ${data.interval}`);
|
|
// Update chart
|
|
}
|
|
|
|
updateTicker(data) {
|
|
console.log(`Ticker update: ${data.symbol}`);
|
|
// Update ticker display
|
|
}
|
|
|
|
addTrade(data) {
|
|
console.log(`New trade: ${data.symbol} ${data.side} ${data.quantity} @ ${data.price}`);
|
|
// Add to trades list
|
|
}
|
|
|
|
disconnect() {
|
|
if (this.ws) {
|
|
this.ws.close();
|
|
this.ws = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const dashboard = new TradingDashboard();
|
|
dashboard.connect();
|
|
dashboard.subscribe('price:BTCUSDT');
|
|
dashboard.subscribe('klines:ETHUSDT:1m');
|
|
dashboard.subscribe('trades:SOLUSDT');
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### No Data Received
|
|
|
|
1. Check if Binance API is accessible from your server
|
|
2. Verify symbol format (must be uppercase, e.g., `BTCUSDT`)
|
|
3. Check backend logs for connection errors
|
|
|
|
### High Latency
|
|
|
|
1. Ensure server is geographically close to Binance servers
|
|
2. Check network connection quality
|
|
3. Reduce number of subscriptions
|
|
|
|
### Disconnections
|
|
|
|
1. Implement heartbeat/ping mechanism
|
|
2. Check server logs for errors
|
|
3. Verify no rate limiting from Binance
|
|
|
|
## Support
|
|
|
|
For issues or questions:
|
|
- Check backend logs: `npm run dev` (shows real-time logs)
|
|
- WebSocket stats endpoint: `GET /api/v1/ws/stats`
|
|
- Server health: `GET /health`
|
|
|
|
---
|
|
|
|
**Last Updated:** December 6, 2024
|
|
**Backend Version:** 0.1.0
|
|
**WebSocket Protocol:** RFC 6455
|