Project 01 06

Grab

A real-time cryptocurrency arbitrage engine that turns live exchange rates into a graph and hunts for profitable cycles.

Systems engineering
C++20 · Boost.Asio · OpenSSL · Graph algorithms · CMake

Grab watches the cryptocurrency market as it moves and asks one question continuously: is there a sequence of trades that ends with more money than it started with? It keeps secure WebSocket connections open to Binance’s live ticker streams, maintains a cross-rate matrix for fourteen assets, and scans it for profitable triangular and multi-hop loops.

The idea behind it

Arbitrage is a multiplication problem. A cycle of trades pays off when the product of its exchange rates exceeds one. Multiplying floating-point rates across every candidate path is slow and numerically unpleasant, so Grab takes the logarithm of both sides. Multiplication becomes addition, and after negating each weight the condition “product greater than one” turns into “sum less than zero”.

That is the textbook definition of a negative-weight cycle. So the engine builds a directed graph where the vertices are assets, the edges are tradable pairs, and each edge weight is -ln(price) — the ask price for a direct trade, the inverse of the bid for the reverse leg. Bellman-Ford then does the work: relax every edge V-1 times, and if a V-th pass still improves something, walk the predecessor list back to recover the cycle. A back-calculation pass recomputes the compounded yield on the real prices before anything is reported, so no opportunity is announced on the strength of log-space arithmetic alone.

The parts

  • grab::connection — the networking layer. Raw TCP socket, TLS 1.3 handshake through OpenSSL, upgrade to an RFC 6455 WebSocket over Boost.Beast. It answers Binance’s ping frames, cycles the connection every 24 hours to stay ahead of the server’s own age limit, and reconnects with exponential backoff when the network drops.
  • grab::ticker — multiplexes many stream subscriptions onto a shared connection and routes decoded frames to callbacks, so the trading logic never touches frame decoding.
  • grab::graph — the cross-rate matrix, dynamic edge computation, the Bellman-Ford cycle finder, and the profit back-calculation.

Price updates arrive on an Asio thread pool and are serialised through strands rather than mutexes, so the matrix stays consistent without the reader ever taking a lock.

What I learned

The most useful lesson was that a hard problem often becomes an easy one after the right change of representation. Nothing about arbitrage detection is fast until you notice it is the same shape as a shortest-path problem; after the logarithm, a fifty-year-old algorithm solves it outright.

The second lesson was about where the difficulty actually lives. The cycle detection was a few hundred lines and worked early. The remaining months went into everything around it: sessions that die silently, exchanges that enforce connection lifetimes, prices that go stale without any error to signal it, reconnect storms, and the discipline of never trusting a number that arrived some unknown time ago. Correct concurrency turned out to be a design choice made once — strands instead of shared locks — rather than something to debug later.

Writing it in modern C++ also changed how I think about ownership. RAII, smart pointers, and a compiler set to -O3 -march=native with link-time optimisation gave me both the speed the problem needed and fewer places for a lifetime bug to hide.