Testing, Reliability and Deterministic Builds
Why this matters for HFT engineers (beginner-friendly)
- In HFT, a tiny bug in market data parsing or an unreproducible build can cause real money loss or missed trades. Tests + deterministic builds are your safety net.
- Think of tests as pre-game practice drills (free throws, fast-breaks). Deterministic builds are like running the same playbook each time — no surprises at game time.
Key concepts at a glance
- Unit tests: small, fast checks for pure functions (e.g.,
VWAP
, message parsers). - Integration tests: run components together (feed handler → matching logic → order gateway) in a sandbox.
- Fuzz testing: throw random / malformed packets at parsers to find crashes or undefined behavior.
- Deterministic builds: produce byte-for-byte reproducible binaries/artifacts so CI artifacts are trustworthy.
- CI pipelines: automate tests, static analysis, fuzzing, and artifact signing on every commit.
ASCII diagram — a minimal CI flow for HFT microservice
[push to git] -> [CI: compile (deterministic)] -> [unit tests + linters] |-> [integration tests w/ replay] |-> [fuzzing harness (sanitizers)] --> [artifact: signed, reproducible .tar.gz]
Practical tips — testing and determinism for a beginner coming from C++, Python, Java, JS
- Start with unit tests in both languages:
gtest
or a tiny home-grown harness in C++;pytest
in Python. - Keep pure logic (math, parsing) in small, testable functions. If you can test
VWAP
in isolation, you avoid whole-system runs early. - For message parsing, add golden-file tests: store a known binary multicast packet and assert parser fields match expected values.
- Fuzzing path: begin with property-based tests (Hypothesis for Python; libFuzzer/oss-fuzz for C++ when you scale). Run sanitizers (
-fsanitize=address,undefined
) in CI to catch UB. - Deterministic runtime: avoid calling
rand()
without seed. Usestd::mt19937_64
with a fixed seed for deterministic replays (see code). - Deterministic builds: set
SOURCE_DATE_EPOCH
, avoid embedding build timestamps, and strip or fix linker--build-id
. Build with reproducible flags in CI.
Concrete checklist for your repo
- Unit tests for parsing, VWAP, and order-serialization.
- Integration replay tests using deterministic tick generator / pcap replay.
- Fuzz harness for parsers and message handling.
- CI job that sets reproducible env vars and produces signed artifacts.
Challenge — try this now
Run the C++ example below. It:
- generates deterministic
ticks
withstd::mt19937_64
, - computes a
VWAP
and checksum, - verifies deterministic behavior (same seed → same checksum),
- runs a tiny fuzz loop to ensure no NaNs/crashes for many seeds.
- generates deterministic
Modify the seed and
N
(number of ticks) to see when floating-point differences appear — it's like changing a game tempo.
Code (below) is the runnable test-harness. After running it locally, try integrating it into your CI as a unit
job.
1#include <iostream>
2#include <vector>
3#include <numeric>
4#include <random>
5#include <cmath>
6
7using namespace std;
8
9struct Tick { double price; int size; uint64_t ts; };
10
11vector<Tick> gen_ticks(size_t n, uint64_t seed=12345) {
12 std::mt19937_64 rng(seed);
13 std::uniform_real_distribution<double> price_dist(100.0, 101.0);
14 std::uniform_int_distribution<int> size_dist(1, 10);
15 vector<Tick> ticks; ticks.reserve(n);
16 uint64_t ts = 0;
17 for (size_t i = 0; i < n; ++i) {
18 ticks.push_back({price_dist(rng), size_dist(rng), ts++});
19 }
20 return ticks;
21}
22
23// Method A: straightforward VWAP
24double vwap_a(const vector<Tick>& ticks) {
25 double pv = 0.0; double vol = 0.0;
26 for (auto &t : ticks) { pv += t.price * t.size; vol += t.size; }
27 return vol ? pv / vol : 0.0;
28}
29
30// Method B: use std::accumulate with lambdas (same result expected)
31double vwap_b(const vector<Tick>& ticks) {
32 double pv = std::accumulate(ticks.begin(), ticks.end(), 0.0,
33 [](double acc, const Tick &t){ return acc + t.price * t.size; });
34 double vol = std::accumulate(ticks.begin(), ticks.end(), 0.0,
35 [](double acc, const Tick &t){ return acc + t.size; });
36 return vol ? pv / vol : 0.0;
37}
38
39int main() {
40 const size_t N = 1000;
41 const uint64_t seed = 424242ULL; // change this to experiment
42
43 auto ticks1 = gen_ticks(N, seed);
44 auto ticks2 = gen_ticks(N, seed); // regenerate to prove determinism
45
46 double pv1 = vwap_a(ticks1);
47 double pv2 = vwap_b(ticks2);
48
49 // checksum = sum(price * size) to quickly compare streams
50 double checksum1 = std::accumulate(ticks1.begin(), ticks1.end(), 0.0,
51 [](double acc, const Tick &t){ return acc + t.price * t.size; });
52 double checksum2 = std::accumulate(ticks2.begin(), ticks2.end(), 0.0,
53 [](double acc, const Tick &t){ return acc + t.price * t.size; });
54
55 cout << "VWAP method A: " << pv1 << "\n";
56 cout << "VWAP method B: " << pv2 << "\n";
57 cout << "Checksums: " << checksum1 << " " << checksum2 << "\n";
58
59 bool deterministic = fabs(checksum1 - checksum2) < 1e-12;
60 bool agree = fabs(pv1 - pv2) < 1e-12;
61
62 cout << (deterministic ? "[PASS] deterministic replay" : "[FAIL] non-deterministic") << "\n";
63 cout << (agree ? "[PASS] VWAP agreement" : "[FAIL] VWAP mismatch") << "\n";
64
65 // tiny fuzz loop: make sure we never get NaN or inf for many seeds
66 int bad = 0;
67 for (uint64_t s = 0; s < 500; ++s) {
68 auto t = gen_ticks(200, s);
69 double v = vwap_a(t);
70 if (!std::isfinite(v)) ++bad;
71 }
72 cout << "Fuzz checks (NaN/inf count): " << bad << "\n";
73
74 if (!deterministic || !agree || bad > 0) {
75 cout << "One or more tests failed.\n";
76 return 1;
77 }
78
79 cout << "All basic tests passed. Integrate into CI as a unit job.\n";
80 return 0;
81}
Next steps
- Add this harness as a
unit
job in CI and gate merges on it. - Replace the tiny harness with
gtest
for readable test reports when you grow the suite. - For deterministic builds: set
SOURCE_DATE_EPOCH
, avoid embedding timestamps, and ask your CI to produce a signed tarball and store it as a release artifact.
Quick reading suggestions
GoogleTest
(C++) andpytest
(Python) guideslibFuzzer
/oss-fuzz
for C++ fuzzing- Reproducible Builds project for concrete build flags and CI recipes
Now run the example and try changing seed
and N
. If you like basketball, imagine tweaking the tempo of a Kobe-era fast-break: small changes in rhythm can expose weaknesses — same with seeds and test inputs.
xxxxxxxxxx
}
using namespace std;
struct Tick { double price; int size; uint64_t ts; };
vector<Tick> gen_ticks(size_t n, uint64_t seed=12345) {
std::mt19937_64 rng(seed);
std::uniform_real_distribution<double> price_dist(100.0, 101.0);
std::uniform_int_distribution<int> size_dist(1, 10);
vector<Tick> ticks; ticks.reserve(n);
uint64_t ts = 0;
for (size_t i = 0; i < n; ++i) {
ticks.push_back({price_dist(rng), size_dist(rng), ts++});
}
return ticks;
}
// Method A: straightforward VWAP
double vwap_a(const vector<Tick>& ticks) {
double pv = 0.0; double vol = 0.0;
for (auto &t : ticks) { pv += t.price * t.size; vol += t.size; }
return vol ? pv / vol : 0.0;
}