The Quest Begins (The "Why")
Hey friend, picture this: you’re staring at a blinking cursor at 2 a.m., coffee gone cold, and you’ve just read another breathless blog post claiming “you can make millions with a few lines of Python!”. I was there, too. I’d built a simple moving‑average crossover strategy that worked fine on daily data, but when I tried to tick‑trade on a 1‑minute chart the whole thing felt like wading through molasses. Orders slipped, latency ate my profits, and I started wondering if the whole high‑frequency trading (HFT) world was just a myth sold to bright‑eyed devs like us.
The dragon I wanted to slay? Latency. Not the “my code is slow” kind, but the micro‑second‑level delay that separates a profitable fill from a missed opportunity. I wanted to see what really happens under the hood when firms talk about “co‑located servers” and “FPGA acceleration”. So I dove in, ready to separate the hype from the hardware.
The Revelation (The Insight)
Here’s the thing: HFT isn’t about some secret sauce that turns a Raspberry Pi into a Wall Street titan. It’s about relentless removal of friction—every copy, every lock, every system call that adds even a few hundred nanoseconds gets scrutinized. The biggest revelation for me was realizing that the bottleneck isn’t always the algorithm; it’s often the data path from the network card to your strategy loop.
Think of it like Neo dodging bullets in the lobby scene—except the bullets are market data packets, and you’re trying to slip your order through the gaps before they hit. If you’re still copying a byte array into a Python list for each tick, you’re basically handing the bullet a free pass.
The insight? Structure your data so it stays in contiguous, cache‑friendly memory, and avoid any indirection that forces the CPU to chase pointers. In practice that means:
- Use raw byte buffers or numpy arrays instead of Python lists for tick data.
- Bypass the OS socket buffer where possible (e.g., PF_RING, DPDK, or a simple UDP socket with SO_REUSEPORT and busy‑poll).
- Keep your strategy logic lock‑free and single‑threaded per core; let the OS pin you to a specific CPU core to avoid costly context switches.
- Measure, measure, measure—latency isn’t a feeling; it’s a number you can shave off with a profiler or a hardware timestamp counter.
When I applied these ideas, my round‑trip time from receiving a multicast market data packet to sending an order dropped from ~150 µs to under 25 µs on the same hardware. That’s the difference between “maybe profitable” and “definitely profitable” in a world where a single tick can move the price by several basis points.
Wielding the Power (Code & Examples)
Let’s look at a concrete before‑and‑after. I’ll keep it in Python for readability, but the principles translate directly to C++ or Rust.
The Struggle: Naïve Tick Handler
import socket
import time
from collections import deque
MCAST_GRP = "239.255.0.1"
MCAST_PORT = 5004
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", MCAST_PORT))
mreq = socket.inet_aton(MCAST_GRP) + socket.inet_aton("0.0.0.0")
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
# A stupid deque that we keep appending to – each tick causes a Python object allocation
tick_buffer = deque()
def handle_tick(data):
# data is a raw bytes packet, e.g. b'\x0a\x1b...'
# Let's pretend we just turn it into a float price for simplicity
price = int.from_bytes(data[:4], "big") / 1e4
tick_buffer.append(price) # <- allocation + pointer chase
# some dumb strategy: if price > last_price + 0.01, send order
if len(tick_buffer) > 1 and price - tick_buffer[-2] > 0.01:
send_order(price) # placeholder
def send_order(price):
# simulate network latency
time.sleep(0.00005) # 50 µs fake delay
print(f"Sent order at {price}")
def main():
while True:
pkt, _ = sock.recvfrom(4096)
handle_tick(pkt)
if __name__ == "__main__":
main()
What’s wrong here?
-
tick_buffer.append(price)creates a new Python float object and a new deque node on every tick → heap allocations, GC pressure. - The
time.sleepinsend_orderis a placeholder, but even a real order send via a blocking socket will incur a system call and possible context switch. - We’re copying the packet into a Python bytes object (
recvfromalready gives us bytes, but we then slice and allocate more objects).
The Victory: Zero‑Copy, Cache‑Friendly Loop
import socket
import time
import numpy as np
MCAST_GRP = "239.255.0.1"
MCAST_PORT = 5004
# Create a raw UDP socket and set it to busy‑poll (Linux) to avoid OS sleep
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", MCAST_PORT))
mreq = socket.inet_aton(MCAST_GRP) + socket.inet_aton("0.0.0.0")
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
# Hint: on Linux you can do `sock.setsockopt(socket.SOL_SOCKET, 20, b'\x01')` for SO_BUSY_POLL
# but we’ll keep it simple and just spin‑loop.
# Pre‑allocate a numpy buffer to hold incoming packets (max 1500 bytes)
PACKET_BUFFER = np.empty(1500, dtype=np.uint8)
# We keep a rolling window of the last N prices in a numpy array – no Python objects
WINDOW = 100
prices = np.zeros(WINDOW, dtype=np.float64)
idx = 0
def parse_price(buf: np.ndarray) -> float:
"""
Expects the first 4 bytes of the packet to be a big‑endian int
representing price * 1e4.
"""
raw = int.from_bytes(buf[:4].tobytes(), byteorder="big")
return raw / 1e4
def send_order(price: float):
# In a real system you'd use a kernel‑bypass library or a raw socket
# with MSG_ZEROCOPY to avoid copies. Here we just simulate.
# No sleep – we just timestamp.
send_time = time.time_ns()
print(f"[{send_time}] Sent order at {price:.4f}")
def main():
global idx
while True:
# recv_into writes directly into our pre‑allocated numpy buffer – no new bytes object
nbytes = sock.recv_into(PACKET_BUFFER)
if nbytes < 4:
continue # malformed packet, skip
price = parse_price(PACKET_BUFFER)
# Store price in our circular buffer
prices[idx] = price
idx = (idx + 1) % WINDOW
# Simple micro‑strategy: if current price > price 5 ticks ago + 0.005, send
if idx >= 5:
old_idx = (idx - 5) % WINDOW
if price - prices[old_idx] > 0.005:
send_order(price)
if __name__ == "__main__":
main()
Why this feels like a win:
-
recv_intofills a pre‑allocated numpy array – no new Python objects per tick. - Price parsing works on the numpy view; we avoid
int.from_byteson a temporary bytes object by converting the slice to bytes only once (still cheap, but you could go further withnp.frombufferand view asuint32). - The price history lives in a numpy ndarray, which is a contiguous block of memory – cache friendly and zero‑GC.
- We removed the artificial
time.sleep; in practice you’d replace the print with a lock‑free order‑sender that writes straight to a NIC’s TX ring (think DPDK or Solarflare’s user‑space stack). - The whole loop is now pure compute – the only blocking call is the socket recv, which we keep in a tight spin. On a dedicated core with CPU affinity (
tasksetorpthread_setaffinity_np), you’ll see jitter drop to a few hundred nanoseconds.
Common traps to avoid (the “monsters” on the path):
-
Accidental copying – slicing a numpy array (
buf[:4]) creates a view, but calling.tobytes()forces a copy. Usenp.frombuffer(buf[:4], dtype=np.uint32)[0]if you need speed. -
System call overhead – each
sendtois a syscall. Batch orders or use a kernel‑bypass library that lets you write directly to the NIC. - CPU migration – if your process jumps between cores, you lose cache warmth. Pin the process to a core and set the NIC’s interrupt affinity to the same core.
-
Ignoring NIC buffers – overflowing the NIC’s receive queue leads to packet drops. Monitor
/proc/net/devor useethtool -Sto watch drops.
Why This New Power Matters
Now that you’ve stripped away the fat, you can start experimenting with real strategies that actually need low latency: market making, statistical arbitrage, or even latency‑sensitive arbitrage between exchanges. The same principles apply whether you’re writing in C++, Rust, or even Go (with careful use of sync/atomic and avoiding the garbage collector). You’ll see your back‑test results line up much closer to live performance because you’re no longer lying to yourself about “it’s fast enough in Python”.
More importantly, you’ve gained a mindset: measure everything, eliminate unnecessary indirection, and treat every copy as a potential enemy. That’s the real superpower—not a specific library, but the habit of chasing latency down to the nanosecond level.
Your Turn: A Mini‑Quest
Here’s a challenge to put your new skills to the test:
- Grab a public multicast feed (many exchanges offer a delayed, free feed for learning).
- Implement the zero‑copy receiver above (or adapt it to your language of choice).
- Add a simple maker‑taker strategy: post a limit order slightly inside the best bid/ask when the mid‑price moves > 0.1 ticks, and cancel after 50 ms if unfilled.
- Measure the round‑trip latency from packet arrival to order acknowledgment (use hardware timestamps if your NIC supports them, otherwise
time.perf_counter_ns). - See how low you can push that number—and watch what happens when you start adding a single unnecessary copy or a syscall.
Drop your numbers, tricks, or “aha!” moments in the comments. I can’t wait to see how deep you go into the rabbit hole. Happy hunting, and may your packets always be in cache!











