Skip to content
Daniel Esambu
Work
Work
Projects
Things I've built
Leka
A C++20 matching engine
Latency Lab
(opens in a new tab)
A C++ course I wrote
Resume
The formal version
Writing
Writing
Poetry
Poems, shelved by what they're about
Essays
Notes and essays
Math
Math
Notes
Notes I've written up
IB papers
Worked exam questions
Calculus lessons
(opens in a new tab)
On YouTube
Life
Life
Photos
Things I stopped to look at
Training
Reps, not phases
Modeling
Measurements and photos
Resume
Say hi
Say hi
Resume
Menu
Leka
A low-latency C++20 price-time-priority limit order book and matching engine
Replay viewer
Toggle main menu visibility
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
13
namespace
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
*/
46
class
PriceLadder
{
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. */
93
PriceLevel
*
best
();
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
lob::PriceLadder::markOccupied
void markOccupied(Price price)
Marks a level occupied; call exactly once when its order count transitions from zero to nonzero.
Definition
price_ladder.cpp:68
lob::PriceLadder::PriceLadder
PriceLadder(Price minPrice, Price tickSize, std::size_t levelCount, bool descending)
Pre-allocates every level the ladder will ever hold.
Definition
price_ladder.cpp:8
lob::PriceLadder::npos
static constexpr std::size_t npos
Sentinel returned when no level is occupied.
Definition
price_ladder.hpp:49
lob::PriceLadder::occupiedCount
std::size_t occupiedCount() const
Returns the number of currently occupied levels.
Definition
price_ladder.hpp:98
lob::PriceLadder::levelAt
PriceLevel & levelAt(Price price)
Returns the level slot for a price; the slot always exists.
Definition
price_ladder.cpp:60
lob::PriceLadder::forEachOccupied
void forEachOccupied(std::size_t maxLevels, Fn &&fn) const
Walks up to maxLevels occupied levels, best first.
Definition
price_ladder.hpp:115
lob::PriceLadder::markEmpty
void markEmpty(Price price)
Marks a level empty; call exactly once when its order count transitions from nonzero to zero.
Definition
price_ladder.cpp:85
lob::PriceLadder::best
PriceLevel * best()
Returns the best occupied level, or nullptr when the side is empty.
Definition
price_ladder.cpp:115
lob::PriceLevel
Maintains FIFO resting orders at one price.
Definition
price_level.hpp:16
lob::Price
Type-safe nonzero price value used for price ordering.
Definition
price.hpp:9
include
lob
book
price_ladder.hpp
Generated by
1.18.0