Binance Futures User Data Stream with Python
Learn how to connect to Binance Futures User Data Stream using Python to track orders, positions, and account updates in real time.
Learn how to connect to Binance Futures User Data Stream using Python to track orders, positions, and account updates in real time.
If you're building a trading bot or monitoring system on Binance Futures, polling the REST API every second is a rookie mistake. It's slow, burns your rate limits, and misses events between calls. The correct approach is the User Data Stream — a private WebSocket feed that pushes account updates, order fills, and position changes the instant they happen. This guide walks through setting it up in Python from scratch.
The User Data Stream is a private, authenticated WebSocket channel that Binance provides for each account. Unlike public market data streams (which anyone can subscribe to), this one is scoped to your account and delivers three critical event types: ACCOUNT_UPDATE (balance and position changes), ORDER_TRADE_UPDATE (order lifecycle events — new, filled, canceled, expired), and MARGIN_CALL (liquidation warnings). Compared to REST polling, the latency difference is dramatic — you get notified in milliseconds rather than waiting for your next scheduled request.
Binance Futures is the primary exchange where this pattern matters most, but similar private WebSocket feeds exist on Bybit and OKX for their perpetual futures markets. The implementation details differ, but the concept is the same — subscribe once, receive everything. This guide focuses on Binance USD-M Futures (usdfutures), which uses USDT-margined contracts.
The User Data Stream uses a listen key — a temporary token you generate via REST and then pass to the WebSocket connection. Listen keys expire after 60 minutes, so your code must keep them alive by sending a keepalive ping every 30-50 minutes. Here's the flow: generate key → open WebSocket with the key → periodically extend it before it expires.
import requests
import time
API_KEY = "your_binance_api_key"
BASE_URL = "https://fapi.binance.com"
HEADERS = {"X-MBX-APIKEY": API_KEY}
def get_listen_key() -> str:
resp = requests.post(
f"{BASE_URL}/fapi/v1/listenKey",
headers=HEADERS
)
resp.raise_for_status()
return resp.json()["listenKey"]
def extend_listen_key(listen_key: str) -> None:
resp = requests.put(
f"{BASE_URL}/fapi/v1/listenKey",
headers=HEADERS,
params={"listenKey": listen_key}
)
resp.raise_for_status()
listen_key = get_listen_key()
print(f"Listen key: {listen_key}")
Never share your listen key. It grants read access to your account event stream and could reveal open positions, order sizes, and balance changes to anyone who has it.
With a listen key in hand, connecting is straightforward. The WebSocket URL for Binance USD-M Futures is wss://fstream.binance.com/ws/{listenKey}. We'll use the websockets library — it's async-native and handles reconnection logic cleanly. Install it with pip install websockets requests.
import asyncio
import json
import websockets
import threading
import time
import requests
API_KEY = "your_binance_api_key"
BASE_URL = "https://fapi.binance.com"
WS_BASE = "wss://fstream.binance.com/ws"
HEADERS = {"X-MBX-APIKEY": API_KEY}
def get_listen_key() -> str:
r = requests.post(f"{BASE_URL}/fapi/v1/listenKey", headers=HEADERS)
r.raise_for_status()
return r.json()["listenKey"]
def extend_listen_key(listen_key: str) -> None:
requests.put(
f"{BASE_URL}/fapi/v1/listenKey",
headers=HEADERS,
params={"listenKey": listen_key}
)
def keepalive_loop(listen_key: str, interval: int = 1800) -> None:
"""Run in a background thread — extends key every 30 min."""
while True:
time.sleep(interval)
extend_listen_key(listen_key)
print("Listen key extended")
async def handle_message(msg: dict) -> None:
event = msg.get("e")
if event == "ORDER_TRADE_UPDATE":
order = msg["o"]
print(f"Order update: {order['s']} {order['S']} {order['X']} qty={order['q']} price={order['L']}")
elif event == "ACCOUNT_UPDATE":
for balance in msg["a"].get("B", []):
print(f"Balance: {balance['a']} wallet={balance['wb']} cross={balance['cw']}")
for pos in msg["a"].get("P", []):
print(f"Position: {pos['s']} amt={pos['pa']} entry={pos['ep']}")
elif event == "MARGIN_CALL":
print(f"MARGIN CALL: {msg}")
async def stream(listen_key: str) -> None:
url = f"{WS_BASE}/{listen_key}"
async with websockets.connect(url, ping_interval=20) as ws:
print(f"Connected to {url}")
async for raw in ws:
msg = json.loads(raw)
await handle_message(msg)
if __name__ == "__main__":
lk = get_listen_key()
# keepalive runs in background thread
t = threading.Thread(target=keepalive_loop, args=(lk,), daemon=True)
t.start()
asyncio.run(stream(lk))
The keepalive thread is critical. If you skip it, your listen key expires after 60 minutes and the WebSocket disconnects silently — your bot stops receiving fills without any obvious error. A 30-minute interval gives comfortable headroom.
Each event from the stream has a different payload structure. Understanding what each field means is essential before you build anything on top of this data.
| Field | Key | Example Value | Meaning |
|---|---|---|---|
| Symbol | o.s | BTCUSDT | Trading pair |
| Side | o.S | BUY / SELL | Order direction |
| Order Status | o.X | FILLED / NEW / CANCELED | Current state |
| Original Qty | o.q | 0.01 | Total order quantity |
| Last Filled Qty | o.l | 0.01 | Qty filled in this event |
| Last Fill Price | o.L | 67500.00 | Price of last fill |
| Realized PnL | o.rp | 12.50 | Realized PnL on close |
| Commission | o.n | 0.034 | Fee paid for this fill |
| Commission Asset | o.N | USDT | Fee currency |
| Section | Key | Field | Meaning |
|---|---|---|---|
| Balance | a.B[].a | Asset | Currency symbol (USDT, BNB) |
| Balance | a.B[].wb | Wallet Balance | Total wallet balance |
| Balance | a.B[].cw | Cross Wallet Balance | Available in cross margin |
| Position | a.P[].s | Symbol | Contract symbol |
| Position | a.P[].pa | Position Amount | Current position size |
| Position | a.P[].ep | Entry Price | Average entry price |
| Position | a.P[].up | Unrealized PnL | Current floating PnL |
For MARGIN_CALL events, the payload contains a list of positions approaching liquidation. In practice, you should treat this event as an emergency signal — log it, send an alert, and if your bot is risk-aware, consider reducing position size immediately. Platforms like VoiceOfChain use real-time position data from exactly these kinds of streams to surface whale-level margin pressure signals before they move markets.
Production code needs to handle disconnections gracefully. Network blips, Binance maintenance windows, or IP rate limits can drop your WebSocket. The pattern below wraps the connection in a retry loop with exponential backoff and regenerates the listen key on each reconnect — because the old key may have expired during the downtime.
import asyncio
import json
import websockets
import threading
import time
import requests
from websockets.exceptions import ConnectionClosed
API_KEY = "your_api_key"
BASE_URL = "https://fapi.binance.com"
WS_BASE = "wss://fstream.binance.com/ws"
HEADERS = {"X-MBX-APIKEY": API_KEY}
def get_listen_key() -> str:
r = requests.post(f"{BASE_URL}/fapi/v1/listenKey", headers=HEADERS)
r.raise_for_status()
return r.json()["listenKey"]
async def handle_message(msg: dict) -> None:
event = msg.get("e")
if event == "ORDER_TRADE_UPDATE":
o = msg["o"]
if o["X"] == "FILLED":
print(f"FILL {o['s']} {o['S']} qty={o['l']} @ {o['L']} pnl={o['rp']}")
elif event == "ACCOUNT_UPDATE":
reason = msg["a"]["m"]
print(f"Account update reason: {reason}")
elif event == "MARGIN_CALL":
print(f"!!! MARGIN CALL: {json.dumps(msg, indent=2)}")
async def run_stream() -> None:
retry_delay = 1
while True:
try:
lk = get_listen_key()
url = f"{WS_BASE}/{lk}"
print(f"Connecting to stream...")
async with websockets.connect(url, ping_interval=20) as ws:
retry_delay = 1 # reset on successful connect
async for raw in ws:
await handle_message(json.loads(raw))
except (ConnectionClosed, Exception) as e:
print(f"Stream error: {e}, retrying in {retry_delay}s")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 2, 60) # cap at 60s
if __name__ == "__main__":
asyncio.run(run_stream())
Always regenerate the listen key on reconnect. Don't try to reuse the old key — it may have been invalidated. The extra REST call costs microseconds and prevents a hard-to-debug silent failure.
Once you have the stream running reliably, there are several high-value ways to use it in a real trading setup.
If you're running strategies on both Binance and Bybit, note that Bybit's equivalent is the Private WebSocket topic execution — the event schema differs but the architecture is identical. OKX uses a similar authenticated WebSocket with a separate login step. The patterns from this guide transfer directly; only the field names change.
The Binance Futures User Data Stream is foundational infrastructure for any serious algorithmic trading setup. REST polling is fine for low-frequency strategies and one-off queries, but anything that needs to react to fills, position changes, or margin warnings in real time requires a WebSocket connection. The pattern — generate listen key, connect WebSocket, keepalive in background, reconnect with fresh key on failure — is simple enough to implement in an afternoon and robust enough to run in production for months. Combine it with real-time signal feeds like VoiceOfChain to correlate your own account activity with broader market order flow, and you have a genuinely powerful information advantage over traders flying blind on delayed data.