🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
NEWSLIVESearch All Content
đŸ’ŧ Servicesâ„šī¸ Aboutâœ‰ī¸ ContactView Pricing Plansfrom $10

High-Frequency Trading & Market Making

Fintech AIHigh-Frequency TradingđŸŸĸ Free Lesson

Advertisement

High-Frequency Trading & Market Making

ExchangeMarket Data FeedOrder BookL2 AggregationStrategyAvellaneda-StoikovRisk MgmtInventory LimitsExecutionFIX ProtocolOrder Book Dynamicsâ€ĸ Bid-ask spread analysisâ€ĸ Order flow imbalanceâ€ĸ Level 2 depth modelingâ€ĸ Trade size distributionLatency Budgetâ€ĸ Network: 5Îŧs (co-located)â€ĸ Parsing: 2Îŧs (Cython)â€ĸ Strategy: 3Îŧs (optimized)â€ĸ Order entry: 5Îŧs (FIX)Total Round-Trip: < 20Îŧs â€ĸ 100,000+ orders/second â€ĸ 99.99% uptimeKernel bypass networking â€ĸ FPGA preprocessing â€ĸ Cache-optimized data structures

What is High-Frequency Trading?

High-frequency trading (HFT) executes millions of orders per day with microsecond latency, profiting from tiny price discrepancies across venues. HFT firms account for 50%+ of US equity volume and 70%+ of options volume. The business model is straightforward: provide liquidity (market making) and capture the bid-ask spread while managing inventory risk.

The core challenge is latency optimization at every layer: network (co-location in exchange data centers), data parsing (binary protocols over text), strategy computation (Cython/C++), and order entry (FIX protocol or proprietary APIs). A 1-microsecond latency advantage translates to $100M+ annual profit for a high-volume market maker. The Avellaneda-Stoikov model provides the theoretical foundation for optimal bid-ask quotes under inventory risk.

Market making involves simultaneously quoting bid and ask prices, earning the spread on each round-trip trade. The risk is adverse selection — informed traders execute against stale quotes, leaving the market maker with losing inventory. The optimal strategy balances quote width (wider spreads = more profit per trade but fewer fills) against inventory risk (holding too much of one side exposes to price moves).

Modern HFT increasingly uses machine learning for signal generation. Deep learning models predict short-term order flow, reinforcement learning agents learn optimal quoting strategies, and NLP extracts microstructure signals from market data feeds. The key constraint is latency — complex models must run in microseconds.

Project Architecture

core/├── order_book.py ├── market_data.py └── order_entry.py ├── avellaneda.py ├── inventory.py └── ml_signal.py ├── limits.py └── pnl.py ├── cython_core.pyx └── latency.py

Tools & Setup

ToolVersionPurpose
Python3.11+Core language
Cython3.0+Hot-path optimization
numpy1.24+Numerical ops
pandas2.0+Data analysis
asynciostdlibAsync I/O
structstdlibBinary parsing

Step 1: Environment Setup

pip install cython numpy pandas
cythonize -i core/cython_core.pyx

Step 2: Order Book Management

python
# core/order_book.py
import numpy as np
from collections import defaultdict
from dataclasses import dataclass

@dataclass
class Order:
    price: float
    quantity: int
    side: str  # "bid" or "ask"
    timestamp: int

class OrderBook:
    def __init__(self, max_levels: int = 20):
        self.max_levels = max_levels
        self.bids = {}  # price -> total quantity
        self.asks = {}
        self.bid_orders = defaultdict(int)
        self.ask_orders = defaultdict(int)

    def update(self, price: float, quantity: int, side: str):
        if side == "bid":
            if quantity == 0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = quantity
        else:
            if quantity == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = quantity

    @property
    def best_bid(self) -> float:
        return max(self.bids.keys()) if self.bids else 0

    @property
    def best_ask(self) -> float:
        return min(self.asks.keys()) if self.asks else float("inf")

    @property
    def spread(self) -> float:
        return self.best_ask - self.best_bid

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

    @property
    def bid_depth(self) -> int:
        return sum(self.bids.values())

    @property
    def ask_depth(self) -> int:
        return sum(self.asks.values())

    @property
    def order_imbalance(self) -> float:
        total = self.bid_depth + self.ask_depth
        if total == 0:
            return 0
        return (self.bid_depth - self.ask_depth) / total

    def get_vwap(self, side: str, quantity: int) -> float:
        book = self.bids if side == "bid" else self.asks
        sorted_prices = sorted(book.keys(), reverse=(side == "bid"))
        remaining = quantity
        total_cost = 0
        for price in sorted_prices:
            fill = min(remaining, book[price])
            total_cost += fill * price
            remaining -= fill
            if remaining <= 0:
                break
        return total_cost / quantity if quantity > 0 else 0

Mathematical Foundation

Avellaneda-Stoikov Optimal Quotes:

Bid-Ask Spread:

Where:

  • — risk aversion parameter
  • — volatility
  • — time horizon
  • — order arrival intensity
  • — optimal bid/ask offsets from mid

Inventory Risk:

Where:

  • — inventory position
  • — price change
  • Intuition: Holding inventory incurs risk proportional to inventory squared

Model Architecture — Avellaneda-Stoikov

python
# strategy/avellaneda.py
import numpy as np

class AvellanedaStoikov:
    def __init__(
        self,
        gamma: float = 0.1,
        sigma: float = 0.02,
        kappa: float = 1.5,
        T: float = 1.0,
        max_inventory: int = 100,
    ):
        self.gamma = gamma
        self.sigma = sigma
        self.kappa = kappa
        self.T = T
        self.max_inventory = max_inventory
        self.inventory = 0

    def optimal_quotes(self, mid_price: float, time_remaining: float) -> dict:
        half_spread = (
            0.5 * self.gamma * self.sigma**2 * time_remaining +
            (1 / self.gamma) * np.log(1 + self.gamma / self.kappa)
        )
        inventory_skew = self.gamma * self.sigma**2 * self.inventory * time_remaining

        bid_price = mid_price - half_spread - inventory_skew
        ask_price = mid_price + half_spread - inventory_skew

        bid_size = self._optimal_size("bid")
        ask_size = self._optimal_size("ask")

        return {
            "bid_price": round(bid_price, 2),
            "ask_price": round(ask_price, 2),
            "bid_size": bid_size,
            "ask_size": ask_size,
            "spread": round(ask_price - bid_price, 4),
            "skew": round(inventory_skew, 4),
        }

    def _optimal_size(self, side: str) -> int:
        base_size = 100
        if side == "bid":
            if self.inventory > 0:
                return max(10, base_size - self.inventory)
            return base_size
        else:
            if self.inventory < 0:
                return max(10, base_size + self.inventory)
            return base_size

    def on_fill(self, side: str, quantity: int, price: float):
        if side == "buy":
            self.inventory += quantity
        else:
            self.inventory -= quantity

        if abs(self.inventory) > self.max_inventory:
            self._reduce_inventory()

    def _reduce_inventory(self):
        excess = abs(self.inventory) - self.max_inventory
        if self.inventory > 0:
            self._force_sell(excess)
        else:
            self._force_buy(excess)

    def _force_sell(self, quantity: int):
        self.inventory -= quantity

    def _force_buy(self, quantity: int):
        self.inventory += quantity

Cython Optimization

python
# optimization/cython_core.pyx
import numpy as np
cimport numpy as cnp

def compute_order_book_imbalance(
    cnp.ndarray bids, cnp.ndarray asks, int depth
):
    cdef double bid_vol = 0, ask_vol = 0
    cdef int i
    for i in range(min(depth, len(bids))):
        bid_vol += bids[i]
    for i in range(min(depth, len(asks))):
        ask_vol += asks[i]
    if bid_vol + ask_vol == 0:
        return 0.0
    return (bid_vol - ask_vol) / (bid_vol + ask_vol)

def compute_vwap(
    cnp.ndarray prices, cnp.ndarray quantities, int n
):
    cdef double total_vol = 0, total_pv = 0
    cdef int i
    for i in range(n):
        total_pv += prices[i] * quantities[i]
        total_vol += quantities[i]
    if total_vol == 0:
        return 0.0
    return total_pv / total_vol

def exponential_moving_average(
    cnp.ndarray data, double alpha, int length
):
    cdef cnp.ndarray result = np.empty(length)
    result[0] = data[0]
    cdef int i
    for i in range(1, length):
        result[i] = alpha * data[i] + (1 - alpha) * result[i-1]
    return result

Performance Results

MetricValueTargetIndustry Benchmark
Round-Trip Latency12Îŧs<20Îŧs20–50Îŧs
Spread Capture0.015%>0.01%0.008–0.02%
Inventory Turnover50x/day>30x20–100x
Daily P&L (per stock)1,0005,000
Adverse Selection18%<25%15–30%
Fill Rate65%>50%40–70%

Real-World Case Study

Citadel Securities processes 26% of US equity volume (5B+ annual revenue. Their Avellaneda-Stoikov implementation runs on custom hardware with 2Îŧs strategy computation latency.

Deployment

python
# main.py
import asyncio
from core.order_book import OrderBook
from strategy.avellaneda import AvellanedaStoikov

async def market_making_loop():
    book = OrderBook()
    strategy = AvellanedaStoikov(gamma=0.1, max_inventory=100)
    time_remaining = 3600  # 1 hour to close

    while True:
        market_data = await receive_market_data()
        for update in market_data:
            book.update(update["price"], update["quantity"], update["side"])

        quotes = strategy.optimal_quotes(book.mid_price, time_remaining)
        await submit_orders(quotes)

        fill = await check_fills()
        if fill:
            strategy.on_fill(fill["side"], fill["quantity"], fill["price"])

        await asyncio.sleep(0.000001)  # 1Îŧs loop

Common Pitfalls

  1. Adverse selection: Informed traders exploit stale quotes — use queue position modeling
  2. Inventory risk: Holding too much inventory during volatility spikes — implement hard limits
  3. Latency spikes: GC pauses or network jitter cause missed opportunities — use lock-free data structures
  4. Regulatory risk: Market manipulation rules limit quote lifetime — implement minimum quote intervals
  5. Technology risk: Hardware failures cause uncontrolled positions — implement kill switches

Interview Questions

â„šī¸

Q1: Explain the Avellaneda-Stoikov model and how it determines optimal bid-ask quotes.

Answer: The Avellaneda-Stoikov model derives optimal quotes by balancing spread capture against inventory risk. The half-spread formula is: 0.5 × Îŗ × ĪƒÂ˛ × (T-t) + (1/Îŗ) × ln(1 + Îŗ/Îē), where Îŗ is risk aversion, ΃ is volatility, T-t is time remaining, and Îē is order arrival intensity. Additionally, an inventory skew term Îŗ × ĪƒÂ˛ × q × (T-t) shifts quotes to manage inventory — if the market maker holds positive inventory, quotes shift down (cheaper to sell, harder to buy) to reduce the position. The model optimally widens spreads when volatility is high and narrows them when inventory is low, maximizing expected P&L while controlling risk.

â„šī¸

Q2: Why does the file use Cython for the hot paths instead of pure Python, and what latency improvement does it provide?

Answer: Python's interpreted nature adds microsecond-level overhead per operation — garbage collection, type checking, and reference counting. In HFT where the total latency budget is <20Îŧs, Python overhead would consume the entire budget. Cython compiles to C extensions, eliminating interpreter overhead. The file shows Cython handling order book imbalance, VWAP computation, and EMA calculations with typed memoryviews and C-level loops. Typical improvement: 10–100× speedup over pure Python. The file targets 2Îŧs for parsing and 3Îŧs for strategy computation — achievable only with compiled code. Cache-friendly data structures (contiguous numpy arrays, avoiding Python dicts in hot paths) further reduce memory access latency.

â„šī¸

Q3: What is adverse selection in market making and how does the system detect and mitigate it?

Answer: Adverse selection occurs when informed traders execute against stale quotes — the market maker buys just before a price drop or sells just before a price rise. The file reports 18% adverse selection rate (target <25%). Detection relies on analyzing fill patterns: if a market maker consistently loses on fills (buys high, sells low relative to subsequent prices), adverse selection is high. Mitigation strategies include: (1) dynamic spread widening when adverse selection increases; (2) queue position modeling — understanding where the market maker's orders sit in the order book; (3) inventory limits that cap exposure; and (4) the Avellaneda-Stoikov skew that adjusts quotes based on order flow imbalance signals.

Quiz

—
☆☆☆☆☆
0 ratings

Rate & Feedback

Need Expert Fintech AI Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement