BotStudio9
Trading Agent
Featured

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 archive
src/arb.cppC++20
1#include "bs9/arb.hpp"
2
3#include <algorithm>
4#include <cmath>
5
6namespace bs9 {
7namespace {
8
9constexpr double kBps = 10'000.0;
10
11} // namespace
12
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