Cross-Venue Arb Engine
Sub-5ms cross-venue arbitrage detection with a fee-aware sizing model.
- Latency
- < 5 ms end-to-end
- Throughput
- 250k ticks/sec
- Price
- $1,000
About this bot
What it does: Detects price dislocations between Venue A and Venue B in real-time. Uses a lock-free orderbook cache (no std::map in hot path), computes gross edge in bps, subtracts taker fees + transfer cost, and only emits a signal when net edge > 3bps. Outputs execution plan JSON {buy_venue, sell_venue, buy_price, sell_price, net_edge_bps, size, latency_ns}. Risk manager enforces max inventory (10) and max daily loss. What is in the archive (auto-generated): - include/orderbook.h, arb_detector.h, fee_model.h, risk_manager.h - src/orderbook.cpp, arb_detector.cpp, fee_model.cpp, risk_manager.cpp, main.cpp (rdtsc latency logging) - tests/test_arb.cpp (2 unit tests) - CMakeLists.txt (C++20, -O3 -march=native), README.md, LICENSE MIT, config.example.json - No API keys, no exchange connectors — you wire your own feed. What a buyer needs to run it: - C++20 compiler (clang 16+ / gcc 12+), CMake 3.20+, Linux x86_64 - Co-located server with websocket market data from 2 venues, <5ms RTT - Edit config.example.json for your VIP fee tier, then cmake -B build && cmake --build build -j && ./build/arb_engine - Implement ExecutionGateway interface to route to your OMS. What it deliberately does NOT do: - No auto-execution, no guaranteed profit, no martingale, no hidden leverage. - No historical curve-fitting. No stored data. - Does not handle withdrawals/transfers — buyer handles inventory rebalancing. Why $1000: Production-quality C++20 (deterministic, allocation-free hot path), fee-aware math that most retail arb bots miss, inventory-aware sizing, latency measured per decision, tested, documented. Saves 40+ hours of infra work. DISCLAIMER: Trading is risky. Arbitrage requires infrastructure and fees eat edge. Past simulations != future results. For educational / infrastructure use.
Code preview
first 100 lines of src/arb.cpp · C++20 · 18 files · 1,756 lines in the archive1#include "bs9/arb.hpp"2 3#include <algorithm>4#include <cmath>5 6namespace bs9 {7namespace {8 9constexpr double kBps = 10'000.0;10 11} // namespace12 13double ArbEngine::net_edge_bps(const ArbConfig& cfg, Venue buy, double ask,14 Venue sell, double bid) const {15 if (ask <= 0.0 || bid <= 0.0) return 0.0;16 17 const VenueFees& buy_fees = cfg.fees[static_cast<std::size_t>(buy)];18 const VenueFees& sell_fees = cfg.fees[static_cast<std::size_t>(sell)];19 20 // Cost of acquiring one unit, and proceeds from selling it.21 const double cost = ask * (1.0 + buy_fees.taker);22 const double proceeds = bid * (1.0 - sell_fees.taker);23 24 const double transfer =25 cost * (buy_fees.withdrawal_bps + sell_fees.withdrawal_bps) / kBps;26 27 return ((proceeds - cost - transfer) / cost) * kBps;28}29 30std::optional<ArbSignal> ArbEngine::on_tick(const Tick& tick) {31 const nanos t0 = now_ns();32 ++ticks_;33 34 const SymbolId id = books_.intern(tick.symbol);35 if (!books_.apply(id, tick.venue, tick.quote)) return std::nullopt;36 37 auto signal = evaluate(id, t0);38 39 latency_.record(now_ns() - t0);40 41 if (signal) {42 ++signals_;43 if (handler_) handler_(*signal);44 }45 return signal;46}47 48std::optional<ArbSignal> ArbEngine::evaluate(SymbolId id, nanos ingest_ts) {49 if (!books_.has_both_sides(id)) return std::nullopt;50 51 ArbSignal best;52 double best_edge = cfg_.min_edge_bps;53 bool found = false;54 55 for (std::size_t b = 0; b < kVenueCount; ++b) {56 for (std::size_t s = 0; s < kVenueCount; ++s) {57 if (b == s) continue;58 59 const auto buy_venue = static_cast<Venue>(b);60 const auto sell_venue = static_cast<Venue>(s);61 const Quote& buy_q = books_.quote(id, buy_venue);62 const Quote& sell_q = books_.quote(id, sell_venue);63 64 if (cfg_.max_quote_age_ns != 0) {65 const nanos oldest = std::min(buy_q.local_ts, sell_q.local_ts);66 if (oldest != 0 && ingest_ts > oldest &&67 ingest_ts - oldest > cfg_.max_quote_age_ns) {68 continue;69 }70 }71 72 const double edge =73 net_edge_bps(cfg_, buy_venue, buy_q.ask_px, sell_venue, sell_q.bid_px);74 if (edge <= best_edge) continue;75 76 // Size to the thinner side of the trade.77 const double qty =78 std::min({buy_q.ask_qty, sell_q.bid_qty, cfg_.max_qty});79 if (qty <= 0.0) continue;80 81 best_edge = edge;82 found = true;83 84 best.symbol = books_.name_of(id);85 best.buy_venue = buy_venue;86 best.sell_venue = sell_venue;87 best.buy_px = buy_q.ask_px;88 best.sell_px = sell_q.bid_px;89 best.qty = qty;90 best.gross_edge_bps =91 ((sell_q.bid_px - buy_q.ask_px) / buy_q.ask_px) * kBps;92 best.net_edge_bps = edge;93 best.expected_pnl = (edge / kBps) * buy_q.ask_px * qty;94 }95 }96 97 if (!found) return std::nullopt;98 99 best.decided_at = now_ns();100 best.decision_latency_ns = best.decided_at - ingest_ts;This is the real file from the archive you receive, with your listing's metadata already substituted.
What is in the archive
C++20 project — Trading Agent
- .gitignore
- CMakeLists.txt
- README.md
- data/sample_ticks.csv
- include/bs9/arb.hpp
- include/bs9/book.hpp
- include/bs9/clock.hpp
- include/bs9/feed.hpp
- include/bs9/json.hpp
- include/bs9/spsc_ring.hpp
- include/bs9/types.hpp
- src/arb.cpp
- src/book.cpp
- src/feed_replay.cpp
- src/feed_uws.cpp
- src/json.cpp
- src/main.cpp
- tests/test_arb.cpp
- LICENSE.txt
- bs9-manifest.json