DEV Community

Vinicius Chelles
Vinicius Chelles

Posted on

Event-Driven Architecture for Crypto Trading Bots: A Practical Guide

Event-Driven Architecture for Crypto Trading Bots: A Practical Guide

Building a trading bot that reacts to market movements in real-time requires more than just a loop that checks prices every second. You need an event-driven architecture that can handle thousands of price updates, execute orders, and manage risk β€” all without missing a beat.

Here's how I built Lucromatic's event-driven core, and why it matters for your own trading bot.

The Problem with Polling

Most beginners start with something like this:

// Don't do this 😱
async function trade() {
  while (true) {
    const price = await getPrice('BTC/USDT');
    if (price < supportLevel) {
      await buy();
    }
    await sleep(1000);
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach has three fatal flaws:

  1. Latency β€” By the time you check, the opportunity is gone
  2. Race conditions β€” Multiple checks can trigger multiple orders
  3. API limits β€” Polling every second hits rate limits fast

The solution? Event-driven architecture. Instead of asking "what's the price?", you listen for "the price changed."

The Event-Driven Core

Here's the architecture I use:

const { EventEmitter } = require('events');
class TradingEngine extends EventEmitter {
  constructor() {
    super();
    this.positions = new Map();
    this.orders = new Map();
    this.symbols = new Set();
  }

  // Price update from websocket
  async onPriceUpdate(symbol, price, volume) {
    this.emit('price', { symbol, price, volume, timestamp: Date.now() });

    // Check pending orders
    for (const [orderId, order] of this.orders) {
      if (this.shouldExecute(order, price)) {
        await this.executeOrder(order, price);
      }
    }
  }

  // Strategy signals
  onSignal(strategy, signal) {
    this.emit('signal', { strategy, ...signal });
  }

  // Risk checks before order execution
  async executeOrder(order, price) {
    if (!this.checkRisk(order, price)) {
      this.emit('risk_rejected', { order, price });
      return;
    }

    // Execute via exchange API
    const result = await this.executor.execute(order, price);
    this.emit('order_executed', result);
  }
}
Enter fullscreen mode Exit fullscreen mode

The key components:

  • Event emitters β€” Price updates, signals, orders all flow through events
  • Async handlers β€” Non-blocking, can handle multiple symbols
  • Risk module β€” Validates every order before execution

Real-Time Data with Websockets

The polling example above checks prices every second. With websockets, you get pushed updates instantly:

const Binance = require('binance-api-node').default;

class WSPriceFeed {
  constructor(tradingEngine) {
    this.client = Binance();
    this.engine = tradingEngine;
  }

  start(symbols) {
    symbols.forEach(symbol => {
      this.client.ws.trade(symbol, trade => {
        this.engine.onPriceUpdate(
          symbol,
          parseFloat(trade.p),
          parseFloat(trade.q)
        );
      });
    });
  }
}

// Usage
const feed = new WSPriceFeed(engine);
feed.start(['BTC/USDT', 'ETH/USDT', 'SOL/USDT']);
Enter fullscreen mode Exit fullscreen mode

This gives you real-time price updates as they happen β€” sub-second latency vs. 1+ second with polling.

Strategy as Event Listeners

Strategies are just event listeners:

class GridStrategy {
  constructor(engine, params) {
    this.engine = engine;
    this.gridLevels = params.levels || 10;
    this.gridSpacing = params.spacing || 0.5; // 0.5% between levels
  }

  start(symbol, basePrice) {
    // Create grid levels
    const levels = [];
    for (let i = 0; i < this.gridLevels; i++) {
      levels.push({
        price: basePrice * (1 - (i + 1) * this.gridSpacing / 100),
        side: 'BUY',
        filled: false
      });
      levels.push({
        price: basePrice * (1 + (i + 1) * this.gridSpacing / 100),
        side: 'SELL',
        filled: false
      });
    }

    // Listen for price events
    this.engine.on('price', async ({ symbol: s, price }) => {
      if (s !== symbol) return;

      for (const level of levels) {
        if (!level.filled && price <= level.price && level.side === 'BUY') {
          await this.engine.executeOrder({
            symbol,
            side: 'BUY',
            quantity: this.calculateQty(price)
          });
          level.filled = true;
        }
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Each strategy subscribes to price events and makes decisions independently. You can run multiple strategies on the same engine.

Handling Edge Cases

Trading bots need to handle failures gracefully:

class CircuitBreaker {
  constructor(maxFailures = 5, resetMs = 60000) {
    this.failures = 0;
    this.maxFailures = maxFailures;
    this.resetMs = resetMs;
    this.lastFailure = 0;
  }

  async execute(fn) {
    if (this.failures >= this.maxFailures) {
      const elapsed = Date.now() - this.lastFailure;
      if (elapsed < this.resetMs) {
        throw new Error('Circuit open');
      }
      this.failures = 0;
    }

    try {
      return await fn();
    } catch (err) {
      this.failures++;
      this.lastFailure = Date.now();
      throw err;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And position tracking:

class PositionManager {
  constructor(engine) {
    this.positions = new Map();
    this.engine = engine;
  }

  onOrderExecuted({ orderId, symbol, side, filled }) {
    const key = `${symbol}-${side}`;
    const pos = this.positions.get(key) || { symbol, side, qty: 0, avgPrice: 0 };

    const totalValue = pos.qty * pos.avgPrice + filled.qty * filled.price;
    pos.qty += filled.qty;
    pos.avgPrice = pos.qty > 0 ? totalValue / pos.qty : 0;

    this.positions.set(key, pos);
    this.engine.emit('position_update', pos);
  }

  getPosition(symbol) {
    return this.positions.get(symbol);
  }
}
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

The finalζžΆζž„ looks like this:

[WebSocket] ──> [Price Feed] ──> [Trading Engine] ──> [Strategies]
                    β”‚                 β”‚
                    β”‚                 v
                    └──────────────> [Risk Manager]
                                      β”‚
                                      v
                               [Exchange API]
Enter fullscreen mode Exit fullscreen mode

Each component is independent, testable, and handles its own domain. The engine orchestrates, but doesn't know the details of any single part.

Results

With this architecture, Lucromatic handles:

  • 50+ symbols concurrently without lagging
  • Sub-second order execution from signal to exchange
  • Built-in risk management on every order
  • Automatic reconnection on websocket disconnect

The key insight: trading is event-driven by nature. Your bot should be too.


I'm building Lucromatic, a self-hosted trading bot for Binance with 50+ indicators, grid trading, and futures support. Check the live demo to see it in action.

Top comments (0)