Real-time cross-exchange (Binance / Kraken) order book scanner and arbitrage-spread detector, in C++20.
For the reasoning behind non-obvious design decisions, see
docs/DESIGN.md. For a chronological record of bugs
found and fixed, see docs/CHANGELOG.md.
BinanceClient (own thread, own io_context) --\
'-- REST snapshot fetch on a dedicated --> moodycamel::ConcurrentQueue<MarketEvent> --> ArbitrageEngine (own thread)
thread, never blocks the io_context |-- OrderBook (Binance) -- sync state machine, U/u bridging
KrakenClient (own thread, own io_context) --/ |-- OrderBook (Kraken) -- sync state machine, checksum-validated
'-- native "type":"snapshot" message `-- spread check -> spdlog
handles its own sync, no REST needed
- Networking:
Boost.Beast/Boost.Asio, one WebSocket connection and one dedicated thread per exchange. - JSON:
simdjson(ondemandAPI). Prices/quantities go straight fromsimdjson'sstring_viewintostd::from_chars(FastParse.hpp) -- nostd::string/std::stodround-trip on the hot path. - Thread handoff:
moodycamel::ConcurrentQueue. Delta events are trivially-copyable and allocation-free; snapshot events (rare, once per connect) carry ashared_ptr<SnapshotData>. - Order book: two preallocated, fixed arrays (bids desc, asks asc) per exchange, no locks or node allocations -- the strategy thread is their only reader and writer.
- Decision gate: depth-weighted VWAP, per-venue taker fees, an explicit slippage buffer, lot-step truncation, and minimum-order checks precede every execution request. A raw crossed quote alone is never sufficient.
- Risk gate: every order passes through
RiskManager(open-order cap, rate limit, exposure ceiling, kill switch) before it's placed. - Execution: market orders only, dual-leg arbitrage attempts tracked
through fill/reject with legging protection. See
docs/DESIGN.md.
Dependencies pulled automatically via FetchContent: simdjson,
concurrentqueue, spdlog.
Boost and OpenSSL are not vendored (too large / better handled by your system package manager or vcpkg):
# using vcpkg
vcpkg install boost-beast boost-asio openssl
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=<vcpkg-root>/scripts/buildsystems/vcpkg.cmake
cmake --build build --config ReleaseRun the test suite (all fixture-based, no network required):
ctest --test-dir build -C Release --output-on-failure.\build\Release\hft_scanner.exeConnects to Binance's btcusdt@depth stream and Kraken's book channel
for BTC/USD, and logs a warning line only when a depth-weighted trade
remains profitable after both taker fees and the configured slippage
buffer (5bps minimum net edge by default, set in main.cpp).
# Performance / capture (all opt-in)
HFT_BUSY_SPIN=1 # strategy uses _mm_pause() while the queue is empty
HFT_PIN_THREADS=1 # best-effort core affinity for network/strategy workers
HFT_RECORD_PATH=run.hftcap # asynchronously capture binary raw WS frames
# Execution (off by default -- see docs/DESIGN.md before enabling)
BINANCE_API_KEY, BINANCE_API_SECRET
KRAKEN_API_KEY, KRAKEN_API_SECRET
HFT_ENABLE_EXECUTION=1 # required to place real orders at all
# Testnet validation (Binance only -- Kraken has no Spot sandbox)
HFT_USE_TESTNET=1 # points Binance at testnet; Kraken execution
# is never constructed in this mode, regardless
# of Kraken credentials present
HFT_TESTNET_PLACE_TEST_ORDER=1 # fires one real diagnostic order on testnet funds
Never hardcode credentials, never paste them into a chat, never commit
them. Read main.cpp's comment block above the execution wiring before
ever setting HFT_ENABLE_EXECUTION=1 against a funded account.
AccountReconciler polls Binance /api/v3/account and Kraken
/0/private/Balance every 30s when execution is enabled; InventoryManager
fails closed until both replies arrive and rejects any order that would
take either venue's balance below 15% of its initial allocation.
Verified with real regression tests (ctest, all fixture/oracle-based,
no network required): order book (including array capacity-eviction),
Kraken checksum (against Kraken's own worked example), HMAC signing
(against RFC 4231), risk manager limits, trading rules, market replay,
Binance and Kraken order-response parsing.
Verified live: Binance's execution path (HFT_USE_TESTNET=1 +
HFT_TESTNET_PLACE_TEST_ORDER=1) -- confirmed against a real testnet fill,
2026-09-01. See docs/CHANGELOG.md for what that run caught and fixed.
Not yet verified:
- Kraken's execution path against a live connection -- no Spot sandbox
exists (only
demo-futures.kraken.com, which is Futures-only). Fixture tests (kraken_order_parsing_test) are the practical ceiling for pre-live confidence right now; closing this fully means either a small, tightlyRiskLimits-bounded real order, or a local mock WS/REST server. - Reconnect backoff under an actual sustained outage (implemented, unexercised beyond reasoning through it).
- Kraken's cancel-before-conclude reconciliation step is fire-and-forget,
not response-correlated (sends
cancel_order, sleeps 300ms, re-checks -- shrinks but doesn't eliminate the race it targets).
Not implemented:
- Multi-pair orchestration -- hardcoded to one Binance/Kraken pair.
Scaling this is an open design question (thread-per-pair vs. a shared
strategy thread), not a small change, given how much of
RiskManagerandExecutionEnginecurrently assume a single pair. - Limit orders -- market orders only. Not purely additive: the legging/reconciliation logic assumes orders resolve near-instantly, an assumption a resting limit order breaks. Adding this means revisiting that logic alongside the order type, not after.