Tutorial: Stream real-time crypto indicators over WebSocket
Most trading bots still get their indicator values the old way: call the REST API on a timer, compare the result with the last one, and repeat. It works, but you’re always choosing between polling often (and burning through requests) or polling rarely (and reacting late).
With WebSocket streaming you subscribe once and we push fresh indicator values to you as the market moves, including while the current candle is still forming. This tutorial takes you from a first connection to a client you can run in production, with copy-paste examples along the way.
What you’ll build:
- A minimal client that streams live RSI for BTC/USDT
- A multi-symbol watchlist with several indicators over one connection
- An RSI alert that only fires when the value actually crosses a threshold
- The same client in Python
- A production checklist: reconnects, limits and common mistakes
You’ll need a TAAPI.IO API key with streaming included (see pricing for the subscription limits on each plan) and either Node.js 18+ or Python 3.9+.
How streaming works in 30 seconds
There’s one endpoint:
wss://v2.taapi.io/streaming
The conversation always goes the same way:
- Connect and send an
authmessage with your API key. - Wait for
auth_ok. - Send one or more
subscribemessages: exchange, symbol, timeframe, indicator and parameters. - Receive
updatemessages as new values are calculated.
Every subscription carries an id that you choose. The server combines it with the exchange, symbol and timeframe into a compound ID, for example binance_btcusdt_1m_rsi, and that’s the key you’ll find your value under in each update. That’s how you tell your streams apart when hundreds of them share one connection.
Example 1: Your first stream (Node.js)
Install the ws package:
npm install ws
Then save this as stream.js:
const WebSocket = require('ws');
const ws = new WebSocket('wss://v2.taapi.io/streaming');
ws.on('open', () => {
ws.send(JSON.stringify({ type: 'auth', token: 'Bearer YOUR_API_KEY' }));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'auth_ok') {
ws.send(JSON.stringify({
type: 'subscribe',
id: 'rsi',
exchange: 'binance',
symbol: 'BTCUSDT',
interval: '1m',
indicator: 'rsi',
params: { period: 14 }
}));
}
if (msg.type === 'update') {
const rsi = msg.data['binance_btcusdt_1m_rsi'];
console.log(new Date().toISOString(), 'BTC/USDT 1m RSI:', rsi.value.at(-1));
}
if (msg.type === 'error' || msg.type === 'auth_error') {
console.error('Server says:', msg.error);
}
});
Run it with node stream.js and you’ll see the RSI update in your terminal every few seconds, with no polling loop and no wasted requests.
Notice that the symbol is written without a slash (BTCUSDT) in streaming subscriptions.
Example 2: A multi-symbol watchlist in one message
Subscribing to indicators one at a time gets tedious quickly. With subscribe_bulk you send a list of symbols, timeframes and indicators, and the server subscribes you to every combination:
const WebSocket = require('ws');
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'];
const INTERVALS = ['5m', '1h'];
const ws = new WebSocket('wss://v2.taapi.io/streaming');
const watchlist = {}; // { "BTCUSDT 5m": { rsi: 52.1, ema50: 67012.4, ... } }
ws.on('open', () => {
ws.send(JSON.stringify({ type: 'auth', token: 'Bearer YOUR_API_KEY' }));
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'auth_ok') {
ws.send(JSON.stringify({
type: 'subscribe_bulk',
exchange: 'binance',
symbols: SYMBOLS,
intervals: INTERVALS,
indicators: [
{ id: 'rsi', name: 'rsi', params: { period: 14 } },
{ id: 'ema50', name: 'ema', params: { period: 50 } },
{ id: 'ema200', name: 'ema', params: { period: 200 } }
]
}));
}
if (msg.type === 'update') {
// msg.subscription looks like "binance:btcusdt:5m"
const [, symbol, interval] = msg.subscription.split(':');
const row = (watchlist[`${symbol.toUpperCase()} ${interval}`] ??= {});
for (const [compoundId, result] of Object.entries(msg.data)) {
const myId = compoundId.split('_').at(-1); // "rsi", "ema50", "ema200"
if (result?.value) row[myId] = result.value.at(-1);
}
console.clear();
console.table(watchlist);
}
if (msg.type === 'error') console.error(msg.error);
});
That’s 3 symbols × 2 timeframes × 3 indicators, which is 18 live values from one message and one connection. The terminal table keeps refreshing as values arrive.
Tip: the subscription limit on your plan counts each exchange + symbol + timeframe combination, and each one can carry many indicators. The watchlist above uses 6 subscriptions, not 18. Put all the indicators you need for a symbol and timeframe in the same subscription; you get the extra indicators without using up more of your limit.
Example 3: An alert that fires on the cross, not on every update
Streaming gives you a value every few seconds, including while a candle is still forming. If you write if (rsi < 30) sendAlert(), you’ll get the same alert over and over for as long as RSI stays oversold. Track the previous value and react only when the value crosses the threshold:
const lastRsi = {};
function onRsi(key, rsi) {
const prev = lastRsi[key];
lastRsi[key] = rsi;
if (prev === undefined) return; // first value, nothing to compare yet
if (prev >= 30 && rsi < 30) alert(`${key}: RSI crossed below 30 (${rsi.toFixed(1)}), oversold`);
if (prev <= 70 && rsi > 70) alert(`${key}: RSI crossed above 70 (${rsi.toFixed(1)}), overbought`);
}
function alert(text) {
console.log('🔔', text); // swap in Telegram, Discord, email, your order logic…
}
Call onRsi('BTCUSDT 5m', value) from your update handler instead of logging the value. The same pattern works for any threshold or crossover strategy: EMA 50 crossing EMA 200, price crossing a Bollinger Band, and so on.
Forming candle or closed candle? Values on a forming candle move as price moves, so a cross can appear and then disappear before the candle closes. That’s exactly what you want for early warnings and live dashboards. If your strategy only trades confirmed signals, use the stream as the trigger and check the closed-candle value with a single REST request using backtrack=1 before acting.
Example 4: The same client in Python
Install the websockets package:
pip install websockets
import asyncio
import json
import websockets
API_KEY = "YOUR_API_KEY"
async def main():
async with websockets.connect("wss://v2.taapi.io/streaming") as ws:
await ws.send(json.dumps({"type": "auth", "token": f"Bearer {API_KEY}"}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "auth_ok":
await ws.send(json.dumps({
"type": "subscribe_bulk",
"exchange": "binance",
"symbols": ["BTCUSDT", "ETHUSDT"],
"intervals": ["15m"],
"indicators": [
{"id": "rsi", "name": "rsi", "params": {"period": 14}},
{"id": "macd", "name": "macd"},
],
}))
elif msg["type"] == "update":
for compound_id, result in msg["data"].items():
print(msg["subscription"], compound_id, result)
elif msg["type"] in ("error", "auth_error"):
print("Server says:", msg["error"])
asyncio.run(main())
Some indicators return several values at once. MACD, for example, returns the MACD line, the signal line and the histogram, so print the whole result first to see its shape before you pick fields out of it.
Example 5: Making it production-ready
The examples above are enough to explore. Before you run a bot on streaming around the clock, handle these:
Reconnect, then re-authenticate and re-subscribe. Connections drop because of network blips, deploys and laptops going to sleep. The server doesn’t remember your subscriptions after a disconnect, so keep your subscription list in your own code and send it again every time you get auth_ok. Use exponential backoff so you don’t hammer the server while it’s unreachable:
const WebSocket = require('ws');
const SUBSCRIPTIONS = [
{
type: 'subscribe_bulk',
exchange: 'binance',
symbols: ['BTCUSDT', 'ETHUSDT'],
intervals: ['5m'],
indicators: [{ id: 'rsi', name: 'rsi', params: { period: 14 } }]
}
];
let delay = 1000;
function connect() {
const ws = new WebSocket('wss://v2.taapi.io/streaming');
let heartbeat;
ws.on('open', () => {
ws.send(JSON.stringify({ type: 'auth', token: 'Bearer YOUR_API_KEY' }));
heartbeat = setInterval(() => ws.ping(), 30000); // keeps proxies from closing idle sockets
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'auth_ok') {
delay = 1000; // healthy again, reset the backoff
SUBSCRIPTIONS.forEach((sub) => ws.send(JSON.stringify(sub)));
}
if (msg.type === 'update') handleUpdate(msg);
if (msg.type === 'error') console.error('Stream error:', msg.error);
});
ws.on('close', () => {
clearInterval(heartbeat);
console.log(`Disconnected, reconnecting in ${delay / 1000}s`);
setTimeout(connect, delay);
delay = Math.min(delay * 2, 30000);
});
ws.on('error', (err) => console.error('Socket error:', err.message)); // 'close' follows
}
function handleUpdate(msg) {
console.log(msg.subscription, msg.data);
}
connect();
Use one connection per API key. A new connection authenticated with the same key replaces the previous one: the older socket is closed, and its subscriptions are cleared. If several services need the same data, run one streaming client and fan the values out internally (through a queue, Redis pub/sub or your own event bus), rather than opening a connection in every process.
Know your update rate. Each subscription pushes at most one update every 5 seconds on Basic and Pro, and every 2 seconds on Expert and Enterprise. The rate doesn’t change no matter how fast the market moves, so design around it.
Use REST for history, streaming for now. A stream always sends the latest value only, so backtrack and results aren’t accepted in streaming subscriptions. To warm up a strategy with past values, make one bulk REST request at startup, then switch to the stream.
Read the error messages. Invalid subscriptions don’t close your connection; you get a { "type": "error" } message that tells you exactly what’s wrong: an unknown indicator, an unsupported symbol, a timeframe that isn’t available for streaming, or a subscription limit reached. Log them, especially when you change your subscription list.
Clean up as you go. Send { "type": "list" } to see your active subscriptions, unsubscribe with a compound ID to drop one, or unsubscribe_all to start over without reconnecting.
Where to go from here
- Full message reference: WebSocket streaming documentation
- Browse the indicators library for anything you’d like to stream, from RSI, MACD and EMA to Supertrend and VWAP
- Compare streaming limits and update rates on the pricing page
If you’re polling the REST API on a timer today, try moving one strategy to streaming this week. The change is usually less than 50 lines of code. Once you have, we’d love to hear what you built, so get in touch.