Leka
A low-latency C++20 price-time-priority limit order book and matching engine
▶ Replay viewer
Loading...
Searching...
No Matches
price_ladder.hpp
1// include/lob/book/price_ladder.hpp
2#ifndef PRICE_LADDER_HPP
3#define PRICE_LADDER_HPP
4
5#include "lob/book/price_level.hpp"
6#include "lob/types/price.hpp"
7
8#include <concepts>
9#include <cstddef>
10#include <cstdint>
11#include <vector>
12
13namespace lob {
14
15/**
16 * @brief Fixed-range, tick-indexed array of PriceLevel slots for one side of a book.
17 *
18 * A price is not treated as an ordered-map key; it is treated as an index.
19 * Every level in [minPrice, minPrice + (levelCount-1)*tickSize] is constructed
20 * once, at PriceLadder construction, and never destroyed or moved for the
21 * ladder's lifetime. Adding an order at a price that already has resting
22 * orders, or that has never had one, is the same O(1) array access with no
23 * allocation and no tree rebalancing; the old std::map<Price, PriceLevel>
24 * design paid a node allocation for the former and a pointer-chasing tree
25 * descent for the latter on every best-of-book query.
26 *
27 * Because PriceLevel addresses are stable for the ladder's lifetime, Order's
28 * cached PriceLevel* pointer stays valid across every insert and remove at
29 * any other price: nothing is ever reallocated out from under it.
30 *
31 * "Best" is always the lowest occupied index. Index 0 is the worst price a
32 * resting order can have on that side, so the two sides differ only in which
33 * direction price increases with index: ascending for asks (index 0 is the
34 * lowest, hence best, ask) and descending for bids (index 0 is the highest,
35 * hence best, bid). This lets both sides share one implementation and one
36 * "lowest set bit" query instead of a min-heap on one side and a max-heap on
37 * the other.
38 *
39 * Occupancy is tracked in a bitset rather than by asking each PriceLevel
40 * whether it is empty, so finding the best level after it empties is a
41 * hardware find-first-set over a handful of 64-bit words rather than a linear
42 * scan of PriceLevel objects. The best index is additionally cached, so the
43 * common case, a level away from the current best changing occupancy, costs
44 * one array access and one bit flip with no search at all.
45 */
47 public:
48 /** Sentinel returned when no level is occupied. */
49 static constexpr std::size_t npos = static_cast<std::size_t>(-1);
50
51 /**
52 * @brief Pre-allocates every level the ladder will ever hold.
53 *
54 * @param minPrice Lowest representable price on this side.
55 * @param tickSize Price increment between adjacent indices.
56 * @param levelCount Number of representable price levels.
57 * @param descending True for the bid side, where index 0 is the
58 * highest price rather than the lowest.
59 * @throws std::invalid_argument if minPrice is zero, tickSize is
60 * zero, levelCount is zero, or the configured range overflows.
61 */
62 PriceLadder(Price minPrice, Price tickSize, std::size_t levelCount,
63 bool descending);
64
65 PriceLadder(const PriceLadder&) = delete;
66 PriceLadder& operator=(const PriceLadder&) = delete;
67 PriceLadder(PriceLadder&&) = delete;
68 PriceLadder& operator=(PriceLadder&&) = delete;
69 ~PriceLadder() = default;
70
71 /**
72 * @brief Returns the level slot for a price; the slot always exists.
73 * @throws std::out_of_range if the price falls outside the
74 * configured range.
75 * @throws std::invalid_argument if the price does not fall on a
76 * configured tick boundary.
77 */
78 PriceLevel& levelAt(Price price);
79
80 /**
81 * @brief Marks a level occupied; call exactly once when its order
82 * count transitions from zero to nonzero.
83 */
84 void markOccupied(Price price);
85
86 /**
87 * @brief Marks a level empty; call exactly once when its order count
88 * transitions from nonzero to zero.
89 */
90 void markEmpty(Price price);
91
92 /** Returns the best occupied level, or nullptr when the side is empty. */
94 /** Returns the best occupied level for read-only inspection. */
95 const PriceLevel* best() const;
96
97 /** Returns the number of currently occupied levels. */
98 std::size_t occupiedCount() const { return occupied; }
99
100 /**
101 * @brief Walks up to @p maxLevels occupied levels, best first.
102 *
103 * Read-only traversal for market-data snapshots. Because "best" is
104 * always the lowest occupied index on either side (see the class
105 * comment), walking outward from the touch is the same forward
106 * bit-scan on both bids and asks, and it visits levels in true
107 * price priority order without sorting anything.
108 *
109 * This deliberately stops after @p maxLevels rather than walking the
110 * whole ladder: a snapshot consumer wants the top of book, and the
111 * ladder may span hundreds of thousands of mostly-empty levels.
112 */
113 template <typename Fn>
114 requires std::invocable<Fn&, const PriceLevel&>
115 void forEachOccupied(std::size_t maxLevels, Fn&& fn) const {
116 std::size_t index = bestIndex;
117 for (std::size_t seen = 0; seen < maxLevels && index != npos; ++seen) {
118 fn(levels[index]);
119 index = nextSetBit(index + 1);
120 }
121 }
122
123 private:
124 std::size_t indexOf(Price price) const;
125 std::size_t nextSetBit(std::size_t from) const;
126
127 std::uint64_t minPrice;
128 std::uint64_t tickSize;
129 std::uint64_t maxPrice; // minPrice + (levelCount - 1) * tickSize, cached at construction
130 std::size_t levelCount;
131 bool descending;
132
133 // One entry per tick; constructed once and never resized, so every
134 // PriceLevel's address is stable for the life of the ladder.
135 std::vector<PriceLevel> levels;
136 // Occupancy bitset, 64 levels per word.
137 std::vector<std::uint64_t> words;
138 // Lowest occupied index, kept exact; npos when occupied == 0.
139 std::size_t bestIndex{npos};
140 std::size_t occupied{0};
141};
142
143} // namespace lob
144
145#endif // PRICE_LADDER_HPP
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.
std::size_t occupiedCount() const
Returns the number of currently occupied levels.
PriceLevel & levelAt(Price price)
Returns the level slot for a price; the slot always exists.
void forEachOccupied(std::size_t maxLevels, Fn &&fn) const
Walks up to maxLevels occupied levels, best first.
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