Quant Trading Systems: Architecture & Infrastructure › Data Infrastructure

Michael BrenndoerferJanuary 21, 202610 min read
Quantitative FinanceAll chapters
Part 15 of 15Trading Systems and Production Deployment
Chapter 77Quant Trading Systems: Architecture & Infrastructure › Data Infrastructure
Reading section 3 of 14 · Data Infrastructure · 10 minSection 3 of 14 · 10 min

Book contents

Section 3 of 14 · 10 min read

Quant Trading Systems: Architecture & Infrastructure › Data InfrastructureOne section · Sections in this chapter3 / 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

Data is the lifeblood of quantitative trading. The quality, timeliness, and comprehensiveness of your data infrastructure often determine whether a strategy succeeds or fails in production. A strategy that appears profitable in research may fail in production simply because the production data differs subtly from the research data, or because data quality issues introduce noise that swamps the signal.

The fundamental challenge of data infrastructure is managing the tradeoff between speed, cost, and coverage. Low-latency feeds require significant capital, while large historical databases consume substantial storage and engineering resources. Firms must prioritize data sources based on specific strategy requirements.

Market Data Feeds

Market data arrives through various channels depending on speed requirements and cost constraints. Understanding these channels and their characteristics is essential for matching your data infrastructure to your strategy needs.

Direct exchange feeds provide the lowest latency access to order book updates and trades. Exchanges like NYSE, NASDAQ, and CME offer proprietary feeds (NYSE Integrated Feed, NASDAQ TotalView, CME Market Data Platform) that deliver updates in microseconds. These feeds typically require co-location (placing your servers in the exchange's data center) and significant infrastructure investment. The data arrives in exchange-specific binary formats that require custom parsing code, and the feeds themselves can generate millions of messages per second during active trading periods.

Consolidated feeds aggregate data from multiple exchanges. In the US, the Securities Information Processor (SIP) consolidates quotes and trades from all equity exchanges. While slower than direct feeds (typically tens of milliseconds), consolidated feeds are simpler to consume and sufficient for most systematic strategies. They provide a unified view of the national best bid and offer without requiring you to manage connections to each individual exchange.

Vendor feeds from providers like Bloomberg, Refinitiv, or Interactive Brokers offer convenient APIs but add latency. For end-of-day strategies or research, this latency is irrelevant. For intraday strategies with holding periods of minutes or longer, vendor feeds are often adequate. These vendors handle the complexity of exchange connectivity, data normalization, and symbol mapping, allowing you to focus on strategy development rather than data engineering.

The following code demonstrates a basic structure for handling incoming market data, including validation logic that protects against common data quality issues:

In[3]:
Code
from collections import deque
from dataclasses import dataclass
from datetime import datetime
from typing import Optional


@dataclass
class MarketDataTick:
    """Represents a single market data update."""

    symbol: str
    timestamp: datetime
    bid_price: float
    bid_size: int
    ask_price: float
    ask_size: int
    last_price: Optional[float] = None
    last_size: Optional[int] = None

    @property
    def mid_price(self) -> float:
        return (self.bid_price + self.ask_price) / 2

    @property
    def spread(self) -> float:
        return self.ask_price - self.bid_price

    @property
    def spread_bps(self) -> float:
        return (self.spread / self.mid_price) * 10000


class MarketDataHandler:
    """Handles incoming market data with validation and buffering."""

    def __init__(self, buffer_size: int = 1000):
        self.current_quotes: dict[str, MarketDataTick] = {}
        self.tick_buffer: dict[str, deque] = {}
        self.buffer_size = buffer_size
        self.stale_threshold_seconds = 5.0

    def on_tick(self, tick: MarketDataTick) -> bool:
        """Process incoming tick with validation. Returns True if valid."""
        # Validate tick data
        if not self._validate_tick(tick):
            return False

        # Initialize buffer if needed
        if tick.symbol not in self.tick_buffer:
            self.tick_buffer[tick.symbol] = deque(maxlen=self.buffer_size)

        # Update current quote and buffer
        self.current_quotes[tick.symbol] = tick
        self.tick_buffer[tick.symbol].append(tick)
        return True

    def _validate_tick(self, tick: MarketDataTick) -> bool:
        """Validate tick data for common errors."""
        # Check for crossed markets (bid > ask)
        if tick.bid_price >= tick.ask_price:
            return False

        # Check for zero or negative prices
        if tick.bid_price <= 0 or tick.ask_price <= 0:
            return False

        # Check for unreasonable spread (> 10%)
        if tick.spread_bps > 1000:
            return False

        return True

    def is_data_stale(self, symbol: str, current_time: datetime) -> bool:
        """Check if data for a symbol is stale."""
        if symbol not in self.current_quotes:
            return True

        last_update = self.current_quotes[symbol].timestamp
        age = (current_time - last_update).total_seconds()
        return age > self.stale_threshold_seconds
In[4]:
Code
# Demonstrate market data handling
handler = MarketDataHandler(buffer_size=100)

# Simulate incoming ticks
sample_ticks = [
    MarketDataTick(
        "AAPL",
        datetime(2024, 1, 15, 9, 30, 0),
        185.50,
        100,
        185.52,
        200,
        185.51,
        50,
    ),
    MarketDataTick(
        "AAPL",
        datetime(2024, 1, 15, 9, 30, 1),
        185.48,
        150,
        185.51,
        100,
        185.50,
        75,
    ),
    MarketDataTick(
        "AAPL", datetime(2024, 1, 15, 9, 30, 2), 185.55, 200, 185.45, 100
    ),  # Invalid: crossed
]

tick_log = []
for tick in sample_ticks:
    valid = handler.on_tick(tick)
    tick_log.append(
        f"Tick at {tick.timestamp.time()}: bid={tick.bid_price}, ask={tick.ask_price}, valid={valid}"
    )

current_aapl = None
spread_bps = 0.0
if "AAPL" in handler.current_quotes:
    current_aapl = handler.current_quotes["AAPL"]
    spread_bps = current_aapl.spread_bps
Out[5]:
Console
Tick at 09:30:00: bid=185.5, ask=185.52, valid=True
Tick at 09:30:01: bid=185.48, ask=185.51, valid=True
Tick at 09:30:02: bid=185.55, ask=185.45, valid=False

Current AAPL quote: 185.48 / 185.51
Spread: 1.62 bps
Out[6]:
Visualization
Line chart of bid and ask prices over 100 ticks with a shaded red region between ticks 60 and 70 where the bid exceeds the ask, indicating a crossed-market anomaly.
Visualizing market data integrity: bid and ask prices over time. Shading marks the observations within ticks 60-69 where the injected anomaly makes the bid exceed the ask.

The validation logic catches the third tick where the bid price exceeds the ask price, a "crossed market" condition that indicates bad data. Production systems encounter such anomalies regularly and must handle them gracefully. Crossed markets can occur due to feed delays, exchange glitches, or data corruption during transmission. A system that blindly trusts such data might attempt to trade at impossible prices, leading to rejected orders or, worse, execution at unfavorable prices.

Alternative Data Integration

As we discussed in the Alternative Data chapter, non-traditional data sources can provide alpha. Integrating these feeds requires different infrastructure considerations than traditional market data. Alternative data often arrives less frequently, in unstructured formats, and with significant publication lags that must be carefully tracked.

Batch data like satellite imagery, credit card transactions, or SEC filings arrives periodically. This data flows through ETL (Extract, Transform, Load) pipelines that clean, normalize, and load it into your historical database. The key challenge is ensuring data is point-in-time correct. You must know exactly when information became available to avoid look-ahead bias. For example, a credit card transaction might occur on Monday, be aggregated by the data vendor on Wednesday, and become available to you on Thursday. Using that data to make trading decisions "as of Monday" in a backtest would introduce severe look-ahead bias.

Streaming data like social media sentiment or news feeds requires real-time processing. Natural language processing models extract signals from text, and these signals must align temporally with your market data. A tweet about a company might move its stock price within seconds; if your system processes that tweet minutes later, the trading opportunity has passed.

The following code illustrates the concept of point-in-time correctness, which is fundamental to both research integrity and production system design:

In[7]:
Code
from datetime import timedelta


@dataclass
class AlternativeDataPoint:
    """Represents an alternative data observation."""

    symbol: str
    data_type: str
    observation_date: datetime  # When the data relates to
    available_date: datetime  # When the data became available
    value: float

    @property
    def publication_lag(self) -> timedelta:
        """Time between observation and availability."""
        return self.available_date - self.observation_date


class PointInTimeDatabase:
    """
    Database wrapper ensuring point-in-time correctness.
    Only returns data that was available at the query time.
    """

    def __init__(self):
        self.data: list[AlternativeDataPoint] = []

    def insert(self, data_point: AlternativeDataPoint):
        self.data.append(data_point)

    def query(
        self, symbol: str, data_type: str, as_of: datetime
    ) -> Optional[AlternativeDataPoint]:
        """
        Get the most recent data point that was available as of the given time.
        This prevents look-ahead bias in backtesting and research.
        """
        relevant = [
            d
            for d in self.data
            if d.symbol == symbol
            and d.data_type == data_type
            and d.available_date <= as_of  # Only data that was available
        ]

        if not relevant:
            return None

        # Return most recent available observation
        return max(relevant, key=lambda d: d.available_date)
In[8]:
Code
# Demonstrate point-in-time correctness
pit_db = PointInTimeDatabase()

# Insert earnings surprise data with publication lag
pit_db.insert(
    AlternativeDataPoint(
        symbol="AAPL",
        data_type="earnings_surprise",
        observation_date=datetime(2024, 1, 25),  # Earnings call date
        available_date=datetime(
            2024, 1, 25, 16, 30
        ),  # Available after market close
        value=0.05,  # 5% positive surprise
    )
)

# Query at different times
before_release = datetime(2024, 1, 25, 12, 0)
after_release = datetime(2024, 1, 25, 17, 0)

result_before = pit_db.query("AAPL", "earnings_surprise", before_release)
result_after = pit_db.query("AAPL", "earnings_surprise", after_release)
Out[9]:
Console
Query at 12:00:00: None
Query at 17:00:00: 0.05

The point-in-time query correctly returns None before the earnings were released, preventing the look-ahead bias that would occur if we used the earnings surprise data before it was available. This seemingly simple distinction is one of the most common sources of error in quantitative research. Strategies that appear highly profitable in backtests often fail in production because they inadvertently used information that wasn't available at decision time.

Historical Data Storage

Historical data storage must balance several competing requirements: query speed for research and backtesting, storage efficiency for cost management, and data integrity for reproducibility. The choices you make here affect system performance and the validity of your research conclusions.

Time-series databases like InfluxDB, TimescaleDB, or kdb+ are optimized for financial data. They support efficient time-range queries, downsampling, and compression. kdb+ in particular is ubiquitous in quantitative finance due to its exceptional performance with tick data, though it requires learning the q programming language. These specialized databases understand the temporal nature of financial data and can exploit that structure for dramatic performance improvements over general-purpose databases.

Data organization typically follows a hierarchy: asset class, then symbol, then date, then intraday data. Partitioning by date allows efficient pruning of historical queries and simplifies data retention policies. When you query for AAPL data from January 2024, the database can immediately skip all partitions except those containing January 2024 data, dramatically reducing the amount of data that must be scanned.

In[10]:
Code
import pandas as pd

# Demonstrate efficient data organization patterns
np.random.seed(42)

# Create sample OHLCV data
dates = pd.date_range("2024-01-01", "2024-01-31", freq="D")
n_days = len(dates)

ohlcv_data = pd.DataFrame(
    {
        "date": dates,
        "open": 100 + np.cumsum(np.random.randn(n_days) * 0.5),
        "high": 0.0,  # Will calculate
        "low": 0.0,  # Will calculate
        "close": 0.0,  # Will calculate
        "volume": np.random.randint(1000000, 5000000, n_days),
    }
)

# Generate realistic OHLC from open
for i in range(n_days):
    daily_range = np.random.uniform(0.5, 2.0)
    ohlcv_data.loc[i, "high"] = ohlcv_data.loc[i, "open"] + np.random.uniform(
        0, daily_range
    )
    ohlcv_data.loc[i, "low"] = ohlcv_data.loc[i, "open"] - np.random.uniform(
        0, daily_range
    )
    ohlcv_data.loc[i, "close"] = np.random.uniform(
        ohlcv_data.loc[i, "low"], ohlcv_data.loc[i, "high"]
    )

ohlcv_data = ohlcv_data.set_index("date")
mem_usage_kb = ohlcv_data.memory_usage(deep=True).sum() / 1024
Out[11]:
Console
Sample OHLCV data structure:
              open    high     low   close   volume
date                                               
2024-01-01  100.25  100.28   99.99  100.10  3669995
2024-01-02  100.18  100.93   99.86  100.16  1256508
2024-01-03  100.50  100.69   99.45   99.54  1803591
2024-01-04  101.26  102.79  100.87  100.88  1896942
2024-01-05  101.15  102.37   99.89  101.80  3203682
2024-01-06  101.03  101.25  100.96  101.21  1604365
2024-01-07  101.82  102.29  101.73  101.90  2870928
2024-01-08  102.20  102.92  101.57  102.77  3557489
2024-01-09  101.97  102.11  101.11  101.87  2773415
2024-01-10  102.24  103.27  101.58  102.46  3055555

Data shape: (31, 5)
Memory usage: 1.45 KB

For tick-level data, storage requirements grow dramatically. A single liquid stock can generate millions of ticks per day, and storing order book snapshots multiplies this further. Compression and careful schema design become essential. Consider that storing full order book snapshots (all price levels with their sizes) at millisecond frequency for thousands of symbols would require petabytes of storage per year. Most firms make pragmatic decisions about what level of detail to store based on their research needs.