Slick OrderBook 1.1.0
High-Performance C++23 OrderBook Library
Loading...
Searching...
No Matches
orderbook_l2_impl.hpp
Go to the documentation of this file.
1// Copyright 2026 Slick Quant
2// SPDX-License-Identifier: MIT
3
4#pragma once
5
6#ifndef SLICK_OB_INLINE
7#define SLICK_OB_INLINE inline
8#endif
9
11
12SLICK_OB_INLINE OrderBookL2::OrderBookL2(SymbolId symbol, std::size_t initial_capacity)
13 : symbol_(symbol),
14 tob_seq_(0),
15 last_seq_num_(0) {
16 // Initialize level containers
17 // Note: Clang-17 with GCC-14 libstdc++ has issues with aggregate initialization in member init list
20
21 // Initialize cached top-of-book
23}
24
25// Move constructor - manually implement due to std::atomic member
27 : symbol_(other.symbol_),
28 sides_(std::move(other.sides_)),
29 observers_(std::move(other.observers_)),
30 cached_tob_(other.cached_tob_),
31 cached_best_bid_(other.cached_best_bid_),
32 cached_best_ask_(other.cached_best_ask_),
33 tob_seq_(other.tob_seq_.load(std::memory_order_relaxed)),
34 last_seq_num_(other.last_seq_num_) {
35}
36
37// Move assignment - manually implement due to std::atomic member
39 if (this != &other) {
40 symbol_ = other.symbol_;
41 sides_ = std::move(other.sides_);
42 observers_ = std::move(other.observers_);
43 cached_tob_ = other.cached_tob_;
44 cached_best_bid_ = other.cached_best_bid_;
45 cached_best_ask_ = other.cached_best_ask_;
46 tob_seq_.store(other.tob_seq_.load(std::memory_order_relaxed), std::memory_order_relaxed);
47 last_seq_num_ = other.last_seq_num_;
48 }
49 return *this;
50}
51
53 uint64_t seq_num, bool is_last_in_batch) {
54 SLICK_ASSERT(side < SideCount);
55
56 // Validate sequence number - reject out-of-order (seq_num < last_seq_num)
57 if (seq_num > 0) {
58 if (seq_num < last_seq_num_) {
59 // Out of order - reject silently
60 return;
61 }
62 last_seq_num_ = seq_num;
63 }
64
65 if (quantity == 0) {
66 // Find level index before deletion
67 auto it = sides_[side].find(price);
68 if (it != sides_[side].end()) {
69 uint16_t level_idx = static_cast<uint16_t>(std::distance(sides_[side].begin(), it));
70
71 // track starting index
72 change_starting_index_ = std::min<uint16_t>(change_starting_index_, level_idx);
73
74 // Delete the level
75 sides_[side].erase(it);
76
77 // Deletion: both price and quantity changed
78 uint8_t change_flags = PriceChanged | QuantityChanged;
79 if (is_last_in_batch) {
80 change_flags |= LastInBatch;
81 }
82
83 // Notify with level_index, flags, and seq_num
84 PriceLevelUpdate update{timestamp, symbol_, side, price, 0, 0, level_idx, change_flags, seq_num};
86 if (change_starting_index_ == 0 && is_last_in_batch) {
87 notifyTopOfBookIfChanged(timestamp);
89 }
90 }
91 } else {
92 // Insert or update level
93 auto [it, inserted] = sides_[side].insertOrUpdate(price, quantity, timestamp);
94
95 // Calculate level index
96 uint16_t level_idx = static_cast<uint16_t>(std::distance(sides_[side].begin(), it));
97
98 // track starting index
99 change_starting_index_ = std::min<uint16_t>(change_starting_index_, level_idx);
100
101 // Determine change flags based on what actually changed
102 uint8_t change_flags = 0;
103 if (inserted) {
104 // New level: both price and quantity changed
105 change_flags = PriceChanged | QuantityChanged;
106 } else {
107 // Existing level: only quantity changed
108 change_flags = QuantityChanged;
109 }
110
111 // Add LastInBatch flag if this is the last update
112 if (is_last_in_batch) {
113 change_flags |= LastInBatch;
114 }
115
116 // Notify observers
117 PriceLevelUpdate update{timestamp, symbol_, side, price, quantity, 0, level_idx, change_flags, seq_num};
119 if (change_starting_index_ == 0 && is_last_in_batch) {
120 notifyTopOfBookIfChanged(timestamp);
122 }
123 }
124}
125
127 SLICK_ASSERT(side < SideCount);
128 if (!sides_[side].erase(price)) {
129 return false;
130 }
131 // Keep cached top-of-book / best levels in sync with the live sides
132 notifyTopOfBookIfChanged(timestamp);
133 return true;
134}
135
137 SLICK_ASSERT(side < SideCount);
138 sides_[side].clear();
139 notifyTopOfBookIfChanged(timestamp);
140}
141
143 sides_[Side::Buy].clear();
144 sides_[Side::Sell].clear();
145 notifyTopOfBookIfChanged(timestamp);
146}
147
149 // Use sequence lock to read cached best bid atomically
150 // This is thread-safe for concurrent reads while a writer is updating
151 uint64_t seq1, seq2;
152 do {
153 seq1 = tob_seq_.load(std::memory_order_acquire);
154 // If seq1 is odd, writer is in progress, spin
155 while (seq1 & 1) {
156 seq1 = tob_seq_.load(std::memory_order_acquire);
157 }
158 // Check if there's a valid bid
159 if (cached_tob_.best_bid == 0) {
160 return nullptr;
161 }
162 // Verify sequence didn't change during read
163 seq2 = tob_seq_.load(std::memory_order_acquire);
164 } while (seq1 != seq2);
165
166 // Return pointer to cached value (const, read-only)
167 return &cached_best_bid_;
168}
169
171 // Use sequence lock to read cached best ask atomically
172 // This is thread-safe for concurrent reads while a writer is updating
173 uint64_t seq1, seq2;
174 do {
175 seq1 = tob_seq_.load(std::memory_order_acquire);
176 // If seq1 is odd, writer is in progress, spin
177 while (seq1 & 1) {
178 seq1 = tob_seq_.load(std::memory_order_acquire);
179 }
180 // Check if there's a valid ask
181 if (cached_tob_.best_ask == 0) {
182 return nullptr;
183 }
184 // Verify sequence didn't change during read
185 seq2 = tob_seq_.load(std::memory_order_acquire);
186 } while (seq1 != seq2);
187
188 // Return pointer to cached value (const, read-only)
189 return &cached_best_ask_;
190}
191
193 // Use sequence lock to read cached top-of-book atomically
194 TopOfBook tob;
195 uint64_t seq1, seq2;
196 do {
197 seq1 = tob_seq_.load(std::memory_order_acquire);
198 // If seq1 is odd, writer is in progress, spin
199 while (seq1 & 1) {
200 seq1 = tob_seq_.load(std::memory_order_acquire);
201 }
202 // Read the cached value
203 tob = cached_tob_;
204 // Check if a write occurred during read
205 seq2 = tob_seq_.load(std::memory_order_acquire);
206 } while (seq1 != seq2);
207 return tob;
208}
209
210SLICK_OB_INLINE std::vector<detail::PriceLevelL2> OrderBookL2::getLevels(Side side, std::size_t depth) const {
211 SLICK_ASSERT(side < SideCount);
212 return sides_[side].getLevels(depth);
213}
214
216 SLICK_ASSERT(side < SideCount);
217 return sides_[side].getLevel(price);
218}
219
220SLICK_OB_INLINE const detail::PriceLevelL2* OrderBookL2::getLevelByIndex(Side side, uint16_t index) const noexcept {
221 SLICK_ASSERT(side < SideCount);
222 const auto& container = sides_[side];
223 if (index >= container.size()) {
224 return nullptr;
225 }
226 return &container[index];
227}
228
229SLICK_OB_INLINE std::size_t OrderBookL2::levelCount(Side side) const noexcept {
230 SLICK_ASSERT(side < SideCount);
231 return sides_[side].size();
232}
233
234SLICK_OB_INLINE bool OrderBookL2::isEmpty(Side side) const noexcept {
235 SLICK_ASSERT(side < SideCount);
236 return sides_[side].empty();
237}
238
240 return sides_[Side::Buy].empty() && sides_[Side::Sell].empty();
241}
242
244 // Compute current top-of-book (direct access, writer-side only)
245 const auto* bid = sides_[Side::Buy].best();
246 const auto* ask = sides_[Side::Sell].best();
247
248 Price new_best_bid = bid ? bid->price : 0;
249 Quantity new_bid_qty = bid ? bid->quantity : 0;
250 Price new_best_ask = ask ? ask->price : 0;
251 Quantity new_ask_qty = ask ? ask->quantity : 0;
252
253 // Check if best bid or ask changed
254 uint8_t bid_change_flags = 0;
255 uint8_t ask_change_flags = 0;
256 bid_change_flags += cached_tob_.best_bid != new_best_bid ? ChangeFlag::PriceChanged : 0;
257 bid_change_flags += cached_tob_.bid_quantity != new_bid_qty ? ChangeFlag::QuantityChanged : 0;
258 ask_change_flags += cached_tob_.best_ask != new_best_ask ? ChangeFlag::PriceChanged : 0;
259 ask_change_flags += cached_tob_.ask_quantity != new_ask_qty ? ChangeFlag::QuantityChanged : 0;
260
261 if (bid_change_flags || ask_change_flags) {
262 // Update cached values using sequence lock (odd = writing, even = readable)
263 uint64_t seq = tob_seq_.load(std::memory_order_relaxed);
264 tob_seq_.store(seq + 1, std::memory_order_release); // Mark as writing (odd)
265
266 // Update cached TopOfBook
267 cached_tob_.best_bid = new_best_bid;
268 cached_tob_.bid_quantity = new_bid_qty;
269 cached_tob_.best_ask = new_best_ask;
270 cached_tob_.ask_quantity = new_ask_qty;
271 cached_tob_.timestamp = timestamp;
272 cached_tob_.change_flags[0] = bid_change_flags;
273 cached_tob_.change_flags[1] = ask_change_flags;
274
275 // Update cached best bid/ask levels for thread-safe access
276 if (bid) {
277 cached_best_bid_ = *bid;
278 } else {
280 }
281 if (ask) {
282 cached_best_ask_ = *ask;
283 } else {
285 }
286
287 tob_seq_.store(seq + 2, std::memory_order_release); // Mark as readable (even)
288
289 // Notify observers
291 }
292}
293
295 // Notify snapshot begin
297
298 // Emit all bid levels
299 uint16_t level_idx = 0;
300 for (const auto& level : sides_[Side::Buy]) {
301 PriceLevelUpdate update{
302 timestamp,
303 symbol_,
304 Side::Buy,
305 level.price,
306 level.quantity,
307 0,
308 level_idx++,
309 static_cast<uint8_t>(PriceChanged | QuantityChanged)
310 };
312 }
313
314 // Emit all ask levels
315 level_idx = 0;
316 for (const auto& level : sides_[Side::Sell]) {
317 PriceLevelUpdate update{
318 timestamp,
319 symbol_,
321 level.price,
322 level.quantity,
323 0,
324 level_idx++,
325 static_cast<uint8_t>(PriceChanged | QuantityChanged)
326 };
328 }
329
330 // Notify snapshot end
332}
333
void notifyPriceLevelUpdate(const PriceLevelUpdate &update) const
Notify all observers of price level update.
Definition observer.hpp:94
void notifyTopOfBookUpdate(const TopOfBook &tob) const
Notify all observers of top-of-book update.
Definition observer.hpp:115
void notifySnapshotEnd(SymbolId symbol, uint64_t seq_num, Timestamp timestamp) const
Notify all observers that snapshot processing is complete.
Definition observer.hpp:129
void notifySnapshotBegin(SymbolId symbol, uint64_t seq_num, Timestamp timestamp) const
Notify all observers that snapshot processing is beginning.
Definition observer.hpp:122
Level 2 OrderBook - aggregated price levels.
std::vector< detail::PriceLevelL2 > getLevels(Side side, std::size_t depth=0) const
Get price levels for a side.
detail::PriceLevelL2 cached_best_ask_
std::atomic< uint64_t > tob_seq_
std::size_t levelCount(Side side) const noexcept
Get number of levels on a side.
void notifyTopOfBookIfChanged(Timestamp timestamp)
Notify observers of top-of-book update if best changed Updates cached_tob_ after notification.
const detail::PriceLevelL2 * getBestBid() const noexcept
Get best bid (highest buy price)
const detail::PriceLevelL2 * getLevel(Side side, Price price) const noexcept
Get a specific price level by price.
OrderBookL2 & operator=(const OrderBookL2 &)=delete
TopOfBook getTopOfBook() const noexcept
Get top-of-book snapshot.
void emitSnapshot(Timestamp timestamp)
Emit complete orderbook snapshot to observers Useful for replaying full book state to a new observer ...
bool isEmpty() const noexcept
Check if book is empty (both sides)
void updateLevel(Side side, Price price, Quantity quantity, Timestamp timestamp, uint64_t seq_num=0, bool is_last_in_batch=true)
Add or update a price level If quantity is 0, the level is deleted.
const detail::PriceLevelL2 * getLevelByIndex(Side side, uint16_t index) const noexcept
Get a price level by index (0 = best)
bool deleteLevel(Side side, Price price, Timestamp timestamp=0)
Delete a price level Does not emit a PriceLevelUpdate, but refreshes the cached top-of-book (and noti...
void clearSide(Side side, Timestamp timestamp=0)
Clear all levels on one side Refreshes the cached top-of-book (and notifies onTopOfBookUpdate if it c...
detail::PriceLevelL2 cached_best_bid_
const detail::PriceLevelL2 * getBestAsk() const noexcept
Get best ask (lowest sell price)
OrderBookL2(SymbolId symbol, std::size_t initial_capacity=32)
Constructor.
void clear(Timestamp timestamp=0)
Clear all levels (both sides) Refreshes the cached top-of-book (and notifies onTopOfBookUpdate if it ...
std::array< detail::LevelContainer, SideCount > sides_
Container for storing sorted price levels Uses a sorted vector for cache-friendly storage (optimal fo...
#define SLICK_NAMESPACE_END
Definition config.hpp:127
#define SLICK_NAMESPACE_BEGIN
Definition config.hpp:126
#define SLICK_ASSERT(expr)
Definition config.hpp:135
int64_t Quantity
Quantity type - volume or size.
Definition types.hpp:21
int64_t Price
Price type - fixed-point representation Example: For 4 decimal places, price = actual_price * 10000 T...
Definition types.hpp:18
Side
Order side - using enum for easy array indexing.
Definition types.hpp:35
constexpr uint16_t INVALID_INDEX
Definition types.hpp:32
uint16_t SymbolId
Symbol identifier - can be hash or sequential ID.
Definition types.hpp:27
uint64_t Timestamp
Timestamp in nanoseconds since epoch.
Definition types.hpp:30
#define SLICK_OB_INLINE
Level 2 price level update event Note: quantity = 0 implies the price level should be deleted.
Definition events.hpp:21
Top-of-book snapshot - best bid and ask.
Definition events.hpp:142
std::array< uint8_t, 2 > change_flags
Definition events.hpp:149
Price level for Level 2 orderbook (aggregated) Stores total quantity at a specific price point.