Build a Real-Time Crypto Trading Bot with Python, React, and WebSockets
Real-time price data used to require expensive Bloomberg terminals. Now you can stream Binance order book updates for free and run a trading bot that reacts in under 200ms.
This tutorial walks through a complete stack: Python backend for price ingestion and signal generation, React frontend for portfolio tracking, and a simple execution loop you can extend.
Tech choices and why
- Python + asyncio for the trading engine (native websockets, easy to add indicators)
- React + Vite for the dashboard (fast refresh, clean component model)
- Binance public WebSocket streams (no API key needed for market data)
- REST calls only when placing orders (you will wire your own exchange client)
1. Python backend: connect to real-time prices
Create a file price_stream.py. We maintain a local order book snapshot and emit mid-price every tick.
import asyncio
import json
import websockets
from collections import deque
from datetime import datetime
SYMBOL = "btcusdt"
STREAM = f"wss://stream.binance.com:9443/ws/{SYMBOL}@depth5@100ms"
class PriceFeed:
def __init__(self):
self.bids = {}
self.asks = {}
self.callbacks = []
async def start(self):
async with websockets.connect(STREAM) as ws:
async for msg in ws:
data = json.loads(msg)
self._update_book(data)
mid = self._mid_price()
if mid:
for cb in self.callbacks:
await cb(mid, datetime.utcnow())
def _update_book(self, data):
for bid in data.get("b", []):
price, qty = float(bid[0]), float(bid[1])
self.bids[price] = qty if qty > 0 else None
for ask in data.get("a", []):
price, qty = float(ask[0]), float(ask[1])
self.asks[price] = qty if qty > 0 else None
def _mid_price(self):
best_bid = max(self.bids.keys()) if self.bids else None
best_ask = min(self.asks.keys()) if self.asks else None
if best_bid and best_ask:
return (best_bid + best_ask) / 2
return None
feed = PriceFeed()
Run it: python price_stream.py. You should see no errors and steady connection.
2. Add a simple technical signal
Attach an RSI-style momentum check. We keep a 14-period rolling window of mids.
class SignalEngine:
def __init__(self, period=14):
self.period = period
self.prices = deque(maxlen=period + 1)
self.last_signal = 0
async def on_price(self, price, ts):
self.prices.append(price)
if len(self.prices) < self.period:
return
rsi = self._rsi()
if rsi < 30 and self.last_signal != -1:
print(f"{ts} OVERSOLD signal at {price:.2f}")
self.last_signal = -1
elif rsi > 70 and self.last_signal != 1:
print(f"{ts} OVERBOUGHT signal at {price:.2f}")
self.last_signal = 1
def _rsi(self):
deltas = [self.prices[i+1] - self.prices[i] for i in range(len(self.prices)-1)]
gains = [d for d in deltas if d > 0]
losses = [-d for d in deltas if d < 0]
avg_gain = sum(gains) / len(gains) if gains else 0
avg_loss = sum(losses) / len(losses) if losses else 0.0001
rs = avg_gain / avg_loss
return 100 - (100 / (1 + rs))
engine = SignalEngine()
feed.callbacks.append(engine.on_price)
Drop this engine into the feed callbacks and you now have live signals in the console.
3. React frontend: portfolio tracker
Scaffold a Vite app:
npm create vite@latest frontend -- --template react-ts
cd frontend && npm install
Create src/components/Portfolio.tsx:
import { useEffect, useState } from 'react'
interface Position {
symbol: string
qty: number
avgPrice: number
}
export function Portfolio() {
const [positions, setPositions] = useState<Position[]>([
{ symbol: 'BTCUSDT', qty: 0.25, avgPrice: 67200 }
])
const [livePrice, setLivePrice] = useState<number | null>(null)
useEffect(() => {
const ws = new WebSocket('wss://stream.binance.com:9443/ws/btcusdt@trade')
ws.onmessage = (event) => {
const t = JSON.parse(event.data)
setLivePrice(parseFloat(t.p))
}
return () => ws.close()
}, [])
const totalValue = positions.reduce((sum, p) => {
const price = livePrice ?? p.avgPrice
return sum + p.qty * price
}, 0)
return (
<div className="p-6 font-mono">
<h2 className="text-xl mb-4">Live Portfolio</h2>
<div className="text-3xl mb-6">${totalValue.toFixed(2)}</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b"><th>Symbol</th><th>Qty</th><th>Avg</th><th>Live</th><th>PnL</th></tr>
</thead>
<tbody>
{positions.map(p => {
const live = livePrice ?? p.avgPrice
const pnl = (live - p.avgPrice) * p.qty
return (
<tr key={p.symbol} className="border-b">
<td>{p.symbol}</td>
<td>{p.qty}</td>
<td>${p.avgPrice}</td>
<td>${live.toFixed(2)}</td>
<td className={pnl >= 0 ? 'text-green-400' : 'text-red-400'}>
{pnl.toFixed(2)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
Mount it in App.tsx. You now have a live updating dashboard that tracks unrealized PnL.
4. Wiring execution
The bot is intentionally split. When SignalEngine emits a signal, call your exchange client:
async def execute_order(side: str, symbol: str, qty: float):
# replace with your signed REST call or ccxt
print(f"EXEC {side} {qty} {symbol}")
In production you would:
- Add API key signing (HMAC)
- Implement position sizing from account equity
- Add circuit breakers (max daily loss, max open orders)
Next steps and production notes
- Run the Python process behind systemd or supervisor
- Add Redis to persist the order book across restarts
- Serve the React build behind the same domain as your API
- Store signals in TimescaleDB for later backtesting
The full source for this tutorial lives in the Market Masters public examples repo (linked in bio). Replace the console prints with real execution, point the frontend at your own REST endpoints, and you have a working prototype.
If you want production-grade market data, order flow, and AI-generated setups without managing every websocket yourself, sign up for the Market Masters developer API at marketmasters.ai. The free tier includes 5 real-time alerts and full REST access to 2,500+ instruments.
Try it, break it, improve it. Then tell me what you added in the comments.
Top comments (0)