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);
}
}
This approach has three fatal flaws:
- Latency β By the time you check, the opportunity is gone
- Race conditions β Multiple checks can trigger multiple orders
- 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);
}
}
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']);
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;
}
}
});
}
}
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;
}
}
}
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);
}
}
Putting It All Together
The finalζΆζ looks like this:
[WebSocket] ββ> [Price Feed] ββ> [Trading Engine] ββ> [Strategies]
β β
β v
βββββββββββββββ> [Risk Manager]
β
v
[Exchange API]
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)