Technical guide

RabbitMQ Streams for Algorithmic Trading

Building scalable, resilient trading infrastructure with RabbitMQ Streams for market data distribution and order routing.

Engineering
By Flowistic
Difficulty: Advanced
Read time: 35 min
Published: August 14, 2026
Last modified: August 14, 2026

Introduction

Most trading systems fail not because of bad algorithms, but because of infrastructure brittleness. A single market data feed lag, a dropped order, or a network glitch can wipe out profits.

RabbitMQ Streams is a distributed message broker designed for exactly these problems: guaranteed delivery, persistent storage, replay capability, and horizontal scaling. Unlike traditional request-response systems, Streams enable:

  • Multiple consumers of the same market data without coupling
  • Replay from history - rewind and reprocess all trades for debugging
  • Backpressure handling - gracefully handle data spikes without dropping messages
  • Fault tolerance - automatic failover and recovery

This guide shows how to build production-grade trading infrastructure on RabbitMQ Streams.


Part 1: RabbitMQ Streams Fundamentals

RabbitMQ Streams are append-only logs (like Kafka), but with RabbitMQ’s simplicity. Every message is persisted, and consumers can replay from any point in time.

import pika
from pika.adapters.blocking_connection import BlockingConnection
import json
from datetime import datetime

class MarketDataPublisher:
    """Publish tick data to RabbitMQ Streams"""
    
    def __init__(self, rabbitmq_host: str = "localhost", 
                 stream_name: str = "market_data"):
        self.stream_name = stream_name
        
        # Connect with stream plugin
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(
            host=rabbitmq_host,
            credentials=credentials
        )
        
        self.connection = pika.BlockingConnection(parameters)
        self.channel = self.connection.channel()
        
        # Declare stream (idempotent - safe to call multiple times)
        self.channel.queue_declare(
            queue=stream_name,
            durable=True,
            arguments={
                'x-queue-type': 'stream',
                # Retention: keep messages for 7 days
                'x-max-age': '7d',
                # Max size: 10GB per stream
                'x-stream-max-length-bytes': 10737418240
            }
        )
    
    def publish_tick(self, tick_data: dict):
        """Publish a single tick to the stream"""
        
        message = json.dumps({
            'timestamp': datetime.utcnow().isoformat(),
            'ticker': tick_data['ticker'],
            'price': tick_data['price'],
            'volume': tick_data['volume'],
            'bid': tick_data.get('bid'),
            'ask': tick_data.get('ask'),
            'bid_size': tick_data.get('bid_size'),
            'ask_size': tick_data.get('ask_size'),
        })
        
        # Publish with confirmation
        self.channel.basic_publish(
            exchange='',
            routing_key=self.stream_name,
            body=message.encode(),
            properties=pika.BasicProperties(
                delivery_mode=2,  # Persistent delivery
                content_type='application/json'
            )
        )
    
    def publish_batch(self, ticks: list):
        """Publish multiple ticks efficiently"""
        
        for tick in ticks:
            self.publish_tick(tick)
        
        # Flush to ensure delivery
        self.connection.process_data_events()
    
    def close(self):
        """Close connection"""
        self.connection.close()

Key concept: Unlike queues where messages disappear after consumption, Streams persist all messages. Consumers independently track their position in the stream—new consumers can start from the beginning or the tail.


Part 2: Stream Consumers with Offset Management

A single market data stream can feed multiple trading strategies. Each strategy independently consumes messages from wherever it wants in the stream.

from typing import Callable, Dict
import threading

class StreamConsumer:
    """Subscribe to market data stream and process ticks"""
    
    def __init__(self, rabbitmq_host: str = "localhost",
                 stream_name: str = "market_data",
                 consumer_name: str = "strategy_1"):
        self.stream_name = stream_name
        self.consumer_name = consumer_name
        
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(host=rabbitmq_host)
        
        self.connection = pika.BlockingConnection(parameters)
        self.channel = self.connection.channel()
        
        # Declare stream (idempotent)
        self.channel.queue_declare(
            queue=stream_name,
            durable=True,
            arguments={'x-queue-type': 'stream'}
        )
    
    def consume_from_beginning(self, callback: Callable):
        """Subscribe from the start of the stream"""
        
        # Consumer name identifies this consumer for offset tracking
        self.channel.basic_qos(prefetch_count=100)
        
        self.channel.basic_consume(
            queue=self.stream_name,
            on_message_callback=self._make_callback(callback),
            consumer_tag=self.consumer_name,
            # Start from beginning
            arguments={
                'x-stream-offset': 'first'
            }
        )
        
        print(f"Consuming from {self.stream_name} (from beginning)")
        self.channel.start_consuming()
    
    def consume_from_latest(self, callback: Callable):
        """Subscribe from latest message only (new ticks going forward)"""
        
        self.channel.basic_qos(prefetch_count=100)
        
        self.channel.basic_consume(
            queue=self.stream_name,
            on_message_callback=self._make_callback(callback),
            consumer_tag=self.consumer_name,
            # Start from latest
            arguments={
                'x-stream-offset': 'last'
            }
        )
        
        print(f"Consuming from {self.stream_name} (from latest)")
        self.channel.start_consuming()
    
    def consume_from_timestamp(self, timestamp: str, callback: Callable):
        """Subscribe from specific timestamp (e.g., "2025-02-01T09:30:00")"""
        
        self.channel.basic_consume(
            queue=self.stream_name,
            on_message_callback=self._make_callback(callback),
            consumer_tag=self.consumer_name,
            arguments={
                'x-stream-offset': 'timestamp',
                'x-stream-offset-timestamp': timestamp
            }
        )
        
        print(f"Consuming from {self.stream_name} (from {timestamp})")
        self.channel.start_consuming()
    
    def _make_callback(self, user_callback: Callable):
        """Wrap user callback with message parsing and offset management"""
        
        def callback(ch, method, properties, body):
            try:
                tick = json.loads(body.decode())
                
                # Call user function
                user_callback(tick)
                
                # Acknowledge message (offset is saved automatically)
                ch.basic_ack(delivery_tag=method.delivery_tag)
                
            except Exception as e:
                print(f"Error processing message: {e}")
                # NACK to retry
                ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
        
        return callback
    
    def close(self):
        self.connection.close()

Why this matters: Two trading strategies consume the same market data stream independently. Strategy A processes all ticks from 9:30 AM. Strategy B starts at 2:00 PM. The stream keeps track of each consumer’s offset automatically.


Part 3: Event Sourcing - Complete Audit Trail

Every trade, order, and decision should be logged immutably. RabbitMQ Streams enables complete event sourcing: rebuild any historical state by replaying events.

from enum import Enum
from dataclasses import dataclass, asdict

class EventType(Enum):
    MARKET_DATA = "market_data"
    ORDER_CREATED = "order_created"
    ORDER_FILLED = "order_filled"
    ORDER_CANCELLED = "order_cancelled"
    TRADE_EXECUTED = "trade_executed"
    POSITION_UPDATED = "position_updated"
    ERROR = "error"

@dataclass
class TradingEvent:
    """Immutable event in the audit log"""
    event_type: EventType
    timestamp: str
    data: dict
    
    def to_json(self) -> str:
        return json.dumps(asdict(self))

class AuditLogPublisher:
    """Publish all trading events to audit stream"""
    
    def __init__(self, rabbitmq_host: str = "localhost"):
        self.stream_name = "trading_audit_log"
        
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(host=rabbitmq_host)
        
        self.connection = pika.BlockingConnection(parameters)
        self.channel = self.connection.channel()
        
        # Create audit stream with longer retention
        self.channel.queue_declare(
            queue=self.stream_name,
            durable=True,
            arguments={
                'x-queue-type': 'stream',
                # Keep audit logs for 1 year
                'x-max-age': '365d'
            }
        )
    
    def log_market_data(self, ticker: str, price: float, volume: int):
        """Log incoming market data"""
        event = TradingEvent(
            event_type=EventType.MARKET_DATA,
            timestamp=datetime.utcnow().isoformat(),
            data={'ticker': ticker, 'price': price, 'volume': volume}
        )
        self._publish_event(event)
    
    def log_order_created(self, order_id: str, ticker: str, 
                         quantity: int, price: float, side: str):
        """Log order creation"""
        event = TradingEvent(
            event_type=EventType.ORDER_CREATED,
            timestamp=datetime.utcnow().isoformat(),
            data={
                'order_id': order_id,
                'ticker': ticker,
                'quantity': quantity,
                'price': price,
                'side': side
            }
        )
        self._publish_event(event)
    
    def log_order_filled(self, order_id: str, filled_quantity: int,
                        filled_price: float):
        """Log order execution"""
        event = TradingEvent(
            event_type=EventType.ORDER_FILLED,
            timestamp=datetime.utcnow().isoformat(),
            data={
                'order_id': order_id,
                'filled_quantity': filled_quantity,
                'filled_price': filled_price
            }
        )
        self._publish_event(event)
    
    def log_error(self, error_message: str, context: dict = None):
        """Log errors with full context"""
        event = TradingEvent(
            event_type=EventType.ERROR,
            timestamp=datetime.utcnow().isoformat(),
            data={'message': error_message, 'context': context or {}}
        )
        self._publish_event(event)
    
    def _publish_event(self, event: TradingEvent):
        """Publish event to audit stream"""
        self.channel.basic_publish(
            exchange='',
            routing_key=self.stream_name,
            body=event.to_json().encode(),
            properties=pika.BasicProperties(delivery_mode=2)
        )

class AuditLogReplayer:
    """Replay audit logs to reconstruct historical state"""
    
    def __init__(self, rabbitmq_host: str = "localhost"):
        self.stream_name = "trading_audit_log"
        
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(host=rabbitmq_host)
        
        self.connection = pika.BlockingConnection(parameters)
        self.channel = self.connection.channel()
    
    def replay_all_events(self, start_timestamp: str = None):
        """Replay entire audit log from beginning or timestamp"""
        
        events = []
        
        def collect_events(tick):
            events.append(tick)
        
        # Consumer with callback
        if start_timestamp:
            self.channel.basic_consume(
                queue=self.stream_name,
                on_message_callback=self._wrap_callback(collect_events),
                arguments={'x-stream-offset': 'timestamp',
                          'x-stream-offset-timestamp': start_timestamp}
            )
        else:
            self.channel.basic_consume(
                queue=self.stream_name,
                on_message_callback=self._wrap_callback(collect_events),
                arguments={'x-stream-offset': 'first'}
            )
        
        self.channel.start_consuming()
        return events
    
    def get_position_at_timestamp(self, timestamp: str) -> dict:
        """Reconstruct trading position at any point in time"""
        
        position = {'long': 0, 'short': 0, 'realized_pnl': 0}
        
        def update_position(event_dict):
            event_type = event_dict.get('event_type')
            data = event_dict.get('data', {})
            
            if event_type == EventType.ORDER_FILLED.value:
                quantity = data.get('filled_quantity', 0)
                side = data.get('side', 'BUY')
                
                if side == 'BUY':
                    position['long'] += quantity
                else:
                    position['short'] += quantity
        
        events = self.replay_all_events(start_timestamp=timestamp)
        
        for event in events:
            event_dict = json.loads(event) if isinstance(event, str) else event
            update_position(event_dict)
        
        return position
    
    def _wrap_callback(self, user_callback):
        def callback(ch, method, properties, body):
            event_dict = json.loads(body.decode())
            user_callback(event_dict)
            ch.basic_ack(delivery_tag=method.delivery_tag)
        return callback

Power of event sourcing: At 3:47 PM, you discover a bug. Replay the audit log from 9:30 AM. See every trade that was executed based on the buggy logic. Calculate exact P&L impact. Perfect for compliance and debugging.


Part 4: Handling Backpressure & Flow Control

Market data can spike (earnings announcements, flash crashes). Naive systems drop messages. RabbitMQ Streams handle this with prefetch limits and consumer groups.

class BackpressureAwareConsumer:
    """Process stream messages with proper flow control"""
    
    def __init__(self, rabbitmq_host: str = "localhost",
                 stream_name: str = "market_data",
                 prefetch_count: int = 100):
        self.stream_name = stream_name
        self.prefetch_count = prefetch_count
        
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(
            host=rabbitmq_host,
            connection_attempts=3,
            retry_delay=2
        )
        
        self.connection = pika.BlockingConnection(parameters)
        self.channel = self.connection.channel()
        
        # Set prefetch: process max 100 messages before requiring ACK
        # Higher prefetch = better throughput, but more memory
        # Lower prefetch = lower latency
        self.channel.basic_qos(prefetch_count=prefetch_count)
        
        self.channel.queue_declare(
            queue=stream_name,
            durable=True,
            arguments={'x-queue-type': 'stream'}
        )
    
    def process_stream_with_batching(self, 
                                     batch_callback,
                                     batch_size: int = 100):
        """Process messages in batches for efficiency"""
        
        batch = []
        
        def callback(ch, method, properties, body):
            nonlocal batch
            
            tick = json.loads(body.decode())
            batch.append(tick)
            
            # Process when batch is full
            if len(batch) >= batch_size:
                try:
                    batch_callback(batch)
                    batch = []
                    ch.basic_ack(delivery_tag=method.delivery_tag)
                except Exception as e:
                    print(f"Batch processing error: {e}")
                    ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
            else:
                # Keep the message until batch is ready
                ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
        
        self.channel.basic_consume(
            queue=self.stream_name,
            on_message_callback=callback,
            arguments={'x-stream-offset': 'last'}
        )
        
        self.channel.start_consuming()
    
    def process_with_timeout_and_retry(self, callback, 
                                       max_retries: int = 3):
        """Handle slow processing with retry logic"""
        
        def wrapped_callback(ch, method, properties, body):
            tick = json.loads(body.decode())
            
            retry_count = 0
            while retry_count < max_retries:
                try:
                    callback(tick)
                    ch.basic_ack(delivery_tag=method.delivery_tag)
                    break
                except Exception as e:
                    retry_count += 1
                    print(f"Error (attempt {retry_count}): {e}")
                    
                    if retry_count >= max_retries:
                        # Give up, send to dead letter queue
                        self._send_to_dlq(tick, str(e))
                        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
                    else:
                        # Retry
                        import time
                        time.sleep(0.5 * retry_count)  # Exponential backoff
        
        self.channel.basic_consume(
            queue=self.stream_name,
            on_message_callback=wrapped_callback,
            arguments={'x-stream-offset': 'last'}
        )
        
        self.channel.start_consuming()
    
    def _send_to_dlq(self, message: dict, error: str):
        """Send failed messages to dead letter queue"""
        dlq_message = {
            'original_message': message,
            'error': error,
            'timestamp': datetime.utcnow().isoformat()
        }
        
        self.channel.basic_publish(
            exchange='',
            routing_key='trading_dlq',
            body=json.dumps(dlq_message).encode()
        )

Flow control: When strategy processing slows down, RabbitMQ automatically throttles incoming messages. No data loss, no crashes.


Part 5: Consumer Groups - Scaling Strategies

Run multiple instances of the same strategy. Consumer groups automatically distribute messages so each is processed exactly once.

class ConsumerGroup:
    """Distribute work across multiple consumer instances"""
    
    def __init__(self, rabbitmq_host: str = "localhost",
                 stream_name: str = "market_data",
                 group_name: str = "strategy_group"):
        self.stream_name = stream_name
        self.group_name = group_name
        
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(host=rabbitmq_host)
        
        self.connection = pika.BlockingConnection(parameters)
        self.channel = self.connection.channel()
        
        # Declare stream
        self.channel.queue_declare(
            queue=stream_name,
            durable=True,
            arguments={'x-queue-type': 'stream'}
        )
    
    def create_consumer_in_group(self, consumer_id: str,
                                callback: Callable):
        """Join consumer group to process stream partitions"""
        
        # RabbitMQ Streams automatically partition data across consumers
        # Each message processed by exactly one consumer in the group
        
        self.channel.basic_qos(prefetch_count=100)
        
        def wrapped_callback(ch, method, properties, body):
            tick = json.loads(body.decode())
            
            try:
                callback(tick)
                ch.basic_ack(delivery_tag=method.delivery_tag)
            except Exception as e:
                print(f"Consumer {consumer_id} error: {e}")
                ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
        
        # Consumer tag identifies this instance in the group
        self.channel.basic_consume(
            queue=self.stream_name,
            on_message_callback=wrapped_callback,
            consumer_tag=f"{self.group_name}_{consumer_id}",
            arguments={'x-stream-offset': 'last'}
        )
        
        print(f"Consumer {consumer_id} joined {self.group_name}")
        self.channel.start_consuming()

# Example: Run multiple strategy instances in parallel
def deploy_strategy_instances(num_instances: int = 3):
    """Deploy multiple instances of trading strategy"""
    
    import threading
    
    def strategy_worker(instance_id: int):
        group = ConsumerGroup(group_name="momentum_strategy")
        
        def process_tick(tick):
            # Trading logic
            price = tick['price']
            volume = tick['volume']
            
            # Strategy calculation...
            signal = calculate_signal(price, volume)
            
            if signal:
                print(f"Instance {instance_id}: Signal on {tick['ticker']}")
        
        group.create_consumer_in_group(f"instance_{instance_id}", process_tick)
    
    # Start workers in threads
    threads = []
    for i in range(num_instances):
        t = threading.Thread(target=strategy_worker, args=(i,))
        t.daemon = True
        t.start()
        threads.append(t)
    
    # Keep main thread alive
    for t in threads:
        t.join()

Scaling: Start with 1 strategy instance. As volume grows, launch 2, 3, 10 instances. RabbitMQ automatically distributes work. Each message processed exactly once.


Part 6: Real-Time Aggregation Across Exchanges

Combine market data from multiple exchanges into a single stream for cross-exchange strategies.

import threading
from queue import Queue

class MultiExchangeAggregator:
    """Aggregate ticks from multiple exchanges into unified stream"""
    
    def __init__(self, rabbitmq_host: str = "localhost"):
        self.rabbitmq_host = rabbitmq_host
        self.unified_stream = "unified_market_data"
        
        # Setup publisher to unified stream
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(host=rabbitmq_host)
        
        self.connection = pika.BlockingConnection(parameters)
        self.channel = self.connection.channel()
        
        self.channel.queue_declare(
            queue=self.unified_stream,
            durable=True,
            arguments={'x-queue-type': 'stream'}
        )
    
    def aggregate_exchanges(self, exchange_names: list):
        """Subscribe to all exchanges and republish to unified stream"""
        
        # Create consumer for each exchange feed
        for exchange in exchange_names:
            exchange_stream = f"market_data_{exchange.lower()}"
            
            # Each exchange in its own thread
            consumer_thread = threading.Thread(
                target=self._consume_and_aggregate,
                args=(exchange, exchange_stream)
            )
            consumer_thread.daemon = True
            consumer_thread.start()
    
    def _consume_and_aggregate(self, exchange: str, stream_name: str):
        """Consume from exchange stream and republish"""
        
        credentials = pika.PlainCredentials("guest", "guest")
        parameters = pika.ConnectionParameters(host=self.rabbitmq_host)
        
        connection = pika.BlockingConnection(parameters)
        channel = connection.channel()
        
        def callback(ch, method, properties, body):
            tick = json.loads(body.decode())
            
            # Add exchange metadata
            enriched_tick = {
                **tick,
                'exchange': exchange,
                'received_at': datetime.utcnow().isoformat()
            }
            
            # Republish to unified stream
            self.channel.basic_publish(
                exchange='',
                routing_key=self.unified_stream,
                body=json.dumps(enriched_tick).encode(),
                properties=pika.BasicProperties(delivery_mode=2)
            )
            
            ch.basic_ack(delivery_tag=method.delivery_tag)
        
        channel.basic_consume(
            queue=stream_name,
            on_message_callback=callback,
            arguments={'x-stream-offset': 'last'}
        )
        
        print(f"Aggregating {exchange} from {stream_name}")
        channel.start_consuming()
    
    def subscribe_to_unified(self, callback: Callable):
        """Subscribe to unified stream"""
        
        def wrapped_callback(ch, method, properties, body):
            tick = json.loads(body.decode())
            callback(tick)
            ch.basic_ack(delivery_tag=method.delivery_tag)
        
        consumer_conn = pika.BlockingConnection(
            pika.ConnectionParameters(self.rabbitmq_host)
        )
        consumer_channel = consumer_conn.channel()
        
        consumer_channel.basic_consume(
            queue=self.unified_stream,
            on_message_callback=wrapped_callback,
            arguments={'x-stream-offset': 'last'}
        )
        
        consumer_channel.start_consuming()

Cross-exchange strategies: Subscribe to unified stream. Process ticks from NYSE, NASDAQ, CBOE in order. Execute arbitrage or statistical strategies across venues.


Best Practices for Production

  1. Set retention policies: Keep audit logs forever, market data for 30 days
  2. Monitor consumer lag: Alert if any strategy falls behind
  3. Test failover: Simulate broker crashes, verify recovery
  4. Use consumer groups: Scale horizontally as volume grows
  5. Implement dead letter queues: Never silently drop messages
  6. Log everything: Stream is immutable audit trail
  7. Version your messages: Add version field for schema evolution

Guide Version: 1.0 (Draft) Last Updated: February 2025

Published: August 14, 2026
Author: Flowistic

Ready to optimize your system?

Let our team help you implement these strategies and achieve peak performance.

Schedule a Consultation