A gap-probability market maker for Bybit spot/linear markets, implemented as a Rust + Python + Numba stack.
WARNING: This software places real orders on a live exchange. Running it risks financial loss. You are solely responsible for your trading activity.
gap-mm is a two-sided quoting bot that uses order-book gap analysis to skew its bid/ask spread in real time:
- TradingNode (Rust): subscribes to Bybit's public WebSocket, maintains a lock-free L2 order book, and scans for gaps (empty tick ranges) on each side of the best bid/ask.
- Gap score: for each update, computes
gap_prob_resistance_up— a normalized score of ask-side gap liquidity. High value → more resistance above → contrarian SELL skew. Low value → more support below → contrarian BUY skew. - Signal encoding (Numba JIT): thresholds the gap score into
SIGNAL_UP / SIGNAL_DOWN / SIGNAL_NEUTRALwithCONF_HIGH / CONF_MED / CONF_LOW. - Quote calculation (Numba JIT): places the tight side 1 tick from mid and the wide side 100 ticks from mid. Neutral → both 100 ticks (sit out).
- ExecutionNode (Rust): reconciles target bid/ask with working orders via Bybit REST v5, preferring amend over cancel+replace. Tracks fills via private WebSocket.
Bybit public WS
└─> TradingNode (Rust)
└─> OrderBook-rs (lock-free L2 book)
└─> gap_prob_resistance_up + market metrics
└─> encode_signal + calculate_quotes_fast (Numba)
└─> ExecutionNode.reconcile (Rust REST OMS)
└─> Bybit REST v5 (submit / amend)
└─> Bybit private WS (fills / position)
gap_prob_resistance_up |
Signal | Quote skew |
|---|---|---|
| > 0.70 | DOWN (HIGH) | wide bid, tight ask |
| 0.50–0.70 | DOWN (MED) | wide bid, tight ask |
| ≈ 0.50 | NEUTRAL (LOW) | both wide — sit out |
| 0.30–0.50 | UP (MED) | tight bid, wide ask |
| < 0.30 | UP (HIGH) | tight bid, wide ask |
calculate_quotes_fast has no inventory control. It skews quotes purely based on the
gap-resistance alpha signal and does not penalise a growing position. Running it as-is will
expose you to adverse selection and unbounded inventory risk.
To use this as a proper market-making strategy, compute a reservation mid that combines
the alpha signal with an inventory penalty before calling calculate_quotes_fast:
reservation_mid = fair_mid + alpha_adjustment - gamma * sigma² * inventory
where:
fair_mid— raw exchange mid pricealpha_adjustment— your short-term price prediction (e.g. derived from the gap signal)gamma— risk-aversion / inventory-penalty coefficient (tune to your position limit)sigma— short-term realised volatilityinventory— current net position (positive = long, negative = short)
Pass reservation_mid as the mid_price argument and the spread will automatically
centre around an inventory-aware fair value instead of the raw mid.
The canonical reference for this approach is Avellaneda & Stoikov (2008), "High-frequency trading in a limit order book."
- No backtested edge. The gap-probability signal is a microstructure heuristic. It has not been statistically validated. Treat this as a reference implementation, not a proven profitable strategy.
- No inventory control. See the section above. You must implement your own reservation-price adjustment to run this safely in production.
- Bybit only. The WS and REST code is Bybit-specific (v5 API).
- Tick size must match your symbol. Pass
tick_sizeboth in.envand toTradingNode.start_stream(tick_size=...). Default is0.10(BTCUSDT spot). Other symbols need their correct tick size. - PostOnly orders only. The bot never crosses the spread. If the market is too fast, orders are amended rather than re-submitted.
- Rust 1.75.0+
- Python 3.12+
- Poetry
- Git (with submodule support)
# 1. Clone
git clone https://github.com/alihaskar/gap-mm
cd gap-mm
# 2. Install Python dependencies
poetry install --with dev
# 3. Build the Rust extension
cd rust_engine
poetry run maturin develop --release
cd ..
# 4. Configure
cp .env.example .env
# Edit .env — add your Bybit API key and secretAll settings live in .env (copy from .env.example):
| Variable | Default | Description |
|---|---|---|
BYBIT_API_KEY |
— | Bybit API key |
BYBIT_API_SECRET |
— | Bybit API secret |
TRADING_SYMBOL |
BTCUSDT |
Trading pair |
MARKET_TYPE |
spot |
spot or linear |
TICK_SIZE |
0.10 |
Minimum price increment for the symbol |
MAX_POSITION_SIZE |
0.01 |
Maximum net position in base currency |
MIN_ORDER_SIZE |
0.001 |
Minimum order size per quote |
MIN_UPDATE_INTERVAL |
0.0 |
Seconds between order updates (0 = unlimited) |
poetry run python -m gap_mmThe bot will ask for YES confirmation before placing any orders.
poetry run python examples/minimal_stream.pyfrom rust_engine import TradingNode, ExecutionNode
from gap_mm.engine import encode_signal, calculate_quotes_fast
stream = TradingNode()
executor = ExecutionNode(
api_key="...", api_secret="...",
symbol="BTCUSDT", market_type="spot",
tick_size=0.10, max_position=0.01, min_order_size=0.001,
)
def on_update(data):
mid = data["mid_price"]
gap_score = data["gap_prob_resistance_up"]
signal, conf = encode_signal(gap_score)
bid, ask, _, _, _ = calculate_quotes_fast(mid, signal, conf, tick_size=0.10)
result = executor.reconcile(target_bid=bid, target_ask=ask)
stream.start_stream(on_update, symbol="BTCUSDT", tick_size=0.10)Each callback receives a dict with:
| Field | Description |
|---|---|
bid, ask |
Best bid/ask |
mid_price |
Mid price |
spread, spread_bps |
Absolute spread / spread in basis points |
imbalance |
Order-book imbalance (−1 to +1) |
bid_depth_5, ask_depth_5 |
Quantity in top 5 levels |
timestamp |
Exchange timestamp (ms) |
gap_prob_resistance_up |
Normalized ask-side gap score (0–1) |
gap_distance_up |
Empty ticks above best ask (0–100) |
gap_distance_dn |
Empty ticks below best bid (0–100) |
liquidity_up |
Volume in 5 levels beyond ask gap |
liquidity_dn |
Volume in 5 levels beyond bid gap |
Measured on an AMD Ryzen 9 7945HX (24 cores) / Windows 11 machine against a localhost mock HTTP server (loopback RTT ≈ 50 µs). Run the benchmark yourself with:
poetry run python tests/benchmarks/bench_latency.py| Segment | p50 | p99 | What it covers |
|---|---|---|---|
| Numba signal path | 200 ns | 300 ns | encode_signal + calculate_quotes_fast |
| Python tick dispatch | 300 ns | 500 ns | dict unpack + Numba calls + price-change guard |
reconcile() roundtrip |
432 µs | ~900 µs | Python→Rust FFI + 2× concurrent reqwest HTTP + JSON parse |
| Full pipeline | 438 µs | ~900 µs | tick arrival → both orders on the wire |
| Segment | p50 | p99 |
|---|---|---|
reconcile() roundtrip |
647 µs | 901 µs |
| Full pipeline | 649 µs | 901 µs |
What changed:
reconcile_ordersnow fires bid and ask viatokio::join!— both HTTP calls in-flight simultaneously, cutting median latency ~33 % (647 µs → 432 µs).OrderState/PositionStateconverted fromRwLock<HashMap>toDashMap— removes async lock contention when both sides update concurrently.- Tokio worker threads auto-pinned to the last 2 physical cores at startup (
[gap-mm] runtime 'exec' pinning workers to cores [23, 22]), reducing scheduling jitter.
Key takeaway: The signal math is ~300 ns — effectively free. The bottleneck is the REST round-trip. Moving to WebSocket order placement would collapse end-to-end latency to the ~300 ns range.
# Python unit + integration tests
poetry run pytest tests/ -v
# Rust unit tests
cd rust_engine && cargo test
# Latency benchmarks (prints report, no pass/fail)
poetry run python tests/benchmarks/bench_latency.pygap-mm/
├── src/
│ └── gap_mm/
│ ├── __init__.py # public API
│ ├── engine.py # Numba JIT: signal encoding, quote calc, P&L
│ ├── live.py # LiveTradingEngine
│ └── __main__.py # CLI entrypoint: python -m gap_mm
├── rust_engine/
│ └── src/
│ ├── bybit.rs # WS streaming, OrderBookState, gap analysis
│ ├── execution.rs # OMS/EMS: order state, reconciliation, position
│ ├── private_ws.rs # Private WS: fills
│ ├── lib.rs # PyO3 bindings
│ └── main.rs # Standalone binary (optional)
├── tests/
│ ├── unit/ # Pure Python, no network
│ ├── integration/ # Requires rust_engine build; REST mocked
│ └── benchmarks/ # Latency benchmark script
├── examples/
│ └── minimal_stream.py # Stream only, no orders
├── .env.example
├── pyproject.toml
├── LICENSE # MIT
└── CONTRIBUTING.md
The order-book engine uses orderbook-rs by Joaquín Béjar García (MIT license), pulled from crates.io.
MIT. See LICENSE.
Ali Askar (@alihaskar)