Quant Trading Systems: Architecture & Infrastructure › Strategy Engine Architecture

Michael BrenndoerferJanuary 21, 20269 min read
Quantitative FinanceAll chapters
Part 15 of 15Trading Systems and Production Deployment
Chapter 77Quant Trading Systems: Architecture & Infrastructure › Strategy Engine Architecture
Reading section 4 of 14 · Strategy Engine Architecture · 9 minSection 4 of 14 · 9 min

Book contents

Section 4 of 14 · 9 min read

Quant Trading Systems: Architecture & Infrastructure › Strategy Engine ArchitectureOne section · Sections in this chapter4 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

The strategy engine is where your quantitative models execute. It consumes data, calculates signals, and generates orders. The design of this component critically affects both performance and maintainability. A well-designed strategy engine makes it easy to test new ideas, debug production issues, and scale to more instruments or strategies.

The central challenge in strategy engine design is managing complexity. Strategies start simple but accumulate complexity over time as edge cases are discovered, market conditions change, and new features are added. Without careful architecture, this complexity can make strategies unmaintainable and bug-prone.

Event-Driven vs. Batch Processing

Strategies operate in two fundamental modes, and choosing the right mode depends on your strategy's time horizon and performance requirements.

Event-driven systems react to each market data update. When a new tick arrives, the system immediately recalculates relevant signals and potentially generates orders. This architecture is essential for high-frequency strategies where speed matters. Every microsecond of delay represents potential slippage or missed opportunities. Event-driven architectures require careful attention to processing efficiency, as the system must handle potentially millions of events per second during peak trading periods.

Batch processing runs at fixed intervals: every minute, every hour, or end-of-day. The system accumulates data, then processes it all at once. This approach is simpler, easier to debug, and sufficient for most systematic strategies with holding periods of days or longer. When you only trade once per day, there's no benefit to processing each tick in real-time. Batch processing allows you to use simpler, more readable code and focus engineering effort on strategy logic rather than low-level performance optimization.

The following code demonstrates a basic strategy framework that can operate in either mode. The abstraction captures the essential pattern: receive data, update state, generate signals, and calculate target positions.

In[12]:
Code
from abc import ABC, abstractmethod
from enum import Enum


class Signal(Enum):
    """Trading signal types."""

    LONG = 1
    NEUTRAL = 0
    SHORT = -1


class StrategyBase(ABC):
    """Abstract base class for trading strategies."""

    def __init__(self, symbols: list[str]):
        self.symbols = symbols
        self.positions: dict[str, float] = {s: 0 for s in symbols}
        self.signals: dict[str, Signal] = {s: Signal.NEUTRAL for s in symbols}

    @abstractmethod
    def on_data(self, data: dict) -> dict[str, Signal]:
        """Process new data and generate signals."""
        pass

    @abstractmethod
    def calculate_target_positions(self) -> dict[str, float]:
        """Convert signals to target position sizes."""
        pass


class MomentumStrategy(StrategyBase):
    """
    Simple momentum strategy for demonstration.
    Goes long when returns exceed threshold, short when below.
    """

    def __init__(
        self, symbols: list[str], lookback: int = 20, threshold: float = 0.02
    ):
        super().__init__(symbols)
        self.lookback = lookback
        self.threshold = threshold
        self.price_history: dict[str, list] = {s: [] for s in symbols}

    def on_data(self, data: dict) -> dict[str, Signal]:
        """Update price history and generate signals."""
        signals = {}

        for symbol in self.symbols:
            if symbol not in data:
                signals[symbol] = Signal.NEUTRAL
                continue

            # Update price history
            self.price_history[symbol].append(data[symbol])

            # Keep only lookback period
            if len(self.price_history[symbol]) > self.lookback:
                self.price_history[symbol] = self.price_history[symbol][
                    -self.lookback :
                ]

            # Calculate signal if we have enough history
            if len(self.price_history[symbol]) >= self.lookback:
                prices = np.array(self.price_history[symbol])
                momentum = (prices[-1] - prices[0]) / prices[0]

                if momentum > self.threshold:
                    signals[symbol] = Signal.LONG
                elif momentum < -self.threshold:
                    signals[symbol] = Signal.SHORT
                else:
                    signals[symbol] = Signal.NEUTRAL
            else:
                signals[symbol] = Signal.NEUTRAL

        self.signals = signals
        return signals

    def calculate_target_positions(self) -> dict[str, float]:
        """Equal weight positions based on signals."""
        n_active = sum(1 for s in self.signals.values() if s != Signal.NEUTRAL)
        if n_active == 0:
            return {s: 0 for s in self.symbols}

        weight = 1.0 / n_active
        return {
            symbol: weight * signal.value
            for symbol, signal in self.signals.items()
        }
In[13]:
Code
# Simulate strategy execution
strategy = MomentumStrategy(["SPY", "QQQ"], lookback=5, threshold=0.01)

# Simulate 10 days of price data
np.random.seed(42)
spy_prices = 450 * np.cumprod(1 + np.random.randn(10) * 0.01)
qqq_prices = 380 * np.cumprod(1 + np.random.randn(10) * 0.015)

execution_log = []
for day in range(10):
    data = {"SPY": spy_prices[day], "QQQ": qqq_prices[day]}
    signals = strategy.on_data(data)
    positions = strategy.calculate_target_positions()

    log_entry = (
        f"{day + 1:3d} | {spy_prices[day]:9.2f} | {qqq_prices[day]:9.2f} | "
        f"{signals['SPY'].name:10s} | {signals['QQQ'].name:10s} | "
        f"SPY: {positions['SPY']:+.2f}, QQQ: {positions['QQQ']:+.2f}"
    )
    execution_log.append(log_entry)
Out[14]:
Console
Day | SPY Price | QQQ Price | SPY Signal | QQQ Signal | Target Positions
---------------------------------------------------------------------------
  1 |    452.24 |    377.36 | NEUTRAL    | NEUTRAL    | SPY: +0.00, QQQ: +0.00
  2 |    451.61 |    374.72 | NEUTRAL    | NEUTRAL    | SPY: +0.00, QQQ: +0.00
  3 |    454.53 |    376.08 | NEUTRAL    | NEUTRAL    | SPY: +0.00, QQQ: +0.00
  4 |    461.46 |    365.29 | NEUTRAL    | NEUTRAL    | SPY: +0.00, QQQ: +0.00
  5 |    460.38 |    355.84 | LONG       | SHORT      | SPY: +0.50, QQQ: -0.50
  6 |    459.30 |    352.84 | LONG       | SHORT      | SPY: +0.50, QQQ: -0.50
  7 |    466.55 |    347.48 | LONG       | SHORT      | SPY: +0.50, QQQ: -0.50
  8 |    470.13 |    349.11 | LONG       | SHORT      | SPY: +0.50, QQQ: -0.50
  9 |    467.93 |    344.36 | LONG       | SHORT      | SPY: +0.50, QQQ: -0.50
 10 |    470.46 |    337.06 | LONG       | SHORT      | SPY: +0.50, QQQ: -0.50
Out[15]:
Visualization
Line chart of simulated asset price over 100 days showing upward and downward trends used to generate momentum signals.
Simulated asset price history over 100 days used to demonstrate the momentum strategy. The price series exhibits large trends and reversals, and gives the necessary volatility for signal generation.
Line chart of momentum indicator over time with dashed green and red threshold lines marking buy and sell trigger levels.
Calculated momentum indicator relative to buy and sell thresholds. Signals are generated when momentum crosses the dashed threshold lines.
Step chart of target position over 100 days with three levels: Long (1), Neutral (0), and Short (-1), changing in response to momentum signals.
Resulting target positions (Long, Short, Neutral). The step function shows how the position changes in response to momentum signals.

The strategy begins generating signals once it accumulates enough price history. Notice how signals change as momentum shifts, and target positions adjust accordingly. During the initial lookback period, all signals are NEUTRAL because the strategy doesn't have enough data to make a decision. This "warm-up" period is a common feature of strategies that depend on historical data.

Key Parameters

The key parameters for the Momentum strategy control the tradeoff between responsiveness and stability:

  • lookback: The number of periods used to calculate price changes. A longer lookback filters noise but introduces lag. A 5-day lookback responds quickly to new trends but may generate false signals from short-term fluctuations. A 60-day lookback provides more reliable signals but may enter trends late and exit late.
  • threshold: The minimum return required to generate a signal. Higher thresholds reduce trading frequency and transaction costs, but may miss smaller profitable moves. The optimal threshold depends on your transaction costs and the signal-to-noise ratio in your data.

State Management

Production strategies must maintain state correctly across restarts, market closes, and error conditions. State management is one of the most challenging aspects of production systems because it involves coordinating information across multiple components that may fail independently.

Key state includes:

  • Current positions: Actual holdings, reconciled with broker records. Your internal position tracking must match what the broker believes you hold, or trades may fail or produce unexpected results.
  • Open orders: Orders submitted but not yet filled or cancelled. If your system crashes and restarts, it must know about any orders that were submitted before the crash.
  • Strategy state: Model parameters, accumulated signals, indicator values. A momentum strategy needs its price history; a mean-reversion strategy needs its running average.
  • Risk state: Current exposures, drawdown levels, limit utilization. Risk limits apply continuously, not just at the moment an order is submitted.
In[16]:
Code
import json
from pathlib import Path


@dataclass
class StrategyState:
    """Complete state snapshot for a strategy."""

    timestamp: datetime
    positions: dict[str, float]
    signals: dict[str, int]
    open_orders: list[dict]
    pnl_today: float
    pnl_total: float
    max_drawdown: float

    def to_dict(self) -> dict:
        return {
            "timestamp": self.timestamp.isoformat(),
            "positions": self.positions,
            "signals": self.signals,
            "open_orders": self.open_orders,
            "pnl_today": self.pnl_today,
            "pnl_total": self.pnl_total,
            "max_drawdown": self.max_drawdown,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "StrategyState":
        data["timestamp"] = datetime.fromisoformat(data["timestamp"])
        return cls(**data)


class StateManager:
    """Manages strategy state persistence and recovery."""

    def __init__(self, state_dir: str = "./state"):
        self.state_dir = Path(state_dir)
        self.state_dir.mkdir(exist_ok=True)

    def save_state(self, strategy_id: str, state: StrategyState):
        """Persist state to disk."""
        filepath = self.state_dir / f"{strategy_id}_state.json"
        with open(filepath, "w") as f:
            json.dump(state.to_dict(), f, indent=2)

    def load_state(self, strategy_id: str) -> Optional[StrategyState]:
        """Load state from disk if it exists."""
        filepath = self.state_dir / f"{strategy_id}_state.json"
        if not filepath.exists():
            return None
        with open(filepath, "r") as f:
            return StrategyState.from_dict(json.load(f))

    def checkpoint(self, strategy_id: str, state: StrategyState):
        """
        Create a timestamped checkpoint for recovery.
        Useful for debugging and audit trails.
        """
        timestamp_str = state.timestamp.strftime("%Y%m%d_%H%M%S")
        filepath = (
            self.state_dir / f"{strategy_id}_checkpoint_{timestamp_str}.json"
        )
        with open(filepath, "w") as f:
            json.dump(state.to_dict(), f, indent=2)

State persistence ensures that if your system crashes at 2 PM, you can restart it without losing track of your positions or partially executed orders. Regular checkpointing provides an audit trail for post-trade analysis and compliance purposes. When regulators ask why a particular trade was executed, you need to be able to reconstruct the exact state that led to that decision.