├── .gitignore ├── .travis.yml ├── README.md ├── engine.go ├── engine_test.go ├── limits.go ├── main.go ├── score_feed.go └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | .idea 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | go-quantcup 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | notifications: 3 | email: 4 | recipients: rdingwall@gmail.com 5 | on_success: change 6 | on_failure: always 7 | install: 8 | - go get github.com/stretchr/testify/assert 9 | - go get github.com/grd/stat 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Go Limit Order Book 2 | 3 | [![Build Status](https://travis-ci.org/rdingwall/go-quantcup.svg?branch=master)](https://travis-ci.org/rdingwall/go-quantcup) 4 | 5 | Port of winning www.quantcup.org competition entry (implementing a fast stock exchange matching engine for an HFT bot) from C to Go. Emphasis on Go language features and style over raw performance. 6 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | /***************************************************************************** 2 | * QuantCup 1: Price-Time Matching Engine 3 | * 4 | * Submitted by: voyager (Go port by rdingwall@gmail.com) 5 | * 6 | * Design Overview: 7 | * In this implementation, the limit order book is represented using 8 | * a flat linear array (pricePoints), indexed by the numeric price value. 9 | * Each entry in this array corresponds to a specific price point and holds 10 | * an instance of struct pricePoint. This data structure maintains a list 11 | * of outstanding buy/sell orders at the respective price. Each outstanding 12 | * limit order is represented by an instance of struct orderBookEntry. 13 | * 14 | * askMin and bidMax are global variables that maintain starting points, 15 | * at which the matching algorithm initiates its search. 16 | * askMin holds the lowest price that contains at least one outstanding 17 | * sell order. Analogously, bidMax represents the maximum price point that 18 | * contains at least one outstanding buy order. 19 | * 20 | * When a Buy order arrives, we search the book for outstanding Sell orders 21 | * that cross with the incoming order. We start the search at askMin and 22 | * proceed upwards, incrementing askMin until: 23 | * a) The incoming Buy order is filled. 24 | * b) We reach a price point that no longer crosses with the incoming 25 | * limit price (askMin > BuyOrder.price) 26 | * In case b), we create a new orderBookEntry to record the 27 | * remainder of the incoming Buy order and add it to the global order 28 | * book by appending it to the list at pricePoints[BuyOrder.price]. 29 | * 30 | * Incoming Sell orders are handled analogously, except that we start at 31 | * bidMax and proceed downwards. 32 | * 33 | * Although this matching algorithm runs in linear time and may, in 34 | * degenerate cases, require scanning a large number of array slots, 35 | * it appears to work reasonably well in practice, at least on the 36 | * simulated data feed (score_feed.h). The vast majority of incoming 37 | * limit orders can be handled by examining no more than two distinct 38 | * price points and no order requires examining more than five price points. 39 | * 40 | * To avoid incurring the costs of dynamic heap-based memory allocation, 41 | * this implementation maintains the full set of orderBookEntry instances 42 | * in a statically-allocated contiguous memory arena (arenaBookEntries). 43 | * Allocating a new entry is simply a matter of bumping up the orderID 44 | * counter (curOrderID) and returning a pointer to arenaBookEntries[curOrderID]. 45 | * 46 | * To cancel an order, we simply set its size to zero. Notably, we avoid 47 | * unhooking its orderBookEntry from the list of active orders in order to 48 | * avoid incurring the costs of pointer manipulation and conditional branches. 49 | * This allows us to handle order cancellation requests very efficiently; the 50 | * current implementation requires only one memory store instruction on 51 | * x86_64. During order matching, when we walk the list of outstanding orders, 52 | * we simply skip these zero-sized entries. 53 | * 54 | * The current implementation uses a custom version of strcpy() to copy the string 55 | * fields ("symbol" and "trader") between data structures. This custom version 56 | * has been optimized for the case STRINGLEN=5 and implements loop unrolling 57 | * to eliminate the use of induction variables and conditional branching. 58 | * 59 | * The memory layout of struct orderBookEntry has been optimized for 60 | * efficient cache access. 61 | *****************************************************************************/ 62 | 63 | package main 64 | 65 | type Engine struct { 66 | 67 | // Optional callback function that is called when a trade is executed. 68 | Execute func(Execution) 69 | 70 | // An array of pricePoint structures representing the entire limit order 71 | // book. 72 | pricePoints [uint(maxPrice) + 1]pricePoint 73 | 74 | curOrderID OrderID // Monotonically-increasing orderID. 75 | askMin uint // Minimum Ask price. 76 | bidMax uint // Maximum Bid price. 77 | 78 | // Statically-allocated memory arena for order book entries. This data 79 | // structure allows us to avoid the overhead of heap-based memory 80 | // allocation. 81 | bookEntries [maxNumOrders]orderBookEntry 82 | } 83 | 84 | // struct orderBookEntry: Describes a single outstanding limit order (Buy or 85 | // Sell). 86 | type orderBookEntry struct { 87 | size Size 88 | next *orderBookEntry 89 | trader string 90 | } 91 | 92 | // struct pricePoint: Describes a single price point in the limit order book. 93 | type pricePoint struct { 94 | listHead *orderBookEntry 95 | listTail *orderBookEntry 96 | } 97 | 98 | const maxNumOrders uint = 1010000 99 | 100 | func (e *Engine) Reset() { 101 | for _, pricePoint := range e.pricePoints { 102 | pricePoint.listHead = nil 103 | pricePoint.listTail = nil 104 | } 105 | 106 | for _, bookEntry := range e.bookEntries { 107 | bookEntry.size = 0 108 | bookEntry.next = nil 109 | bookEntry.trader = "" 110 | } 111 | 112 | e.curOrderID = 0 113 | e.askMin = uint(maxPrice) + 1 114 | e.bidMax = uint(minPrice) - 1 115 | } 116 | 117 | // Process an incoming limit order. 118 | func (e *Engine) Limit(order Order) OrderID { 119 | 120 | var price Price = order.price 121 | var orderSize Size = order.size 122 | 123 | if order.side == Bid { // Buy order. 124 | // Look for outstanding sell orders that cross with the incoming order. 125 | if uint(price) >= e.askMin { 126 | ppEntry := &e.pricePoints[e.askMin] 127 | 128 | for { 129 | bookEntry := ppEntry.listHead 130 | 131 | for bookEntry != nil { 132 | if bookEntry.size < orderSize { 133 | execute(e.Execute, order.symbol, order.trader, bookEntry.trader, price, bookEntry.size) 134 | 135 | orderSize -= bookEntry.size 136 | bookEntry = bookEntry.next 137 | } else { 138 | execute(e.Execute, order.symbol, order.trader, bookEntry.trader, price, orderSize) 139 | 140 | if bookEntry.size > orderSize { 141 | bookEntry.size -= orderSize 142 | } else { 143 | bookEntry = bookEntry.next 144 | } 145 | 146 | ppEntry.listHead = bookEntry 147 | e.curOrderID++ 148 | return e.curOrderID 149 | } 150 | } 151 | 152 | // We have exhausted all orders at the askMin price point. Move 153 | // on to the next price level. 154 | ppEntry.listHead = nil 155 | e.askMin++ 156 | ppEntry = &e.pricePoints[e.askMin] 157 | 158 | if uint(price) < e.askMin { 159 | break 160 | } 161 | } 162 | } 163 | 164 | e.curOrderID++ 165 | entry := &e.bookEntries[e.curOrderID] 166 | entry.size = orderSize 167 | entry.trader = order.trader 168 | ppInsertOrder(&e.pricePoints[price], entry) 169 | 170 | if e.bidMax < uint(price) { 171 | e.bidMax = uint(price) 172 | } 173 | 174 | return e.curOrderID 175 | } else { // Sell order. 176 | // Look for outstanding Buy orders that cross with the incoming order. 177 | if uint(price) <= e.bidMax { 178 | ppEntry := &e.pricePoints[e.bidMax] 179 | 180 | for { 181 | bookEntry := ppEntry.listHead 182 | 183 | for bookEntry != nil { 184 | if bookEntry.size < orderSize { 185 | execute(e.Execute, order.symbol, bookEntry.trader, order.trader, price, bookEntry.size) 186 | 187 | orderSize -= bookEntry.size 188 | bookEntry = bookEntry.next 189 | } else { 190 | execute(e.Execute, order.symbol, bookEntry.trader, order.trader, price, orderSize) 191 | 192 | if bookEntry.size > orderSize { 193 | bookEntry.size -= orderSize 194 | } else { 195 | bookEntry = bookEntry.next 196 | } 197 | 198 | ppEntry.listHead = bookEntry 199 | e.curOrderID++ 200 | return e.curOrderID 201 | } 202 | } 203 | 204 | // We have exhausted all orders at the bidMax price point. Move 205 | // on to the next price level. 206 | ppEntry.listHead = nil 207 | e.bidMax-- 208 | ppEntry = &e.pricePoints[e.bidMax] 209 | 210 | if uint(price) > e.bidMax { 211 | break 212 | } 213 | } 214 | } 215 | 216 | e.curOrderID++ 217 | entry := &e.bookEntries[e.curOrderID] 218 | entry.size = orderSize 219 | entry.trader = order.trader 220 | ppInsertOrder(&e.pricePoints[price], entry) 221 | 222 | if e.askMin > uint(price) { 223 | e.askMin = uint(price) 224 | } 225 | 226 | return e.curOrderID 227 | } 228 | } 229 | 230 | func (e *Engine) Cancel(orderID OrderID) { 231 | e.bookEntries[orderID].size = 0 232 | } 233 | 234 | // Report trade execution. 235 | func execute(hook func(Execution), symbol, buyTrader, sellTrader string, price Price, size Size) { 236 | if hook == nil { 237 | return // No callback defined. 238 | } 239 | 240 | if size == 0 { 241 | return // Skip orders that have been cancelled. 242 | } 243 | 244 | var exec Execution = Execution{symbol: symbol, price: price, size: size} 245 | 246 | exec.side = Bid 247 | exec.trader = buyTrader 248 | 249 | hook(exec) // Report the buy-side trade. 250 | 251 | exec.side = Ask 252 | exec.trader = sellTrader 253 | hook(exec) // Report the sell-side trade. 254 | } 255 | 256 | // Insert a new order book entry at the tail of the price point list. 257 | func ppInsertOrder(ppEntry *pricePoint, entry *orderBookEntry) { 258 | if ppEntry.listHead != nil { 259 | ppEntry.listTail.next = entry 260 | } else { 261 | ppEntry.listHead = entry 262 | } 263 | ppEntry.listTail = entry 264 | } 265 | -------------------------------------------------------------------------------- /engine_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type Test struct { 10 | Orders []Order 11 | Cancels []OrderID 12 | Orders2 []Order 13 | Expected []Execution 14 | } 15 | 16 | const maxExecutionCount int = 100 17 | 18 | var ( 19 | oa101x100 = Order{"JPM", "MAX", Ask, 101, 100} 20 | ob101x100 = Order{"JPM", "MAX", Bid, 101, 100} 21 | oa101x50 = Order{"JPM", "MAX", Ask, 101, 50} 22 | ob101x50 = Order{"JPM", "MAX", Bid, 101, 50} 23 | oa101x25 = Order{"JPM", "MAX", Ask, 101, 25} 24 | ob101x25 = Order{"JPM", "MAX", Bid, 101, 25} 25 | ob101x25x = Order{"JPM", "XAM", Bid, 101, 25} 26 | 27 | xa101x100 = Execution{"JPM", "MAX", Ask, 101, 100} 28 | xb101x100 = Execution{"JPM", "MAX", Bid, 101, 100} 29 | xa101x50 = Execution{"JPM", "MAX", Ask, 101, 50} 30 | xb101x50 = Execution{"JPM", "MAX", Bid, 101, 50} 31 | xa101x25 = Execution{"JPM", "MAX", Ask, 101, 25} 32 | xb101x25 = Execution{"JPM", "MAX", Bid, 101, 25} 33 | xb101x25x = Execution{"JPM", "XAM", Bid, 101, 25} 34 | ) 35 | 36 | func TestAsk(t *testing.T) { 37 | runTest(t, &Test{Orders: []Order{oa101x100}}) 38 | } 39 | 40 | func TestBid(t *testing.T) { 41 | runTest(t, &Test{Orders: []Order{ob101x100}}) 42 | } 43 | 44 | func TestExecution(t *testing.T) { 45 | runTest(t, &Test{Orders: []Order{oa101x100, ob101x100}, Expected: []Execution{xa101x100, xb101x100}}) 46 | } 47 | 48 | func TestReordering1(t *testing.T) { 49 | runTest(t, &Test{Orders: []Order{oa101x100, ob101x100}, Expected: []Execution{xb101x100, xa101x100}}) 50 | } 51 | 52 | func TestReordering2(t *testing.T) { 53 | runTest(t, &Test{Orders: []Order{ob101x100, oa101x100}, Expected: []Execution{xa101x100, xb101x100}}) 54 | } 55 | 56 | func TestReordering3(t *testing.T) { 57 | runTest(t, &Test{Orders: []Order{ob101x100, oa101x100}, Expected: []Execution{xb101x100, xa101x100}}) 58 | } 59 | 60 | func TestPartialFill1(t *testing.T) { 61 | runTest(t, &Test{Orders: []Order{oa101x100, ob101x50}, Expected: []Execution{xa101x50, xb101x50}}) 62 | } 63 | 64 | func TestPartialFill2(t *testing.T) { 65 | runTest(t, &Test{Orders: []Order{oa101x50, ob101x100}, Expected: []Execution{xa101x50, xb101x50}}) 66 | } 67 | 68 | func TestIncrementalOverFill1(t *testing.T) { 69 | runTest(t, &Test{Orders: []Order{oa101x100, ob101x25, ob101x25, ob101x25, ob101x25, ob101x25}, Expected: []Execution{xa101x25, xb101x25, xa101x25, xb101x25, xa101x25, xb101x25, xa101x25, xb101x25}}) 70 | } 71 | 72 | func TestIncrementalOverFill2(t *testing.T) { 73 | runTest(t, &Test{Orders: []Order{ob101x100, oa101x25, oa101x25, oa101x25, oa101x25, oa101x25}, Expected: []Execution{xa101x25, xb101x25, xa101x25, xb101x25, xa101x25, xb101x25, xa101x25, xb101x25}}) 74 | } 75 | 76 | func TestQueuePosition(t *testing.T) { 77 | runTest(t, &Test{Orders: []Order{ob101x25x, ob101x25, oa101x25}, Expected: []Execution{xa101x25, xb101x25x}}) 78 | } 79 | 80 | func TestCancel(t *testing.T) { 81 | runTest(t, &Test{Orders: []Order{ob101x25}, Cancels: []OrderID{1}, Orders2: []Order{oa101x25}}) 82 | } 83 | 84 | func TestCancelFromFrontOfQueue(t *testing.T) { 85 | runTest(t, &Test{Orders: []Order{ob101x25x, ob101x25}, Cancels: []OrderID{1}, Orders2: []Order{oa101x25}, Expected: []Execution{xa101x25, xb101x25}}) 86 | } 87 | 88 | func TestCancelFrontBackOutOfOrderThenPartialExecution(t *testing.T) { 89 | runTest(t, &Test{Orders: []Order{ob101x100, ob101x25x, ob101x25x, ob101x50}, Cancels: []OrderID{1, 4, 3}, Orders2: []Order{oa101x50}, Expected: []Execution{xb101x25x, xa101x25}}) 90 | } 91 | 92 | func runTest(t *testing.T, test *Test) { 93 | var executions []Execution 94 | var e Engine 95 | e.Reset() 96 | 97 | e.Execute = func(e Execution) { 98 | t.Logf("<- received execution: %v", &e) 99 | executions = append(executions, e) 100 | assert.False(t, len(executions) > maxExecutionCount, "too many executions, test array overflow") 101 | } 102 | 103 | curOrderID := feedOrders(t, &e, 0, &test.Orders) 104 | feedCancels(t, &e, &test.Cancels) 105 | feedOrders(t, &e, curOrderID, &test.Orders2) 106 | 107 | assert.Equal(t, len(test.Expected), len(executions), "incorrect number of executions") 108 | 109 | // Assert executions. 110 | for i := 0; i < len(test.Expected); i += 2 { 111 | expected1 := &test.Expected[i] 112 | expected2 := &test.Expected[i+1] 113 | actual1 := &executions[i] 114 | actual2 := &executions[i+1] 115 | 116 | match1 := compare(expected1, actual1) && compare(expected2, actual2) 117 | match2 := compare(expected1, actual2) && compare(expected2, actual1) 118 | 119 | assert.True(t, match1 || match2, `executions #%v and #%v, 120 | %v, 121 | %v 122 | should have been 123 | %v, 124 | %v. 125 | Stopped there.`, i, i+1, actual1, actual2, expected1, expected2) 126 | } 127 | } 128 | 129 | func feedOrders(t *testing.T, e *Engine, curOrderID OrderID, orders *[]Order) OrderID { 130 | if orders != nil { 131 | for i, order := range *orders { 132 | id := e.Limit(order) 133 | t.Logf("-> submitted order #%v: %v", id, &order) 134 | curOrderID++ 135 | assert.Equal(t, id, curOrderID, "orderid returned was %v, should have been %v.", id, i+1) 136 | } 137 | } 138 | 139 | return curOrderID 140 | } 141 | 142 | func feedCancels(t *testing.T, e *Engine, cancels *[]OrderID) { 143 | if cancels != nil { 144 | for _, orderID := range *cancels { 145 | e.Cancel(orderID) 146 | t.Logf("-> cancelled #%v", orderID) 147 | } 148 | } 149 | } 150 | 151 | func compare(a, b *Execution) bool { 152 | return a.symbol == b.symbol && 153 | a.trader == b.trader && 154 | a.side == b.side && 155 | a.price == b.price && 156 | a.size == b.size 157 | } 158 | -------------------------------------------------------------------------------- /limits.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | maxPrice Price = 65535 5 | minPrice = 1 6 | maxLiveOrders uint16 = 65535 7 | ) 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/grd/stat" 8 | ) 9 | 10 | const ( 11 | batchSize int = 10 12 | replayCount = 200 13 | ) 14 | 15 | func main() { 16 | var e Engine 17 | 18 | // batch latency measurements. 19 | latencies := make([]time.Duration, replayCount*(len(ordersFeed)/batchSize)) 20 | 21 | for j := 0; j < replayCount; j++ { 22 | e.Reset() 23 | for i := batchSize; i < len(ordersFeed); i += batchSize { 24 | begin := time.Now() 25 | feed(&e, i-batchSize, i) 26 | end := time.Now() 27 | latencies[i/batchSize-1+(j*(len(ordersFeed)/batchSize))] = end.Sub(begin) 28 | } 29 | } 30 | 31 | data := DurationSlice(latencies) 32 | 33 | var mean float64 = stat.Mean(data) 34 | var stdDev = stat.SdMean(data, mean) 35 | var score = 0.5 * (mean + stdDev) 36 | 37 | fmt.Printf("mean(latency) = %1.2f, sd(latency) = %1.2f\n", mean, stdDev) 38 | fmt.Printf("You scored %1.2f. Try to minimize this.\n", score) 39 | } 40 | 41 | func feed(e *Engine, begin, end int) { 42 | for i := begin; i < end; i++ { 43 | var order Order = ordersFeed[i] 44 | if order.price == 0 { 45 | orderID := OrderID(order.size) 46 | e.Cancel(orderID) 47 | } else { 48 | e.Limit(order) 49 | } 50 | } 51 | } 52 | 53 | type DurationSlice []time.Duration 54 | 55 | func (f DurationSlice) Get(i int) float64 { return float64(f[i]) } 56 | func (f DurationSlice) Len() int { return len(f) } 57 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Price uint16 // 0-65536 eg the price 123.45 = 12345 8 | type OrderID uint64 9 | type Size uint64 10 | type Side int 11 | 12 | type Order struct { 13 | symbol string 14 | trader string 15 | side Side 16 | price Price 17 | size Size 18 | } 19 | 20 | // Execution Report (send one per opposite-sided order completely filled). 21 | type Execution Order 22 | 23 | const ( 24 | Bid Side = iota 25 | Ask 26 | ) 27 | 28 | func (o *Execution) String() string { 29 | return fmt.Sprintf("{symbol: %v, trader: %v, side: %v, price: %v, size: %v}", o.symbol, o.trader, o.side, o.price, o.size) 30 | } 31 | 32 | func (o *Order) String() string { 33 | return fmt.Sprintf("{symbol: %v, trader: %v, side: %v, price: %v, size: %v}", o.symbol, o.trader, o.side, o.price, o.size) 34 | } 35 | 36 | func (s Side) String() string { 37 | switch s { 38 | case Bid: 39 | return "Bid" 40 | default: 41 | return "Ask" 42 | } 43 | } 44 | --------------------------------------------------------------------------------