← Back to Blog
TUTORIALS

LMEX WebSocket API in Python: Real-Time Order Book and Trade Streaming

June 13, 2026 · 8 min read · LMEX.AI

REST APIs work fine for most trading bot operations. WebSocket is what you need when fine-grained order book or trade data matters in real time. This article walks through what WebSocket is, when to use it on LMEX, and how to build a reliable Python client that survives the inevitable disconnections.


When you actually need WebSocket


For most retail strategies, REST is sufficient. You poll the order book every few seconds, place orders when conditions trigger, and check positions periodically. Total latency: 100-300ms per cycle. Fine for trend following, mean reversion, DCA, grid trading.


WebSocket becomes necessary for:

  • **Market making**: quote updates need to react to order book changes in 10-50ms
  • **Order book imbalance strategies**: detecting brief pressure shifts before they show up in price
  • **High-frequency arbitrage**: spread capture across venues where 100ms matters
  • **Real-time risk management**: position tracking for strategies with many concurrent orders

  • If your strategy doesn't fit one of those categories, WebSocket adds complexity without meaningful benefit. Stick with REST.


    LMEX WebSocket basics


    LMEX exposes WebSocket endpoints at `wss://ws.lmex.io` (production) and `wss://testnet.lmex.io/ws` (testnet). The protocol is JSON-based: send subscription messages, receive streaming updates.


    Available channels:

  • `orderBookL2`: full order book updates (snapshot + deltas)
  • `orderBookApi`: aggregated order book (lighter weight)
  • `trade`: recent trades as they happen
  • `kline`: candles in real time
  • `marketSummary`: ticker data
  • `userInfo` (authenticated): your orders, fills, position updates

  • For most use cases, you want `orderBookL2` + `trade` (for market data) and `userInfo` (for your own state).


    A working Python implementation


    The simplest path is using `ccxt.pro` (the async version of CCXT) which handles the protocol details. The bare-bones version:


    import asyncio
    import ccxt.pro as ccxtpro
    
    async def main():
        exchange = ccxtpro.lmex({
            'apiKey': '...',
            'secret': '...',
        })
        
        symbol = 'BTC-PERP'
        
        while True:
            try:
                orderbook = await exchange.watch_order_book(symbol)
                bid = orderbook['bids'][0]
                ask = orderbook['asks'][0]
                spread = (ask[0] - bid[0]) / bid[0] * 10000  # in basis points
                print(f"Bid: {bid[0]} ({bid[1]}) | Ask: {ask[0]} ({ask[1]}) | Spread: {spread:.2f} bps")
            except Exception as e:
                print(f"Error: {e}")
                await asyncio.sleep(1)
        
        await exchange.close()
    
    asyncio.run(main())

    This prints the best bid/ask spread every time the order book updates — typically 5-20 times per second on active markets.


    For a more useful real implementation, subscribe to multiple streams simultaneously:


    import asyncio
    import ccxt.pro as ccxtpro
    
    class MarketDataClient:
        def __init__(self, api_key, secret, symbol='BTC-PERP'):
            self.exchange = ccxtpro.lmex({
                'apiKey': api_key,
                'secret': secret,
            })
            self.symbol = symbol
            self.latest_book = None
            self.latest_trade = None
            self.recent_trades = []
        
        async def watch_orderbook(self):
            while True:
                try:
                    ob = await self.exchange.watch_order_book(self.symbol)
                    self.latest_book = ob
                except Exception as e:
                    print(f"Orderbook error: {e}")
                    await asyncio.sleep(1)
        
        async def watch_trades(self):
            while True:
                try:
                    trades = await self.exchange.watch_trades(self.symbol)
                    for trade in trades:
                        self.latest_trade = trade
                        self.recent_trades.append(trade)
                        if len(self.recent_trades) > 1000:
                            self.recent_trades = self.recent_trades[-500:]
                except Exception as e:
                    print(f"Trade error: {e}")
                    await asyncio.sleep(1)
        
        async def watch_my_orders(self):
            while True:
                try:
                    orders = await self.exchange.watch_orders(self.symbol)
                    for order in orders:
                        print(f"Order update: {order['id']} {order['status']}")
                except Exception as e:
                    print(f"Order error: {e}")
                    await asyncio.sleep(1)
        
        async def run(self):
            await asyncio.gather(
                self.watch_orderbook(),
                self.watch_trades(),
                self.watch_my_orders(),
            )
    
    async def main():
        client = MarketDataClient('...', '...')
        await client.run()
    
    asyncio.run(main())

    Three concurrent subscriptions, all running until the process stops. `latest_book` and `latest_trade` are always current; your strategy logic reads from these instead of polling REST.


    Handling reconnects (the hard part)


    The example above looks robust because of the try/except blocks. In practice, those catch transient errors but not all of them. A few failure modes to plan for:


    **Silent disconnections.** The TCP connection is still open but the server stopped sending data. Detection: track the timestamp of the last message. If more than 30 seconds elapse without an update on an active channel, treat it as disconnected and force a reconnect.


    import time
    
    class MarketDataClient:
        def __init__(self, ...):
            self.last_book_update = time.time()
        
        async def watch_orderbook(self):
            while True:
                try:
                    ob = await asyncio.wait_for(
                        self.exchange.watch_order_book(self.symbol),
                        timeout=30
                    )
                    self.latest_book = ob
                    self.last_book_update = time.time()
                except asyncio.TimeoutError:
                    print("Orderbook stale — reconnecting")
                    await self.exchange.close()
                    self.exchange = ccxtpro.lmex({'apiKey': self.api_key, 'secret': self.secret})
                except Exception as e:
                    print(f"Orderbook error: {e}")
                    await asyncio.sleep(1)

    **Authentication expiry.** Auth tokens have lifetimes. After ~24 hours, you may need to re-authenticate. ccxt.pro handles this automatically for most exchanges, but verify by leaving the bot running for 48 hours and checking it still receives data.


    **Order book gaps.** With L2 streaming, you receive a snapshot followed by incremental deltas. If a delta gets lost, your local book diverges from the exchange's. Solution: periodically refresh by re-subscribing (effectively forcing a new snapshot). Once a minute is reasonable for non-HFT use.


    **Exchange downtime.** LMEX occasionally has maintenance windows. Your bot should detect this (no order book updates, or specific error codes) and pause new orders until service resumes.


    Going from prototype to production


    The skeleton above is the foundation. Production code needs:


    **Persistent state.** Save the latest order book and recent trades to disk periodically. On restart, restore state so the bot doesn't start completely blind.


    **Health monitoring.** Track latency between exchange events and local processing. If latency creeps up past expected thresholds, alert.


    **Proper logging.** Every order placed, fill received, error encountered. Structured JSON logging makes post-mortem analysis tractable.


    **Graceful shutdown.** Cancel open orders on shutdown, save state, close connections cleanly. The default behaviour on Ctrl+C leaves orders open and disconnects abruptly.


    These together add another 500-1000 lines of code. The 50-line skeleton is for proving the concept. The 1500-line production version is what runs real money.


    Performance comparison


    For a market-making strategy on BTC-PERP:

  • REST polling (1-second interval): ~5 quote updates per second, 500ms average reaction time
  • WebSocket: 10-50 quote updates per second, 30ms average reaction time

  • The WebSocket version reacts ~16x faster. For market making, that translates to better fills, less adverse selection, and meaningfully higher returns. For trend following, it translates to almost nothing useful.


    Frequently Asked Questions


    Q: Can I run WebSocket from a residential connection?

    Yes, but expect occasional reconnects from your ISP's NAT behaviour. Production WebSocket strategies generally run from VPS instances with stable connections.


    Q: Does LMEX charge for WebSocket usage?

    No additional charges. The same API key works for both REST and WebSocket. Rate limits are generally generous — you'd have to deliberately abuse the connection to hit them.


    Q: Is ccxt.pro a free library?

    It's a paid commercial library, but there's a free public version of CCXT (without `.pro`) that handles REST only. For WebSocket without ccxt.pro, you can use libraries like `websockets` or `websocket-client` directly with LMEX's WebSocket protocol.


    Q: Should I use one connection for multiple symbols or separate?

    One connection with multiple subscriptions is more efficient than parallel connections. ccxt.pro handles this transparently — just call `watch_*` on different symbols and they share the underlying connection.


    Related Articles


    → WebSocket vs REST for Trading Bots: When Each One Wins
    → Order Book Imbalance Strategies on LMEX: A Python Implementation Guide
    → Building a Crypto Perpetuals Trading Bot in Python: Complete Guide
    ← All ArticlesBuild a Bot →