DEV Community

Market Masters
Market Masters

Posted on

Build a Real-Time Crypto Trading Bot with Python, React, and WebSockets

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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)