A high-performance, in-memory order matching engine written in Go. Designed for crypto exchanges, trading simulations, and financial systems requiring precise and fast order execution.
- High Performance: Pure in-memory matching using efficient SkipList data structures ($O(\log N)$) and Disruptor pattern (RingBuffer) for microsecond latency.
- Single Thread Actor: Adopts a Lock-Free architecture where a single pinned goroutine processes all state mutations. This eliminates context switching and mutex contention, maximizing CPU cache locality.
- Concurrency Safe: All state mutations are serialized through the RingBuffer, eliminating race conditions without heavy lock contention.
-
Low Allocation Hot Paths: Uses
udecimal(uint64-based), intrusive lists, and object pooling to minimize GC pressure on performance-critical paths. -
Multi-Market Support: Manages multiple trading pairs (e.g., BTC-USDT, ETH-USDT) within a single
MatchingEngineinstance. - Management Commands: Dynamic market management (Create, Suspend, Resume, UpdateConfig) via Event Sourcing.
-
Comprehensive Order Types:
-
Limit,Market(Size or QuoteSize),IOC,FOK,Post Only - Iceberg Orders: Support for hidden size with automatic replenishment.
-
-
Event Sourcing: Generates detailed
OrderBookLogevents allows for deterministic replay and state reconstruction.
go get github.com/0x5487/matching-enginepackage main
import (
"context"
"fmt"
"time"
match "github.com/0x5487/matching-engine"
"github.com/0x5487/matching-engine/protocol"
"github.com/quagmt/udecimal"
)
func main() {
ctx := context.Background()
// 1. Create a PublishLog handler (implement your own for non-memory usage)
publish := match.NewMemoryPublishLog()
// 2. Initialize the Matching Engine
engine := match.NewMatchingEngine("engine-1", publish)
// 3. Start the Engine (Actor Loop)
// This must be run in a separate goroutine
go func() {
if err := engine.Run(); err != nil {
panic(err)
}
}()
// 4. Create a Market
createCmd := &protocol.Command{
CommandID: "create-btc-usdt",
MarketID: "BTC-USDT",
UserID: 9001,
Timestamp: time.Now().UnixNano(),
}
_ = createCmd.SetPayload(&protocol.CreateMarketParams{
MinLotSize: "0.00000001",
})
// Management commands return a Future for synchronous-like waiting.
future, err := engine.Submit(ctx, createCmd)
if err != nil {
panic(err)
}
// Wait until the market is visible on the read path before submitting orders.
if _, err := future.Wait(ctx); err != nil {
panic(err)
}
// 5. Place a Sell Limit Order
sellCmd := &protocol.Command{
CommandID: "sell-1-cmd",
MarketID: "BTC-USDT",
UserID: 1001,
Timestamp: time.Now().UnixNano(),
}
_ = sellCmd.SetPayload(&protocol.PlaceOrderParams{
OrderID: "sell-1",
OrderType: protocol.OrderTypeLimit,
Side: protocol.SideSell,
Price: udecimal.MustFromInt64(50000, 0).String(), // 50000
Size: udecimal.MustFromInt64(1, 0).String(), // 1.0
})
if err := engine.SubmitAsync(ctx, sellCmd); err != nil {
fmt.Printf("Error placing sell order: %v\n", err)
}
// 6. Place a Buy Limit Order (Matches immediately)
buyCmd := &protocol.Command{
CommandID: "buy-1-cmd",
MarketID: "BTC-USDT",
UserID: 1002,
Timestamp: time.Now().UnixNano(),
}
_ = buyCmd.SetPayload(&protocol.PlaceOrderParams{
OrderID: "buy-1",
OrderType: protocol.OrderTypeLimit,
Side: protocol.SideBuy,
Price: udecimal.MustFromInt64(50000, 0).String(), // 50000
Size: udecimal.MustFromInt64(1, 0).String(), // 1.0
})
if err := engine.SubmitAsync(ctx, buyCmd); err != nil {
fmt.Printf("Error placing buy order: %v\n", err)
}
// Allow some time for async processing
time.Sleep(100 * time.Millisecond)
// 7. Check Logs
fmt.Printf("Total events: %d\n", publish.Count())
logs := publish.Logs()
for _, log := range logs {
switch log.Type {
case protocol.LogTypeMatch:
fmt.Printf("[MATCH] TradeID: %d, Price: %s, Size: %s\n",
log.TradeID, log.Price, log.Size)
case protocol.LogTypeOpen:
fmt.Printf("[OPEN] OrderID: %s, Price: %s\n", log.OrderID, log.Price)
}
}
}PlaceOrder,CancelOrder,AmendOrder, and management commands enqueue work into the engine event loop. A returnederrormeans enqueue/serialization failure, not business rejection.- Every command must carry an upstream-assigned non-empty
CommandID. Engine helpers reject empty command IDs before enqueue. - Every state-changing command must carry an upstream-assigned logical
Timestamp.Timestamp <= 0is rejected asinvalid_payload. For engine helper methods such asCreateMarket,SuspendMarket,ResumeMarket,UpdateConfig, andSendUserEvent, pass the timestamp explicitly from your Gateway / Sequencer / OMS. - Business-level failures are emitted as
OrderBookLogentries withType == protocol.LogTypeReject. - Commands sent to a missing market generate a reject event with
RejectReasonMarketNotFound. - The
Query()method (e.g., forprotocol.GetStatsRequestorprotocol.GetDepthRequest) returnsErrNotFoundimmediately when the market does not exist.
The engine supports dynamic market management:
// Suspend a market (rejects new Place/Amend orders)
suspendCmd := &protocol.Command{
CommandID: "suspend-btc-usdt",
MarketID: "BTC-USDT",
UserID: 9001,
Timestamp: time.Now().UnixNano(),
}
_ = suspendCmd.SetPayload(&protocol.SuspendMarketParams{
Reason: "maintenance",
})
future, err := engine.Submit(ctx, suspendCmd)
_, err = future.Wait(ctx)
// Resume a market
resumeCmd := &protocol.Command{
CommandID: "resume-btc-usdt",
MarketID: "BTC-USDT",
UserID: 9001,
Timestamp: time.Now().UnixNano(),
}
_ = resumeCmd.SetPayload(&protocol.ResumeMarketParams{})
future, err = engine.Submit(ctx, resumeCmd)
_, err = future.Wait(ctx)
// Update market configuration (e.g. MinLotSize)
newLotSize := "0.01"
updateCmd := &protocol.Command{
CommandID: "update-btc-usdt-lot",
MarketID: "BTC-USDT",
UserID: 9001,
Timestamp: time.Now().UnixNano(),
}
_ = updateCmd.SetPayload(&protocol.UpdateConfigParams{
MinLotSize: newLotSize,
})
future, err = engine.Submit(ctx, updateCmd)
_, err = future.Wait(ctx)Successful management commands are emitted as LogTypeAdmin. Invalid management commands are reported through the same event stream as trading rejects. For example:
- duplicate market creation emits
RejectReasonMarketAlreadyExists - invalid
MinLotSizeemitsRejectReasonInvalidPayload - management reject logs preserve the operator
UserID
| Type | Description |
|---|---|
Limit |
Buy/sell at a specific price or better |
Market |
Execute immediately at best available price. Supports Size (base currency) or QuoteSize (quote currency). |
IOC |
Fill immediately, cancel unfilled portion. |
FOK |
Fill entirely immediately or cancel completely. |
PostOnly |
Add to book as maker only, reject if would match immediately. |
Implement Publisher interface to handle order book events:
type MyHandler struct{}
func (h *MyHandler) Publish(logs []*match.OrderBookLog) {
for _, log := range logs {
// If you need local ingest / publish time, add it here instead of relying on engine-generated fields.
if log.Type == protocol.LogTypeUser {
fmt.Printf("User Event: %s, Data: %s\n", log.EventType, string(log.Data))
} else if log.Type == protocol.LogTypeAdmin {
fmt.Printf("Admin Event: %s | Market: %s\n", log.EventType, log.MarketID)
} else {
fmt.Printf("Event: %s | OrderID: %s\n", log.Type, log.OrderID)
}
}
}Inject custom events into the matching engine's log stream. These events are processed sequentially with trades, ensuring deterministic ordering for valid use cases like L1 Block Boundaries, Audit Checkpoints, or Oracle Updates.
// Example: Sending an End-Of-Block signal from an L1 Blockchain
blockHash := []byte("0x123abc...")
userEventCmd := &protocol.Command{
CommandID: "block-100-event",
UserID: 999,
Timestamp: time.Now().UnixNano(),
}
_ = userEventCmd.SetPayload(&protocol.UserEventParams{
EventType: "EndOfBlock",
Key: "block-100",
Data: blockHash,
})
err := engine.SubmitAsync(ctx, userEventCmd)The event will appear in the PublishLog stream as LogTypeUser with your custom data payload. Malformed user-event payloads are emitted as LogTypeReject with RejectReasonInvalidPayload.
Use snapshots to persist engine state and restore it after restart:
meta, err := engine.TakeSnapshot("./snapshot")
if err != nil {
panic(err)
}
restored := match.NewMatchingEngine("engine-1-restored", publish)
meta, err = restored.RestoreFromSnapshot("./snapshot")
if err != nil {
panic(err)
}
_ = meta // contains GlobalLastCmdSeqID for replay positioningPlease refer to docs for detailed benchmarks.