FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
l2_book.hpp
Go to the documentation of this file.
1#pragma once
2// L2Book<kMaxDepth>: price-aggregated book for crypto feeds (5.4).
3//
4// Each side is a sorted StaticVector<Level> with the BEST level at back(): top-of-book is
5// O(1), and the common near-top insert/erase only memmoves a handful of 16-byte levels.
6// Bids are stored ascending by price, asks descending, so both sides share one code path
7// keyed on "distance from best". Lookups scan the top 16 linearly and bisect the rest.
8//
9// Depth is capped at kMaxDepth: levels worse than the worst stored one are dropped and the
10// side is flagged truncated() until the next snapshot (crypto venues resend snapshots on
11// resync; we deliberately do not try to repair). Crossed books are tolerated (Binance can
12// cross for ~100 ms between updates); the engine polls crossed_for(now) against a grace.
14#include "fastmm/core/config_macros.hpp"
15#include "fastmm/core/containers/static_vector.hpp"
17
18#include <cstddef>
19#include <cstdint>
20#include <span>
21#include <type_traits>
22
23namespace fastmm {
24
25template <std::size_t kMaxDepth = 256>
26class L2Book {
27 static_assert(kMaxDepth >= 2);
29 public:
30 static constexpr std::size_t kDepth = kMaxDepth;
31 static constexpr std::size_t kLinearScan = 16;
33 L2Book() noexcept = default;
34
35 // ---- mutation --------------------------------------------------------------------------
36
37 void clear() noexcept {
38 bids_.clear();
39 asks_.clear();
40 truncated_[0] = truncated_[1] = false;
41 have_snapshot_ = false;
42 crossed_since_ = Timestamp{};
43 seq_ = 0;
44 }
45
46 // Sets/updates/deletes (qty == 0) one level. Returns false if the level was dropped
47 // because the side is full and the price is worse than everything stored.
48 FASTMM_FORCE_INLINE bool apply_level(Side side, Price price, Qty qty) noexcept {
49 auto& v = levels(side);
50 // Position such that v[i].price is the first level at-or-better than `price` when
51 // scanning from the worst (front) - i.e. lower_bound in "goodness" order.
52 std::size_t i = v.size();
53 bool found = false;
54 // Linear scan from the best (back) over the top kLinearScan levels.
55 const std::size_t stop = v.size() > kLinearScan ? v.size() - kLinearScan : 0;
56 while (i > stop) {
57 const Price p = v[i - 1].price;
58 if (p == price) {
59 found = true;
60 --i;
61 break;
62 }
63 if (better(side, price, p)) break; // insert after v[i-1]
64 --i;
65 }
66 if (!found && i == stop && stop > 0) {
67 // Bisect the remainder [0, stop): find first index whose price is at-or-better.
68 std::size_t lo = 0;
69 std::size_t hi = stop;
70 while (lo < hi) {
71 const std::size_t mid = lo + (hi - lo) / 2;
72 if (better(side, price, v[mid].price)) {
73 lo = mid + 1;
74 } else {
75 hi = mid;
76 }
77 }
78 i = lo;
79 found = i < v.size() && v[i].price == price;
80 }
81 if (found) {
82 if (qty.is_zero()) {
83 v.erase_at(i);
84 } else {
85 v[i].qty = qty;
86 }
87 return true;
88 }
89 if (qty.is_zero()) return true; // delete of an unknown level: no-op
90 if (FASTMM_UNLIKELY(v.full())) {
91 truncated_[static_cast<std::size_t>(side)] = true;
92 if (i == 0) return false; // worse than everything we hold
93 v.erase_front(); // drop the worst level to make room
94 --i;
95 }
96 v.insert_at(i, Level{price, qty});
97 return true;
98 }
99
100 void apply_snapshot(std::span<const Level> bids,
101 std::span<const Level> asks,
102 std::uint64_t seq,
103 Timestamp ts) noexcept {
104 clear();
105 for (const Level& l : bids) apply_level(Side::Buy, l.price, l.qty);
106 for (const Level& l : asks) apply_level(Side::Sell, l.price, l.qty);
107 have_snapshot_ = true;
108 seq_ = seq;
109 last_update_ = ts;
110 update_crossed(ts);
111 }
112
113 // Applies a BookDeltaMsg (or snapshot with kSnapshot). Uses last_update_id as seq and
114 // exch_ts (falling back to recv_ts) as the update time.
115 void apply_delta(const BookDeltaMsg& d) noexcept {
116 const Timestamp ts = d.hdr.exch_ts.valid() ? d.hdr.exch_ts : d.hdr.recv_ts;
117 if (d.is_snapshot()) {
118 apply_snapshot(d.bids(), d.asks(), d.last_update_id, ts);
119 return;
120 }
121 for (const Level& l : d.bids()) apply_level(Side::Buy, l.price, l.qty);
122 for (const Level& l : d.asks()) apply_level(Side::Sell, l.price, l.qty);
123 constexpr std::uint8_t kCut = EventHeader::kTruncatedBids | EventHeader::kTruncatedAsks;
124 if (FASTMM_UNLIKELY((d.hdr.flags & kCut) != 0)) trim_truncated(d);
125 seq_ = d.last_update_id;
126 last_update_ = ts;
127 update_crossed(ts);
128 }
129
130 void set_seq(std::uint64_t s) noexcept { seq_ = s; }
131 void set_last_update(Timestamp t) noexcept { last_update_ = t; }
132 void mark_snapshot() noexcept { have_snapshot_ = true; }
134 // ---- BookView --------------------------------------------------------------------------
135
136 [[nodiscard]] FASTMM_FORCE_INLINE Level best_bid() const noexcept {
137 return bids_.empty() ? Level{} : bids_.back();
138 }
139 [[nodiscard]] FASTMM_FORCE_INLINE Level best_ask() const noexcept {
140 return asks_.empty() ? Level{} : asks_.back();
141 }
142 [[nodiscard]] Price mid() const noexcept {
143 if (bids_.empty() || asks_.empty()) return Price{};
144 return Price::from_raw((bids_.back().price.raw + asks_.back().price.raw) / 2);
145 }
146 [[nodiscard]] Price spread() const noexcept {
147 if (bids_.empty() || asks_.empty()) return Price{};
148 return asks_.back().price - bids_.back().price;
149 }
150 // i == 0 is best. Level{} if out of range.
151 [[nodiscard]] Level level(Side side, std::size_t i) const noexcept {
152 const auto& v = levels(side);
153 return i < v.size() ? v[v.size() - 1 - i] : Level{};
154 }
155 [[nodiscard]] std::size_t depth(Side side) const noexcept { return levels(side).size(); }
156 [[nodiscard]] std::size_t depth() const noexcept { return bids_.size() + asks_.size(); }
157
158 // Total quantity on `side` at prices at-or-better than `limit`.
159 [[nodiscard]] Qty qty_at_or_better(Side side, Price limit) const noexcept {
160 const auto& v = levels(side);
161 Qty sum{};
162 for (std::size_t i = v.size(); i > 0; --i) {
163 const Level& l = v[i - 1];
164 if (!at_or_better(side, l.price, limit)) break;
165 sum += l.qty;
166 }
167 return sum;
168 }
169 // Price of the level at which cumulative quantity from the best reaches `qty`;
170 // Price{} if the side is too thin.
171 [[nodiscard]] Price price_for_qty(Side side, Qty qty) const noexcept {
172 const auto& v = levels(side);
173 Qty cum{};
174 for (std::size_t i = v.size(); i > 0; --i) {
175 cum += v[i - 1].qty;
176 if (cum >= qty) return v[i - 1].price;
177 }
178 return Price{};
179 }
180 // Visits levels best-first. F(const Level&) may return bool (false stops) or void.
181 template <class F>
182 void for_each_level(Side side, F&& f) const noexcept {
183 const auto& v = levels(side);
184 for (std::size_t i = v.size(); i > 0; --i) {
185 if constexpr (std::is_same_v<std::invoke_result_t<F, const Level&>, bool>) {
186 if (!f(v[i - 1])) return;
187 } else {
188 f(v[i - 1]);
189 }
190 }
191 }
192 template <std::size_t N>
193 [[nodiscard]] TopLevels<N> top(Side side) const noexcept {
194 TopLevels<N> out;
195 const auto& v = levels(side);
196 const std::size_t n = v.size() < N ? v.size() : N;
197 for (std::size_t i = 0; i < n; ++i) out.levels[i] = v[v.size() - 1 - i];
198 out.count = static_cast<std::uint32_t>(n);
199 return out;
200 }
201
202 [[nodiscard]] bool crossed() const noexcept {
203 return !bids_.empty() && !asks_.empty() && bids_.back().price >= asks_.back().price;
204 }
205 // How long the book has been crossed as of `now` (zero if not crossed).
206 [[nodiscard]] Duration crossed_for(Timestamp now) const noexcept {
207 return crossed_since_.valid() ? now - crossed_since_ : Duration{};
208 }
209 [[nodiscard]] bool truncated(Side side) const noexcept {
210 return truncated_[static_cast<std::size_t>(side)];
211 }
212 [[nodiscard]] bool truncated() const noexcept { return truncated_[0] || truncated_[1]; }
213 [[nodiscard]] bool has_snapshot() const noexcept { return have_snapshot_; }
214 [[nodiscard]] bool is_valid() const noexcept {
215 return have_snapshot_ && !bids_.empty() && !asks_.empty() && !crossed();
216 }
217 [[nodiscard]] std::uint64_t seq() const noexcept { return seq_; }
218 [[nodiscard]] Timestamp last_update() const noexcept { return last_update_; }
219
220 // Raw access (worst..best order) for tests/tools.
221 [[nodiscard]] const StaticVector<Level, kMaxDepth>& raw(Side side) const noexcept {
222 return levels(side);
223 }
224
225 private:
226 FASTMM_FORCE_INLINE StaticVector<Level, kMaxDepth>& levels(Side s) noexcept {
227 return s == Side::Buy ? bids_ : asks_;
228 }
229 FASTMM_FORCE_INLINE const StaticVector<Level, kMaxDepth>& levels(Side s) const noexcept {
230 return s == Side::Buy ? bids_ : asks_;
231 }
232 // A side flagged kTruncatedBids/Asks left out the venue's changes behind the last level it
233 // carries: the book's levels behind that one may be stale, so they go (the side is truncated()
234 // until the next snapshot). A no-op for a book at most half as deep as the side carried
235 // (level_spill.hpp), such as the engine's.
236 [[gnu::noinline, gnu::cold]] void trim_truncated(const BookDeltaMsg& d) noexcept {
237 if ((d.hdr.flags & EventHeader::kTruncatedBids) != 0) trim_behind(Side::Buy, d.bids());
238 if ((d.hdr.flags & EventHeader::kTruncatedAsks) != 0) trim_behind(Side::Sell, d.asks());
239 }
240 void trim_behind(Side side, std::span<const Level> carried) noexcept {
241 if (carried.empty()) return;
242 Price bound = carried[0].price;
243 for (const Level& l : carried) {
244 if (better(side, bound, l.price)) bound = l.price;
245 }
246 auto& v = levels(side);
247 std::size_t k = 0;
248 while (k < v.size() && better(side, bound, v[k].price)) ++k;
249 if (k == 0) return;
250 v.erase_front(k);
251 truncated_[static_cast<std::size_t>(side)] = true;
252 }
253 void update_crossed(Timestamp ts) noexcept {
254 if (crossed()) {
255 if (!crossed_since_.valid()) crossed_since_ = ts;
256 } else {
257 crossed_since_ = Timestamp{};
258 }
259 }
260
261 StaticVector<Level, kMaxDepth> bids_;
262 StaticVector<Level, kMaxDepth> asks_;
263 std::uint64_t seq_ = 0;
264 Timestamp last_update_{};
265 Timestamp crossed_since_{};
266 bool truncated_[2] = {false, false};
267 bool have_snapshot_ = false;
268};
269
270static_assert(BookView<L2Book<256>>);
271
272} // namespace fastmm
BookView: the read interface every book implementation (L2Book, L3Book::to_l2 view,...
void mark_snapshot() noexcept
Definition l2_book.hpp:133
std::size_t depth() const noexcept
Definition l2_book.hpp:156
Price price_for_qty(Side side, Qty qty) const noexcept
Price of the level at which cumulative quantity from the best reaches qty; Price{} if the side is too...
Definition l2_book.hpp:171
void clear() noexcept
Definition l2_book.hpp:38
Price mid() const noexcept
Definition l2_book.hpp:142
std::uint64_t seq() const noexcept
Definition l2_book.hpp:217
bool is_valid() const noexcept
Definition l2_book.hpp:214
Qty qty_at_or_better(Side side, Price limit) const noexcept
Total quantity on side at prices at-or-better than limit.
Definition l2_book.hpp:159
Level level(Side side, std::size_t i) const noexcept
i == 0 is best. Level{} if out of range.
Definition l2_book.hpp:151
Timestamp last_update() const noexcept
Definition l2_book.hpp:218
void for_each_level(Side side, F &&f) const noexcept
Visits levels best-first. F(const Level&) may return bool (false stops) or void.
Definition l2_book.hpp:182
static constexpr std::size_t kLinearScan
Definition l2_book.hpp:33
Price spread() const noexcept
Definition l2_book.hpp:146
std::size_t depth(Side side) const noexcept
Definition l2_book.hpp:155
bool truncated(Side side) const noexcept
Definition l2_book.hpp:209
bool crossed() const noexcept
Definition l2_book.hpp:202
bool truncated() const noexcept
Definition l2_book.hpp:212
void set_seq(std::uint64_t s) noexcept
Definition l2_book.hpp:131
void apply_snapshot(std::span< const Level > bids, std::span< const Level > asks, std::uint64_t seq, Timestamp ts) noexcept
Definition l2_book.hpp:101
FASTMM_FORCE_INLINE Level best_ask() const noexcept
Definition l2_book.hpp:139
FASTMM_FORCE_INLINE bool apply_level(Side side, Price price, Qty qty) noexcept
Sets/updates/deletes (qty == 0) one level.
Definition l2_book.hpp:49
static constexpr std::size_t kDepth
Definition l2_book.hpp:32
const StaticVector< Level, kMaxDepth > & raw(Side side) const noexcept
Raw access (worst..best order) for tests/tools.
Definition l2_book.hpp:221
void set_last_update(Timestamp t) noexcept
Definition l2_book.hpp:132
Duration crossed_for(Timestamp now) const noexcept
How long the book has been crossed as of now (zero if not crossed).
Definition l2_book.hpp:206
bool has_snapshot() const noexcept
Definition l2_book.hpp:213
FASTMM_FORCE_INLINE Level best_bid() const noexcept
Definition l2_book.hpp:136
L2Book() noexcept=default
void apply_delta(const BookDeltaMsg &d) noexcept
Applies a BookDeltaMsg (or snapshot with kSnapshot).
Definition l2_book.hpp:116
TopLevels< N > top(Side side) const noexcept
Definition l2_book.hpp:193
Event bus message structs (5.5).
Definition strategy.hpp:16
Side
Definition enums.hpp:12
Fixed< struct QtyTag > Qty
Definition fixed_point.hpp:333
constexpr bool at_or_better(Side s, Price p, Price q) noexcept
true if p is at least as good as q from the point of view of side s.
Definition book_view.hpp:45
Fixed< struct PriceTag > Price
Definition fixed_point.hpp:332
constexpr bool better(Side s, Price p, Price q) noexcept
Definition book_view.hpp:48
Bids then asks follow the fixed part as a flexible array of Level (bid_count + ask_count).
Definition messages.hpp:99
Definition time.hpp:22
@ kTruncatedBids
BookDelta: the venue's update changed more levels on this side than the message carries,...
Definition messages.hpp:50
@ kTruncatedAsks
Definition messages.hpp:51
static constexpr Fixed from_raw(std::int64_t r) noexcept
Definition fixed_point.hpp:155
One price level. Shared by book messages and the L2 book.
Definition messages.hpp:30
Price price
Definition messages.hpp:31
Qty qty
Definition messages.hpp:32
Definition time.hpp:45
constexpr bool valid() const noexcept
Definition time.hpp:55
Fixed-size result of top<N>().
Definition book_view.hpp:20
std::uint32_t count
Definition book_view.hpp:22
Level levels[N]
Definition book_view.hpp:21