An educational rebuild of a centralized perpetual futures trading platform. The project is meant to make exchange internals easier to inspect: authenticated users, collateral, order placement, order books, fills, positions, liquidations, market data, and the matching engine loop that eventually sits at the center of the system.
This is not production trading software. It is a learning project, and the matching engine is still in progress.
Futures exchanges look simple from the outside: place an order, open a long or short, track PnL, cancel orders, and get liquidated if margin is gone. Under the hood, the interesting parts are systems problems:
- fast order book reads and writes
- price-time priority
- queues at each price level
- balance locking and settlement
- position updates after fills
- liquidation checks from live mark prices
- async communication between API and engine
- persistence of users, markets, orders, and fills
This repository is an attempt to rebuild those pieces from first principles before moving the core engine to Rust.
client / tests
|
v
apps/backend
|
| Redis request/response queue
v
apps/engine
|
| Binance Futures mark-price websocket
v
in-memory prices, users, collateral, order books, positions, fills
packages/db
|
v
Prisma + Postgres for persisted auth, markets, orders, and fills
The codebase now uses a Bun workspace / Turborepo monorepo:
apps/backendowns the HTTP API.apps/engineowns the current TypeScript exchange engine.apps/testscontains Bun-based API/integration tests.packages/dbowns Prisma schema, migrations, generated client, and the shared Prisma export.packages/eslint-configandpackages/typescript-configcontain shared workspace tooling.
.
+-- apps/
| +-- backend/
| | +-- src/
| | +-- controllers/
| | +-- routes/
| | +-- store/
| | +-- types/
| | +-- utils/
| +-- engine/
| | +-- src/
| | +-- helpers/
| | +-- store/
| | +-- types/
| | +-- utils/
| | +-- ws/
| +-- tests/
| +-- index.test.ts
+-- packages/
| +-- db/
| | +-- prisma/
| | | +-- schema.prisma
| | | +-- migrations/
| | | +-- generated/
| | +-- index.ts
| | +-- prisma.config.ts
| +-- eslint-config/
| +-- typescript-config/
+-- package.json
+-- turbo.json
- Bun workspaces
- Turborepo
- TypeScript
- Express
- Redis
- Prisma
- Postgres
- WebSocket market data from Binance Futures
- Zod
- JWT auth
- Bun test
Express API responsible for:
- user signup and signin
- JWT authentication
- request validation with Zod
- admin market creation
- persisted auth and market data through
packages/db - sending exchange commands to the engine over Redis
- waiting for correlated engine responses
TypeScript engine process responsible for:
- listening for backend commands on a Redis queue
- keeping in-memory users, collateral, orders, positions, fills, and order books
- consuming Binance Futures mark-price websocket data
- filling marketable orders against the current market price
- storing non-marketable limit orders in an in-memory order book
- cancelling open limit orders
- checking liquidation conditions from live market updates
The real price-time-priority matching engine is not complete yet. The engine has a working flow skeleton, but it does not fully match incoming orders against opposite-side resting liquidity.
Shared database package responsible for:
- Prisma schema
- Prisma migrations
- generated Prisma client
- shared
prismaexport used by the backend
The Prisma schema currently models:
UserMarketsOrdersFills- order side/type/status enums
Bun test workspace for API/integration tests. These tests expect the backend, engine, Redis, and Postgres to be running.
- Bun
1.3.2or newer - Redis
- Postgres
From the repository root:
bun installCreate .env files where the processes run from.
For apps/backend/.env:
PORT=3000
DATABASE_URL=postgresql://USER:PASSWORD@localhost:5433/DATABASE
REDIS_URL=redis://localhost:6379
JWT_SECRET=replace-this-with-a-real-secret
ADMIN_SECRET=replace-this-with-an-admin-secret
INCOMING_QUEUE=backend-to-engine-broker
ENGINE_TIMEOUT_MS=30000For apps/engine/.env:
REDIS_URL=redis://localhost:6379
INCOMING_QUEUE=backend-to-engine-brokerFor packages/db/.env:
DATABASE_URL=postgresql://USER:PASSWORD@localhost:5433/DATABASEpackages/db needs DATABASE_URL when running Prisma commands. The backend also needs DATABASE_URL at runtime because it imports the shared Prisma client.
From packages/db:
bunx prisma generate
bunx prisma migrate devStart both services locally before running the backend and engine.
Example Redis command:
redis-serverFrom the repository root, run all persistent dev tasks:
bun run devOr run them separately:
bun run dev --filter=backend
bun run dev --filter=engineThe backend defaults to:
http://localhost:3000
Health check:
GET /healthThe tests are in apps/tests and target http://localhost:3000.
Start Postgres, Redis, the backend, and the engine first, then run:
bun test apps/testsPOST /signup
POST /signinBoth accept:
{
"username": "alice",
"password": "password"
}Protected exchange routes expect:
Authorization: Bearer <token>POST /admin/marketAdmin routes expect:
token: <ADMIN_SECRET>Example market:
{
"symbol": "SOL",
"imageUrl": ""
}POST /onramp
POST /order
DELETE /order/:orderId
GET /equity/available
GET /positions/open/:marketId
GET /positions/closed/:marketId
GET /orders/open/:marketId
GET /orders/:marketId
GET /fillsExample onramp:
{
"price": 1000
}Example limit order:
{
"orderType": "limit",
"positionType": "long",
"equity": 100,
"market": "BTCUSDT",
"price": 65000,
"qty": 0.01
}Example market order:
{
"orderType": "market",
"positionType": "short",
"equity": 100,
"market": "ETHUSDT",
"qty": 0.1,
"slippage": 1
}The current in-memory engine state lives in apps/engine/src/store/exchange-store.ts.
Important structures:
USERS: user collateral, positions, and ordersORDERBOOKS: per-market bids and asks grouped by priceFILLS: executed fillsMARKET_PRICES: latest Binance mark prices
Current order book shape:
type Bid = {
availableQty: number;
openOrders: {
userId: string;
qty: number;
filledQty: number;
orderId: string;
createdAt: number;
}[];
};
interface Orderbook {
bids: Record<number, Bid>;
asks: Record<number, Bid>;
lastTradedPrice: number;
indexPrice: number;
}This is good enough for learning the flow, but it is not the final structure for a serious matching engine.
- Monorepo workspace split into apps and packages
- Signup and signin with hashed passwords
- JWT-protected exchange routes
- Admin market creation
- Shared Prisma database package
- Prisma schema and migrations for users, markets, orders, and fills
- Redis request-response flow between backend and engine
- Basic order creation
- Basic collateral locking
- Market-price based immediate fills
- Limit order placement into bid or ask buckets
- Open order cancellation
- Position updates after fills
- Basic liquidation checks from mark-price updates
- Initial Bun test workspace
- Full price-time-priority matching
- Partial fill handling
- Matching against opposite-side resting orders
- Fee accounting
- Funding payments
- Realized and unrealized PnL
- Isolated vs cross margin
- Robust liquidation engine
- Durable engine snapshots or event sourcing
- Database persistence for all engine state transitions
- WebSocket subscriptions for user/order/depth updates
- Rust implementation of the core engine
- Broader automated test coverage
The long-term plan is to move the core engine to Rust and keep the TypeScript backend as the API layer.
The Rust engine should eventually own:
- order book storage
- matching loop
- balance and margin checks
- fill generation
- position accounting
- liquidation queue
- deterministic state transitions
- persistence or replay from an append-only event log
Likely data structures to explore:
BTreeMapfor sorted price levelsVecDequefor FIFO orders at each price level- hash maps for direct
orderId -> orderlookup - heaps or priority queues for liquidation candidates
- append-only logs for replayable engine events
Target shape:
backend API
|
| Redis / NATS / gRPC
v
Rust matching engine
|
v
event log + snapshots + database projections
bun install
bun run dev
bun run dev --filter=backend
bun run dev --filter=engine
bun test apps/tests
bun run formatDatabase commands:
cd packages/db
bunx prisma generate
bunx prisma migrate devThis project intentionally starts with a simple TypeScript engine so the exchange flow is easy to inspect and modify. Once the behavior is clear, the performance-sensitive parts can be redesigned in Rust with stronger data-structure choices and stricter state management.
The main learning milestone is not just "place an order"; it is understanding every state transition caused by that order.