Mark As Completed Discussion

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. Use std::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 with std::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.
  • 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.

TEXT/X-C++SRC
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++) and pytest (Python) guides
  • libFuzzer / 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.

CPP
OUTPUT
:001 > Cmd/Ctrl-Enter to run, Cmd/Ctrl-/ to comment