Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mercury Trading Engine

Mercury is a high-performance, event-driven algorithmic trading engine written in Rust. It is designed for low-latency market-making, momentum, and ML-based strategies across multiple exchanges.

Key features

  • Event-driven core — all components communicate through a single lock-free EventBus backed by a crossbeam bounded channel.
  • Multiple exchanges — Binance, Coinbase, Bybit, Kraken, OKX, and Yahoo Finance (equities).
  • Paper trading — built-in PaperGateway with configurable simulated latency; no API keys needed.
  • Backtesting — deterministic replay of recorded Parquet tick data.
  • ML strategies — ONNX-based inference, online logistic regression, and reinforcement learning.
  • Risk controls — position limits, daily-loss guard, per-order rate limiting, and a kill switch.
  • TUI monitor — real-time ratatui dashboard connected via Unix socket IPC.
  • Prometheus metrics — latency histograms (HDR), PnL tracking, and Prometheus export.

Design philosophy

Mercury favors explicitness and performance over abstraction. Financial values use rust_decimal::Decimal throughout — no floating-point arithmetic on prices or quantities. Hot-path data structures are stack-allocated where possible (e.g. Symbol is a [u8; 16] array).

Installation

Prerequisites

  • Rust toolchain ≥ 1.85 (edition 2024) — install via rustup
  • SQLite (system library, used by sqlx)
  • OpenSSL or native-tls (used by reqwest / tokio-tungstenite)

On macOS:

brew install openssl sqlite

On Ubuntu/Debian:

apt install libssl-dev libsqlite3-dev pkg-config

Build

git clone https://github.com/amjadjibon/mercury
cd mercury
cargo build --workspace --release

Dev tools

The following tools are used for documentation and benchmarking. Install them once:

cargo install mdbook           # build this book
cargo install cargo-criterion  # benchmark runner

Verify

cargo test --workspace

All tests should pass with no warnings.

Quick Start

Paper trading (no API keys)

The fastest way to run Mercury is in paper-trading mode against a live Binance feed:

cargo run --bin mercury -- run \
  --symbol BTCUSDT \
  --paper \
  --strategy market_maker

Mercury will connect to the Binance WebSocket, receive live order-book updates, and simulate order matching with a 5 ms round-trip latency. No real orders are placed.

Live trading

Set your exchange credentials as environment variables, then pass them at run time:

export BINANCE_API_KEY=your_key
export BINANCE_SECRET_KEY=your_secret

cargo run --bin mercury -- run \
  --symbol BTCUSDT \
  --strategy rsi

TUI monitor

In a second terminal, start the dashboard:

cargo run --bin mercury-tui

The TUI connects to Mercury via a Unix socket at /tmp/mercury.sock and displays live order-book depth, fills, PnL, and latency percentiles.

Adjust log verbosity

cargo run --bin mercury -- --log-level debug run --symbol BTCUSDT --paper --strategy rsi

Valid levels: trace, debug, info, warn, error.

Configuration

Mercury is configured primarily through CLI flags. There is no config file required for basic operation.

CLI flags

FlagDefaultDescription
--symbol—Trading symbol, e.g. BTCUSDT or AAPL
--exchangebinanceExchange: binance, coinbase, yahoo
--strategy—Strategy name: market_maker, rsi, momentum, arbitrage
--paperfalseEnable paper trading via PaperGateway
--log-levelinfoTracing log level

Environment variables

VariableDescription
BINANCE_API_KEYBinance REST/WS API key
BINANCE_SECRET_KEYBinance HMAC secret
COINBASE_API_KEYCoinbase Advanced Trade key
COINBASE_SECRET_KEYCoinbase secret

Storage

Mercury writes its SQLite database to mercury.db in the current working directory. Override with --db-path:

cargo run --bin mercury -- run --symbol BTCUSDT --paper --strategy rsi --db-path /tmp/mercury.db

Use :memory: for ephemeral in-process storage (useful in tests).

Architecture Overview

Mercury is an event-driven trading engine. All inter-component communication flows through a single EventBus — a crossbeam bounded channel with a default capacity of 100,000 events.

Event flow

Market Feeds (WebSocket) → FeedManager → EventBus
                                              │
             ┌────────────────────────────────┼──────────────────┐
             ▼                                ▼                  ▼
      StrategyRunner                    OrderManager         StorageManager
      (on_book/on_trade)               (submits orders       (persists fills
             │                          via Gateway)          to SQLite)
             ▼                                │
         Signal events                    Fill events
         → EventBus                       → EventBus
                                              │
                                         IpcServer
                                    (/tmp/mercury.sock)
                                    (broadcasts to TUI)

Key traits

TraitCratePurpose
Strategymercury-strategyImplement for new trading strategies. Methods: on_book, on_trade, on_fill, reset.
ExchangeGatewaymercury-gatewayImplement for new exchanges. Async: submit_order, cancel_order, connect.
FeedParsermercury-marketImplement for new exchange WebSocket feeds. Methods: parse, ws_url, subscribe_message.

Component lifecycle

Each long-running component is spawned as a tokio::spawn task:

  1. FeedManager — opens WebSocket connections, parses messages, publishes BookUpdate and Trade events.
  2. StrategyRunner — subscribes to market events, calls strategy methods, publishes Signal events.
  3. OrderManager — subscribes to Signal events, applies risk checks, submits orders via the gateway.
  4. StorageManager — subscribes to Fill events and persists them to SQLite.
  5. IpcServer — fans out events to connected TUI clients over a Unix socket.

Event Bus

The EventBus is the central nervous system of Mercury. It is a thin wrapper around a crossbeam bounded channel.

Design

  • Capacity: 100,000 events (configurable at construction).
  • Back-pressure: producers block when the channel is full, providing natural flow control.
  • Lock-free: no mutex on the hot path — crossbeam’s channel uses atomic operations internally.
  • Single channel: all event types share one channel. Components filter by EventPayload variant.

Event types (EventPayload)

VariantPublished byConsumed by
BookUpdate(Arc<BookUpdate>)FeedManagerStrategyRunner, PaperGateway
Trade(Trade)FeedManagerStrategyRunner
Signal(Signal)StrategyRunnerOrderManager
Fill(Fill)OrderManagerStorageManager, StrategyRunner, IpcServer
OrderUpdate(Order)OrderManagerIpcServer

Arc wrapping

BookUpdate is wrapped in Arc before publishing so that multiple subscribers can hold a reference to the same allocation without copying the order-book snapshot.

Usage

#![allow(unused)]
fn main() {
// Publish
bus.publish(EventPayload::Signal(signal));

// Subscribe (returns a Receiver)
let rx = bus.subscribe();
while let Ok(event) = rx.recv() {
    match event.payload {
        EventPayload::Fill(fill) => { /* handle */ }
        _ => {}
    }
}
}

Crate Structure

Mercury is a Cargo workspace. Crates are ordered by dependency depth.

Dependency order

core ← market, gateway, strategy, risk, execution, replay, metrics, storage ← cli, tui

Crate descriptions

mercury-core

Shared types and infrastructure used by every other crate.

  • Event, EventBus, EventPayload
  • OrderBook, BookUpdate, Trade
  • Pool (object pool backed by parking_lot)
  • IpcServer (Unix socket broadcaster)
  • All domain types: Symbol, Exchange, Side, Order, Fill, Signal, OrderId, TimeInForce, FixedPoint

mercury-market

Market data ingestion.

  • FeedManager — manages WebSocket connections and reconnect logic
  • BinanceParser, CoinbaseParser — implement FeedParser
  • YahooFeed — HTTP polling feed for equities
  • BookBuilder — reconstructs a full order book from incremental updates

mercury-gateway

Exchange connectivity for order submission.

  • BinanceGateway, CoinbaseGateway, BybitGateway, KrakenGateway, OkxGateway
  • PaperGateway — local matching engine, configurable RTT latency
  • All implement ExchangeGateway

mercury-strategy

Strategy implementations and technical indicators.

  • StrategyRunner — dispatches events to strategies
  • Strategies: MarketMaker, Momentum, RsiStrategy, ArbitrageStrategy, InferenceStrategy
  • Indicators: SMA, EMA, RSI, MACD

mercury-risk

Pre-trade risk checks.

  • RiskManager — position limits, daily loss guard, per-order rate limiting, kill switch

mercury-execution

Order lifecycle management.

  • OrderManager — wraps gateway with risk checks, tracks open orders
  • SimulatedExchange — for backtesting
  • SmartOrderRouter — best-execution routing across venues
  • ExecutionMetrics

mercury-replay

Data recording and deterministic replay.

  • Recorder — writes tick data to Parquet files
  • Player — replays a Parquet file at configurable speed

mercury-metrics

Observability.

  • LatencyTracker — HDR histograms for fill-to-ack, event-to-signal
  • PnlTracker — realized and unrealized PnL
  • Prometheus export endpoint

mercury-storage

Persistence.

  • StorageManager — SQLite via sqlx, persists Fill events to a trades table
  • Migrations in crates/storage/migrations/

mercury-cli (binary: mercury)

Top-level binary with subcommands: run, replay, backtest, record.

mercury-tui (binary: mercury-tui)

Real-time ratatui dashboard. Connects to mercury via /tmp/mercury.sock.

Concurrency Model

Hot path is lock-free

  • EventBus uses crossbeam bounded channels — no mutex on publish/receive.
  • Pool uses parking_lot::Mutex, which is fair and low-overhead.
  • Symbol is a [u8; 16] stack array — no heap allocation on the critical path.

Task topology

Each long-running component runs as an independent tokio::spawn task:

TaskNotes
FeedManagerOne task per exchange connection. Handles WebSocket reconnection.
StrategyRunnerSingle-threaded. Owns all strategy instances. Calls on_book/on_trade synchronously.
OrderManagerAwaits signals; submits orders to the gateway asynchronously.
StorageManagerBatches fill writes to SQLite.
IpcServerFans out to connected TUI clients. Non-blocking; slow clients are dropped.

Strategy thread safety

Strategy is Send + Sync, but on_book and on_trade take &mut self. StrategyRunner owns the strategies and calls them from a single Tokio task — no concurrent mutation, no locking needed inside strategy implementations.

Back-pressure

The EventBus channel has a bounded capacity (default 100,000). If a slow consumer (e.g. StorageManager during a disk flush) falls behind, producers will block. This prevents unbounded memory growth but means the market feed task may stall. Size the channel according to your expected burst rate.

Data Types

Decimal arithmetic

All prices and quantities use rust_decimal::Decimal. Never use f64 for financial values — floating-point rounding errors compound over time and can cause incorrect P&L calculations.

Use the dec!() macro for literals:

#![allow(unused)]
fn main() {
use rust_decimal_macros::dec;

let price = dec!(42_150.50);
let qty   = dec!(0.001);
let notional = price * qty; // Decimal, exact
}

Symbol

Symbol is a fixed-size [u8; 16] stack array. This avoids heap allocation on the hot path.

#![allow(unused)]
fn main() {
let sym: Symbol = Symbol::new("BTCUSDT");
let sym: Symbol = "BTCUSDT".into(); // same thing
}

Strings longer than 16 bytes are silently truncated. Keep symbol names short.

Order

#![allow(unused)]
fn main() {
pub struct Order {
    pub id:            OrderId,
    pub symbol:        Symbol,
    pub exchange:      Exchange,
    pub side:          Side,        // Bid | Ask
    pub order_type:    OrderType,   // Limit | Market | PostOnly
    pub price:         Decimal,
    pub quantity:      Decimal,
    pub time_in_force: TimeInForce, // GTC | IOC | FOK | PostOnly
}
}

Fill

#![allow(unused)]
fn main() {
pub struct Fill {
    pub order_id:   OrderId,
    pub symbol:     Symbol,
    pub exchange:   Exchange,
    pub side:       Side,
    pub price:      Decimal,
    pub quantity:   Decimal,
    pub fee:        Decimal,
    pub timestamp:  i64,  // Unix millis
}
}

Signal

Strategies emit Signal values that the OrderManager converts into orders:

#![allow(unused)]
fn main() {
pub struct Signal {
    pub symbol:    Symbol,
    pub side:      Side,
    pub price:     Decimal,
    pub quantity:  Decimal,
    pub strategy:  &'static str,
}
}

Market Feeds

The mercury-market crate manages all inbound market data.

FeedManager

FeedManager owns one or more WebSocket connections and routes parsed messages onto the EventBus. It handles reconnection automatically on disconnect.

Implementing a new feed

Implement the FeedParser trait:

#![allow(unused)]
fn main() {
pub trait FeedParser: Send + Sync {
    /// Return the WebSocket URL for this exchange/symbol.
    fn ws_url(&self, symbol: &Symbol) -> String;

    /// Return the JSON subscription message to send after connecting.
    fn subscribe_message(&self, symbol: &Symbol) -> String;

    /// Parse a raw WebSocket message into zero or more events.
    fn parse(&self, msg: &str) -> Vec<EventPayload>;
}
}

Once implemented, register your parser with FeedManager::add_feed.

Supported feeds

ParserExchangeData
BinanceParserBinanceOrder book depth (L2), trades
CoinbaseParserCoinbaseOrder book depth (L2), trades
BybitParserBybitOrder book depth (L2), trades
KrakenParserKrakenOrder book depth (L2), trades
OkxParserOKXOrder book depth (L2), trades
PolymarketParserPolymarket CLOBOrder book snapshots and deltas
KalshiParserKalshiOrder book snapshots and deltas
YahooFeedYahoo FinanceOHLCV bars (HTTP polling)

BookBuilder

BookBuilder maintains a full L2 order book from incremental BookUpdate events. It applies sequence-numbered deltas and publishes a fresh Arc<BookUpdate> snapshot after each delta is applied.

Gateways

Gateways implement the ExchangeGateway trait and handle order submission to exchanges.

ExchangeGateway trait

#![allow(unused)]
fn main() {
#[async_trait]
pub trait ExchangeGateway: Send + Sync {
    async fn connect(&mut self) -> GatewayResult<()>;
    async fn submit_order(&self, order: &Order) -> GatewayResult<OrderId>;
    async fn cancel_order(&self, id: &OrderId, symbol: &Symbol) -> GatewayResult<()>;
    fn exchange(&self) -> Exchange;
}
}

Supported gateways

GatewayExchangeNotes
BinanceGatewayBinance SpotHMAC-SHA256 signing
CoinbaseGatewayCoinbase Advanced TradeHMAC-SHA256 signing
BybitGatewayBybit v5HMAC-SHA256 signing
KrakenGatewayKraken v0HMAC-SHA512 with nonce
OkxGatewayOKX v5HMAC-SHA256, base64
PolymarketGatewayPolymarket CLOBHMAC-SHA256; fills via WebSocket user channel
KalshiGatewayKalshi v2RSA-SHA256 private key signing
PaperGateway—Local simulated matching

PaperGateway

PaperGateway is a local matching engine that simulates order fills against live BookUpdate events on the bus. It is the recommended way to develop and test strategies.

  • Default simulated RTT: 5 ms
  • No API keys required
  • Enabled with --paper CLI flag

Configurable latency:

#![allow(unused)]
fn main() {
PaperGateway::with_latency(Duration::from_millis(10))
}

Adding a new gateway

  1. Create crates/gateway/src/myexchange.rs
  2. Implement ExchangeGateway
  3. Add the variant to Exchange enum in mercury-core
  4. Register in crates/cli/src/main.rs (gateway match and feed_manager match)

Order Manager

OrderManager sits between strategies and the exchange gateway. It applies risk checks before forwarding orders and tracks their lifecycle.

Responsibilities

  1. Receive Signal events from the EventBus.
  2. Convert signals to Order values.
  3. Call RiskManager::check — reject orders that breach limits.
  4. Submit approved orders via ExchangeGateway::submit_order.
  5. Track open orders by OrderId.
  6. Publish Fill events back onto the EventBus when acknowledgements arrive.

Smart order routing

SmartOrderRouter wraps multiple gateways and selects the venue with the best available price for the given symbol and side. It is opt-in; the default configuration uses a single gateway.

Execution algorithms

Large parent orders can be worked using algorithmic executors. Each executor runs as a background tokio task and emits child Signal events onto the EventBus, which OrderManager receives and processes normally.

ExecutorDescription
TwapExecutorSplits a parent order into N equal-sized slices emitted at uniform time intervals (time-weighted average price).
VwapExecutorSizes each slice in proportion to observed market trade volume for the interval; falls back to equal-weight when no volume is seen.
PovExecutorParticipates as a configurable percentage of observed market volume, bounded by min/max slice sizes, with a wall-clock deadline that flushes any residual quantity.

Usage

#![allow(unused)]
fn main() {
use mercury_execution::{TwapExecutor, VwapExecutor, PovExecutor};

// TWAP — 10 equal slices over 5 minutes
TwapExecutor::new(bus.clone()).execute(signal.clone(), 300, 10);

// VWAP — 8 volume-proportional slices over 5 minutes
VwapExecutor::new(bus.clone()).execute(signal.clone(), 300, 8);

// POV — 10% participation, min 0.01 / max 1.0 per slice, 10-minute deadline
PovExecutor::new(bus.clone()).execute(
    signal,
    0.10,                                    // participation_rate
    FixedPoint::from_decimal(dec!(0.01)),    // min_slice_qty
    FixedPoint::from_decimal(dec!(1.0)),     // max_slice_qty
    600,                                     // deadline_secs
);
}

All three executors observe Trade events on the EventBus to measure market volume. No additional configuration is required beyond passing the shared Arc<EventBus>.

Risk Manager

RiskManager enforces pre-trade risk controls. Every order passes through it before reaching the gateway.

Checks

CheckDescription
Position limitRejects orders that would exceed the configured max position size
Daily loss guardHalts trading when realized + unrealized loss exceeds the daily limit
Rate limitEnforces a maximum number of orders per second
Kill switchImmediately blocks all new orders; drains open orders via cancel

Configuration

#![allow(unused)]
fn main() {
RiskConfig {
    max_position:    dec!(1.0),      // max open qty per symbol
    daily_loss_limit: dec!(500.0),   // USD
    max_orders_per_sec: 10,
}
}

Kill switch

Activate programmatically:

#![allow(unused)]
fn main() {
risk_manager.kill();
}

Or via the TUI with the K keybinding. Once triggered, the engine stops submitting new orders and attempts to cancel all open positions.

Storage

StorageManager persists Fill events to a SQLite database using sqlx.

Schema

Migrations live in crates/storage/migrations/. The current schema has one table:

CREATE TABLE trades (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id    TEXT    NOT NULL,
    symbol      TEXT    NOT NULL,
    exchange    TEXT    NOT NULL,
    side        TEXT    NOT NULL,  -- 'buy' | 'sell'
    price       TEXT    NOT NULL,  -- stored as decimal string
    quantity    TEXT    NOT NULL,
    fee         TEXT    NOT NULL,
    timestamp   INTEGER NOT NULL   -- Unix millis
);

Prices and quantities are stored as decimal strings to preserve exact precision.

Configuration

Default database path: mercury.db in the working directory.

Use :memory: for tests:

#![allow(unused)]
fn main() {
StorageManager::new(":memory:").await?;
}

Adding new tables

  1. Create a new migration file in crates/storage/migrations/ following the NNNN_description.sql naming convention.
  2. sqlx applies migrations automatically on StorageManager startup.

Replay & Recorder

The mercury-replay crate provides tools for recording live market data and replaying it deterministically.

Recorder

Recorder subscribes to BookUpdate and Trade events on the bus and writes them to a Parquet file.

# Record 120 seconds of BTCUSDT tick data
cargo run --bin mercury -- record \
  --symbol BTCUSDT \
  --output data.parquet \
  --duration 120

Default duration is 60 seconds. The output file can be replayed or used for backtesting.

Player

Player reads a Parquet file and replays events onto the EventBus at a configurable speed multiplier.

# Replay at real-time speed
cargo run --bin mercury -- replay --file data.parquet --speed 1.0

# Replay at 10x speed
cargo run --bin mercury -- replay --file data.parquet --speed 10.0

Backtest

The backtest subcommand combines Player with a SimulatedExchange to run a strategy over historical data and report PnL:

cargo run --bin mercury -- backtest \
  --file data.parquet \
  --strategy market_maker

Results are printed to stdout and written to backtest_results.json.

Parquet schema

Each row represents one tick:

ColumnTypeDescription
timestampINT64Unix millis
symbolBYTE_ARRAYTrading symbol
bid_priceBYTE_ARRAYBest bid (decimal string)
ask_priceBYTE_ARRAYBest ask (decimal string)
bid_qtyBYTE_ARRAYBest bid quantity
ask_qtyBYTE_ARRAYBest ask quantity

Metrics

mercury-metrics provides latency histograms, PnL tracking, and a Prometheus export endpoint.

LatencyTracker

Uses HDR histograms (via hdrhistogram) to record latency with microsecond precision across several orders of magnitude.

Key measurements:

  • Feed-to-strategy: time from BookUpdate publish to on_book return
  • Signal-to-submit: time from Signal publish to submit_order call
  • Submit-to-fill: exchange round-trip time

Access percentiles:

#![allow(unused)]
fn main() {
let p99 = tracker.percentile(99.0); // nanoseconds
let p999 = tracker.percentile(99.9);
}

PnlTracker

Tracks realized and unrealized PnL per symbol:

#![allow(unused)]
fn main() {
tracker.on_fill(&fill);
let pnl = tracker.unrealized_pnl(&symbol, current_mid_price);
}

Prometheus

Metrics are exported on 0.0.0.0:9090/metrics when --metrics is passed. Compatible with any Prometheus scrape target.

Available metrics:

  • mercury_fill_latency_ns — histogram
  • mercury_pnl_realized_usd — gauge per symbol
  • mercury_orders_submitted_total — counter
  • mercury_orders_cancelled_total — counter
  • mercury_risk_rejections_total — counter

TUI Monitor

mercury-tui is a standalone binary that connects to a running mercury process via a Unix socket and displays a real-time dashboard.

Starting the TUI

cargo run --bin mercury-tui

The TUI connects to /tmp/mercury.sock. Start mercury first.

Dashboard panels

  • Order book — live L2 depth, colour-coded bid/ask
  • Fills — scrolling list of recent fills with price, quantity, and exchange
  • PnL — realized and unrealized PnL per symbol
  • Latency — p50/p99/p999 for feed-to-signal and signal-to-fill
  • Orders — open order list with status

Keybindings

KeyAction
qQuit
KActivate kill switch (cancel all, halt trading)
↑/↓Scroll fills / order list
TabSwitch panel focus

IPC protocol

mercury broadcasts JSON-encoded Event values over /tmp/mercury.sock. The TUI deserializes them and updates its state. Multiple TUI instances can connect simultaneously.

Desktop GUI

Mercury features a premium, GPU-accelerated native desktop trading console built on the modern, functional APIs of Iced v0.14.0.

The GUI is themed around a highly polished Obsidian Cyberpunk palette, designed to deliver a gorgeous visual flow matching professional institutional trading terminals (like Bloomberg and TradingView):

  • Deep Obsidian Black ([10, 10, 12] / [20, 20, 25]): Solid, high-depth backgrounds.
  • Neon Teal ([0, 240, 200]): Highlight color for buying, live status, inventory increases, and active input highlights.
  • Neon Pink/Coral ([255, 60, 120]): Highlight color for selling, offline state, and risk alerts.
  • Slate Gray ([150, 150, 155]): Muted color for secondary indicators, grid lines, and subtitles.

Architecture Overview

The GUI operates on a non-blocking asynchronous design conforming to the Elm architecture (State, Subscription, Update, View) without introducing heap-allocation overheads to the trading hot-path.

       Desktop GUI (Iced)
        ├── Spawner Task ──►tokio::process::Child (Engine process)
        │                         │ (pipes stdout/stderr)
        │                         ▼
        │                  System Logs Terminal Panel
        │
        └── Telemetry Stream ◄──Unix Socket (/tmp/mercury.sock)
                                  │ (receives L2 snapshots & latencies)
                                  ▼
                           - Depth Map Canvas
                           - Sparkline Canvas
                           - Header Metrics Card Deck

The GUI consists of three main architectural subsystems:

1. Detached Subprocess Spawner & Log Piping

To provide a comprehensive control console, the GUI allows starting and stopping the CLI trading engine directly:

  • State Safety: Storing child process streams directly in Iced state structs breaks Rust’s compilation because the handles are not Sync. To circumvent this, Mercury registers a thread-safe static communication channel (LOG_TX: Mutex<Option<mpsc::UnboundedSender<Message>>>) mapped to an unfolding asynchronous subscription stream.
  • stdout/stderr Piping: Spawns mercury executable (resolved dynamically via std::env::current_exe()) in the background. Standard output and errors are captured, routed through a non-blocking tokio::select! block, and piped into the scrolling System Logs terminal panel.
  • Graceful Shutdown: Sends a termination trigger via a oneshot channel to safely kill (child.kill()) and wait on (child.wait()) the background task, returning the dashboard to offline state.

2. Unix Socket Telemetry Stream

Upon launching, the GUI automatically begins subscribing to the Unix domain socket /tmp/mercury.sock published by the engine’s IpcServer. An auto-reconnecting stream reads serialized JSON events, unpacking:

  • BookUpdate: High-frequency order book depth layers.
  • Trade: Active fills matching on the matching engine.
  • LatencyReport: Latency calculations showing fast packet delivery statistics.

3. GPU-Accelerated Graphic Canvases

The L2 Liquidity Wall and Telemetry Sparklines are drawn inside Iced’s canvas::Program widgets, which leverage GPU acceleration for instantaneous redraws on every tick:

  • L2 Depth Map (depth_map.rs): Renders cumulative bid (Teal) and ask (Pink) volume walls. It maps price ranges (min_price, mid_price, max_price) and volume bars mathematically to dynamic grid ticks along the margins.
  • Telemetry Sparkline (sparkline.rs): Tracks real-time p50 latency historical movements with a glowing line chart, calculating and presenting latency markers (e.g. max: 18.5 μs, mid, min) on the vertical margin.

Visual Control Features

The dashboard layout partitions into three specialized control zones:

1. Top Card Ticker Deck

An obsidian-style deck composed of individual sub-cards with thin borders and subtle padding:

  • Branding Badge: Displaying "MERCURY" in Neon Teal and subtitle "HFT ENGINE v1.0.0".
  • Active Symbol Card: Keeps active currency focus (e.g., BTCUSDT).
  • System Status Card: Real-time engine health using a dynamic green-teal or pink-red dot indicator and active label.
  • Net Position Exposure: Instantly sums the net position size for the current symbol and color-codes the badge (Neon Teal for positive inventory, Neon Pink for short positions, Gray for neutral).
  • Unrealized & Active PnL Card: Displays the live dollar-value strategy PnL, highlighting positive or negative returns with dynamic HFT color schemes.
  • Bid/Ask Spread Ticker: Compares best_bid and best_ask in real-time, displaying the absolute and percentage-based bid-ask spread dynamically.

2. Quick Execution & Launch Console

Located in the right panel:

  • Engine Launcher: Type the target symbol, select strategy tabs (“RSI”, “Maker”, “Mom”, “Arb”, “ML”), select Paper or Live, and click START ENGINE. The action button glows vibrant Neon Teal (Start) or Neon Pink (Stop) on hover.
  • Manual Overrides Console: Allows setting manual execution quantities and custom limit prices (with focused teal highlight borders) to place urgent BUY / LONG or SELL / SHORT orders.
  • Emergency Kill Switch: A prominent red button that calls the risk engine to cancel all outstanding orders and immediately flatten active positions.

3. Middle Pane Tabs

Traders can swap content between:

  • Price Chart: A high-end OHLCV Candlestick chart featuring real-time trade aggregation, bottom volume bars, dynamic right price tick scales, and transaction execution marks mapped as diamonds.
  • Matching Prints: Live feed table showing historical ticks, fill times, buy/sell flags, and filled quantities.
  • System Logs: A color-coded terminal viewport displaying real-time stdout/stderr lines from the running subprocess, highlighted for warnings and system updates.

Compiling & Running

Ensure you have the required graphics libraries on your OS (Vulkan, Metal, or DX12 compatible GPU drivers).

# Build and execute the GUI terminal
cargo run -p mercury-gui

Strategies Overview

Strategies implement the Strategy trait and are driven by the StrategyRunner.

Strategy trait

#![allow(unused)]
fn main() {
pub trait Strategy: Send + Sync {
    fn on_book(&mut self, book: &Arc<BookUpdate>) -> Vec<Signal>;
    fn on_trade(&mut self, trade: &Trade) -> Vec<Signal>;
    fn on_fill(&mut self, fill: &Fill);
    fn reset(&mut self);
    fn name(&self) -> &'static str;
}
}
  • on_book — called on every order-book snapshot; return signals to trade.
  • on_trade — called on every tape print.
  • on_fill — called when one of this strategy’s orders is filled; use to update inventory.
  • reset — clear all state (used before backtests and replays).

Built-in strategies

NameFlagDescription
MarketMakermarket_makerSymmetric quotes with inventory skew and HMM regime gating (2× spread when trending, 0.75× when mean-reverting)
MomentummomentumTrend-following using EMA crossover
RsiStrategyrsiMean-reversion using RSI overbought/oversold
ArbitrageStrategyarbitrageCross-exchange price discrepancy
InferenceStrategyinferenceONNX or online Adam-SGD classifier on 12-feature vector with Welford z-score normalisation
RlQuotePlacementStrategyrlDQN quote placement; 7-state input adds HMM trend probability alongside 6 market features

Technical indicators

All indicators live in mercury-strategy and operate on Decimal values:

  • SMA(period) — simple moving average
  • EMA(period) — exponential moving average
  • RSI(period) — relative strength index
  • MACD(fast, slow, signal) — moving average convergence/divergence

Market Maker

MarketMaker posts symmetric limit orders on both sides of the mid-price and adjusts quotes on every book update.

Algorithm

  1. Compute mid = (best_bid + best_ask) / 2.
  2. Feed the mid price into the HMM regime filter and apply a spread multiplier based on the detected regime.
  3. Apply inventory skew: if long, widen the ask and tighten the bid to mean-revert.
  4. Place a bid at mid - half_spread and an ask at mid + half_spread.
  5. On the next book update, cancel stale quotes and replace with fresh ones (peg mode reduces churn by only replacing when price moves beyond a threshold).

Parameters

ParameterDefaultDescription
spreaddec!(0.002)Base total spread as a fraction of mid
order_qtydec!(0.01)Order size in base currency
inventory_skewdec!(0.5)Multiplier for inventory-based skew
peg_thresholddec!(0.0005)Min price move before requoting (peg mode)

Regime-gated spread scaling

MarketMaker embeds an HmmFilter (two-state hidden Markov model) that classifies the market into Trending or MeanReverting based on the sign persistence of consecutive log returns.

RegimeSpread multiplierRationale
Trending2.0×Informed directional flow → widen to reduce adverse selection
MeanReverting0.75×Noise-driven oscillation → tighten to capture more fills
Uncertain (< 0.60 confidence)1.0×Default spread, no adjustment

The HMM needs at least two price updates before emitting a confident regime. During warm-up the multiplier is 1.0.

Customise via RegimeConfig:

#![allow(unused)]
fn main() {
use mercury_strategy::{MarketMaker, RegimeConfig, HmmConfig};

let regime = RegimeConfig {
    hmm: HmmConfig {
        stay_probability: 0.95,
        confidence_threshold: 0.65,  // require higher confidence before scaling
        ..HmmConfig::default()
    },
    trending_spread_mult: 2.5,        // even wider in strong trends
    mean_revert_spread_mult: 0.80,
};

let mm = MarketMaker::new("BTCUSDT", dec!(0.002), dec!(0.01))
    .with_regime_config(regime);
}

Peg mode

Peg mode (enabled by default) avoids unnecessary cancel/replace cycles. A quote is only refreshed when the mid-price moves more than peg_threshold from the last quoted price. This reduces exchange fees and order rate usage.

Momentum

Momentum is a trend-following strategy that uses an EMA crossover to generate directional signals.

Algorithm

  1. Maintain a fast EMA (default period 12) and a slow EMA (default period 26) over trade prices.
  2. When fast EMA crosses above slow EMA: emit a buy signal.
  3. When fast EMA crosses below slow EMA: emit a sell signal.
  4. No signal is emitted until both EMAs have warmed up (require slow_period trades).

Parameters

ParameterDefaultDescription
fast_period12Fast EMA period
slow_period26Slow EMA period
order_qtydec!(0.01)Order size per signal

RSI Strategy

RsiStrategy is a mean-reversion strategy based on the Relative Strength Index.

Algorithm

  1. Compute RSI over the last period trade prices (default 14).
  2. When RSI drops below oversold (default 30): emit a buy signal.
  3. When RSI rises above overbought (default 70): emit a sell signal.
  4. No signal is emitted until the RSI has warmed up.

Parameters

ParameterDefaultDescription
period14RSI lookback period
oversold30RSI level that triggers a buy
overbought70RSI level that triggers a sell
order_qtydec!(0.01)Order size per signal

Arbitrage Strategy

ArbitrageStrategy monitors the same symbol across two exchanges and trades when the price discrepancy exceeds transaction costs.

Algorithm

  1. Maintain best bid/ask for the symbol on both exchanges.
  2. When exchange_a.ask < exchange_b.bid - threshold: buy on A, sell on B.
  3. When exchange_b.ask < exchange_a.bid - threshold: buy on B, sell on A.
  4. Block re-entry until both legs of the previous trade are filled.

Parameters

ParameterDefaultDescription
min_spreaddec!(0.001)Minimum price discrepancy to trade
order_qtydec!(0.01)Order size per leg

Notes

True cross-exchange arbitrage requires near-simultaneous execution on both venues. Network latency between exchanges will erode the opportunity. This strategy is most useful in paper-trading mode to study spread behaviour.

ML Inference Strategy

InferenceStrategy generates buy/sell signals from a 12-feature order-book vector using either a pre-trained ONNX model or an online Adam-SGD classifier that trains itself in real-time.

How it works

  1. On every on_book call, FeatureComputer builds a 12-element feature vector from the current book snapshot and recent trade history.
  2. RunningNormalizer standardises the vector to approximately zero mean / unit variance (active after a 30-tick warm-up; raw features are used before that).
  3. The normalised vector is either:
    • ONNX path — fed to a tract-onnx plan loaded from disk.
    • Online fallback — classified by an incremental 3-class softmax trained with Adam via delayed self-labelling (label assigned after lookahead_ticks ticks using mid-price movement).
  4. [sell_prob, hold_prob, buy_prob] drives a threshold gate (0.65 for ONNX, 0.60 for online); a signal is emitted only when one class exceeds the threshold.

Feature vector (12 features)

IndexFeatureRange
0Order imbalance (bid_qty − ask_qty) / total[−1, 1]
1Spread bps / 100[0, ∞)
2RSI-14 / 100[0, 1]
3MACD histogram (sign × clamped magnitude)[−1, 1]
4Depth ratio top-5 bid / total[0, 1]
5EMA-50 deviation (mid − ema) / emauncapped
6ATR-14 / mid[0, ∞)
7VWAP deviation, 100-trade rollinguncapped
8Depth slope (bid[0] − bid[4]) / miduncapped
9Trade-flow imbalance, 20-trade rolling[0, 1]
10Kyle’s Lambda (tanh-squashed price-impact coeff)(−1, 1)
11VPIN trade toxicity[0, 1]

compute_extended() appends a 13th feature (index 12): Hawkes trade-arrival intensity.

Online normalisation

RunningNormalizer runs Welford’s one-pass algorithm to accumulate per-feature mean and variance. Features are normalised in-place before reaching the classifier:

z_i = (x_i − μ_i) / max(σ_i, 1e-8)

Normalisation activates after 30 observations. Before warm-up, raw features are used unchanged.

Online learning (no ONNX model)

When no model file is supplied, the strategy trains an in-process softmax classifier with the Adam optimizer (β₁ = 0.9, β₂ = 0.999, ε = 1e-8, lr = 0.001, L2 = 1e-4):

  • Each tick the feature vector is stored alongside the current mid price.
  • After lookahead_ticks ticks, the mid-price change determines the label: +1 (price rose > threshold), -1 (fell), 0 (flat).
  • An Adam gradient step updates the 3-class weight matrix (3 × 12).
  • Signals are emitted only after min_updates_before_signal updates (default 25) to avoid noise during cold-start.
#![allow(unused)]
fn main() {
let config = OnlineLearningConfig {
    lookahead_ticks: 10,
    threshold: 0.0005,           // 0.05% price move to assign a label
    min_updates_before_signal: 25,
    classifier: OnlineClassifierConfig {
        learning_rate: 0.001,
        l2: 0.0001,
        beta1: 0.9,
        beta2: 0.999,
        epsilon: 1e-8,
    },
};
let strat = InferenceStrategy::new("BTCUSDT", dec!(0.01), None, Some(bus))
    .with_online_config(config);
}

Loading an ONNX model

cargo run --bin mercury -- run \
  --symbol BTCUSDT \
  --paper \
  --strategy inference \
  --model path/to/model.onnx

The expected ONNX input shape is [1, 12] with output shape [1, 3] (softmax over [SELL, HOLD, BUY]). A [1, 2, 20] LOB CNN model is also supported via InferenceStrategy::new_lob_cnn.

Model checkpointing

Online classifier weights can be saved and restored between sessions:

#![allow(unused)]
fn main() {
// Save after a live session
clf.save("btcusdt_weights.json")?;

// Restore at startup
let mut clf = OnlineClassifier::new(OnlineClassifierConfig::default());
clf.load("btcusdt_weights.json")?;
}

The checkpoint format is compact JSON containing updates, bias[3], and weights[36] (3 classes × 12 features).

ML Components

Mercury’s strategy crate ships self-contained machine-learning building blocks — no Python, no external service, all pure Rust.


Feature engineering (FeatureComputer)

FeatureComputer maintains running indicator state and produces a 12-element f32 vector on every on_book call.

IndexFeatureRangeNotes
0Order imbalance[−1, 1](bid_qty − ask_qty) / total
1Spread bps / 100[0, ∞)
2RSI-14 / 100[0, 1]
3MACD histogram[−1, 1]sign × clamped magnitude
4Depth ratio top-5[0, 1]bid depth fraction
5EMA-50 deviationuncapped(mid − ema) / ema
6ATR-14 / mid[0, ∞)
7VWAP deviationuncapped100-trade rolling
8Depth slopeuncapped(bid[0] − bid[4]) / mid
9Trade-flow imbalance[0, 1]20-trade rolling buy fraction
10Kyle’s Lambda(−1, 1)tanh(λ × 1000) — price-impact coefficient
11VPIN[0, 1]volume-synchronised toxicity

compute_extended() appends a 13th feature at index 12: bounded Hawkes trade-arrival intensity.


Online normalisation (RunningNormalizer)

Implements Welford’s one-pass algorithm to track per-feature mean and variance without storing history.

#![allow(unused)]
fn main() {
pub struct RunningNormalizer {
    mean: [f64; FEATURE_COUNT],
    m2:   [f64; FEATURE_COUNT],   // Welford M2 accumulator
    count: u64,
    min_count: u64,               // warm-up ticks before normalisation activates
}
}

Features are standardised in-place: z_i = (x_i − μ_i) / max(σ_i, 1e-8). Before min_count observations, features are returned unchanged.

InferenceStrategy uses a 30-tick warm-up by default.


Adam optimizer (OnlineClassifier)

3-class softmax logistic regression trained with the Adam adaptive gradient method.

HyperparameterDefaultDescription
learning_rate0.001Adam α
beta10.9First-moment decay
beta20.999Second-moment decay
epsilon1e-8Numerical stability
l21e-4L2 weight decay

Update rule (per weight):

m_t = β₁·m_{t-1} + (1−β₁)·g
v_t = β₂·v_{t-1} + (1−β₂)·g²
α_t = α · √(1−β₂ᵗ) / (1−β₁ᵗ)
w ← w − α_t · m̂_t / (√v̂_t + ε)

HMM regime filter (HmmFilter)

Two-state online forward filter classifying the market into Trending or MeanReverting.

Observation: sign persistence of consecutive log returns (+1 same direction, −1 alternating).

States:

  • Trending — emission mean +0.75 (returns persist in the same direction).
  • MeanReverting — emission mean −0.75 (returns alternate sign).
#![allow(unused)]
fn main() {
let mut hmm = HmmFilter::default();
hmm.update_price(50_000.0);  // 1st price — no output
hmm.update_price(50_050.0);  // 2nd — computes 1st return, no output
let regime = hmm.update_price(50_100.0);  // 3rd — first regime estimate
// Some(Regime::Trending)

let trend_prob = hmm.trend_probability();  // ∈ [0, 1]
}

A confident regime is declared only when trend_probability ≥ confidence_threshold (default 0.60).

Integration points:

  • MarketMaker — spread multiplier: 2.0× trending, 0.75× mean-reverting.
  • RlQuotePlacementStrategy — trend_probability − 0.5 as state dimension 6.

Microstructure estimators (microstructure.rs)

Kyle’s Lambda

Price-impact coefficient estimated via rolling OLS on signed order flow:

λ = Σ(Q_i · ΔP_i) / Σ(Q_i²)

where Q is signed volume (+buy, −sell) and ΔP is the concurrent mid-price change. Rising λ indicates informed directional flow.

#![allow(unused)]
fn main() {
let mut kl = KyleLambda::new(20);         // 20-trade rolling window
kl.on_trade(1.0, Side::Buy, 0.01);        // qty, side, price_change
let lambda = kl.lambda();                  // Some(f64) once window is full
}

Roll Spread

Estimates the effective half-spread from the serial covariance of price changes (Roll, 1984):

c = √(−Cov(ΔP_t, ΔP_{t−1}))

Valid only when serial covariance is negative (as expected when bid-ask bounce dominates).

#![allow(unused)]
fn main() {
let mut rs = RollSpread::new(20);
rs.on_price(100.0);
let half_spread = rs.half_spread();       // Some(f64) once window full and Cov < 0
}

VPIN

Volume-synchronised probability of informed trading (Easley et al., 2012):

  • Volume is divided into fixed-size buckets.
  • Each bucket: vpin = |buy_vol − sell_vol| / bucket_size.
  • Rolling average over the last window_buckets gives VPIN ∈ [0, 1].
  • Values > 0.5 indicate a toxic order-flow environment.
#![allow(unused)]
fn main() {
let mut vpin = Vpin::new(10.0, 20);       // bucket_size=10, 20-bucket window
vpin.on_trade(1.0, Side::Buy);
let score = vpin.vpin();                   // 0.0 until first bucket completes
}

Model checkpointing (checkpoint.rs)

Online classifier (JSON)

#![allow(unused)]
fn main() {
// Save weights + bias + update count.
clf.save("model.json")?;

// Restore.
let mut clf = OnlineClassifier::new(OnlineClassifierConfig::default());
clf.load("model.json")?;
}

Format: {"updates": u64, "bias": [f32; 3], "weights": [f32; 36]} (3 × 12 features).

DQN network (binary f32 LE)

#![allow(unused)]
fn main() {
// Flatten each weight matrix then save as one binary blob.
save_f32_weights("dqn.bin", &[&w1_flat, &w2_flat, &w3_flat])?;

// Load and split by known sizes.
let weights = load_f32_weights("dqn.bin", total_params)?;
}

Dimension mismatch on load returns io::Error::InvalidData.

Writing a Custom Strategy

1. Create the file

Add crates/strategy/src/my_strategy.rs and implement the Strategy trait:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use mercury_core::{BookUpdate, Fill, Signal, Trade};
use crate::Strategy;

pub struct MyStrategy {
    // your state here
}

impl MyStrategy {
    pub fn new() -> Self {
        Self {}
    }
}

impl Strategy for MyStrategy {
    fn name(&self) -> &'static str { "my_strategy" }

    fn on_book(&mut self, book: &Arc<BookUpdate>) -> Vec<Signal> {
        // compute signals from the order book
        vec![]
    }

    fn on_trade(&mut self, _trade: &Trade) -> Vec<Signal> {
        vec![]
    }

    fn on_fill(&mut self, _fill: &Fill) {
        // update inventory / reinforce learning
    }

    fn reset(&mut self) {
        // clear all state
        *self = Self::new();
    }
}
}

2. Export from the crate

In crates/strategy/src/lib.rs:

#![allow(unused)]
fn main() {
pub mod my_strategy;
pub use my_strategy::MyStrategy;
}

3. Register the CLI flag

In crates/cli/src/run.rs, add a match arm for the new strategy name:

#![allow(unused)]
fn main() {
"my_strategy" => Box::new(MyStrategy::new()),
}

4. Run it

cargo run --bin mercury -- run --symbol BTCUSDT --paper --strategy my_strategy

Tips

  • Keep on_book fast — it is called on every tick. Avoid allocations on the hot path.
  • Use rust_decimal::Decimal for all price/quantity arithmetic.
  • Initialise warm-up state in reset() so backtests start clean.
  • Use on_fill to track inventory rather than counting signals, since orders may be partially filled or rejected.

Paper Trading

Paper trading runs Mercury against a live market feed but routes all orders through PaperGateway — a local matching engine. No real money is at risk.

Enable paper trading

cargo run --bin mercury -- run --symbol BTCUSDT --paper --strategy market_maker

How PaperGateway works

  1. Receives live BookUpdate events from the market feed.
  2. When a new order arrives, checks whether it crosses the current best bid/ask.
  3. If it fills, publishes a synthetic Fill event onto the EventBus after a configurable simulated delay.
  4. Default simulated round-trip latency: 5 ms.

Custom latency

The paper gateway latency is set in code:

#![allow(unused)]
fn main() {
// crates/cli/src/run.rs
let gateway = PaperGateway::with_latency(Duration::from_millis(10));
}

Limitations

  • No partial fills — orders either fill completely or not at all.
  • No queue position — the paper gateway assumes your order is at the top of the queue.
  • Simulated latency is constant; real latency is variable.

These simplifications make paper results optimistic. Use backtesting on recorded data for more realistic simulation.

Prediction Markets

Mercury supports Polymarket (CLOB-based) and Kalshi (regulated US) alongside crypto exchanges. Binary outcome contracts (YES/NO shares) map directly onto the existing OrderBook, BookUpdate, and Fill types — prices are stored as probabilities in FixedPoint (e.g. 0.42 = 42% probability).

Symbol derivation

Polymarket

Polymarket identifies markets by a 64-character hex condition_id. Because Symbol is 16 bytes, Mercury derives a stable key using the last 16 characters of the condition ID:

condition_id: 0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af
Symbol key:                                                  0ca09bd4532f84af

The low-order bytes of the uint256 are unique across all active markets, so no collision occurs in practice. The helper condition_id_to_symbol(cid) in mercury-market performs this mapping.

Kalshi

Kalshi market tickers (e.g. TRUMPWIN-2024) are ≤ 15 characters and fit directly in Symbol with no truncation.


Polymarket

Feed (market data)

PolymarketParser subscribes to the Polymarket CLOB WebSocket:

  • URL: wss://ws-subscriptions-clob.polymarket.com/ws/market
  • Subscription: {"assets_ids": ["<condition_id>"], "type": "market"}
  • Messages: book (full snapshot) → DepthSnapshot; price_change (delta) → DepthUpdate

Gateway (order execution)

PolymarketGateway uses the Polymarket CLOB REST API (https://clob.polymarket.com):

OperationEndpoint
Submit orderPOST /order
Cancel orderDELETE /orders/{order_id}
Cancel allDELETE /orders
FillsWebSocket user channel wss://ws-subscriptions-clob.polymarket.com/ws/user

Authentication: HMAC-SHA256 over {timestamp}{nonce}POST{body} using api_secret.

Configuration

#![allow(unused)]
fn main() {
PolymarketConfig {
    api_key:        String,   // POLY-API-KEY header
    api_secret:     String,   // HMAC signing key
    api_passphrase: String,   // POLY-PASSPHRASE header
}
}

Environment variables: EXCHANGE_API_KEY, EXCHANGE_SECRET_KEY, POLYMARKET_PASSPHRASE.

Usage

# Paper trade (live feed, simulated orders)
cargo run --bin mercury -- run \
  --exchange polymarket \
  --symbol 0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af \
  --paper \
  --strategy momentum

# Live trading
EXCHANGE_API_KEY=your_api_key \
EXCHANGE_SECRET_KEY=your_api_secret \
POLYMARKET_PASSPHRASE=your_passphrase \
  cargo run --bin mercury -- run \
  --exchange polymarket \
  --symbol 0x...condition_id... \
  --strategy rsi

Kalshi

Feed (market data)

KalshiParser subscribes to the Kalshi WebSocket:

  • URL: wss://api.elections.kalshi.com/trade-api/ws/v2
  • Subscription: {"cmd": "subscribe", "params": {"channels": ["orderbook_delta"], "market_tickers": ["TICKER"]}}
  • Messages: orderbook_snapshot → DepthSnapshot; orderbook_delta → DepthUpdate
  • Prices: Kalshi sends integer cents (e.g. 42 = 42 cents = 0.42 probability). The parser converts to FixedPoint automatically.

Gateway (order execution)

KalshiGateway uses the Kalshi REST API (https://api.elections.kalshi.com/trade-api/v2):

OperationEndpoint
Submit orderPOST /portfolio/orders
Cancel orderDELETE /portfolio/orders/{order_id}
Cancel allDELETE /portfolio/orders?ticker={ticker}
FillsWebSocket fills channel

Authentication: RSA-SHA256 (PKCS#1v1.5). Signature message: {timestamp_ms}{method}{path}. The signed bytes are base64-encoded and sent in the KALSHI-ACCESS-SIGNATURE header.

Configuration

#![allow(unused)]
fn main() {
KalshiConfig {
    api_key_id:      String,   // UUID from the Kalshi dashboard
    private_key_pem: String,   // PEM-encoded RSA private key
}
}

Environment variables: KALSHI_API_KEY_ID, KALSHI_PRIVATE_KEY (PEM string or path to a .pem file).

Usage

# Paper trade
cargo run --bin mercury -- run \
  --exchange kalshi \
  --symbol TRUMPWIN-2024 \
  --paper \
  --strategy rsi

# Live trading — pass PEM file path
KALSHI_API_KEY_ID=your-uuid \
KALSHI_PRIVATE_KEY=./kalshi_private.pem \
  cargo run --bin mercury -- run \
  --exchange kalshi \
  --symbol TRUMPWIN-2024 \
  --strategy rsi

Price representation

Both markets price YES shares between 0 and 1 (in cents: 0–100). FixedPoint handles this with 8 decimal places of precision — no floating-point rounding errors.

Kalshi:     42 cents  →  parser converts to  0.42  FixedPoint
Polymarket: "0.42"    →  FixedPoint::from_str("0.42")

Side::Buy means buying YES shares. Side::Sell means selling YES shares (equivalent to buying NO at 1 - price).

Backtesting

The backtest subcommand replays a recorded Parquet file through a strategy and reports PnL.

Run a backtest

cargo run --bin mercury -- backtest \
  --file data.parquet \
  --strategy market_maker

How it works

  1. Player reads the Parquet file and replays BookUpdate events in timestamp order.
  2. StrategyRunner drives the strategy as if events were live.
  3. SimulatedExchange matches orders against the replayed book with zero latency (conservative: assumes worst-case fill at the top of the book).
  4. Results are printed to stdout and written to backtest_results.json.

Output

Symbol:   BTCUSDT
Strategy: market_maker
Trades:   1,243
Win rate: 54.2%
PnL:      +$312.47
Max DD:   -$48.20
Sharpe:   1.84

Tips

  • Record a representative data set (at least a few hours) before running a backtest.
  • Use --speed 0 to replay as fast as possible (no wall-clock sleep between ticks).
  • Run multiple strategies on the same file to compare them fairly.

Recording Market Data

The record subcommand captures live tick data to a Parquet file for later replay or backtesting.

Basic usage

# Record 60 seconds (default)
cargo run --bin mercury -- record --symbol BTCUSDT --output data.parquet

# Record 10 minutes
cargo run --bin mercury -- record --symbol BTCUSDT --output data.parquet --duration 600

Output format

The Parquet file contains one row per BookUpdate event with columns:

  • timestamp — Unix milliseconds
  • symbol — trading symbol
  • bid_price, bid_qty — best bid
  • ask_price, ask_qty — best ask

Prices and quantities are stored as UTF-8 decimal strings to preserve exact precision.

Storage size

Roughly 1–3 MB per minute for a single symbol at normal Binance update frequency (~10 updates/sec). A one-hour recording of BTCUSDT is typically 60–180 MB.

Using recorded data

# Replay at real-time speed
cargo run --bin mercury -- replay --file data.parquet --speed 1.0

# Run a backtest
cargo run --bin mercury -- backtest --file data.parquet --strategy rsi

Adding a New Exchange

Adding exchange support requires implementing two traits: FeedParser (market data) and ExchangeGateway (order routing).

1. Market feed

Create crates/market/src/myexchange.rs:

#![allow(unused)]
fn main() {
pub struct MyExchangeParser;

impl FeedParser for MyExchangeParser {
    fn ws_url(&self, symbol: &Symbol) -> String {
        format!("wss://stream.myexchange.com/ws/{}", symbol)
    }

    fn subscribe_message(&self, symbol: &Symbol) -> String {
        serde_json::json!({ "op": "subscribe", "channel": symbol.to_string() }).to_string()
    }

    fn parse(&self, msg: &str) -> Vec<EventPayload> {
        // deserialise msg and return BookUpdate / Trade events
        vec![]
    }
}
}

2. Gateway

Create crates/gateway/src/myexchange.rs and implement ExchangeGateway.

See crates/gateway/src/binance.rs as a reference for HMAC signing, REST error handling, and order ID mapping.

3. Add the Exchange variant

In crates/core/src/types.rs, add to the Exchange enum:

#![allow(unused)]
fn main() {
pub enum Exchange {
    Binance,
    Coinbase,
    MyExchange, // add this
    // ...
}
}

4. Wire up in the CLI

In crates/cli/src/run.rs, add a match arm for "myexchange" that constructs your feed parser and gateway.

5. Test

Write unit tests for parse() with real WebSocket message payloads captured from the exchange. Use cargo test -p mercury-market myexchange to run them.

Contributing

Development setup

git clone https://github.com/amjadjibon/mercury
cd mercury
cargo build --workspace
cargo test --workspace

Code conventions

  • No f64 for financial values — use rust_decimal::Decimal and the dec!() macro.
  • Symbol is 16 bytes — strings longer than 16 bytes are silently truncated.
  • No heap allocation on the hot path — prefer stack types in on_book and on_trade.
  • Comments only when the why is non-obvious — well-named identifiers document the what.

Running the docs locally

cargo install mdbook
mdbook serve docs --open

Submitting changes

  1. Fork the repository and create a feature branch.
  2. Run cargo test --workspace and cargo build --workspace — both must pass with zero warnings.
  3. Open a pull request with a description of what changed and why.