Leka
A low-latency C++20 price-time-priority limit order book and matching engine
▶ Replay viewer
Loading...
Searching...
No Matches
price_ladder.cpp
1#include "lob/book/price_ladder.hpp"
2
3#include <bit>
4#include <stdexcept>
5
6namespace lob {
7
8PriceLadder::PriceLadder(Price minPriceArg, Price tickSizeArg,
9 std::size_t levelCountArg, bool descendingArg)
10 : minPrice(minPriceArg.getPrice()), tickSize(tickSizeArg.getPrice()),
11 maxPrice(0), levelCount(levelCountArg), descending(descendingArg) {
12 if (minPrice == 0) {
13 throw std::invalid_argument("PriceLadder minPrice must be nonzero");
14 }
15 if (tickSize == 0) {
16 throw std::invalid_argument("PriceLadder tickSize must be nonzero");
17 }
18 if (levelCount == 0) {
19 throw std::invalid_argument("PriceLadder levelCount must be nonzero");
20 }
21 const std::uint64_t span = static_cast<std::uint64_t>(levelCount - 1);
22 if (tickSize != 0 && span > (UINT64_MAX - minPrice) / tickSize) {
23 throw std::invalid_argument("PriceLadder range overflows a 64-bit price");
24 }
25 maxPrice = minPrice + span * tickSize;
26
27 // Every level is constructed here, once, with its permanent price. There
28 // is no later insertion or erasure of a PriceLevel: only its occupancy
29 // bit and its FIFO contents change for the rest of the ladder's life.
30 levels.reserve(levelCount);
31 for (std::size_t index = 0; index < levelCount; ++index) {
32 const std::uint64_t raw = descending
33 ? maxPrice - static_cast<std::uint64_t>(index) * tickSize
34 : minPrice + static_cast<std::uint64_t>(index) * tickSize;
35 levels.emplace_back(Price{raw});
36 }
37 words.assign((levelCount + 63) / 64, 0);
38}
39
40std::size_t PriceLadder::indexOf(Price price) const {
41 const std::uint64_t raw = price.getPrice();
42 std::uint64_t offset = 0;
43 if (descending) {
44 if (raw > maxPrice || raw < minPrice) {
45 throw std::out_of_range("Price is outside the configured ladder range");
46 }
47 offset = maxPrice - raw;
48 } else {
49 if (raw < minPrice || raw > maxPrice) {
50 throw std::out_of_range("Price is outside the configured ladder range");
51 }
52 offset = raw - minPrice;
53 }
54 if (offset % tickSize != 0) {
55 throw std::invalid_argument("Price does not fall on a configured tick boundary");
56 }
57 return static_cast<std::size_t>(offset / tickSize);
58}
59
61 return levels[indexOf(price)];
62}
63
64/**
65 * @details Setting bits and lowering a cached minimum are both O(1); nothing
66 * here ever searches.
67 */
69 const std::size_t index = indexOf(price);
70 words[index / 64] |= (std::uint64_t{1} << (index % 64));
71 if (occupied == 0 || index < bestIndex) {
72 bestIndex = index;
73 }
74 ++occupied;
75}
76
77/**
78 * @details Clearing a bit is O(1). Re-deriving the best index is only needed
79 * when the level that just emptied WAS the best index; every other call is
80 * O(1) as well. That one case scans forward from the vacated index for the
81 * next set bit, which is a handful of instructions per 64-bit word rather
82 * than a per-level check, and in practice terminates almost immediately
83 * because resting liquidity clusters near the touch.
84 */
86 const std::size_t index = indexOf(price);
87 words[index / 64] &= ~(std::uint64_t{1} << (index % 64));
88 --occupied;
89 if (index == bestIndex) {
90 bestIndex = occupied == 0 ? npos : nextSetBit(index + 1);
91 }
92}
93
94std::size_t PriceLadder::nextSetBit(std::size_t from) const {
95 if (from >= levelCount) {
96 return npos;
97 }
98 const std::size_t startWord = from / 64;
99 const unsigned startBit = static_cast<unsigned>(from % 64);
100
101 const std::uint64_t firstWord = words[startWord] >> startBit;
102 if (firstWord != 0) {
103 return from + static_cast<std::size_t>(std::countr_zero(firstWord));
104 }
105 for (std::size_t word = startWord + 1; word < words.size(); ++word) {
106 if (words[word] != 0) {
107 const std::size_t index =
108 word * 64 + static_cast<std::size_t>(std::countr_zero(words[word]));
109 return index < levelCount ? index : npos;
110 }
111 }
112 return npos;
113}
114
116 return bestIndex == npos ? nullptr : &levels[bestIndex];
117}
118
120 return bestIndex == npos ? nullptr : &levels[bestIndex];
121}
122
123} // namespace lob
void markOccupied(Price price)
Marks a level occupied; call exactly once when its order count transitions from zero to nonzero.
PriceLadder(Price minPrice, Price tickSize, std::size_t levelCount, bool descending)
Pre-allocates every level the ladder will ever hold.
static constexpr std::size_t npos
Sentinel returned when no level is occupied.
PriceLevel & levelAt(Price price)
Returns the level slot for a price; the slot always exists.
void markEmpty(Price price)
Marks a level empty; call exactly once when its order count transitions from nonzero to zero.
PriceLevel * best()
Returns the best occupied level, or nullptr when the side is empty.
Maintains FIFO resting orders at one price.
Type-safe nonzero price value used for price ordering.
Definition price.hpp:9
std::uint64_t getPrice() const
Returns the underlying numeric price.
Definition price.hpp:21