From 00a9817f23d56b5a1b83103526c8f3bc8558a01a Mon Sep 17 00:00:00 2001 From: Ilya Petrusenko Date: Fri, 23 Oct 2026 00:08:41 +0200 Subject: [PATCH 1/7] side entries carry the price again, shared gallop search --- book/book.go | 16 +-- book/side.go | 37 +++--- book/side_test.go | 309 +++++++++++++++++++++++++++++----------------- 3 files changed, 230 insertions(+), 132 deletions(-) diff --git a/book/book.go b/book/book.go index 7f5124c..0bdd4fa 100644 --- a/book/book.go +++ b/book/book.go @@ -41,15 +41,15 @@ func (b *Book) Rest(s Side, h levels.Handle, price, qty uint64) error { } sd := b.side(s) - i, found := sd.find(b.levels, price) + i, found := sd.find(price) if found { - b.levels.Rest(h, sd.levels[i], qty) + b.levels.Rest(h, levels.LevelIndex(sd.levels[i].idx), qty) return nil } lvl := b.levels.NewLevel(price) b.levels.Rest(h, lvl, qty) - sd.insertAt(i, lvl) + sd.insertAt(i, lvl, price) return nil } @@ -71,7 +71,7 @@ func (b *Book) LevelAt(s Side, depth int) (price, totalQty uint64, ok bool) { if i < 0 { return 0, 0, false } - l := sd.levels[i] + l := levels.LevelIndex(sd.levels[i].idx) return b.levels.Price(l), b.levels.TotalQty(l), true } @@ -133,15 +133,15 @@ func (b *Book) side(s Side) *side { func (b *Book) dropLevel(idx levels.LevelIndex) { price := b.levels.Price(idx) - i, found := b.bids.find(b.levels, price) - if found && b.bids.levels[i] == idx { + i, found := b.bids.find(price) + if found && b.bids.levels[i].idx == int32(idx) { b.bids.removeAt(i) b.levels.FreeLevel(idx) return } - i, found = b.asks.find(b.levels, price) - if found && b.asks.levels[i] == idx { + i, found = b.asks.find(price) + if found && b.asks.levels[i].idx == int32(idx) { b.asks.removeAt(i) b.levels.FreeLevel(idx) return diff --git a/book/side.go b/book/side.go index ad9e514..1031bbf 100644 --- a/book/side.go +++ b/book/side.go @@ -6,31 +6,36 @@ import ( "orderbook/levels" ) +type entry struct { + price uint64 + idx int32 +} + type side struct { - levels []levels.LevelIndex + levels []entry bid bool } -func (s *side) find(lv *levels.Levels, price uint64) (int, bool) { - n := len(s.levels) +func gallop(s []entry, bid bool, price uint64) (int, bool) { + n := len(s) if n == 0 { return 0, false } r := n - 1 - rPrice := lv.Price(s.levels[r]) + rPrice := s[r].price if rPrice == price { return r, true } - if (s.bid && rPrice < price) || - (!s.bid && rPrice > price) { + if (bid && rPrice < price) || + (!bid && rPrice > price) { return n, false } l, step := r, 1 for l >= 0 { - lPrice := lv.Price(s.levels[l]) - if (s.bid && lPrice <= price) || (!s.bid && lPrice >= price) { + lPrice := s[l].price + if (bid && lPrice <= price) || (!bid && lPrice >= price) { break } r = l @@ -43,13 +48,13 @@ func (s *side) find(lv *levels.Levels, price uint64) (int, bool) { for l <= r { m := l + (r-l)/2 - mPrice := lv.Price(s.levels[m]) + mPrice := s[m].price if mPrice == price { return m, true } - if (s.bid && mPrice > price) || - (!s.bid && mPrice < price) { + if (bid && mPrice > price) || + (!bid && mPrice < price) { r = m - 1 } else { l = m + 1 @@ -59,8 +64,12 @@ func (s *side) find(lv *levels.Levels, price uint64) (int, bool) { return l, false } -func (s *side) insertAt(i int, idx levels.LevelIndex) { - s.levels = slices.Insert(s.levels, i, idx) +func (s *side) find(price uint64) (int, bool) { + return gallop(s.levels, s.bid, price) +} + +func (s *side) insertAt(i int, idx levels.LevelIndex, price uint64) { + s.levels = slices.Insert(s.levels, i, entry{price: price, idx: int32(idx)}) } func (s *side) removeAt(i int) { @@ -71,5 +80,5 @@ func (s *side) best() (levels.LevelIndex, bool) { if len(s.levels) == 0 { return -1, false } - return s.levels[len(s.levels)-1], true + return levels.LevelIndex(s.levels[len(s.levels)-1].idx), true } diff --git a/book/side_test.go b/book/side_test.go index 47c2d26..8b51558 100644 --- a/book/side_test.go +++ b/book/side_test.go @@ -9,269 +9,358 @@ import ( "orderbook/levels" ) -func makeLevels() (*levels.Levels, []levels.LevelIndex) { - l := levels.New(100, 100) - l.NewLevel(0) - l.NewLevel(0) - l6 := l.NewLevel(6) - l.NewLevel(0) - l8 := l.NewLevel(8) - l2 := l.NewLevel(2) - l.NewLevel(0) - l.NewLevel(0) - l.NewLevel(0) - l3 := l.NewLevel(3) - l.NewLevel(0) - l.NewLevel(0) - l7 := l.NewLevel(7) - l.NewLevel(0) - l4 := l.NewLevel(4) - return l, []levels.LevelIndex{l2, l3, l4, l6, l7, l8} -} - func TestBidFind(t *testing.T) { - l, lvls := makeLevels() s := side{ - bid: true, - levels: lvls, + bid: true, + levels: []entry{ + {price: 2, idx: 10}, + {price: 3, idx: 12}, + {price: 4, idx: 37}, + {price: 6, idx: 99}, + {price: 7, idx: 45}, + {price: 8, idx: 18}, + }, } - idx, ok := s.find(l, 4) + idx, ok := s.find(4) require.True(t, ok) require.Equal(t, 2, idx) - idx, ok = s.find(l, 5) + idx, ok = s.find(5) require.False(t, ok) require.Equal(t, 3, idx) - idx, ok = s.find(l, 6) + idx, ok = s.find(6) require.True(t, ok) require.Equal(t, 3, idx) - idx, ok = s.find(l, 1) + idx, ok = s.find(1) require.False(t, ok) require.Equal(t, 0, idx) - idx, ok = s.find(l, 8) + idx, ok = s.find(8) require.True(t, ok) require.Equal(t, 5, idx) - idx, ok = s.find(l, 9) + idx, ok = s.find(9) require.False(t, ok) require.Equal(t, 6, idx) } func TestAskFind(t *testing.T) { - l, lvls := makeLevels() - slices.Reverse(lvls) s := side{ - bid: false, - levels: lvls, + bid: false, + levels: []entry{ + {price: 8, idx: 18}, + {price: 7, idx: 45}, + {price: 6, idx: 99}, + {price: 4, idx: 37}, + {price: 3, idx: 12}, + {price: 2, idx: 10}, + }, } - idx, ok := s.find(l, 4) + idx, ok := s.find(4) require.True(t, ok) require.Equal(t, 3, idx) - idx, ok = s.find(l, 5) + idx, ok = s.find(5) require.False(t, ok) require.Equal(t, 3, idx) - idx, ok = s.find(l, 6) + idx, ok = s.find(6) require.True(t, ok) require.Equal(t, 2, idx) - idx, ok = s.find(l, 1) + idx, ok = s.find(1) require.False(t, ok) require.Equal(t, 6, idx) - idx, ok = s.find(l, 8) + idx, ok = s.find(8) require.True(t, ok) require.Equal(t, 0, idx) - idx, ok = s.find(l, 9) + idx, ok = s.find(9) require.False(t, ok) require.Equal(t, 0, idx) } func TestBidInsert(t *testing.T) { - l, lvls := makeLevels() - l2, l3, l4, l6, l7, l8 := lvls[0], lvls[1], lvls[2], lvls[3], lvls[4], lvls[5] s := side{ - bid: true, - levels: slices.Grow(lvls, 1), + bid: true, + levels: slices.Grow([]entry{ + {price: 2, idx: 10}, + {price: 3, idx: 12}, + {price: 4, idx: 37}, + {price: 6, idx: 99}, + {price: 7, idx: 45}, + {price: 8, idx: 18}, + }, 1), } - l5 := l.NewLevel(5) - idx, ok := s.find(l, 5) + idx, ok := s.find(5) require.False(t, ok) require.Equal(t, 3, idx) - s.insertAt(idx, l5) - require.Equal(t, []levels.LevelIndex{l2, l3, l4, l5, l6, l7, l8}, s.levels) - - l1 := l.NewLevel(1) - idx, ok = s.find(l, 1) + s.insertAt(idx, 67, 5) + require.Equal(t, []entry{ + {2, 10}, + {3, 12}, + {4, 37}, + {5, 67}, + {6, 99}, + {7, 45}, + {8, 18}, + }, s.levels) + + idx, ok = s.find(1) require.False(t, ok) require.Equal(t, 0, idx) - s.insertAt(idx, l1) - require.Equal(t, []levels.LevelIndex{l1, l2, l3, l4, l5, l6, l7, l8}, s.levels) + s.insertAt(idx, 81, 1) + require.Equal(t, []entry{ + {1, 81}, + {2, 10}, + {3, 12}, + {4, 37}, + {5, 67}, + {6, 99}, + {7, 45}, + {8, 18}, + }, s.levels) best, found := s.best() require.True(t, found) - require.Equal(t, l8, best) + require.Equal(t, levels.LevelIndex(18), best) - l9 := l.NewLevel(9) - idx, ok = s.find(l, 9) + idx, ok = s.find(9) require.False(t, ok) require.Equal(t, 8, idx) - s.insertAt(idx, l9) - require.Equal(t, []levels.LevelIndex{l1, l2, l3, l4, l5, l6, l7, l8, l9}, s.levels) + s.insertAt(idx, 22, 9) + require.Equal(t, []entry{ + {1, 81}, + {2, 10}, + {3, 12}, + {4, 37}, + {5, 67}, + {6, 99}, + {7, 45}, + {8, 18}, + {9, 22}, + }, s.levels) best, found = s.best() require.True(t, found) - require.Equal(t, l9, best) + require.Equal(t, levels.LevelIndex(22), best) } func TestAskInsert(t *testing.T) { - l, lvls := makeLevels() - l2, l3, l4, l6, l7, l8 := lvls[0], lvls[1], lvls[2], lvls[3], lvls[4], lvls[5] - slices.Reverse(lvls) s := side{ - bid: false, - levels: slices.Grow(lvls, 1), + bid: false, + levels: slices.Grow([]entry{ + {price: 8, idx: 18}, + {price: 7, idx: 45}, + {price: 6, idx: 99}, + {price: 4, idx: 37}, + {price: 3, idx: 12}, + {price: 2, idx: 10}, + }, 1), } - l5 := l.NewLevel(5) - idx, ok := s.find(l, 5) + idx, ok := s.find(5) require.False(t, ok) require.Equal(t, 3, idx) - s.insertAt(idx, l5) - require.Equal(t, []levels.LevelIndex{l8, l7, l6, l5, l4, l3, l2}, s.levels) + s.insertAt(idx, 67, 5) + require.Equal(t, []entry{ + {8, 18}, + {7, 45}, + {6, 99}, + {5, 67}, + {4, 37}, + {3, 12}, + {2, 10}, + }, s.levels) best, found := s.best() require.True(t, found) - require.Equal(t, l2, best) + require.Equal(t, levels.LevelIndex(10), best) - l1 := l.NewLevel(1) - idx, ok = s.find(l, 1) + idx, ok = s.find(1) require.False(t, ok) require.Equal(t, 7, idx) - s.insertAt(idx, l1) - require.Equal(t, []levels.LevelIndex{l8, l7, l6, l5, l4, l3, l2, l1}, s.levels) + s.insertAt(idx, 81, 1) + require.Equal(t, []entry{ + {8, 18}, + {7, 45}, + {6, 99}, + {5, 67}, + {4, 37}, + {3, 12}, + {2, 10}, + {1, 81}, + }, s.levels) best, found = s.best() require.True(t, found) - require.Equal(t, l1, best) + require.Equal(t, levels.LevelIndex(81), best) - l9 := l.NewLevel(9) - idx, ok = s.find(l, 9) + idx, ok = s.find(9) require.False(t, ok) require.Equal(t, 0, idx) - s.insertAt(idx, l9) - require.Equal(t, []levels.LevelIndex{l9, l8, l7, l6, l5, l4, l3, l2, l1}, s.levels) + s.insertAt(idx, 22, 9) + require.Equal(t, []entry{ + {9, 22}, + {8, 18}, + {7, 45}, + {6, 99}, + {5, 67}, + {4, 37}, + {3, 12}, + {2, 10}, + {1, 81}, + }, s.levels) } func TestBidRemove(t *testing.T) { - l, lvls := makeLevels() - l2, l3, l6, l7, l8 := lvls[0], lvls[1], lvls[3], lvls[4], lvls[5] s := side{ - bid: true, - levels: lvls, + bid: true, + levels: []entry{ + {price: 2, idx: 10}, + {price: 3, idx: 12}, + {price: 4, idx: 37}, + {price: 6, idx: 99}, + {price: 7, idx: 45}, + {price: 8, idx: 18}, + }, } - idx, ok := s.find(l, 4) + idx, ok := s.find(4) require.True(t, ok) require.Equal(t, 2, idx) s.removeAt(idx) - require.Equal(t, []levels.LevelIndex{l2, l3, l6, l7, l8}, s.levels) + require.Equal(t, []entry{ + {2, 10}, + {3, 12}, + {6, 99}, + {7, 45}, + {8, 18}, + }, s.levels) best, found := s.best() require.True(t, found) - require.Equal(t, l8, best) + require.Equal(t, levels.LevelIndex(18), best) - idx, ok = s.find(l, 2) + idx, ok = s.find(2) require.True(t, ok) require.Equal(t, 0, idx) s.removeAt(idx) - require.Equal(t, []levels.LevelIndex{l3, l6, l7, l8}, s.levels) - - idx, ok = s.find(l, 8) + require.Equal(t, []entry{ + {3, 12}, + {6, 99}, + {7, 45}, + {8, 18}, + }, s.levels) + + idx, ok = s.find(8) require.True(t, ok) require.Equal(t, 3, idx) s.removeAt(idx) - require.Equal(t, []levels.LevelIndex{l3, l6, l7}, s.levels) + require.Equal(t, []entry{ + {3, 12}, + {6, 99}, + {7, 45}, + }, s.levels) best, found = s.best() require.True(t, found) - require.Equal(t, l7, best) + require.Equal(t, levels.LevelIndex(45), best) + } func TestAskRemove(t *testing.T) { - l, lvls := makeLevels() - l2, l3, l6, l7, l8 := lvls[0], lvls[1], lvls[3], lvls[4], lvls[5] - slices.Reverse(lvls) s := side{ - bid: false, - levels: lvls, + bid: false, + levels: []entry{ + {price: 8, idx: 18}, + {price: 7, idx: 45}, + {price: 6, idx: 99}, + {price: 4, idx: 37}, + {price: 3, idx: 12}, + {price: 2, idx: 10}, + }, } - idx, ok := s.find(l, 4) + idx, ok := s.find(4) require.True(t, ok) require.Equal(t, 3, idx) s.removeAt(idx) - require.Equal(t, []levels.LevelIndex{l8, l7, l6, l3, l2}, s.levels) + require.Equal(t, []entry{ + {8, 18}, + {7, 45}, + {6, 99}, + {3, 12}, + {2, 10}, + }, s.levels) best, found := s.best() require.True(t, found) - require.Equal(t, l2, best) + require.Equal(t, levels.LevelIndex(10), best) - idx, ok = s.find(l, 2) + idx, ok = s.find(2) require.True(t, ok) require.Equal(t, 4, idx) s.removeAt(idx) - require.Equal(t, []levels.LevelIndex{l8, l7, l6, l3}, s.levels) + require.Equal(t, []entry{ + {8, 18}, + {7, 45}, + {6, 99}, + {3, 12}, + }, s.levels) best, found = s.best() require.True(t, found) - require.Equal(t, l3, best) + require.Equal(t, levels.LevelIndex(12), best) - idx, ok = s.find(l, 8) + idx, ok = s.find(8) require.True(t, ok) require.Equal(t, 0, idx) s.removeAt(idx) - require.Equal(t, []levels.LevelIndex{l7, l6, l3}, s.levels) + require.Equal(t, []entry{ + {7, 45}, + {6, 99}, + {3, 12}, + }, s.levels) } func TestEmpty(t *testing.T) { - _, lvls := makeLevels() - l7 := lvls[4] - s := side{bid: true} + s := side{ + bid: true, + levels: []entry{}, + } best, found := s.best() require.False(t, found) require.Equal(t, levels.LevelIndex(-1), best) - s.insertAt(0, l7) - require.Equal(t, []levels.LevelIndex{l7}, s.levels) + s.insertAt(0, 45, 7) + require.Equal(t, []entry{{7, 45}}, s.levels) best, found = s.best() require.True(t, found) - require.Equal(t, l7, best) + require.Equal(t, levels.LevelIndex(45), best) s.removeAt(0) - require.Empty(t, s.levels) + require.Equal(t, []entry{}, s.levels) best, found = s.best() require.False(t, found) From 3cc91d61c5f010facc3de9374fa4d59e98faccb3 Mon Sep 17 00:00:00 2001 From: Ilya Petrusenko Date: Fri, 23 Oct 2026 02:16:56 +0200 Subject: [PATCH 2/7] two-level price side, blocks of 256 entries indexed by their first price --- book/book.go | 24 ++-- book/side.go | 106 +++++++++++++++-- book/side_test.go | 298 +++++++++++++++++++++++++++++----------------- 3 files changed, 295 insertions(+), 133 deletions(-) diff --git a/book/book.go b/book/book.go index 0bdd4fa..dc15d83 100644 --- a/book/book.go +++ b/book/book.go @@ -41,15 +41,15 @@ func (b *Book) Rest(s Side, h levels.Handle, price, qty uint64) error { } sd := b.side(s) - i, found := sd.find(price) + p, found := sd.find(price) if found { - b.levels.Rest(h, levels.LevelIndex(sd.levels[i].idx), qty) + b.levels.Rest(h, sd.at(p), qty) return nil } lvl := b.levels.NewLevel(price) b.levels.Rest(h, lvl, qty) - sd.insertAt(i, lvl, price) + sd.insertAt(p, lvl, price) return nil } @@ -66,12 +66,10 @@ func (b *Book) Add(s Side, seq, price, qty uint64) (levels.Handle, error) { } func (b *Book) LevelAt(s Side, depth int) (price, totalQty uint64, ok bool) { - sd := b.side(s) - i := len(sd.levels) - 1 - depth - if i < 0 { + l, ok := b.side(s).atDepth(depth) + if !ok { return 0, 0, false } - l := levels.LevelIndex(sd.levels[i].idx) return b.levels.Price(l), b.levels.TotalQty(l), true } @@ -133,16 +131,16 @@ func (b *Book) side(s Side) *side { func (b *Book) dropLevel(idx levels.LevelIndex) { price := b.levels.Price(idx) - i, found := b.bids.find(price) - if found && b.bids.levels[i].idx == int32(idx) { - b.bids.removeAt(i) + p, found := b.bids.find(price) + if found && b.bids.at(p) == idx { + b.bids.removeAt(p) b.levels.FreeLevel(idx) return } - i, found = b.asks.find(price) - if found && b.asks.levels[i].idx == int32(idx) { - b.asks.removeAt(i) + p, found = b.asks.find(price) + if found && b.asks.at(p) == idx { + b.asks.removeAt(p) b.levels.FreeLevel(idx) return } diff --git a/book/side.go b/book/side.go index 1031bbf..0b40e19 100644 --- a/book/side.go +++ b/book/side.go @@ -6,16 +6,28 @@ import ( "orderbook/levels" ) +const blockCap = 256 + +type block struct { + n int32 + entries [blockCap]entry +} + type entry struct { price uint64 idx int32 } type side struct { - levels []entry + blocks []block + chain []entry bid bool } +type pos struct { + c, i int +} + func gallop(s []entry, bid bool, price uint64) (int, bool) { n := len(s) if n == 0 { @@ -64,21 +76,97 @@ func gallop(s []entry, bid bool, price uint64) (int, bool) { return l, false } -func (s *side) find(price uint64) (int, bool) { - return gallop(s.levels, s.bid, price) +func (s *side) find(price uint64) (pos, bool) { + if len(s.chain) == 0 { + return pos{0, 0}, false + } + + c, found := gallop(s.chain, s.bid, price) + if found { + return pos{c, 0}, true + } + if c > 0 { + c-- + } + + b := &s.blocks[s.chain[c].idx] + i, found := gallop(b.entries[:b.n], s.bid, price) + return pos{c, i}, found +} + +func (s *side) insertAt(p pos, idx levels.LevelIndex, price uint64) { + if len(s.chain) == 0 { + s.blocks = append(s.blocks, block{n: 1, entries: [blockCap]entry{{price: price, idx: int32(idx)}}}) + s.chain = append(s.chain, entry{price: price, idx: int32(len(s.blocks) - 1)}) + return + } + + k := s.chain[p.c].idx + if s.blocks[k].n == blockCap { + m := int32(blockCap / 2) + s.blocks = append(s.blocks, block{n: blockCap - m}) + nk := int32(len(s.blocks) - 1) + copy(s.blocks[nk].entries[:], s.blocks[k].entries[m:]) + s.blocks[k].n = m + s.chain = slices.Insert(s.chain, p.c+1, entry{price: s.blocks[nk].entries[0].price, idx: nk}) + if p.i > int(m) { + p.c++ + p.i -= int(m) + k = nk + } + } + + b := &s.blocks[k] + copy(b.entries[p.i+1:b.n+1], b.entries[p.i:b.n]) + b.entries[p.i] = entry{price: price, idx: int32(idx)} + b.n++ + if p.i == 0 { + s.chain[p.c].price = price + } +} + +func (s *side) removeAt(p pos) { + k := s.chain[p.c].idx + b := &s.blocks[k] + copy(b.entries[p.i:b.n-1], b.entries[p.i+1:b.n]) + b.n-- + + if b.n == 0 { + s.chain = append(s.chain[:p.c], s.chain[p.c+1:]...) + last := int32(len(s.blocks) - 1) + if k != last { + s.blocks[k] = s.blocks[last] + c, _ := gallop(s.chain, s.bid, s.blocks[k].entries[0].price) + s.chain[c].idx = k + } + s.blocks = s.blocks[:last] + return + } + if p.i == 0 { + s.chain[p.c].price = b.entries[0].price + } } -func (s *side) insertAt(i int, idx levels.LevelIndex, price uint64) { - s.levels = slices.Insert(s.levels, i, entry{price: price, idx: int32(idx)}) +func (s *side) at(p pos) levels.LevelIndex { + return levels.LevelIndex(s.blocks[s.chain[p.c].idx].entries[p.i].idx) } -func (s *side) removeAt(i int) { - s.levels = append(s.levels[:i], s.levels[i+1:]...) +func (s *side) atDepth(depth int) (levels.LevelIndex, bool) { + for c := len(s.chain) - 1; c >= 0; c-- { + b := &s.blocks[s.chain[c].idx] + if depth < int(b.n) { + return levels.LevelIndex(b.entries[int(b.n)-1-depth].idx), true + } + depth -= int(b.n) + } + return -1, false } func (s *side) best() (levels.LevelIndex, bool) { - if len(s.levels) == 0 { + if len(s.chain) == 0 { return -1, false } - return levels.LevelIndex(s.levels[len(s.levels)-1].idx), true + c := s.chain[len(s.chain)-1] + b := &s.blocks[c.idx] + return levels.LevelIndex(b.entries[b.n-1].idx), true } diff --git a/book/side_test.go b/book/side_test.go index 8b51558..73abc26 100644 --- a/book/side_test.go +++ b/book/side_test.go @@ -1,7 +1,6 @@ package book import ( - "slices" "testing" "github.com/stretchr/testify/require" @@ -9,98 +8,107 @@ import ( "orderbook/levels" ) -func TestBidFind(t *testing.T) { - s := side{ - bid: true, - levels: []entry{ - {price: 2, idx: 10}, - {price: 3, idx: 12}, - {price: 4, idx: 37}, - {price: 6, idx: 99}, - {price: 7, idx: 45}, - {price: 8, idx: 18}, - }, +func newSide(bid bool, entries []entry) side { + s := side{bid: bid} + for _, e := range entries { + p, _ := s.find(e.price) + s.insertAt(p, levels.LevelIndex(e.idx), e.price) } + return s +} + +func flat(s *side) []entry { + out := []entry{} + for _, c := range s.chain { + b := &s.blocks[c.idx] + out = append(out, b.entries[:b.n]...) + } + return out +} + +func TestBidFind(t *testing.T) { + s := newSide(true, []entry{ + {price: 2, idx: 10}, + {price: 3, idx: 12}, + {price: 4, idx: 37}, + {price: 6, idx: 99}, + {price: 7, idx: 45}, + {price: 8, idx: 18}, + }) idx, ok := s.find(4) require.True(t, ok) - require.Equal(t, 2, idx) + require.Equal(t, pos{0, 2}, idx) idx, ok = s.find(5) require.False(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) idx, ok = s.find(6) require.True(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) idx, ok = s.find(1) require.False(t, ok) - require.Equal(t, 0, idx) + require.Equal(t, pos{0, 0}, idx) idx, ok = s.find(8) require.True(t, ok) - require.Equal(t, 5, idx) + require.Equal(t, pos{0, 5}, idx) idx, ok = s.find(9) require.False(t, ok) - require.Equal(t, 6, idx) + require.Equal(t, pos{0, 6}, idx) } func TestAskFind(t *testing.T) { - s := side{ - bid: false, - levels: []entry{ - {price: 8, idx: 18}, - {price: 7, idx: 45}, - {price: 6, idx: 99}, - {price: 4, idx: 37}, - {price: 3, idx: 12}, - {price: 2, idx: 10}, - }, - } + s := newSide(false, []entry{ + {price: 8, idx: 18}, + {price: 7, idx: 45}, + {price: 6, idx: 99}, + {price: 4, idx: 37}, + {price: 3, idx: 12}, + {price: 2, idx: 10}, + }) idx, ok := s.find(4) require.True(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) idx, ok = s.find(5) require.False(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) idx, ok = s.find(6) require.True(t, ok) - require.Equal(t, 2, idx) + require.Equal(t, pos{0, 2}, idx) idx, ok = s.find(1) require.False(t, ok) - require.Equal(t, 6, idx) + require.Equal(t, pos{0, 6}, idx) idx, ok = s.find(8) require.True(t, ok) - require.Equal(t, 0, idx) + require.Equal(t, pos{0, 0}, idx) idx, ok = s.find(9) require.False(t, ok) - require.Equal(t, 0, idx) + require.Equal(t, pos{0, 0}, idx) } func TestBidInsert(t *testing.T) { - s := side{ - bid: true, - levels: slices.Grow([]entry{ - {price: 2, idx: 10}, - {price: 3, idx: 12}, - {price: 4, idx: 37}, - {price: 6, idx: 99}, - {price: 7, idx: 45}, - {price: 8, idx: 18}, - }, 1), - } + s := newSide(true, []entry{ + {price: 2, idx: 10}, + {price: 3, idx: 12}, + {price: 4, idx: 37}, + {price: 6, idx: 99}, + {price: 7, idx: 45}, + {price: 8, idx: 18}, + }) idx, ok := s.find(5) require.False(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) s.insertAt(idx, 67, 5) require.Equal(t, []entry{ @@ -111,11 +119,11 @@ func TestBidInsert(t *testing.T) { {6, 99}, {7, 45}, {8, 18}, - }, s.levels) + }, flat(&s)) idx, ok = s.find(1) require.False(t, ok) - require.Equal(t, 0, idx) + require.Equal(t, pos{0, 0}, idx) s.insertAt(idx, 81, 1) require.Equal(t, []entry{ @@ -127,7 +135,7 @@ func TestBidInsert(t *testing.T) { {6, 99}, {7, 45}, {8, 18}, - }, s.levels) + }, flat(&s)) best, found := s.best() require.True(t, found) @@ -135,7 +143,7 @@ func TestBidInsert(t *testing.T) { idx, ok = s.find(9) require.False(t, ok) - require.Equal(t, 8, idx) + require.Equal(t, pos{0, 8}, idx) s.insertAt(idx, 22, 9) require.Equal(t, []entry{ @@ -148,7 +156,7 @@ func TestBidInsert(t *testing.T) { {7, 45}, {8, 18}, {9, 22}, - }, s.levels) + }, flat(&s)) best, found = s.best() require.True(t, found) @@ -156,21 +164,18 @@ func TestBidInsert(t *testing.T) { } func TestAskInsert(t *testing.T) { - s := side{ - bid: false, - levels: slices.Grow([]entry{ - {price: 8, idx: 18}, - {price: 7, idx: 45}, - {price: 6, idx: 99}, - {price: 4, idx: 37}, - {price: 3, idx: 12}, - {price: 2, idx: 10}, - }, 1), - } + s := newSide(false, []entry{ + {price: 8, idx: 18}, + {price: 7, idx: 45}, + {price: 6, idx: 99}, + {price: 4, idx: 37}, + {price: 3, idx: 12}, + {price: 2, idx: 10}, + }) idx, ok := s.find(5) require.False(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) s.insertAt(idx, 67, 5) require.Equal(t, []entry{ @@ -181,7 +186,7 @@ func TestAskInsert(t *testing.T) { {4, 37}, {3, 12}, {2, 10}, - }, s.levels) + }, flat(&s)) best, found := s.best() require.True(t, found) @@ -189,7 +194,7 @@ func TestAskInsert(t *testing.T) { idx, ok = s.find(1) require.False(t, ok) - require.Equal(t, 7, idx) + require.Equal(t, pos{0, 7}, idx) s.insertAt(idx, 81, 1) require.Equal(t, []entry{ @@ -201,7 +206,7 @@ func TestAskInsert(t *testing.T) { {3, 12}, {2, 10}, {1, 81}, - }, s.levels) + }, flat(&s)) best, found = s.best() require.True(t, found) @@ -209,7 +214,7 @@ func TestAskInsert(t *testing.T) { idx, ok = s.find(9) require.False(t, ok) - require.Equal(t, 0, idx) + require.Equal(t, pos{0, 0}, idx) s.insertAt(idx, 22, 9) require.Equal(t, []entry{ @@ -222,25 +227,22 @@ func TestAskInsert(t *testing.T) { {3, 12}, {2, 10}, {1, 81}, - }, s.levels) + }, flat(&s)) } func TestBidRemove(t *testing.T) { - s := side{ - bid: true, - levels: []entry{ - {price: 2, idx: 10}, - {price: 3, idx: 12}, - {price: 4, idx: 37}, - {price: 6, idx: 99}, - {price: 7, idx: 45}, - {price: 8, idx: 18}, - }, - } + s := newSide(true, []entry{ + {price: 2, idx: 10}, + {price: 3, idx: 12}, + {price: 4, idx: 37}, + {price: 6, idx: 99}, + {price: 7, idx: 45}, + {price: 8, idx: 18}, + }) idx, ok := s.find(4) require.True(t, ok) - require.Equal(t, 2, idx) + require.Equal(t, pos{0, 2}, idx) s.removeAt(idx) require.Equal(t, []entry{ @@ -249,7 +251,7 @@ func TestBidRemove(t *testing.T) { {6, 99}, {7, 45}, {8, 18}, - }, s.levels) + }, flat(&s)) best, found := s.best() require.True(t, found) @@ -257,7 +259,7 @@ func TestBidRemove(t *testing.T) { idx, ok = s.find(2) require.True(t, ok) - require.Equal(t, 0, idx) + require.Equal(t, pos{0, 0}, idx) s.removeAt(idx) require.Equal(t, []entry{ @@ -265,18 +267,18 @@ func TestBidRemove(t *testing.T) { {6, 99}, {7, 45}, {8, 18}, - }, s.levels) + }, flat(&s)) idx, ok = s.find(8) require.True(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) s.removeAt(idx) require.Equal(t, []entry{ {3, 12}, {6, 99}, {7, 45}, - }, s.levels) + }, flat(&s)) best, found = s.best() require.True(t, found) @@ -285,21 +287,18 @@ func TestBidRemove(t *testing.T) { } func TestAskRemove(t *testing.T) { - s := side{ - bid: false, - levels: []entry{ - {price: 8, idx: 18}, - {price: 7, idx: 45}, - {price: 6, idx: 99}, - {price: 4, idx: 37}, - {price: 3, idx: 12}, - {price: 2, idx: 10}, - }, - } + s := newSide(false, []entry{ + {price: 8, idx: 18}, + {price: 7, idx: 45}, + {price: 6, idx: 99}, + {price: 4, idx: 37}, + {price: 3, idx: 12}, + {price: 2, idx: 10}, + }) idx, ok := s.find(4) require.True(t, ok) - require.Equal(t, 3, idx) + require.Equal(t, pos{0, 3}, idx) s.removeAt(idx) require.Equal(t, []entry{ @@ -308,7 +307,7 @@ func TestAskRemove(t *testing.T) { {6, 99}, {3, 12}, {2, 10}, - }, s.levels) + }, flat(&s)) best, found := s.best() require.True(t, found) @@ -316,7 +315,7 @@ func TestAskRemove(t *testing.T) { idx, ok = s.find(2) require.True(t, ok) - require.Equal(t, 4, idx) + require.Equal(t, pos{0, 4}, idx) s.removeAt(idx) require.Equal(t, []entry{ @@ -324,7 +323,7 @@ func TestAskRemove(t *testing.T) { {7, 45}, {6, 99}, {3, 12}, - }, s.levels) + }, flat(&s)) best, found = s.best() require.True(t, found) @@ -332,37 +331,114 @@ func TestAskRemove(t *testing.T) { idx, ok = s.find(8) require.True(t, ok) - require.Equal(t, 0, idx) + require.Equal(t, pos{0, 0}, idx) s.removeAt(idx) require.Equal(t, []entry{ {7, 45}, {6, 99}, {3, 12}, - }, s.levels) + }, flat(&s)) } func TestEmpty(t *testing.T) { - s := side{ - bid: true, - levels: []entry{}, - } + s := newSide(true, nil) best, found := s.best() require.False(t, found) require.Equal(t, levels.LevelIndex(-1), best) - s.insertAt(0, 45, 7) - require.Equal(t, []entry{{7, 45}}, s.levels) + s.insertAt(pos{0, 0}, 45, 7) + require.Equal(t, []entry{{7, 45}}, flat(&s)) best, found = s.best() require.True(t, found) require.Equal(t, levels.LevelIndex(45), best) - s.removeAt(0) - require.Equal(t, []entry{}, s.levels) + s.removeAt(pos{0, 0}) + require.Equal(t, []entry{}, flat(&s)) best, found = s.best() require.False(t, found) require.Equal(t, levels.LevelIndex(-1), best) } + +func TestSplit(t *testing.T) { + s := newSide(true, nil) + for p := uint64(2); p <= 2*blockCap; p += 2 { + i, ok := s.find(p) + require.False(t, ok) + s.insertAt(i, levels.LevelIndex(p), p) + } + require.Len(t, s.chain, 1) + require.Len(t, s.blocks, 1) + require.Equal(t, int32(blockCap), s.blocks[0].n) + + i, ok := s.find(2*blockCap + 2) + require.False(t, ok) + require.Equal(t, pos{0, blockCap}, i) + s.insertAt(i, 2*blockCap+2, 2*blockCap+2) + require.Len(t, s.chain, 2) + require.Len(t, s.blocks, 2) + require.Equal(t, entry{2, 0}, s.chain[0]) + require.Equal(t, entry{blockCap + 2, 1}, s.chain[1]) + require.Equal(t, int32(blockCap/2), s.blocks[0].n) + require.Equal(t, int32(blockCap/2+1), s.blocks[1].n) + + i, ok = s.find(blockCap + 2) + require.True(t, ok) + require.Equal(t, pos{1, 0}, i) + i, ok = s.find(blockCap) + require.True(t, ok) + require.Equal(t, pos{0, blockCap/2 - 1}, i) + + i, ok = s.find(blockCap + 1) + require.False(t, ok) + require.Equal(t, pos{0, blockCap / 2}, i) + s.insertAt(i, blockCap+1, blockCap+1) + require.Equal(t, int32(blockCap/2+1), s.blocks[0].n) + require.Equal(t, entry{blockCap + 2, 1}, s.chain[1]) + + all := flat(&s) + require.Len(t, all, blockCap+2) + for j := 1; j < len(all); j++ { + require.Less(t, all[j-1].price, all[j].price) + } + + best, found := s.best() + require.True(t, found) + require.Equal(t, levels.LevelIndex(2*blockCap+2), best) + l, ok := s.atDepth(0) + require.True(t, ok) + require.Equal(t, levels.LevelIndex(2*blockCap+2), l) + l, ok = s.atDepth(blockCap / 2) + require.True(t, ok) + require.Equal(t, levels.LevelIndex(blockCap+2), l) + l, ok = s.atDepth(blockCap/2 + 1) + require.True(t, ok) + require.Equal(t, levels.LevelIndex(blockCap+1), l) + l, ok = s.atDepth(blockCap + 1) + require.True(t, ok) + require.Equal(t, levels.LevelIndex(2), l) + _, ok = s.atDepth(blockCap + 2) + require.False(t, ok) + + for len(s.chain) == 2 { + s.removeAt(pos{0, 0}) + } + require.Len(t, s.blocks, 1) + require.Equal(t, entry{blockCap + 2, 0}, s.chain[0]) + require.Equal(t, int32(blockCap/2+1), s.blocks[0].n) + all = flat(&s) + require.Len(t, all, blockCap/2+1) + require.Equal(t, uint64(blockCap+2), all[0].price) + require.Equal(t, uint64(2*blockCap+2), all[len(all)-1].price) + + for len(s.chain) == 1 { + s.removeAt(pos{0, 0}) + } + require.Empty(t, s.blocks) + require.Equal(t, []entry{}, flat(&s)) + _, found = s.best() + require.False(t, found) +} From 09ad79bf87571928330c7467e2e07474a67f0089 Mon Sep 17 00:00:00 2001 From: Ilya Petrusenko Date: Fri, 23 Oct 2026 02:21:17 +0200 Subject: [PATCH 3/7] randomized side test against a sorted-slice model --- book/side_random_test.go | 118 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 book/side_random_test.go diff --git a/book/side_random_test.go b/book/side_random_test.go new file mode 100644 index 0000000..8e2b0da --- /dev/null +++ b/book/side_random_test.go @@ -0,0 +1,118 @@ +package book + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "orderbook/levels" +) + +// Reference model: one sorted slice of entries, best price at the end, +// exactly what side must look like when flattened. + +func modelFind(m []entry, bid bool, price uint64) (int, bool) { + for i, e := range m { + if e.price == price { + return i, true + } + if (bid && e.price > price) || (!bid && e.price < price) { + return i, false + } + } + return len(m), false +} + +// compareSide checks the side against the model and its own invariants: +// flattened entries, chain keys, block fill, block arena size, best and +// atDepth at every depth. +func compareSide(t *testing.T, s *side, m []entry) { + t.Helper() + + require.Equal(t, m, flat(s)) + require.Len(t, s.blocks, len(s.chain), "block arena vs chain") + + seen := map[int32]bool{} + for c, ce := range s.chain { + require.False(t, seen[ce.idx], "block %d linked twice", ce.idx) + seen[ce.idx] = true + b := &s.blocks[ce.idx] + require.Greater(t, b.n, int32(0), "chain %d: empty block", c) + require.LessOrEqual(t, b.n, int32(blockCap), "chain %d: overfull block", c) + require.Equal(t, b.entries[0].price, ce.price, "chain %d: key", c) + } + + best, ok := s.best() + require.Equal(t, len(m) > 0, ok) + if ok { + require.Equal(t, levels.LevelIndex(m[len(m)-1].idx), best) + } + for d := range m { + l, ok := s.atDepth(d) + require.True(t, ok, "depth %d", d) + require.Equal(t, levels.LevelIndex(m[len(m)-1-d].idx), l, "depth %d", d) + } + _, ok = s.atDepth(len(m)) + require.False(t, ok) +} + +func TestSideRandomAgainstModel(t *testing.T) { + for _, bid := range []bool{true, false} { + for seed := int64(1); seed <= 3; seed++ { + t.Run(fmt.Sprintf("bid=%v/seed=%d", bid, seed), func(t *testing.T) { + runSideRandom(t, bid, seed) + }) + } + } +} + +func runSideRandom(t *testing.T, bid bool, seed int64) { + rng := rand.New(rand.NewSource(seed)) + s := side{bid: bid} + var m []entry + var nextIdx int32 + + // Three phases: grow to several blocks, churn at a stable size, + // shrink back to empty. Insert probability per phase. + phases := []struct{ steps, insertPct int }{{6000, 85}, {6000, 50}, {6000, 15}} + step := 0 + for _, ph := range phases { + for k := 0; k < ph.steps; k++ { + step++ + if len(m) == 0 || rng.Intn(100) < ph.insertPct { + price := uint64(rng.Intn(10000) + 1) + mi, found := modelFind(m, bid, price) + p, ok := s.find(price) + require.Equal(t, found, ok, "step %d: find %d", step, price) + if found { + require.Equal(t, levels.LevelIndex(m[mi].idx), s.at(p), "step %d: find %d", step, price) + continue + } + nextIdx++ + e := entry{price: price, idx: nextIdx} + s.insertAt(p, levels.LevelIndex(nextIdx), price) + m = append(m[:mi], append([]entry{e}, m[mi:]...)...) + } else { + mi := rng.Intn(len(m)) + p, ok := s.find(m[mi].price) + require.True(t, ok, "step %d: remove %d", step, m[mi].price) + require.Equal(t, levels.LevelIndex(m[mi].idx), s.at(p), "step %d: remove %d", step, m[mi].price) + s.removeAt(p) + m = append(m[:mi], m[mi+1:]...) + } + + if step%200 == 0 { + compareSide(t, &s, m) + } + } + } + for len(m) > 0 { + p, ok := s.find(m[0].price) + require.True(t, ok) + s.removeAt(p) + m = m[1:] + } + compareSide(t, &s, m) +} From 58edd06cd849347172753d8985ef3e0b1678c77d Mon Sep 17 00:00:00 2001 From: Ilya Petrusenko Date: Fri, 23 Oct 2026 13:06:41 +0200 Subject: [PATCH 4/7] cursor and depth walk, blocks of 128, benchmark against a flat slice --- book/side.go | 65 +++++++++++++++++++++++++++++++++++----- book/side_random_test.go | 19 ++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/book/side.go b/book/side.go index 0b40e19..d06e28e 100644 --- a/book/side.go +++ b/book/side.go @@ -6,7 +6,7 @@ import ( "orderbook/levels" ) -const blockCap = 256 +const blockCap = 128 type block struct { n int32 @@ -81,12 +81,16 @@ func (s *side) find(price uint64) (pos, bool) { return pos{0, 0}, false } - c, found := gallop(s.chain, s.bid, price) - if found { - return pos{c, 0}, true - } - if c > 0 { - c-- + c := len(s.chain) - 1 + if key := s.chain[c].price; (s.bid && price < key) || (!s.bid && price > key) { + var found bool + c, found = gallop(s.chain, s.bid, price) + if found { + return pos{c, 0}, true + } + if c > 0 { + c-- + } } b := &s.blocks[s.chain[c].idx] @@ -162,6 +166,53 @@ func (s *side) atDepth(depth int) (levels.LevelIndex, bool) { return -1, false } +func (s *side) depth(out []levels.LevelIndex) int { + d := 0 + for c := len(s.chain) - 1; c >= 0 && d < len(out); c-- { + b := &s.blocks[s.chain[c].idx] + es := b.entries[:b.n] + for j := len(es) - 1; j >= 0 && d < len(out); j-- { + out[d] = levels.LevelIndex(es[j].idx) + d++ + } + } + return d +} + +type cursor struct { + s *side + b *block + c int + i int +} + +func (s *side) cursor() (cursor, bool) { + c := len(s.chain) - 1 + if c < 0 { + return cursor{}, false + } + b := &s.blocks[s.chain[c].idx] + return cursor{s: s, b: b, c: c, i: int(b.n) - 1}, true +} + +func (cu *cursor) idx() levels.LevelIndex { + return levels.LevelIndex(cu.b.entries[cu.i].idx) +} + +func (cu *cursor) prev() bool { + if cu.i > 0 { + cu.i-- + return true + } + if cu.c == 0 { + return false + } + cu.c-- + cu.b = &cu.s.blocks[cu.s.chain[cu.c].idx] + cu.i = int(cu.b.n) - 1 + return true +} + func (s *side) best() (levels.LevelIndex, bool) { if len(s.chain) == 0 { return -1, false diff --git a/book/side_random_test.go b/book/side_random_test.go index 8e2b0da..0180063 100644 --- a/book/side_random_test.go +++ b/book/side_random_test.go @@ -56,6 +56,25 @@ func compareSide(t *testing.T, s *side, m []entry) { } _, ok = s.atDepth(len(m)) require.False(t, ok) + + cu, ok := s.cursor() + require.Equal(t, len(m) > 0, ok) + for d := len(m) - 1; d >= 0; d-- { + require.True(t, ok, "cursor at %d", d) + require.Equal(t, levels.LevelIndex(m[d].idx), cu.idx(), "cursor at %d", d) + ok = cu.prev() + } + require.False(t, ok) + + out := make([]levels.LevelIndex, len(m)+1) + require.Equal(t, len(m), s.depth(out)) + for d := range m { + require.Equal(t, levels.LevelIndex(m[len(m)-1-d].idx), out[d], "depth %d", d) + } + if len(m) > 3 { + require.Equal(t, 3, s.depth(out[:3])) + require.Equal(t, levels.LevelIndex(m[len(m)-3].idx), out[2]) + } } func TestSideRandomAgainstModel(t *testing.T) { From 5a781bdbb166a46f4c444e3579e3a70b89253f42 Mon Sep 17 00:00:00 2001 From: Ilya Petrusenko Date: Fri, 23 Oct 2026 13:32:46 +0200 Subject: [PATCH 5/7] cursor over price levels instead LevelAt --- matching/engine.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/matching/engine.go b/matching/engine.go index 1086106..6f3368d 100644 --- a/matching/engine.go +++ b/matching/engine.go @@ -55,15 +55,16 @@ func (e *Engine) Place(order Order) (Report, error) { switch order.TIF { case PostOnly: - if price, _, ok := e.book.LevelAt(opp, 0); ok && crossed(order, price) { + if price, _, ok := e.book.Best(opp); ok && crossed(order, price) { e.book.Release(taker) report.Status = Rejected return report, nil } case FOK: var available uint64 - for depth := 0; available < order.Qty; depth++ { - price, totalQty, ok := e.book.LevelAt(opp, depth) + c := e.book.Cursor(opp) + for available < order.Qty { + price, totalQty, ok := c.Next() if !ok || !crossed(order, price) { e.book.Release(taker) report.Status = Rejected From ec30730a19c2725e91b91a4f5449530c335f3551 Mon Sep 17 00:00:00 2001 From: Ilya Petrusenko Date: Fri, 23 Oct 2026 17:07:13 +0200 Subject: [PATCH 6/7] find returns the level index, dropLevel searches the likely side first --- book/bench_test.go | 306 +++++++++++++++++++++++++++++++++++++++ book/book.go | 41 ++++-- book/book_test.go | 23 +-- book/side.go | 22 +-- book/side_random_test.go | 18 +-- book/side_test.go | 86 +++++------ 6 files changed, 404 insertions(+), 92 deletions(-) create mode 100644 book/bench_test.go diff --git a/book/bench_test.go b/book/bench_test.go new file mode 100644 index 0000000..b6d9691 --- /dev/null +++ b/book/bench_test.go @@ -0,0 +1,306 @@ +package book + +import ( + "math/rand" + "slices" + "strconv" + "testing" + + "orderbook/levels" +) + +// Run: +// +// go test -run '^$' -bench . -benchmem -count 3 ./book +// +// Compares the blocked side against a flat sorted slice of the same +// entries with the same gallop search. Bid semantics: best is the +// highest price. + +const ( + rndSize = 1 << 20 + rndMask = rndSize - 1 +) + +var rnd = func() []uint32 { + r := rand.New(rand.NewSource(1)) + out := make([]uint32, rndSize) + for i := range out { + out[i] = r.Uint32() + } + return out +}() + +var ( + sinkInt int + sinkU64 uint64 + sinkBool bool +) + +type flatSide struct { + entries []entry +} + +func (s *flatSide) find(price uint64) (int, levels.LevelIndex, bool) { + i, ok := gallop(s.entries, true, price) + if !ok { + return i, -1, false + } + return i, levels.LevelIndex(s.entries[i].idx), true +} + +func (s *flatSide) insertAt(i int, idx levels.LevelIndex, price uint64) { + s.entries = slices.Insert(s.entries, i, entry{price: price, idx: int32(idx)}) +} + +func (s *flatSide) removeAt(i int) { + s.entries = append(s.entries[:i], s.entries[i+1:]...) +} + +// prices returns k distinct prices spread over a range four times larger, +// in random order, plus the leftover prices as a free pool. +func prices(k int) (live, free []uint64) { + all := make([]uint64, 4*k) + for i := range all { + all[i] = uint64(i + 1) + } + rand.New(rand.NewSource(2)).Shuffle(len(all), func(a, b int) { all[a], all[b] = all[b], all[a] }) + return all[:k], all[k:] +} + +func benchSide(live []uint64) *side { + s := &side{bid: true} + for i, p := range live { + j, _, _ := s.find(p) + s.insertAt(j, levels.LevelIndex(i), p) + } + return s +} + +func benchFlat(live []uint64) *flatSide { + s := &flatSide{} + for i, p := range live { + j, _, _ := s.find(p) + s.insertAt(j, levels.LevelIndex(i), p) + } + return s +} + +// spreadPrices returns k-10 static deep prices plus a 20-price region at the +// top of the book, half of it live and half free, so that churn stays within +// the ten best levels. +func spreadPrices(k int) (deep, live, free []uint64) { + deep = make([]uint64, k-10) + for i := range deep { + deep[i] = uint64(i + 1) + } + region := make([]uint64, 20) + for i := range region { + region[i] = uint64(k - 10 + i + 1) + } + rand.New(rand.NewSource(3)).Shuffle(len(region), func(a, b int) { region[a], region[b] = region[b], region[a] }) + return deep, region[:10], region[10:] +} + +var sizes = []int{100, 1_000, 10_000, 50_000} + +func BenchmarkFind(b *testing.B) { + for _, k := range sizes { + live, _ := prices(k) + b.Run("blocks/k="+strconv.Itoa(k), func(b *testing.B) { + s := benchSide(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + p, _, ok := s.find(live[rnd[i&rndMask]%uint32(k)]) + sinkInt += p.i + sinkBool = ok + } + }) + b.Run("flat/k="+strconv.Itoa(k), func(b *testing.B) { + s := benchFlat(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + j, _, ok := s.find(live[rnd[i&rndMask]%uint32(k)]) + sinkInt += j + sinkBool = ok + } + }) + } +} + +func BenchmarkFindNear(b *testing.B) { + for _, k := range sizes { + live, _ := prices(k) + sorted := slices.Clone(live) + slices.Sort(sorted) + b.Run("blocks/k="+strconv.Itoa(k), func(b *testing.B) { + s := benchSide(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + p, _, ok := s.find(sorted[k-1-int(rnd[i&rndMask]%10)]) + sinkInt += p.i + sinkBool = ok + } + }) + b.Run("flat/k="+strconv.Itoa(k), func(b *testing.B) { + s := benchFlat(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + j, _, ok := s.find(sorted[k-1-int(rnd[i&rndMask]%10)]) + sinkInt += j + sinkBool = ok + } + }) + } +} + +func BenchmarkBest(b *testing.B) { + for _, k := range sizes { + live, _ := prices(k) + b.Run("blocks/k="+strconv.Itoa(k), func(b *testing.B) { + s := benchSide(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + l, ok := s.best() + sinkInt += int(l) + sinkBool = ok + } + }) + b.Run("flat/k="+strconv.Itoa(k), func(b *testing.B) { + s := benchFlat(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + sinkU64 += s.entries[len(s.entries)-1].price + } + }) + } +} + +func BenchmarkChurn(b *testing.B) { + for _, k := range sizes { + b.Run("blocks/k="+strconv.Itoa(k), func(b *testing.B) { + live, free := prices(k) + s := benchSide(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + a := int(rnd[i&rndMask] % uint32(k)) + f := int(rnd[(i+1)&rndMask] % uint32(len(free))) + old, nw := live[a], free[f] + p, _, _ := s.find(old) + s.removeAt(p) + p, _, _ = s.find(nw) + s.insertAt(p, levels.LevelIndex(a), nw) + live[a], free[f] = nw, old + } + }) + b.Run("flat/k="+strconv.Itoa(k), func(b *testing.B) { + live, free := prices(k) + s := benchFlat(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + a := int(rnd[i&rndMask] % uint32(k)) + f := int(rnd[(i+1)&rndMask] % uint32(len(free))) + old, nw := live[a], free[f] + j, _, _ := s.find(old) + s.removeAt(j) + j, _, _ = s.find(nw) + s.insertAt(j, levels.LevelIndex(a), nw) + live[a], free[f] = nw, old + } + }) + } +} + +func BenchmarkChurnSpread(b *testing.B) { + for _, k := range sizes { + b.Run("blocks/k="+strconv.Itoa(k), func(b *testing.B) { + deep, live, free := spreadPrices(k) + s := benchSide(append(deep, live...)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + a := int(rnd[i&rndMask] % 10) + f := int(rnd[(i+1)&rndMask] % 10) + old, nw := live[a], free[f] + p, _, _ := s.find(old) + s.removeAt(p) + p, _, _ = s.find(nw) + s.insertAt(p, levels.LevelIndex(a), nw) + live[a], free[f] = nw, old + } + }) + b.Run("flat/k="+strconv.Itoa(k), func(b *testing.B) { + deep, live, free := spreadPrices(k) + s := benchFlat(append(deep, live...)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + a := int(rnd[i&rndMask] % 10) + f := int(rnd[(i+1)&rndMask] % 10) + old, nw := live[a], free[f] + j, _, _ := s.find(old) + s.removeAt(j) + j, _, _ = s.find(nw) + s.insertAt(j, levels.LevelIndex(a), nw) + live[a], free[f] = nw, old + } + }) + } +} + +func BenchmarkDepth(b *testing.B) { + live, _ := prices(1_000) + for _, n := range []int{10, 50} { + b.Run("cursor/n="+strconv.Itoa(n), func(b *testing.B) { + s := benchSide(live) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var sum int + cu, ok := s.cursor() + for d := 0; d < n && ok; d++ { + sum += int(cu.idx()) + ok = cu.prev() + } + sinkInt += sum + } + }) + b.Run("depth/n="+strconv.Itoa(n), func(b *testing.B) { + s := benchSide(live) + out := make([]levels.LevelIndex, n) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + var sum int + for _, l := range out[:s.depth(out)] { + sum += int(l) + } + sinkInt += sum + } + }) + b.Run("flat/n="+strconv.Itoa(n), func(b *testing.B) { + s := benchFlat(live) + out := make([]levels.LevelIndex, n) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for d := range out { + out[d] = levels.LevelIndex(s.entries[len(s.entries)-1-d].idx) + } + var sum int + for _, l := range out { + sum += int(l) + } + sinkInt += sum + } + }) + } +} diff --git a/book/book.go b/book/book.go index dc15d83..f8dc9b7 100644 --- a/book/book.go +++ b/book/book.go @@ -41,9 +41,9 @@ func (b *Book) Rest(s Side, h levels.Handle, price, qty uint64) error { } sd := b.side(s) - p, found := sd.find(price) + p, l, found := sd.find(price) if found { - b.levels.Rest(h, sd.at(p), qty) + b.levels.Rest(h, l, qty) return nil } @@ -65,12 +65,24 @@ func (b *Book) Add(s Side, seq, price, qty uint64) (levels.Handle, error) { return h, nil } -func (b *Book) LevelAt(s Side, depth int) (price, totalQty uint64, ok bool) { - l, ok := b.side(s).atDepth(depth) - if !ok { +type Cursor struct { + b *Book + cu cursor + ok bool +} + +func (b *Book) Cursor(s Side) Cursor { + cu, ok := b.side(s).cursor() + return Cursor{b: b, cu: cu, ok: ok} +} + +func (c *Cursor) Next() (price, totalQty uint64, ok bool) { + if !c.ok { return 0, 0, false } - return b.levels.Price(l), b.levels.TotalQty(l), true + l := c.cu.idx() + c.ok = c.cu.prev() + return c.b.levels.Price(l), c.b.levels.TotalQty(l), true } func (b *Book) Cancel(h levels.Handle) (uint64, error) { @@ -131,16 +143,21 @@ func (b *Book) side(s Side) *side { func (b *Book) dropLevel(idx levels.LevelIndex) { price := b.levels.Price(idx) - p, found := b.bids.find(price) - if found && b.bids.at(p) == idx { - b.bids.removeAt(p) + sd, other := &b.asks, &b.bids + if n := len(b.bids.chain); n > 0 && price <= b.bids.chain[n-1].price { + sd, other = &b.bids, &b.asks + } + + p, l, found := sd.find(price) + if found && l == idx { + sd.removeAt(p) b.levels.FreeLevel(idx) return } - p, found = b.asks.find(price) - if found && b.asks.at(p) == idx { - b.asks.removeAt(p) + p, l, found = other.find(price) + if found && l == idx { + other.removeAt(p) b.levels.FreeLevel(idx) return } diff --git a/book/book_test.go b/book/book_test.go index 08f64e5..64f76c3 100644 --- a/book/book_test.go +++ b/book/book_test.go @@ -132,9 +132,10 @@ func TestFrontFill(t *testing.T) { require.PanicsWithValue(t, "levels: Fill: stale handle", func() { book.Fill(h1, 1) }) } -func TestLevelAt(t *testing.T) { +func TestCursor(t *testing.T) { book := New(10, 10) - _, _, ok := book.LevelAt(Buy, 0) + c := book.Cursor(Buy) + _, _, ok := c.Next() require.False(t, ok) _, err := book.Add(Buy, 1, 100, 10) @@ -148,34 +149,38 @@ func TestLevelAt(t *testing.T) { _, err = book.Add(Sell, 5, 103, 1) require.NoError(t, err) - price, totalQty, ok := book.LevelAt(Buy, 0) + c = book.Cursor(Buy) + price, totalQty, ok := c.Next() require.True(t, ok) require.Equal(t, uint64(100), price) require.Equal(t, uint64(10), totalQty) - price, totalQty, ok = book.LevelAt(Buy, 1) + price, totalQty, ok = c.Next() require.True(t, ok) require.Equal(t, uint64(99), price) require.Equal(t, uint64(2), totalQty) - price, totalQty, ok = book.LevelAt(Buy, 2) + price, totalQty, ok = c.Next() require.True(t, ok) require.Equal(t, uint64(98), price) require.Equal(t, uint64(3), totalQty) - _, _, ok = book.LevelAt(Buy, 3) + _, _, ok = c.Next() + require.False(t, ok) + _, _, ok = c.Next() require.False(t, ok) - price, totalQty, ok = book.LevelAt(Sell, 0) + c = book.Cursor(Sell) + price, totalQty, ok = c.Next() require.True(t, ok) require.Equal(t, uint64(101), price) require.Equal(t, uint64(7), totalQty) - price, totalQty, ok = book.LevelAt(Sell, 1) + price, totalQty, ok = c.Next() require.True(t, ok) require.Equal(t, uint64(103), price) require.Equal(t, uint64(1), totalQty) - _, _, ok = book.LevelAt(Sell, 2) + _, _, ok = c.Next() require.False(t, ok) } diff --git a/book/side.go b/book/side.go index d06e28e..41b926b 100644 --- a/book/side.go +++ b/book/side.go @@ -76,9 +76,9 @@ func gallop(s []entry, bid bool, price uint64) (int, bool) { return l, false } -func (s *side) find(price uint64) (pos, bool) { +func (s *side) find(price uint64) (pos, levels.LevelIndex, bool) { if len(s.chain) == 0 { - return pos{0, 0}, false + return pos{0, 0}, -1, false } c := len(s.chain) - 1 @@ -86,7 +86,7 @@ func (s *side) find(price uint64) (pos, bool) { var found bool c, found = gallop(s.chain, s.bid, price) if found { - return pos{c, 0}, true + return pos{c, 0}, levels.LevelIndex(s.blocks[s.chain[c].idx].entries[0].idx), true } if c > 0 { c-- @@ -95,7 +95,10 @@ func (s *side) find(price uint64) (pos, bool) { b := &s.blocks[s.chain[c].idx] i, found := gallop(b.entries[:b.n], s.bid, price) - return pos{c, i}, found + if !found { + return pos{c, i}, -1, false + } + return pos{c, i}, levels.LevelIndex(b.entries[i].idx), true } func (s *side) insertAt(p pos, idx levels.LevelIndex, price uint64) { @@ -155,17 +158,6 @@ func (s *side) at(p pos) levels.LevelIndex { return levels.LevelIndex(s.blocks[s.chain[p.c].idx].entries[p.i].idx) } -func (s *side) atDepth(depth int) (levels.LevelIndex, bool) { - for c := len(s.chain) - 1; c >= 0; c-- { - b := &s.blocks[s.chain[c].idx] - if depth < int(b.n) { - return levels.LevelIndex(b.entries[int(b.n)-1-depth].idx), true - } - depth -= int(b.n) - } - return -1, false -} - func (s *side) depth(out []levels.LevelIndex) int { d := 0 for c := len(s.chain) - 1; c >= 0 && d < len(out); c-- { diff --git a/book/side_random_test.go b/book/side_random_test.go index 0180063..1030909 100644 --- a/book/side_random_test.go +++ b/book/side_random_test.go @@ -26,8 +26,8 @@ func modelFind(m []entry, bid bool, price uint64) (int, bool) { } // compareSide checks the side against the model and its own invariants: -// flattened entries, chain keys, block fill, block arena size, best and -// atDepth at every depth. +// flattened entries, chain keys, block fill, block arena size, best, +// cursor walk and depth. func compareSide(t *testing.T, s *side, m []entry) { t.Helper() @@ -49,14 +49,6 @@ func compareSide(t *testing.T, s *side, m []entry) { if ok { require.Equal(t, levels.LevelIndex(m[len(m)-1].idx), best) } - for d := range m { - l, ok := s.atDepth(d) - require.True(t, ok, "depth %d", d) - require.Equal(t, levels.LevelIndex(m[len(m)-1-d].idx), l, "depth %d", d) - } - _, ok = s.atDepth(len(m)) - require.False(t, ok) - cu, ok := s.cursor() require.Equal(t, len(m) > 0, ok) for d := len(m) - 1; d >= 0; d-- { @@ -103,7 +95,7 @@ func runSideRandom(t *testing.T, bid bool, seed int64) { if len(m) == 0 || rng.Intn(100) < ph.insertPct { price := uint64(rng.Intn(10000) + 1) mi, found := modelFind(m, bid, price) - p, ok := s.find(price) + p, _, ok := s.find(price) require.Equal(t, found, ok, "step %d: find %d", step, price) if found { require.Equal(t, levels.LevelIndex(m[mi].idx), s.at(p), "step %d: find %d", step, price) @@ -115,7 +107,7 @@ func runSideRandom(t *testing.T, bid bool, seed int64) { m = append(m[:mi], append([]entry{e}, m[mi:]...)...) } else { mi := rng.Intn(len(m)) - p, ok := s.find(m[mi].price) + p, _, ok := s.find(m[mi].price) require.True(t, ok, "step %d: remove %d", step, m[mi].price) require.Equal(t, levels.LevelIndex(m[mi].idx), s.at(p), "step %d: remove %d", step, m[mi].price) s.removeAt(p) @@ -128,7 +120,7 @@ func runSideRandom(t *testing.T, bid bool, seed int64) { } } for len(m) > 0 { - p, ok := s.find(m[0].price) + p, _, ok := s.find(m[0].price) require.True(t, ok) s.removeAt(p) m = m[1:] diff --git a/book/side_test.go b/book/side_test.go index 73abc26..00713fb 100644 --- a/book/side_test.go +++ b/book/side_test.go @@ -11,7 +11,7 @@ import ( func newSide(bid bool, entries []entry) side { s := side{bid: bid} for _, e := range entries { - p, _ := s.find(e.price) + p, _, _ := s.find(e.price) s.insertAt(p, levels.LevelIndex(e.idx), e.price) } return s @@ -36,27 +36,27 @@ func TestBidFind(t *testing.T) { {price: 8, idx: 18}, }) - idx, ok := s.find(4) + idx, _, ok := s.find(4) require.True(t, ok) require.Equal(t, pos{0, 2}, idx) - idx, ok = s.find(5) + idx, _, ok = s.find(5) require.False(t, ok) require.Equal(t, pos{0, 3}, idx) - idx, ok = s.find(6) + idx, _, ok = s.find(6) require.True(t, ok) require.Equal(t, pos{0, 3}, idx) - idx, ok = s.find(1) + idx, _, ok = s.find(1) require.False(t, ok) require.Equal(t, pos{0, 0}, idx) - idx, ok = s.find(8) + idx, _, ok = s.find(8) require.True(t, ok) require.Equal(t, pos{0, 5}, idx) - idx, ok = s.find(9) + idx, _, ok = s.find(9) require.False(t, ok) require.Equal(t, pos{0, 6}, idx) } @@ -71,27 +71,27 @@ func TestAskFind(t *testing.T) { {price: 2, idx: 10}, }) - idx, ok := s.find(4) + idx, _, ok := s.find(4) require.True(t, ok) require.Equal(t, pos{0, 3}, idx) - idx, ok = s.find(5) + idx, _, ok = s.find(5) require.False(t, ok) require.Equal(t, pos{0, 3}, idx) - idx, ok = s.find(6) + idx, _, ok = s.find(6) require.True(t, ok) require.Equal(t, pos{0, 2}, idx) - idx, ok = s.find(1) + idx, _, ok = s.find(1) require.False(t, ok) require.Equal(t, pos{0, 6}, idx) - idx, ok = s.find(8) + idx, _, ok = s.find(8) require.True(t, ok) require.Equal(t, pos{0, 0}, idx) - idx, ok = s.find(9) + idx, _, ok = s.find(9) require.False(t, ok) require.Equal(t, pos{0, 0}, idx) } @@ -106,7 +106,7 @@ func TestBidInsert(t *testing.T) { {price: 8, idx: 18}, }) - idx, ok := s.find(5) + idx, _, ok := s.find(5) require.False(t, ok) require.Equal(t, pos{0, 3}, idx) @@ -121,7 +121,7 @@ func TestBidInsert(t *testing.T) { {8, 18}, }, flat(&s)) - idx, ok = s.find(1) + idx, _, ok = s.find(1) require.False(t, ok) require.Equal(t, pos{0, 0}, idx) @@ -141,7 +141,7 @@ func TestBidInsert(t *testing.T) { require.True(t, found) require.Equal(t, levels.LevelIndex(18), best) - idx, ok = s.find(9) + idx, _, ok = s.find(9) require.False(t, ok) require.Equal(t, pos{0, 8}, idx) @@ -173,7 +173,7 @@ func TestAskInsert(t *testing.T) { {price: 2, idx: 10}, }) - idx, ok := s.find(5) + idx, _, ok := s.find(5) require.False(t, ok) require.Equal(t, pos{0, 3}, idx) @@ -192,7 +192,7 @@ func TestAskInsert(t *testing.T) { require.True(t, found) require.Equal(t, levels.LevelIndex(10), best) - idx, ok = s.find(1) + idx, _, ok = s.find(1) require.False(t, ok) require.Equal(t, pos{0, 7}, idx) @@ -212,7 +212,7 @@ func TestAskInsert(t *testing.T) { require.True(t, found) require.Equal(t, levels.LevelIndex(81), best) - idx, ok = s.find(9) + idx, _, ok = s.find(9) require.False(t, ok) require.Equal(t, pos{0, 0}, idx) @@ -240,7 +240,7 @@ func TestBidRemove(t *testing.T) { {price: 8, idx: 18}, }) - idx, ok := s.find(4) + idx, _, ok := s.find(4) require.True(t, ok) require.Equal(t, pos{0, 2}, idx) @@ -257,7 +257,7 @@ func TestBidRemove(t *testing.T) { require.True(t, found) require.Equal(t, levels.LevelIndex(18), best) - idx, ok = s.find(2) + idx, _, ok = s.find(2) require.True(t, ok) require.Equal(t, pos{0, 0}, idx) @@ -269,7 +269,7 @@ func TestBidRemove(t *testing.T) { {8, 18}, }, flat(&s)) - idx, ok = s.find(8) + idx, _, ok = s.find(8) require.True(t, ok) require.Equal(t, pos{0, 3}, idx) @@ -296,7 +296,7 @@ func TestAskRemove(t *testing.T) { {price: 2, idx: 10}, }) - idx, ok := s.find(4) + idx, _, ok := s.find(4) require.True(t, ok) require.Equal(t, pos{0, 3}, idx) @@ -313,7 +313,7 @@ func TestAskRemove(t *testing.T) { require.True(t, found) require.Equal(t, levels.LevelIndex(10), best) - idx, ok = s.find(2) + idx, _, ok = s.find(2) require.True(t, ok) require.Equal(t, pos{0, 4}, idx) @@ -329,7 +329,7 @@ func TestAskRemove(t *testing.T) { require.True(t, found) require.Equal(t, levels.LevelIndex(12), best) - idx, ok = s.find(8) + idx, _, ok = s.find(8) require.True(t, ok) require.Equal(t, pos{0, 0}, idx) @@ -366,7 +366,7 @@ func TestEmpty(t *testing.T) { func TestSplit(t *testing.T) { s := newSide(true, nil) for p := uint64(2); p <= 2*blockCap; p += 2 { - i, ok := s.find(p) + i, _, ok := s.find(p) require.False(t, ok) s.insertAt(i, levels.LevelIndex(p), p) } @@ -374,7 +374,7 @@ func TestSplit(t *testing.T) { require.Len(t, s.blocks, 1) require.Equal(t, int32(blockCap), s.blocks[0].n) - i, ok := s.find(2*blockCap + 2) + i, _, ok := s.find(2*blockCap + 2) require.False(t, ok) require.Equal(t, pos{0, blockCap}, i) s.insertAt(i, 2*blockCap+2, 2*blockCap+2) @@ -385,14 +385,14 @@ func TestSplit(t *testing.T) { require.Equal(t, int32(blockCap/2), s.blocks[0].n) require.Equal(t, int32(blockCap/2+1), s.blocks[1].n) - i, ok = s.find(blockCap + 2) + i, _, ok = s.find(blockCap + 2) require.True(t, ok) require.Equal(t, pos{1, 0}, i) - i, ok = s.find(blockCap) + i, _, ok = s.find(blockCap) require.True(t, ok) require.Equal(t, pos{0, blockCap/2 - 1}, i) - i, ok = s.find(blockCap + 1) + i, _, ok = s.find(blockCap + 1) require.False(t, ok) require.Equal(t, pos{0, blockCap / 2}, i) s.insertAt(i, blockCap+1, blockCap+1) @@ -408,20 +408,20 @@ func TestSplit(t *testing.T) { best, found := s.best() require.True(t, found) require.Equal(t, levels.LevelIndex(2*blockCap+2), best) - l, ok := s.atDepth(0) + cu, ok := s.cursor() require.True(t, ok) - require.Equal(t, levels.LevelIndex(2*blockCap+2), l) - l, ok = s.atDepth(blockCap / 2) - require.True(t, ok) - require.Equal(t, levels.LevelIndex(blockCap+2), l) - l, ok = s.atDepth(blockCap/2 + 1) - require.True(t, ok) - require.Equal(t, levels.LevelIndex(blockCap+1), l) - l, ok = s.atDepth(blockCap + 1) - require.True(t, ok) - require.Equal(t, levels.LevelIndex(2), l) - _, ok = s.atDepth(blockCap + 2) - require.False(t, ok) + require.Equal(t, levels.LevelIndex(2*blockCap+2), cu.idx()) + for d := 0; d < blockCap/2; d++ { + require.True(t, cu.prev()) + } + require.Equal(t, levels.LevelIndex(blockCap+2), cu.idx()) + require.True(t, cu.prev()) + require.Equal(t, levels.LevelIndex(blockCap+1), cu.idx()) + for d := blockCap/2 + 1; d < blockCap+1; d++ { + require.True(t, cu.prev()) + } + require.Equal(t, levels.LevelIndex(2), cu.idx()) + require.False(t, cu.prev()) for len(s.chain) == 2 { s.removeAt(pos{0, 0}) From 6eeabee107721612b54c6c78eaa8049ca183012b Mon Sep 17 00:00:00 2001 From: Ilya Petrusenko Date: Fri, 23 Oct 2026 17:23:51 +0200 Subject: [PATCH 7/7] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b92255..d48fe24 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ An order id is a specific code containing both a slot index and a sequence numbe The project consists of three modules. The first, `levels`, maintains queues of orders for each price level, tracking both price and volume. The second, `book`, organizes these levels by price for both buyers and sellers. The third, `matching`, handles the execution of limit orders, supporting GTC, IOC, FOK, and PostOnly order types. All modules have been thoroughly tested, including the use of randomized data to verify correctness against a reference model. The repository includes a benchmark comparing these queues to standard implementations based on `container/list`; the results show that adding and canceling orders is approximately 10 times faster. With one million active orders, performance is 8 times higher—all achieved without a single memory allocation. -Additionally, the price index implementation was benchmarked against red-black trees (from `emirpasic/gods`) and B-trees (from `google/btree`). The simple sorted array I used proved faster for searches: 2–8 times faster for narrow price ranges, 20–40% faster across the entire order book, and 10 times faster for depth-based traversal. Tree structures only performed better when frequent modifications to levels deep within the order book were required—a relatively uncommon operation. These benchmarks are not included in the repository to avoid adding unnecessary dependencies for testing. \ No newline at end of file +The price index is implemented as a two-tier structure. Price levels are stored in blocks of 128 entries each, with a short chain of keys pointing to these blocks. In essence, this is a two-level B+ tree—a configuration sufficient for the order book of a single instrument. Both tiers consist of standard arrays without pointers. Searches begin at the best price and proceed deeper into the book; consequently, a search near the spread affects only one block, and adding an order deeper in the book requires modifying at most one block. Compared to a standard sorted array, this approach increases overhead by 1–3% during the most active operations, but it delivers significant performance gains: it is 2 times faster when modifying 1,000 levels deep in the book, 12 times faster at 10,000 levels, and 59 times faster at 50,000 levels. A benchmark comparing this structure to a simple array can be found in the `book` directory. Red-black trees (from `emirpasic/gods`) and B-trees (from `google/btree`) perform worse in these scenarios—they are 3–5 times slower near the spread and 10 times slower when traversing deeper levels. Furthermore, they underperform the block-based structure even during active trading and require separate memory allocation for each node. These specific measurements are not included in the repository to avoid adding unnecessary dependencies for testing. \ No newline at end of file