A simple, fast and garbage-free matching engine order book that you can use as a starting point for your matching engines.
Note: For a detailed discussion of how a first-class electronic exchange can be built from the ground up using the sequencer architecture you should refer to this article.
CoralME is an order book data-structure that matches orders based on price-time priority. It maintains limit orders resting in an order book until they are either canceled or filled. Whenever an order changes its state, a callback is issued to registered listeners.
- Fast
- Garbage-free
- Listener-based callbacks for order events
- Price levels
- Price improvement for fills
- MARKET and LIMIT order types
- IOC, GTC and DAY
- Re-entry protection for listeners to prevent nested operations that could corrupt order book state
- Listener exception isolation and reporting so a failing listener cannot interrupt the current order book operation
- Garbage-free forward and reverse price-time order iteration
- MAKER (of liquidity) and TAKER (of liquidity) execution sides
- NORMAL, CROSSED, LOCKED, ONESIDED and EMPTY book states
- ClientID, ClientOrderID and OrderID
- ExecutionID and ExecutionMatchID
- Optional cancel-incoming self-trade prevention: when trade to self is disabled, the incoming remainder is canceled at the first eligible resting order from the same client
- Supports cancelation of open size as well as reduction of total size (executed + open)
CoralME supports both OrderBookListener and OrderListener. These external listeners cannot reenter the same order book while any of their listener callback methods are executing. A reentrant attempt throws a ReentrantOrderBookOperationException, and the requested operation is not executed.
An exception thrown by any external listener callback does not interrupt the current order book operation or prevent the remaining listeners from running. CoralME collects these exceptions and reports them after one complete OrderBook operation through the corresponding listener type's onExceptionsThrown method. Exceptions thrown from onExceptionsThrown are ignored so that exception reporting cannot recurse.
A listener that wants to modify or traverse the order book, modify one of its orders, or change listener registration must defer that work until the current OrderBook operation and all listener callbacks have finished. In an event-loop architecture, you do that by enqueueing the work for a later iteration of the same thread that owns CoralME.
Usually when people talk about a Matching Engine, what they are really referring to is the full solution for an electronic exchange. That would include gateways, drop copies, market data, balances, reports, monitors, margins, compliance, fees, etc. Plus the messaging middleware to tie all these pieces together. In that context, the matching engine is really just one of the many parts of an electronic exchange. It is an important part, the central nervous systems of an exchange, which maintains orders resting inside order books, and matches them when liquidity takers meet liquidity providers (i.e. market makers).
Refer to Example.java for a bunch of order matching use-cases.
The OrderBookTest.java might give you some good ideas as well but I find the Example.java easier to follow.
public interface OrderBookListener {
public void onOrderReduced(OrderBook orderBook, long time, Order order,
long canceledSize, long reduceNewTotalSize,
CancelReason cancelReason);
public void onOrderCanceled(OrderBook orderBook, long time, Order order,
long canceledSize, CancelReason cancelReason);
public void onOrderExecuted(OrderBook orderBook, long time, Order order,
ExecuteSide executeSide, long executeSize,
long executePrice, long executeId, long executeMatchId);
public void onOrderAccepted(OrderBook orderBook, long time, Order order);
public void onOrderRejected(OrderBook orderBook, long time, Order order,
RejectReason rejectReason);
public void onOrderRested(OrderBook orderBook, long time, Order order,
long restSize, long restPrice);
public void onOrderTerminated(OrderBook orderBook, long time, Order order);
public void onExceptionsThrown(OrderBook orderBook,
OrderBookListenerExceptions exceptions);
}final long CLIENT_ID = 1001L;
long orderId = 0;
// This OrderBookListener will print all callbacks to System.out
OrderBookLogger orderBookLogger = new OrderBookLogger();
OrderBook orderBook = new OrderBook("AAPL", orderBookLogger);
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.BUY, 200, 150.44, TimeInForce.DAY);
/*
-----> onOrderAccepted called:
orderBook=AAPL
time=1700598048045000000
order=Order [id=1, clientId=1001, clientOrderId=1, side=BUY, security=AAPL, originalSize=200, openSize=200,
executedSize=0, canceledSize=0, price=150.44, type=LIMIT, tif=DAY]
-----> onOrderRested called:
orderBook=AAPL
time=1700598048047000000
order=Order [id=1, clientId=1001, clientOrderId=1, side=BUY, security=AAPL, originalSize=200, openSize=200,
executedSize=0, canceledSize=0, price=150.44, type=LIMIT, tif=DAY]
restSize=200
restPrice=150.44
*/
orderBook.showLevels();
/*
200 @ 150.44 (orders=1)
--------
*/
orderBook.showOrders();
/*
200 @ 150.44 (id=1)
--------
*/
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.BUY, 500, 149.44, TimeInForce.DAY);
/*
-----> onOrderAccepted called:
orderBook=AAPL
time=1700598048049000000
order=Order [id=2, clientId=1001, clientOrderId=2, side=BUY, security=AAPL, originalSize=500, openSize=500,
executedSize=0, canceledSize=0, price=149.44, type=LIMIT, tif=DAY]
-----> onOrderRested called:
orderBook=AAPL
time=1700598048050000000
order=Order [id=2, clientId=1001, clientOrderId=2, side=BUY, security=AAPL, originalSize=500, openSize=500,
executedSize=0, canceledSize=0, price=149.44, type=LIMIT, tif=DAY]
restSize=500
restPrice=149.44
*/
orderBookLogger.off(); // omit callbacks output for clarity
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.BUY, 100, 149.44, TimeInForce.GTC);
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.BUY, 100, 148.14, TimeInForce.DAY);
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.SELL, 300, 153.24, TimeInForce.GTC);
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.SELL, 500, 156.43, TimeInForce.DAY);
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.SELL, 1500, 158.54, TimeInForce.DAY);
orderBook.showLevels();
/*
100 @ 148.14 (orders=1)
600 @ 149.44 (orders=2)
200 @ 150.44 (orders=1)
-------- 2.80
300 @ 153.24 (orders=1)
500 @ 156.43 (orders=1)
1500 @ 158.54 (orders=1)
*/
orderBook.showOrders();
/*
100 @ 148.14 (id=4)
500 @ 149.44 (id=2)
100 @ 149.44 (id=3)
200 @ 150.44 (id=1)
-------- 2.80
300 @ 153.24 (id=5)
500 @ 156.43 (id=6)
1500 @ 158.54 (id=7)
*/
orderBookLogger.on();
// Buy 100 @ market
orderBook.createMarket(CLIENT_ID, String.valueOf(++orderId), orderId, Side.BUY, 100);
/*
-----> onOrderAccepted called:
orderBook=AAPL
time=1700598048051000000
order=Order [id=8, clientId=1001, clientOrderId=8, side=BUY, security=AAPL, originalSize=100, openSize=100,
executedSize=0, canceledSize=0, type=MARKET]
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048051000000
order=Order [id=5, clientId=1001, clientOrderId=5, side=SELL, security=AAPL, originalSize=300, openSize=200,
executedSize=100, canceledSize=0, price=153.24, type=LIMIT, tif=GTC]
executeSide=MAKER
executeSize=100
executePrice=153.24
executeId=1
executeMatchId=1
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048051000000
order=Order [id=8, clientId=1001, clientOrderId=8, side=BUY, security=AAPL, originalSize=100, openSize=0,
executedSize=100, canceledSize=0, type=MARKET]
executeSide=TAKER
executeSize=100
executePrice=153.24
executeId=2
executeMatchId=1
-----> onOrderTerminated called:
orderBook=AAPL
time=1700598048051000000
order=Order [id=8, clientId=1001, clientOrderId=8, side=BUY, security=AAPL, originalSize=100, openSize=0,
executedSize=100, canceledSize=0, type=MARKET]
*/
orderBook.showLevels();
/*
100 @ 148.14 (orders=1)
600 @ 149.44 (orders=2)
200 @ 150.44 (orders=1)
-------- 2.80
200 @ 153.24 (orders=1)
500 @ 156.43 (orders=1)
1500 @ 158.54 (orders=1)
*/
// cancel 100 shares from order with id = 1
Order order = orderBook.getOrder(1);
order.cancel(100);
/*
-----> onOrderReduced called:
orderBook=AAPL
time=1700598048053000000
order=Order [id=1, clientId=1001, clientOrderId=1, side=BUY, security=AAPL, originalSize=200, openSize=100,
executedSize=0, canceledSize=100, price=150.44, type=LIMIT, tif=DAY]
canceledSize=100
reduceNewTotalSize=100
cancelReason=USER
*/
orderBook.showLevels();
/*
100 @ 148.14 (orders=1)
600 @ 149.44 (orders=2)
100 @ 150.44 (orders=1)
-------- 2.80
200 @ 153.24 (orders=1)
500 @ 156.43 (orders=1)
1500 @ 158.54 (orders=1)
*/
// now cancel the order
order.cancel();
/*
-----> onOrderCanceled called:
orderBook=AAPL
time=1700598048053000000
order=Order [id=1, clientId=1001, clientOrderId=1, side=BUY, security=AAPL, originalSize=200, openSize=0,
executedSize=0, canceledSize=200, price=150.44, type=LIMIT, tif=DAY]
canceledSize=100
cancelReason=USER
-----> onOrderTerminated called:
orderBook=AAPL
time=1700598048053000000
order=Order [id=1, clientId=1001, clientOrderId=1, side=BUY, security=AAPL, originalSize=200, openSize=0,
executedSize=0, canceledSize=200, price=150.44, type=LIMIT, tif=DAY]
*/
orderBook.showLevels();
/*
100 @ 148.14 (orders=1)
600 @ 149.44 (orders=2)
-------- 3.80
200 @ 153.24 (orders=1)
500 @ 156.43 (orders=1)
1500 @ 158.54 (orders=1)
*/
// hit the sell side of the book with a LIMIT IOC and notice your price improvement
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.BUY, 3000, 155.00, TimeInForce.IOC);
/*
-----> onOrderAccepted called:
orderBook=AAPL
time=1700598048054000000
order=Order [id=9, clientId=1001, clientOrderId=9, side=BUY, security=AAPL, originalSize=3000, openSize=3000,
executedSize=0, canceledSize=0, price=155.0, type=LIMIT, tif=IOC]
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048054000000
order=Order [id=5, clientId=1001, clientOrderId=5, side=SELL, security=AAPL, originalSize=300, openSize=0,
executedSize=300, canceledSize=0, price=153.24, type=LIMIT, tif=GTC]
executeSide=MAKER
executeSize=200
executePrice=153.24
executeId=3
executeMatchId=2
-----> onOrderTerminated called:
orderBook=AAPL
time=1700598048054000000
order=Order [id=5, clientId=1001, clientOrderId=5, side=SELL, security=AAPL, originalSize=300, openSize=0,
executedSize=300, canceledSize=0, price=153.24, type=LIMIT, tif=GTC]
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048054000000
order=Order [id=9, clientId=1001, clientOrderId=9, side=BUY, security=AAPL, originalSize=3000, openSize=2800,
executedSize=200, canceledSize=0, price=155.0, type=LIMIT, tif=IOC]
executeSide=TAKER
executeSize=200
executePrice=153.24
executeId=4
executeMatchId=2
-----> onOrderCanceled called:
orderBook=AAPL
time=1700598048055000000
order=Order [id=9, clientId=1001, clientOrderId=9, side=BUY, security=AAPL, originalSize=3000, openSize=0,
executedSize=200, canceledSize=2800, price=155.0, type=LIMIT, tif=IOC]
canceledSize=2800
cancelReason=MISSED
-----> onOrderTerminated called:
orderBook=AAPL
time=1700598048055000000
order=Order [id=9, clientId=1001, clientOrderId=9, side=BUY, security=AAPL, originalSize=3000, openSize=0,
executedSize=200, canceledSize=2800, price=155.0, type=LIMIT, tif=IOC]
*/
orderBook.showLevels();
/*
100 @ 148.14 (orders=1)
600 @ 149.44 (orders=2)
-------- 6.99
500 @ 156.43 (orders=1)
1500 @ 158.54 (orders=1)
*/
orderBookLogger.off();
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.SELL, 3000, 160.00, TimeInForce.DAY);
orderBook.showLevels();
/*
100 @ 148.14 (orders=1)
600 @ 149.44 (orders=2)
-------- 6.99
500 @ 156.43 (orders=1)
1500 @ 158.54 (orders=1)
3000 @ 160.00 (orders=1)
*/
// now hit two ask levels, price improve and sit on the book at 159.00
orderBookLogger.on();
orderBook.createLimit(CLIENT_ID, String.valueOf(++orderId), orderId,
Side.BUY, 3900, 159.00, TimeInForce.DAY);
/*
-----> onOrderAccepted called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=11, clientId=1001, clientOrderId=11, side=BUY, security=AAPL, originalSize=3900, openSize=3900,
executedSize=0, canceledSize=0, price=159.0, type=LIMIT, tif=DAY]
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=6, clientId=1001, clientOrderId=6, side=SELL, security=AAPL, originalSize=500, openSize=0,
executedSize=500, canceledSize=0, price=156.43, type=LIMIT, tif=DAY]
executeSide=MAKER
executeSize=500
executePrice=156.43
executeId=5
executeMatchId=3
-----> onOrderTerminated called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=6, clientId=1001, clientOrderId=6, side=SELL, security=AAPL, originalSize=500, openSize=0,
executedSize=500, canceledSize=0, price=156.43, type=LIMIT, tif=DAY]
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=11, clientId=1001, clientOrderId=11, side=BUY, security=AAPL, originalSize=3900, openSize=3400,
executedSize=500, canceledSize=0, price=159.0, type=LIMIT, tif=DAY]
executeSide=TAKER
executeSize=500
executePrice=156.43
executeId=6
executeMatchId=3
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=7, clientId=1001, clientOrderId=7, side=SELL, security=AAPL, originalSize=1500, openSize=0,
executedSize=1500, canceledSize=0, price=158.54, type=LIMIT, tif=DAY]
executeSide=MAKER
executeSize=1500
executePrice=158.54
executeId=7
executeMatchId=4
-----> onOrderTerminated called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=7, clientId=1001, clientOrderId=7, side=SELL, security=AAPL, originalSize=1500, openSize=0,
executedSize=1500, canceledSize=0, price=158.54, type=LIMIT, tif=DAY]
-----> onOrderExecuted called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=11, clientId=1001, clientOrderId=11, side=BUY, security=AAPL, originalSize=3900, openSize=1900,
executedSize=2000, canceledSize=0, price=159.0, type=LIMIT, tif=DAY]
executeSide=TAKER
executeSize=1500
executePrice=158.54
executeId=8
executeMatchId=4
-----> onOrderRested called:
orderBook=AAPL
time=1700598048056000000
order=Order [id=11, clientId=1001, clientOrderId=11, side=BUY, security=AAPL, originalSize=3900, openSize=1900,
executedSize=2000, canceledSize=0, price=159.0, type=LIMIT, tif=DAY]
restSize=1900
restPrice=159.0
*/
orderBook.showOrders();
/*
100 @ 148.14 (id=4)
500 @ 149.44 (id=2)
100 @ 149.44 (id=3)
1900 @ 159.00 (id=11) <==== You order sat here after hitting some asks...
-------- 1.00
3000 @ 160.00 (id=10)
*/Check NoGCTest.java to see that it creates a book and populates this book with 18 orders one million times. And on each of these one million times it does a bunch of executions, rejects, cancelations, reduces, etc. Run this test with -Xlog:gc -Xms32m -Xmx64m and you will always see zero GC activity. No matter how many iterations you perform, the gc activity is always zero. If you want to see some GC activity, you can turn on a flag that forces the creation of garbage by producing some strings in the middle of the loop.
$ ./bin/runGCTest.sh
java -Xlog:gc -Xms32m -Xmx64m -cp target/coralme-all.jar com.coralblocks.coralme.example.NoGCTest false 1000000
[0.008s][info][gc] Using G1
1000000 ... DONE!
$ ./bin/runGCTest.sh true
java -Xlog:gc -Xms32m -Xmx64m -cp target/coralme-all.jar com.coralblocks.coralme.example.NoGCTest true 1000000
[0.008s][info][gc] Using G1
20000[0.114s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 14M->1M(34M) 0.565ms
50000[0.159s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 19M->1M(34M) 0.236ms
90000[0.201s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 19M->1M(34M) 0.237ms
120000[0.241s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 19M->1M(34M) 0.224ms
(...)
890000[1.138s][info][gc] GC(26) Pause Young (Normal) (G1 Evacuation Pause) 19M->1M(34M) 0.108ms
920000[1.177s][info][gc] GC(27) Pause Young (Normal) (G1 Evacuation Pause) 19M->1M(34M) 0.095ms
960000[1.216s][info][gc] GC(28) Pause Young (Normal) (G1 Evacuation Pause) 19M->1M(34M) 0.102ms
990000[1.256s][info][gc] GC(29) Pause Young (Normal) (G1 Evacuation Pause) 19M->1M(34M) 0.095ms
1000000 ... DONE!