Joe Archondis

July 9, 2026 · 10 min read

Fintech & Algo Trading

Crypto Trading Bot Architecture: From Idea to Production

Crypto Trading Bot Architecture: From Idea to Production

Most crypto bot tutorials end when the code runs. Three loops of Python. Buy on RSI 30, sell on RSI 70. The author calls it a trading bot and publishes the GitHub repo.

What they don't cover: what happens when the WebSocket disconnects mid-trade at 2 AM, your bot silently enters a broken state, and you wake up to an open position moving against you with no monitoring in place.

I built trading infrastructure at Enigma Securities. Matching engine, OMS, pre-trade risk, market data stack. Sub-15ms tick-to-order across five venues. Later applied those same patterns to crypto-native systems. This is what a production crypto trading bot actually looks like.

The Five Layers

Every production crypto trading bot has five distinct layers. Most tutorials cover one, maybe two.

A weak risk layer doesn't hurt until it really hurts. Build all five before deploying capital.

Market Data: WebSockets Are Not Reliable Connections

The first mistake is treating a WebSocket connection like a stable TCP socket. It isn't.

Exchanges push order book updates over WebSocket. Binance, Bybit, OKX — each has a documented API with delta update streams. Connect, subscribe, receive updates, maintain your local book state. The tutorial ends here. Production doesn't.

Exchange connections drop daily. CDN hiccups, maintenance windows, network jitter. Your reconnect logic needs to do four things:

Running ccxt's watch_order_book on Binance handles reconnects automatically. Fine for strategies with holding periods above a minute. For anything tighter, a custom feed handler gives more control over reconnect behavior and timestamp accuracy.

REST is your fallback, not your enemy. WebSocket feeds primary market data. REST lets you pull a full snapshot when rebuilding state after a disconnect. Know the rate limits on both before you hit them at a bad moment.

class FeedHandler:
    def __init__(self, ws_url: str, rest_client, symbol: str):
        self.ws_url = ws_url
        self.rest_client = rest_client
        self.symbol = symbol
        self.book: dict = {"bids": {}, "asks": {}}
        self._backoff = 1.0

    async def run(self):
        while True:
            try:
                await self._snapshot()  # rebuild from REST on every connect
                async with websockets.connect(self.ws_url) as ws:
                    self._backoff = 1.0
                    await self._subscribe(ws)
                    await self._process_messages(ws)
            except (websockets.ConnectionClosed, OSError):
                await asyncio.sleep(self._backoff)
                self._backoff = min(self._backoff * 2, 60.0)

Signal Generation: Separate Research from Production

This is where most strategies die. It's a code organization problem, not a math problem.

Research code is optimized for experimentation. pandas DataFrames, Jupyter notebooks, vectorized operations on historical OHLCV bars. Fast to iterate, easy to share. The problem: production signal code running tick-by-tick computes indicators differently from research code running over 1-minute bars. Not slightly differently. Materially differently.

I saw this at Enigma. A strategy backtest on 1-minute bars showed 1.8 Sharpe. Live on tick data: 0.4 Sharpe. Same logic, different computation path. The signal was correct. The implementation wasn't.

The fix: write the production signal class first. It takes a tick or a book update, maintains running indicator state, emits a signal. Then write a vectorized wrapper that runs the same class over historical data, update by update. If the production class can't reproduce the research results, don't deploy.

Crypto-specific pitfall: funding rates and liquidation cascades distort intraday behavior in ways standard OHLCV backtests don't capture. If your strategy trades around high-volatility windows, backtest on tick data.

Order Management: Build the State Machine

The OMS is the center of the system. It owns order state: pending, submitted, acknowledged, partial, filled, cancelled, rejected. It is the single source of truth for open orders and net position. No other component tracks position independently.

Two components maintaining their own position state is how you end up with a real exposure of +2 BTC and a risk dashboard showing flat. That's a position tracking bug disguised as a data discrepancy.

Three crypto-specific OMS concerns:

Fill reconciliation. Exchange fill messages arrive out of sequence over WebSocket. A fill confirmation can arrive before the order acknowledgment. Handle it: queue unexpected fills, reconcile when the ack arrives, never drop a fill message.

Fee modeling. Maker vs. taker fees are not small. On Binance Futures, taker is 0.04%, maker is 0.02%. A strategy that looks profitable on OHLCV data can go negative at production fee rates once you model partial fills and slippage. Build fee tracking into the OMS from day one.

Client order IDs. Generate your own before submitting. Exchange-assigned IDs arrive async. Using client IDs throughout means your system can reason about in-flight orders before the exchange response comes back.

from enum import Enum, auto
from dataclasses import dataclass, field

class OrderState(Enum):
    PENDING    = auto()
    SUBMITTED  = auto()
    ACKNOWLEDGED = auto()
    PARTIAL    = auto()
    FILLED     = auto()
    CANCELLED  = auto()
    REJECTED   = auto()

@dataclass
class Order:
    client_id: str
    exchange_id: str | None
    symbol: str
    side: str           # "buy" | "sell"
    qty: float
    price: float
    state: OrderState   = OrderState.PENDING
    filled_qty: float   = 0.0
    avg_fill_price: float = 0.0
    fees_paid: float    = 0.0
    fills: list         = field(default_factory=list)

Risk Management: The Part That Saves You

Most tutorials skip this entirely. It's the most important part of the system.

Four controls. All required. No exceptions.

Position limit. Maximum net exposure per symbol. Checked on every order before submission. Hard reject, not a warning. Running +5 BTC when your limit is +1 BTC turns a strategy bug into a capital event.

Drawdown cap. Track cumulative P&L from session start. If total loss exceeds a threshold, halt trading and alert immediately. Start conservative at 2% of deployed capital. A system that stops at 2% down is recoverable. A system running unchecked through a 15% drawdown while you sleep is not.

Fat finger check. Is the submitted price more than X% from current mid? Reject it. Crypto markets move fast. A signal computed on a stale book snapshot can generate an order at a price that was swept 300ms ago.

Kill switch. One call: cancel all open orders, block new submissions, alert immediately. Build it on day one. Test it with real orders in flight against a testnet before capital goes live. A kill switch you've only tested in staging isn't a real kill switch.

The kill switch triggers automatically on three conditions: drawdown cap hit, WebSocket feed stale for more than 5 seconds, or OMS state inconsistency detected. Build the automation. Don't rely on watching a dashboard.

Infrastructure: Running in Production

A solid strategy and clean code aren't enough. Bad infrastructure will fail the bot in ways that have nothing to do with signal quality.

Deployment. Docker container on Google Cloud Run or a dedicated VPS. Cloud Run handles auto-scaling and managed process restarts. For crypto strategies with holding periods above 10 seconds, WebSocket latency from GCP's eu-west region to Binance or Bybit runs 30-80ms. Fine for most strategies. For sub-10ms targets, you need co-location near the exchange's matching engine.

Monitoring. Three metrics from day one:

Prometheus and Grafana is the standard stack. Simpler path: structured JSON logs to Cloud Logging, dashboarded in Grafana. Either way, you need live visibility. Running blind on real capital is not an acceptable state.

Alerting. Telegram for immediate notifications. You want to know within 30 seconds when the drawdown cap is hit, a WebSocket fails to reconnect, the OMS logs a state error, or the kill switch triggers. Email is too slow for any of those events.

Watchdog. Cloud Run restart policies handle process crashes. They don't catch a bot that's running but internally stuck — a deadlocked coroutine, a WebSocket that stopped sending without closing the socket. Build a health endpoint that reports feed latency, OMS state, and last-processed timestamp. The watchdog checks every 30 seconds. A failed health check triggers a restart.

Logging. Every order, every fill, every risk check result, every WebSocket reconnect. Structured JSON. You need to reconstruct exactly what the bot did and why, especially after an unexpected position or a P&L event you can't explain.

Scaling to Multiple Strategies or Exchanges

A single strategy on one exchange runs cleanly as a monolith. Feed handler, signal, OMS, and risk in one process. Simple to deploy, simple to debug.

Multiple strategies or multiple exchanges, and you need IPC between components. ZeroMQ pub/sub is clean here: the feed handler publishes to a market data topic, each strategy subscribes and publishes signals, the OMS subscribes to signals and publishes fills. Each component is a separate process. Separately deployable, separately restartable.

For HFT-level crypto (hold times under 100ms), you need exchange co-location, direct binary feed parsing instead of library-normalized feeds, and C++ on the latency-critical path. Most crypto strategies at the retail level operate at 100ms to seconds. That range is entirely addressable with Python asyncio on cloud infrastructure.

The bottleneck is almost never language speed. It's risk controls, reconnect logic, and OMS correctness.

Frequently Asked Questions

What is the architecture of a crypto trading bot?

A production crypto trading bot has five layers: market data (WebSocket feed handler and order book state), signal generation (strategy logic and indicators), order management (OMS state machine tracking order lifecycle and position), risk management (position limits, drawdown cap, fat finger check, kill switch), and infrastructure (deployment, monitoring, alerting, and auto-recovery). Most tutorials only cover signal generation. The risk layer and OMS are what separate a tutorial project from a system you can run with real money.

How do I handle WebSocket disconnections in a trading bot?

Four things: detect disconnects with a heartbeat timeout rather than waiting for a socket close event; reconnect with exponential backoff rather than a hard retry loop; request a full order book snapshot from the REST API after reconnecting to rebuild state cleanly; track exchange sequence numbers to detect gaps even when the socket stays open. Exchange connections drop daily in production. Build reconnect logic before you deploy capital.

Should I use CCXT for a production crypto trading bot?

Depends on your latency target. CCXT's watch_order_book handles reconnects automatically and works well for strategies with holding periods above one minute. For tighter latency requirements, a custom feed handler gives more control over reconnect behavior and timestamp accuracy. For systematic strategies in the seconds-to-minutes range, CCXT is a reasonable choice. For sub-second work, go direct.

How fast does a crypto trading bot need to be?

Depends entirely on your strategy's holding period. For strategies holding above one minute, WebSocket latency from a cloud VM (30-80ms to major exchanges from GCP eu-west) is irrelevant. For seconds-level strategies, cloud infrastructure with a well-structured Python asyncio loop is fast enough. For sub-100ms work, co-location near the exchange's matching engine is required. Genuine sub-10ms crypto HFT needs exchange-specific infrastructure and direct binary feed parsing, not REST or library-normalized feeds.

How do I monitor a crypto trading bot in production?

Three metrics matter most: fill latency (time from order submission to acknowledgment), P&L per session and per strategy, and feed health (seconds since last WebSocket message per connection). Prometheus and Grafana is the standard stack. For simpler setups, structured JSON logs to Cloud Logging dashboarded in Grafana works well. For alerting, a Telegram bot is the right choice — you need to know within 30 seconds when the drawdown cap is hit, a WebSocket fails to reconnect, or the kill switch triggers.

Working on something similar?

I build AI agents and low-latency systems. If you're trying to solve a version of this, let's talk.

Get in touch

Author: Joe Archondis — AI systems engineer and HFT infrastructure builder.

Last updated: 2026-07-09