A delta-neutral arbitrage system for decentralized perpetual futures, built from scratch in Rust.
The same perpetual contract trades on many exchanges, and their prices constantly drift apart. This bot watches several venues' order books at once, and the moment a price gap opens it captures the difference — going long on the cheap venue and short on the expensive one at the same instant. That leaves a market-neutral position, which it holds until the spread converges and then closes. Everything the strategy needs lives in this repository: the exchange WebSocket clients, the cryptographic order signing, the execution engine, the risk manager, the storage layer, a backtester, and a real-time web dashboard — with no trading framework underneath.
The live dashboard — venue balances plus active and closed trades, each with its fills, slippage, fees, funding, and PnL. (click to enlarge)
Trading
- Delta-neutral by construction — each position is long one venue and short another in equal size, so it carries almost no market risk. The only exposure is the spread itself, and spreads converge.
- Multi-venue and self-directing — connects to several exchanges at once, continuously scans every pair of them, and enters wherever the spread is widest, in either direction.
- Parallel two-leg execution — both orders fire at the same instant to catch spreads that last only seconds, with automatic recovery when only one side fills.
- Liquidation-aware risk control — a stop-loss anchored to each position's liquidation price (not an arbitrary percentage), per-exchange capital limits, and a daily-loss kill switch.
- Built to survive a live venue — auto-reconnecting market data, staleness watchdogs, and detection of positions closed outside the bot (server-side stop-outs, liquidations, manual actions).
Operating it
- Real-time dashboard — live positions, balances, spreads, per-leg latency, and full trade history. Most strategy parameters can be changed at runtime, no restart required.
- Telegram bot — open / close / halt alerts, plus interactive status and trade queries.
- Backtesting and optimization — replay recorded market data and search strategy parameters (grid / TPE / CMA-ES / funnel) on the exact same engine the live bot runs.
cheap venue rich venue
┌────────────────────┐ ┌────────────────────┐
│ BUY → go LONG │ ◄───────► │ SELL → go SHORT │
└────────────────────┘ equal └────────────────────┘
│ qty │
└────────── delta-neutral ─────────┘
(market risk ≈ 0)
profit = entry_spread − exit_cost (realized only on close)
- Detect. A spread detector reads both books tick-by-tick and computes the executable spread as a VWAP through the top levels — not just best bid/ask — so a signal reflects the price actually achievable for the intended size, after walking the book.
- Enter. When the net spread (after fees) clears the threshold, both legs fire in parallel — two concurrent tasks, one per venue — to capture the gap before it closes. Leg-risk recovery handles the case where only one side fills.
- Hold. The position is delta-neutral: a price move gains on one leg and loses the same on the other. The only exposure is the spread, which the strategy waits out.
- Exit. When captured PnL crosses the take-profit threshold, both legs unwind in parallel. A liquidation-anchored stop-loss guards each position and re-prices itself as the liquidation point drifts.
Strategy, execution flow, and risk logic in full: docs/DESIGN.md.
┌──────────────────────────────────────────────┐
│ WebSocket connectors (one per exchange) │
│ order books + account / fill streams │
└───────────────────────┬──────────────────────┘
│ best levels (mpsc)
▼
┌──────────────────────────────────────────────┐
│ ArbEngine │
│ SpreadDetector → OpenSignal → execute_open │ parallel legs
│ PositionTracker → CloseSignal → execute_close │ parallel legs
│ stop-loss drift monitor · funding tracking │
└───────┬───────────────┬───────────────┬───────┘
▼ ▼ ▼
RiskManager SQLite store Web dashboard
capital · limits trades · orders SSE · Telegram
kill switch Parquet ticks runtime config
Three Cargo crates — roughly 67k lines of Rust, plus another 30k of tests, many of them integration tests that exercise the live venue APIs:
| Crate | Responsibility |
|---|---|
| bot/ | The arb_bot runtime — exchange connectors, strategy, parallel execution, risk manager, SQLite, order signing, web dashboard, Telegram |
| common/ | Shared core — domain types, constants, PnL & stop-loss formulas, signal detection, the backtest simulator |
| optimizer/ | Offline parameter search over recorded ticks |
Module-level map and data flow: docs/ARCHITECTURE.md.
The parts that were the most interesting to build.
Decentralized exchanges don't authenticate with an API key and a password — every order is a signed cryptographic message, and each venue uses a different scheme. Most have no Rust SDK, so the signing is implemented here directly:
| Venue | Architecture | Order-signing scheme |
|---|---|---|
| Lighter | Custom ZK-rollup L2 | Schnorr over the ECgFp5 curve + Poseidon2 hash, on the Goldilocks field |
| Extended | Off-chain CLOB + StarkNet settlement | STARK-curve ECDSA + Pedersen hash (RFC-6979) |
| Zero1 (01.xyz) | StarkNet perps, off-chain matching | Ed25519 wallet key + ephemeral session keys, Protobuf wire format |
| Hyperliquid | On-chain CLOB on a purpose-built L1 | EIP-712 typed data (secp256k1) over MessagePack |
| Bybit | Centralized exchange | HMAC-SHA256 request authentication |
Lighter needs the most from-scratch work: Goldilocks-field arithmetic, the GFp5 extension field, the ECgFp5 elliptic curve, the Poseidon2 sponge, and Schnorr signatures — 954 lines with no external crypto dependencies (bot/src/signing/ecgfp5.rs), since nothing off the shelf implements it.
Spreads on these venues last seconds, so the trading hot path is kept lean:
- Parallel dual-leg execution — both orders leave the process at once, not one after the other, so the window between legs is bounded by the network rather than by the code.
fastwebsocketswith SIMD frame unmasking and split read/write tasks; the read path is kept offselect!because the frame reader isn't cancel-safe.sonic-rsJSON (2.5–6.8× faster thanserde_json), jemalloc, andrust_decimalinstead off64so prices stay exact.- A single-threaded hot core — the position tracker is a plain
Vec, not anArc<Mutex<…>>, so the critical path stays lock-free; heavy work like serialization and DB writes is offloaded to background tasks. - Per-leg latency instrumentation splits every trade into four measured segments — wire latency, book age at decision, decision→send, and send→fill — shown live in the dashboard, which separates a slow venue from a slow decision.
Adding a venue doesn't touch the strategy. Every exchange plugs in behind two traits — ExchangeFactory (bootstrap: symbols, fees, WebSocket tasks) and ExchangeClient (place / close / stop-loss / positions) — and the detector scans all C(N, 2) pairs of enabled venues, taking the direction with the best spread. Exactly one function in the codebase names concrete exchanges (factory.rs); everything downstream is venue-agnostic, and the same generic core drives the live bot, the backtester, and the optimizer.
The bot records every tick it sees to Parquet, byte-identical to what the detector observed, so a replay reproduces the live view exactly. The simulator (common/src/sim.rs) shares its spread, VWAP, PnL, and stop-loss math with the production path — and byte-exact snapshot tests fail the build if a refactor makes the two diverge. On top of it runs a parameter optimizer with four search modes, parallelized with rayon.
The dashboard is a dependency-free vanilla-JS single-page app served by an Axum backend over server-sent events — 58 endpoints, Argon2-hashed logins, and role-based permissions. It renders live state and doubles as the control surface: most parameters are edited in place and persisted to a local override file, applied without a restart.
Chosen for speed and correctness on the trading path:
- Async runtime — Tokio, with
fastwebsockets(SIMD unmasking, raw frame access) for every market and account stream. - TLS / HTTP —
rustlson theaws-lc-rsbackend,hyperfor REST order placement. - Serialization —
sonic-rsfor hot-path JSON,prost(Protobuf) andrmp-serde(MessagePack) for the venues that require them. - Numerics —
rust_decimalfor all prices and sizes; nof64where money is involved. - Storage —
rusqlite(bundled SQLite) as the single source of truth;parquet+arrowfor tick capture. - Web —
axumwith server-sent events; a vanilla-JS single-page app. - Crypto —
starknet-crypto,ed25519-dalek,alloy(EIP-712), and a from-scratch ECgFp5 / Poseidon2 stack.
Requires a Rust toolchain and SQLite, plus API credentials for at least one enabled venue in a local .env.
# Build and launch in clean mode (flattens any existing exchange positions first)
bash tools/bot_start.sh --clean --buildThe dashboard comes up at http://127.0.0.1:3030. The initial admin password is generated on first run and written back to .env. Stop with bash tools/bot_stop.sh.
Startup modes, config files, and server setup: docs/LOCAL_SETUP.md.
bot/ runtime crate — connectors, strategy, execution, risk, storage, signing, web
common/ shared core — types, formulas, signal detection, backtest simulator
optimizer/ offline parameter search
config/ default.toml (defaults) · local.toml (runtime overrides) · connections.toml
db/ SQLite database
data/ Parquet tick history + funding data
tools/ start/stop scripts and tick utilities
docs/ design, architecture, and per-exchange notes
Further reading: DESIGN.md (strategy, trade lifecycle, leg risk, liquidation math), ARCHITECTURE.md (modules, data flow, storage model), and PATTERNS.md (the project's async, atomic, and testing conventions).