Overview
This guide shows how to fetch real-time Bitcoin (BTC) prices in Python using async/await. We’ll use aiohttp for a WebSocket stream (low latency) and a REST fallback (simple polling). Examples target BTC/USDT from a public endpoint suitable for prototyping algorithmic trading pipelines.
Why async?
- Non-blocking I/O: handle networking without stalling your event loop.
- Easy concurrency: run multiple streams/tasks in one process.
- Low overhead: good for live feeds, order routing, and logging.
WebSocket vs REST
| Method | Pros | Cons | Use when |
|---|---|---|---|
| WebSocket | Real-time, low latency | Requires reconnect handling | Live price streams/triggers |
| REST | Simple, stateless | Polling latency and rate limits | Health checks, fallbacks |
Quickstart
- Requirements
- Python 3.10+
- aiohttp
- Install
pip install aiohttp
Run the minimal example below to stream BTC price ticks.
Optional: add the REST fallback snippet to recover when the socket drops.
Minimal Working Example (WebSocket streaming)
This connects to a public trade stream and prints the latest BTC price in USDT.
import asyncio
import json
import time
from decimal import Decimal
import aiohttp
WS_URL = "wss://stream.binance.com:9443/ws/btcusdt@trade" # BTC/USDT trades
async def stream_btc_prices():
backoff = 1
max_backoff = 30
async with aiohttp.ClientSession() as session:
while True:
try:
async with session.ws_connect(WS_URL, heartbeat=30, ssl=True) as ws:
print("Connected to WebSocket")
backoff = 1 # reset on successful connect
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
data = json.loads(msg.data)
# Price string like "p": "43210.12"
price = Decimal(data.get("p"))
event_ts_ms = data.get("E") # exchange event time
local_ts = time.time()
print(f"{local_ts:.3f} | BTCUSDT = {price}")
elif msg.type == aiohttp.WSMsgType.ERROR:
raise RuntimeError(f"WebSocket error: {msg.data}")
elif msg.type == aiohttp.WSMsgType.CLOSED:
raise RuntimeError("WebSocket closed by server")
except Exception as e:
print(f"Disconnected: {e}. Reconnecting in {backoff}s...")
await asyncio.sleep(backoff)
backoff = min(max_backoff, backoff * 2)
if __name__ == "__main__":
try:
asyncio.run(stream_btc_prices())
except KeyboardInterrupt:
print("Stopped")
Notes:
- The stream emits a message per trade; you always see the latest traded price.
- Decimal avoids floating-point rounding issues common in trading systems.
Numbered Steps (from zero to live)
- Choose a market
- Use a liquid market: BTC/USDT is widely available and usually high-liquidity.
- Pick endpoints
- WebSocket stream of trades for low-latency ticks.
- REST endpoint for fallback and health checks.
- Structure your event loop
- One task for the socket reader.
- Optional task for REST heartbeat or metrics.
- Use asyncio.run() to manage lifecycle.
- Parse incoming messages
- Extract price and event timestamps.
- Normalize to Decimal for precision.
- Resilience
- Exponential backoff on disconnects.
- Heartbeat/ping via ws_connect(heartbeat=...).
- REST fallback (polling)
- Poll when the stream is down or to verify last price.
- Integrate with your strategy
- Push prices into an asyncio.Queue for downstream consumers (signal generation, risk checks, execution).
REST Fallback (polling)
Use this to fetch the latest price when WebSocket is unavailable or for periodic verification.
import asyncio
from decimal import Decimal
import aiohttp
REST_URL = "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT"
async def fetch_btc_price_rest(session: aiohttp.ClientSession) -> Decimal:
async with session.get(REST_URL, timeout=aiohttp.ClientTimeout(total=5)) as resp:
resp.raise_for_status()
data = await resp.json()
return Decimal(data["price"])
async def main_rest_example():
async with aiohttp.ClientSession() as session:
price = await fetch_btc_price_rest(session)
print(f"REST BTCUSDT = {price}")
if __name__ == "__main__":
asyncio.run(main_rest_example())
Tip: Throttle polling (e.g., once per 1–5 seconds) to respect rate limits.
Combining Stream + Queue for Strategies
Push prices to a queue to decouple ingestion from strategy logic.
import asyncio
import json
from decimal import Decimal
import aiohttp
WS_URL = "wss://stream.binance.com:9443/ws/btcusdt@trade"
async def price_producer(q: asyncio.Queue):
async with aiohttp.ClientSession() as session:
async with session.ws_connect(WS_URL, heartbeat=30, ssl=True) as ws:
async for msg in ws:
data = json.loads(msg.data)
price = Decimal(data["p"])
await q.put(price)
async def strategy_consumer(q: asyncio.Queue):
while True:
price = await q.get()
# Example: simple momentum trigger placeholder
# Replace with your strategy logic
print(f"Strategy received price: {price}")
q.task_done()
async def main():
q = asyncio.Queue(maxsize=1000)
producer = asyncio.create_task(price_producer(q))
consumer = asyncio.create_task(strategy_consumer(q))
await asyncio.gather(producer, consumer)
if __name__ == "__main__":
asyncio.run(main())
Pitfalls and How to Avoid Them
- Pair confusion: BTCUSDT (USDT) is not BTCUSD (fiat USD). Pick the correct symbol for your PnL and hedging.
- Floats vs Decimal: Use Decimal for prices/quantities to avoid rounding drift.
- Reconnect storms: Implement capped exponential backoff (e.g., max 30s) and jitter if you scale horizontally.
- Time handling: Exchange timestamps differ from local time; don’t assume clock sync. Use event time for sequencing and NTP-sync your host.
- Rate limits: REST endpoints have limits; cache and throttle. WebSocket may kick you for excessive reconnects.
- Regional availability: Endpoints can differ by region or compliance. Validate the exact host and symbol names.
- Data gaps: During disconnects you miss trades. For trading-critical logic, consider redundant feeds and reconciliation.
Performance Notes
- Single dependency: aiohttp handles both REST and WebSocket to keep footprint small.
- Event loop: Keep handlers lightweight; offload heavy math to worker tasks or processes.
- JSON parsing: Avoid unnecessary copies; parse once and extract only needed fields.
- Backpressure: Use asyncio.Queue with bounded size to prevent memory bloat.
- Logging: Rate-limit logs in hot paths; defer formatting or use structured logs.
- Batching: For analytics, batch prices or sample at intervals to reduce CPU load.
- Multiplexing: For multiple pairs/exchanges, run one task per stream; use gather and cancellation on shutdown.
Tiny FAQ
Do I need an API key?
- Not for public ticker/trade streams shown here.
How “real-time” is this?
- Sub-second trade updates in practice; network and exchange load affect latency.
Can I get USD instead of USDT?
- Subscribe to a USD-quoted pair if the venue lists it; otherwise convert using a USD index.
What if the WebSocket drops?
- The example auto-reconnects with backoff. Use the REST fallback to sanity-check the last price.
How do I scale to multiple exchanges?
- Spin one async task per feed and normalize messages to a common schema before strategy logic.