High-Frequency Trading & Market Making
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
Tools & Setup
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.11+ | Core language |
| Cython | 3.0+ | Hot-path optimization |
| numpy | 1.24+ | Numerical ops |
| pandas | 2.0+ | Data analysis |
| asyncio | stdlib | Async I/O |
| struct | stdlib | Binary parsing |
Step 1: Environment Setup
pip install cython numpy pandas
cythonize -i core/cython_core.pyx
Step 2: Order Book Management
# 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
# 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
# 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
| Metric | Value | Target | Industry Benchmark |
|---|---|---|---|
| Round-Trip Latency | 12Îŧs | <20Îŧs | 20â50Îŧs |
| Spread Capture | 0.015% | >0.01% | 0.008â0.02% |
| Inventory Turnover | 50x/day | >30x | 20â100x |
| Daily P&L (per stock) | 1,000 | 5,000 | |
| Adverse Selection | 18% | <25% | 15â30% |
| Fill Rate | 65% | >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
# 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
- Adverse selection: Informed traders exploit stale quotes â use queue position modeling
- Inventory risk: Holding too much inventory during volatility spikes â implement hard limits
- Latency spikes: GC pauses or network jitter cause missed opportunities â use lock-free data structures
- Regulatory risk: Market manipulation rules limit quote lifetime â implement minimum quote intervals
- 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.