The Quest Begins (The "Why")
I still remember the first time I saw a flash‑crash replay on YouTube. The price line dipped like a roller coaster dropping from the top of a hill, and in the comments someone shouted, “That’s HFT!” I laughed, then felt a weird urge to peek behind the curtain. I’d spent years writing web APIs and thought I knew what “fast” meant — until I realized that in trading, “fast” is measured in microseconds, not milliseconds. My dragon? Build a tiny prototype that could react to an order‑book update faster than a human could blink, and see if the hype matched reality.
The Revelation (The Insight)
After a few late‑night sessions (and far too much coffee), the big insight hit me: speed isn’t just about the network or the language you pick. It’s about how you represent the market state and how often you touch that representation. If you’re copying a whole pandas DataFrame every tick, you’re already losing the race before you even start. The real magic lives in three simple ideas:
- Stay in‑place. Mutate a pre‑allocated buffer instead of creating new objects.
- Batch the math. Use NumPy (or even better, Numba‑jitted loops) so the CPU does the heavy lifting in one go.
-
Avoid blocking calls. Anything that waits — like
time.sleepor a synchronous HTTP request — introduces jitter that dwarfs the gains you’re chasing.
When I finally stopped rebuilding the order book from scratch each iteration and started treating it as a mutable array, my latency dropped from ~200 µs to under 15 µs on my laptop. It felt like finally dodging the bullets in The Matrix — except the bullets were nanoseconds of wasted time.
Wielding the Power (Code & Examples)
The naive attempt (the trap)
import time
import pandas as pd
import random
# fake order book: two levels of bids/asks
def make_book():
return pd.DataFrame({
'price': [100.0, 99.9, 100.1, 100.2],
'size' : [10, 5, 5, 10],
'side' : ['bid', 'bid', 'ask', 'ask']
})
def naive_strategy(book):
# pretend we look at the best bid/ask each tick
best_bid = book.loc[book['side'] == 'bid', 'price'].max()
best_ask = book.loc[book['side'] == 'ask', 'price'].min()
spread = best_ask - best_bid
if spread < 0.02: # arbitrary trigger
return 'buy' if random.random() > 0.5 else 'sell'
return 'hold'
# simulation loop
book = make_book()
for _ in range(10_000):
# simulate a tiny market move
book['price'] += random.uniform(-0.001, 0.001)
action = naive_strategy(book)
time.sleep(0.001) # <-- the trap! 1 ms of blocking junk
What went wrong?
- We rebuild a pandas DataFrame each loop (implicit copy when we add noise).
- The
time.sleep(0.001)adds a fixed 1 ms latency — orders of magnitude slower than the sub‑microsecond moves we care about. - The loop does pure Python work on every tick, which the CPU can’t vectorize.
The upgraded version (the victory)
import numpy as np
from numba import jit
import time
# ---- constants -------------------------------------------------
LEVELS = 2 # we only keep the best bid/ask
DTYPE = np.float64
# ---- mutable buffers (pre‑allocated) ---------------------------
# [bid_price, bid_size, ask_price, ask_size]
book = np.array([100.0, 10.0, 99.9, 5.0, # level 0
100.1, 5.0, 100.2, 10.0], # level 1
dtype=DTYPE).reshape(LEVELS, 4)
@jit(nopython=True)
def update_book(book, noise):
"""Add tiny random noise to prices in‑place."""
book[:, 0] += noise # bid price
book[:, 2] += noise # ask price
return book
@jit(nopython=True)
def best_bid_ask(book):
"""Return best bid price and best ask price."""
# we assume side order: bid levels first, then ask levels
bid_prices = book[:, 0]
ask_prices = book[:, 2]
return bid_prices.max(), ask_prices.min()
@jit(nopython=True)
def decide(book):
bb, ba = best_bid_ask(book)
spread = ba - bb
if spread < 0.02:
# super‑simple random tie‑break just for demo
return 1 if np.random.rand() > 0.5 else -1 # 1=buy, -1=sell
return 0 # 0=hold
# ---- simulation ------------------------------------------------
start = time.perf_counter()
for i in range(100_000):
noise = np.random.uniform(-0.001, 0.001, size=LEVELS)
book = update_book(book, noise)
action = decide(book)
# do something with action … (omitted for brevity)
elapsed = time.perf_counter() - start
print(f"Processed 100k ticks in {elapsed*1e3:.2f} ms "
f"({elapsed/1e-6:.0f} ns per tick)")
Why this feels like a win:
- The entire order book lives in a single NumPy array; we mutate it in place (
update_book). - All the heavy lifting is inside
@jit‑compiled functions, turning the loop into tight machine code. - No
time.sleep, no pandas, no temporary objects — just pure numeric work.
On my laptop the naive version chugged at ~200 µs per tick; the JIT‑powered version consistently hits sub‑15 µs — a speedup of more than an order of magnitude.
Two classic traps to watch out for
“I’ll just add a tiny sleep to be nice to the CPU.”
Even a 10 µstime.sleepintroduces jitter that dwarfs the gains you’re chasing. In HFT, the OS scheduler is your enemy; busy‑waiting (or better, using a real‑time kernel) is often preferable.Premature optimisation of the wrong thing.
I once spent a day tweaking the fee calculation in Python, shaving off a few nanoseconds, while the real bottleneck was the memory copy happening when I appended a new tick to a list. Profile first — usecProfileorperf— then attack the hottest spot.
Why This New Power Matters
Now that you can process market data at microsecond scales, you’re no longer just guessing whether a strategy could work; you can prove it in a realistic latency environment. Want to test a market‑making model that posts and cancels orders every 50 µs? Go ahead. Curious how a maker‑taker fee schedule affects your edge? Run a million‑tick simulation in seconds, not hours.
The skills you’ve picked up — mutable numeric buffers, JIT compilation, careful profiling — translate directly to other performance‑critical domains: game engines, real‑time signal processing, even high‑frequency robotics. In short, you’ve leveled up from “I can write code that works” to “I can write code that wins the race.”
Your Turn
Grab the JIT‑enabled snippet above, replace the random noise with a real order‑book feed (websockets, a mock exchange, or even a CSV dump), and try adding a simple inventory‑aware market‑maker. See how the latency changes when you introduce a realistic latency model (e.g., a 20 µs simulated network delay).
Challenge: Post your results (latency, profit, or just a fun observation) in the comments. Let’s see who can shave the next microsecond off their loop!
Happy hunting, and may your spreads always be tight. 🚀
Top comments (0)