Leka
A low-latency C++20 price-time-priority limit order book and matching engine
▶ Replay viewer
Loading...
Searching...
No Matches
order_index.cpp
1// src/index/order_index.cpp
2
3#include "lob/index/order_index.hpp"
4#include <functional>
5#include <stdexcept>
6#include <vector>
7
8namespace lob {
9
10namespace {
11
12/**
13 * @brief True when index k lies in the circular range (i, j], going forward
14 * from i (exclusive) to j (inclusive), wrapping past capacity if i > j.
15 *
16 * This is the standard backward-shift-deletion test: an entry whose natural
17 * hash index falls in this range still needs the probe run starting at i to
18 * reach it, so it cannot be moved back into slot i without becoming
19 * unreachable. An entry whose natural index falls outside this range no
20 * longer needs slot i's occupant at all and can be pulled back into it.
21 */
22bool inCyclicRange(std::size_t k, std::size_t i, std::size_t j, std::size_t capacity) {
23 (void)capacity;
24 if (i <= j) {
25 return i < k && k <= j;
26 }
27 return k > i || k <= j;
28}
29
30} // namespace
31
32std::size_t OrderIndex::indexFor(const OrderId& id, std::size_t capacity) const {
33 return std::hash<OrderId>{}(id) & (capacity - 1);
34}
35
36/**
37 * @details Scans forward from the natural index until either a matching key
38 * or a genuinely empty slot is found. Backward-shift deletion is what makes
39 * "empty slot" a reliable stopping condition: without it, a naive removal
40 * could leave a hole partway through another key's probe run and this scan
41 * would wrongly report that key as absent.
42 */
43std::size_t OrderIndex::findSlot(const OrderId& id) const {
44 if (slots.empty()) {
45 return static_cast<std::size_t>(-1);
46 }
47 std::size_t index = indexFor(id, slots.size());
48 while (slots[index].value != nullptr) {
49 if (slots[index].key == id) {
50 return index;
51 }
52 index = (index + 1) & (slots.size() - 1);
53 }
54 return index; // the empty slot where `id` would be inserted
55}
56
57/**
58 * @details Shares the doubling-and-rehash body with growIfNeeded() by simply
59 * picking the target capacity up front and looping the same "not big enough
60 * yet, double again" test growIfNeeded() uses one step at a time. Never
61 * shrinks: reserving a smaller count than the table already holds is a no-op.
62 */
63void OrderIndex::reserve(std::size_t orderCount) {
64 std::size_t capacity = slots.empty() ? InitialCapacity : slots.size();
65 while (static_cast<double>(orderCount) > static_cast<double>(capacity) * MaxLoadFactor) {
66 capacity *= 2;
67 }
68 if (!slots.empty() && capacity <= slots.size()) {
69 return;
70 }
71
72 std::vector<Slot> grown(capacity, Slot{});
73 for (const Slot& slot : slots) {
74 if (slot.value == nullptr) {
75 continue;
76 }
77 std::size_t index = indexFor(slot.key, grown.size());
78 while (grown[index].value != nullptr) {
79 index = (index + 1) & (grown.size() - 1);
80 }
81 grown[index] = slot;
82 }
83 slots.swap(grown);
84}
85
86void OrderIndex::growIfNeeded() {
87 if (slots.empty()) {
88 slots.assign(InitialCapacity, Slot{});
89 return;
90 }
91 if (static_cast<double>(count + 1) <= static_cast<double>(slots.size()) * MaxLoadFactor) {
92 return;
93 }
94 std::vector<Slot> grown(slots.size() * 2, Slot{});
95 for (const Slot& slot : slots) {
96 if (slot.value == nullptr) {
97 continue;
98 }
99 std::size_t index = indexFor(slot.key, grown.size());
100 while (grown[index].value != nullptr) {
101 index = (index + 1) & (grown.size() - 1);
102 }
103 grown[index] = slot;
104 }
105 slots.swap(grown);
106}
107
108/**
109 * @details Does not call order->isValid(): OrderBook::addOrderWithQuantities
110 * already does, before the order is linked into any book structure, so
111 * repeating it here would re-check the same fields on every accepted insert
112 * for an order this index cannot yet have any reason to distrust. See the
113 * header for the full reasoning.
114 */
116 if (order == nullptr) {
117 throw std::invalid_argument("Cannot add a null order");
118 }
119
120 // Grown before probing so the probe below operates on the table the
121 // insertion will actually land in.
122 growIfNeeded();
123
124 const OrderId key = order->getOrderId();
125 std::size_t index = indexFor(key, slots.size());
126 while (slots[index].value != nullptr) {
127 if (slots[index].key == key) {
128 throw std::logic_error("Order with the same OrderId already exists in the index");
129 }
130 index = (index + 1) & (slots.size() - 1);
131 }
132 slots[index] = Slot{key, order};
133 ++count;
134}
135
136/** Removes a mapping only when its pointer identity also matches. */
138 if (order == nullptr) {
139 throw std::invalid_argument("Cannot remove a null order");
140 }
141
142 const std::size_t hole = findSlot(order->getOrderId());
143 if (hole == static_cast<std::size_t>(-1) || slots[hole].value == nullptr) {
144 throw std::logic_error("Order not found in the index");
145 }
146 if (slots[hole].value != order) {
147 throw std::logic_error("Order pointer does not match indexed order");
148 }
149
150 slots[hole] = Slot{};
151 --count;
152
153 // Backward-shift deletion: pull each following entry in this probe run
154 // back to fill the hole it left behind, as long as doing so does not
155 // strand a key whose own probe run still needs to pass through here.
156 std::size_t vacated = hole;
157 std::size_t scan = (vacated + 1) & (slots.size() - 1);
158 while (slots[scan].value != nullptr) {
159 const std::size_t natural = indexFor(slots[scan].key, slots.size());
160 if (!inCyclicRange(natural, vacated, scan, slots.size())) {
161 slots[vacated] = slots[scan];
162 slots[scan] = Slot{};
163 vacated = scan;
164 }
165 scan = (scan + 1) & (slots.size() - 1);
166 }
167}
168
169/** Performs average constant-time lookup by OrderId. */
170Order* OrderIndex::findOrder(const OrderId& orderId) const {
171 const std::size_t index = findSlot(orderId);
172 if (index == static_cast<std::size_t>(-1)) {
173 return nullptr;
174 }
175 return slots[index].value;
176}
177
178} // namespace lob
Type-safe identifier for an order; zero is reserved as invalid.
Definition order_id.hpp:10
Order * findOrder(const OrderId &orderId) const
Finds an order by ID, or returns nullptr when absent.
void reserve(std::size_t orderCount)
Grows the table, if needed, so orderCount entries fit under MaxLoadFactor without a later doubling.
void removeOrder(Order *order)
Removes an order after verifying pointer identity.
void addOrder(Order *order)
Adds an order and rejects duplicate IDs.
Represents a resting or incoming order and its lifecycle state.
Definition order.hpp:17
OrderId getOrderId() const
Returns the unique order identifier.
Definition order.cpp:23