Skip to content

alihaskar/gap-mm

Repository files navigation

gap-mm

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.


What it does

gap-mm is a two-sided quoting bot that uses order-book gap analysis to skew its bid/ask spread in real time:

  1. 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.
  2. 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.
  3. Signal encoding (Numba JIT): thresholds the gap score into SIGNAL_UP / SIGNAL_DOWN / SIGNAL_NEUTRAL with CONF_HIGH / CONF_MED / CONF_LOW.
  4. 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).
  5. ExecutionNode (Rust): reconciles target bid/ask with working orders via Bybit REST v5, preferring amend over cancel+replace. Tracks fills via private WebSocket.

Architecture

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)

Signal interpretation

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

Inventory control — bring your own

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 price
  • alpha_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 volatility
  • inventory — 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."


Known limitations / quirks

  • 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_size both in .env and to TradingNode.start_stream(tick_size=...). Default is 0.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.

Requirements

  • Rust 1.75.0+
  • Python 3.12+
  • Poetry
  • Git (with submodule support)

Installation

# 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 secret

Configuration

All 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)

Usage

Run the live bot

poetry run python -m gap_mm

The bot will ask for YES confirmation before placing any orders.

Stream only (no orders)

poetry run python examples/minimal_stream.py

Python API

from 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)

Market data fields

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

Performance

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

After: parallel submit + DashMap + CPU pinning

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

Baseline (sequential submit)

Segment p50 p99
reconcile() roundtrip 647 µs 901 µs
Full pipeline 649 µs 901 µs

What changed:

  • reconcile_orders now fires bid and ask via tokio::join! — both HTTP calls in-flight simultaneously, cutting median latency ~33 % (647 µs → 432 µs).
  • OrderState / PositionState converted from RwLock<HashMap> to DashMap — 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.


Running tests

# 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.py

Project structure

gap-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

Attribution

The order-book engine uses orderbook-rs by Joaquín Béjar García (MIT license), pulled from crates.io.


License

MIT. See LICENSE.

Author

Ali Askar (@alihaskar)

About

No description, website, or topics provided.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors