A spot centralized crypto exchange built with Rust, Tokio, Actix Web, Redis Streams, and PostgreSQL.
Introduction ·
User Interface ·
Features ·
Tech Stack ·
API Endpoints ·
WebSocket Streams ·
Order Matching & Execution ·
Market Maker Simulator ·
Local Development ·
License
CEX_RUST is an event-driven spot exchange written in Rust. It is a Cargo workspace of independent services that communicate over Redis Streams. The matching engine keeps order books and user balances in memory and runs one Tokio task per market (BTC, ETH, SOL, quoted in USDT). Order books use B-Trees for sorted price levels and FIFO queues within each level. A REST gateway, a WebSocket depth feed, and an asynchronous DB poller that writes orders and fills to PostgreSQL surround the engine.
- ⚡ In-memory Matching Engine: Price-time priority matching on in-memory order books, with no database calls on the hot path.
- 🧵 Market-per-Task Concurrency: Every market runs in its own Tokio task. A central balance task owns all user balances, and the tasks talk over
mpscchannels with no shared locks. - 🏦 Balance Reservation & Locking: Funds are reserved before an order reaches the book, locked while it rests, and settled on fills or released on cancellation.
- 🔥 Real-time WebSockets: Order book depth is pushed to subscribed clients every 300ms.
- 📦 Decoupled Persistence: A background DB poller writes orders, fills, and cancellations to PostgreSQL without slowing down the engine.
- 🔌 Request/Response over Streams: The REST gateway tags each engine request with a correlation ID and waits for the matching response on
to-backend. - 🔐 JWT Authentication: bcrypt-hashed passwords and HS256 JWT middleware protect the trading routes.
- Rust: Core language, organized as a Cargo workspace.
- Tokio: Async runtime, tasks, and channels.
- Actix Web: Web framework for the REST API.
- Redis Streams: Message bus between services.
- PostgreSQL: Persistent storage for users, orders, and fills.
- SQLx: Compile-time checked queries and migrations.
- tokio-tungstenite: WebSocket server.
- rust_decimal: Exact decimal arithmetic for prices and quantities.
- jsonwebtoken & bcrypt: Authentication.
-
REST API Gateway (
crates/router)- Handles REST API requests (auth, orders, balances, depth).
- Publishes engine requests to the
to-enginestream with a unique correlation ID and waits up to 10s for the matching response. - Queries PostgreSQL directly for auth and order lookups.
-
Redis Streams Message Bus
to-engine: Inbound requests (create order, cancel order, onramp, get depth, get balance).to-backend: Engine responses (order results with trades and the updated depth, cancellations, balances), read by the router, WebSocket server, and DB poller.
-
Matching Engine (
crates/engine)- An ingester task reads
to-engineand forwards requests to the balance task. - The balance task owns every user's balances, validates and reserves funds, and routes orders to the right market.
- One market task per symbol owns its order book, matches orders, publishes the results, and sends balance updates back to the balance task.
- An ingester task reads
-
WebSocket Server (
crates/web-socket-server)- Reads
to-backendand caches the latest depth for each market. - Pushes order book depth to subscribed clients every 300ms.
- Reads
-
DB Poller (
crates/db-poller)- Background worker that reads engine responses from
to-backend. - Inserts orders and fills, and updates order status on fills and cancellations in PostgreSQL.
- Background worker that reads engine responses from
-
Shared Crates (
crates/types,crates/db)types: The shared wire protocol (EngineRequest/EngineResponse) and order book types.db: PostgreSQL connection pool. It runs migrations at startup.
GET /health-> Service health check
POST /api/v1/sign-up-> Register a new trader accountPOST /api/v1/sign-in-> Authenticate a trader and receive a JWT
POST /api/v1/onramp-> Seed test balances for the user (BTC,ETH,SOL,USDT)GET /api/v1/balance-> Get the user's available, locked, and reserved balances
POST /api/v1/order-> Create a new BUY or SELL order (limit/market)GET /api/v1/order/{order_id}-> Get an order by IDDELETE /api/v1/order/{order_id}-> Cancel an open limit order
GET /api/v1/depth/{market}-> Get live order book depth (bids and asks)
Every route except health, sign-up, sign-in, and depth requires an Authorization: Bearer <jwt> header.
Example order body:
{
"market": "SOL",
"qty": "2.5",
"price": "70.10",
"type": "limit",
"side": "BUY"
}Connect to ws://127.0.0.1:9001:
{ "method": "SUBSCRIBE", "params": ["depth.SOL"] }
{ "method": "UNSUBSCRIBE" }Subscribed clients receive:
{ "data": { "type": "depth", "bids": [["70.10", "2.5"]], "asks": [["70.25", "1.2"]] } }// In-memory Orderbook (B-Tree price levels + FIFO queue per level)
pub struct Orderbook {
pub bids: BTreeMap<Decimal, RestingOrder>,
pub asks: BTreeMap<Decimal, RestingOrder>,
pub last_traded_price: Decimal,
}
pub struct RestingOrder {
pub available_qty: Decimal,
pub orders: VecDeque<Orders>,
}
// Order Definition
pub struct Orders {
pub order_id: String,
pub user_id: String,
pub market: String,
pub side: Side, // BUY | SELL
pub qty: Decimal,
pub r#type: TypeOfOrder, // LIMIT | MARKET
pub price: Decimal,
pub status: OrderStatus, // OPEN | PartialyFilled | FILLED | CANCEL
}
// User Balances (userId -> asset -> UserBalance)
pub struct UserBalance {
pub available_balance: Decimal,
pub locked_balance: Decimal,
pub reserve_balance: Decimal,
}
pub struct Trade {
pub maker_order_id: String,
pub taker_order_id: String,
pub maker_user_id: String,
pub taker_user_id: String,
pub fill_qty: Decimal,
pub price: Decimal,
}- The router publishes a
CreateOrder/DeleteOrderrequest with a correlation ID toto-engine. - The engine's ingester reads the request and hands it to the balance task.
- The balance task checks the user's available balance and reserves the funds:
qty × pricein USDT for a buy,qtyof the asset for a sell. It then sends the order to that market's task. - The market task matches the order against the opposite side of the book with price-time priority. A limit order's unfilled remainder rests on the book and its funds are locked.
- Balance changes from each fill go back to the balance task over channels.
- The result is published to
to-backend: status, filled quantity, trades, and the updated depth. - The router returns it to the waiting HTTP request, the WebSocket server updates its depth cache, and the DB poller saves the order and fills to PostgreSQL.
A fish script simulates two-sided quoting and aggressive crossing orders:
./onramp-service.fish # fund the two test accounts
./market-maker.fishWhat it does:
- Uses two test trader accounts, funded through
/onramp. - Posts random two-sided limit orders around a drifting mid price across the
BTC,ETH, andSOLmarkets. - Occasionally sends aggressive orders that cross the spread and generate fills.
Prerequisites: Rust (edition 2024), PostgreSQL, and Redis.
git clone https://github.com/Dev050x/CEX_RUST.git
cd CEX_RUSTCreate a .env file in the repo root:
DATABASE_URL=postgres://<user>:<password>@localhost:5432/<db>
REDIS_URL=redis://127.0.0.1:6379
JWT_SECRET=<your-secret>Start all services. Migrations run automatically at startup:
cargo run -p engine
cargo run -p router
cargo run -p db-poller
cargo run -p web-socket-serverOr run ./start-service.fish to start them all together, with logs written to logs/<service>.log.
The router uses SQLx compile-time checked queries, so PostgreSQL must be reachable at
DATABASE_URLwhen you build.
CEX_RUST is open-source under the MIT License.
