FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
book_syncer.hpp
Go to the documentation of this file.
1#pragma once
2// BookSyncer: venue-agnostic snapshot/delta synchronisation FSM (5.4), instantiated per
3// instrument on the net thread. Venue rules are supplied by SyncTraits; the engine only
4// ever sees normalised BookSnapshot / BookDelta events, so replay is protocol-free.
5//
6// Idle --start()--> Buffering --snapshot ok--> Synced --gap--> Resync --start()--> Buffering
7//
8// Sink interface (a struct or lambda-holder):
9// void on_snapshot(const BookDeltaMsg&) // kSnapshot set
10// void on_delta(const BookDeltaMsg&)
11// void on_resync(SyncReason) // emitted before the next snapshot request
12// void request_snapshot() // REST fetch (venues with kNeedsRestSnapshot)
13//
14// Buffered deltas are copied into a fixed byte arena (no allocation after construction).
15#include "fastmm/core/config_macros.hpp"
17
18#include <cstddef>
19#include <cstdint>
20#include <cstring>
21#include <memory>
22
23namespace fastmm {
24
25enum class SyncState : std::uint8_t { Idle = 0, Buffering = 1, Synced = 2, Resync = 3 };
26enum class SyncReason : std::uint8_t {
27 None = 0,
30 SnapshotTooOld = 3, // buffered deltas start after the snapshot; need a newer one
31 Explicit = 4,
32 SnapshotMarker = 5, // venue signalled a reset (Bybit u == 1)
33 ChecksumMismatch = 6, // the venue's book checksum disagrees with ours (OKX)
34};
35[[nodiscard]] constexpr const char* to_string(SyncReason r) noexcept {
36 switch (r) {
38 return "none";
40 return "sequence gap";
42 return "buffer overflow";
44 return "snapshot too old";
46 return "explicit";
48 return "venue reset";
50 return "checksum mismatch";
51 }
52 return "?";
53}
54[[nodiscard]] constexpr const char* to_string(SyncState s) noexcept {
55 switch (s) {
57 return "Idle";
59 return "Buffering";
61 return "Synced";
63 return "Resync";
64 }
65 return "?";
66}
67
68// Binance Spot: snapshot via REST (lastUpdateId = L). Drop deltas with u <= L. The first
69// applied delta must satisfy U <= L+1 <= u; each subsequent one U == prev_u + 1.
70struct BinanceSpotSyncTraits {
71 static constexpr bool kNeedsRestSnapshot = true;
72 static constexpr bool kBuffersDeltas = true;
73 static constexpr bool is_stale(const BookDeltaMsg& d, std::uint64_t snap) noexcept {
74 return d.last_update_id <= snap;
75 }
76 static constexpr bool first_applies(const BookDeltaMsg& d, std::uint64_t snap) noexcept {
77 return d.first_update_id <= snap + 1 && snap + 1 <= d.last_update_id;
78 }
79 static constexpr bool next_applies(const BookDeltaMsg& d, std::uint64_t prev_u) noexcept {
80 return d.first_update_id == prev_u + 1;
81 }
82 static constexpr bool is_snapshot_marker(const BookDeltaMsg&) noexcept { return false; }
83};
85// Binance USDT-M futures: first applied delta must have U <= L and u >= L; afterwards each
86// delta's pu must equal the previous delta's u.
88 static constexpr bool kNeedsRestSnapshot = true;
89 static constexpr bool kBuffersDeltas = true;
90 static constexpr bool is_stale(const BookDeltaMsg& d, std::uint64_t snap) noexcept {
91 return d.last_update_id < snap;
92 }
93 static constexpr bool first_applies(const BookDeltaMsg& d, std::uint64_t snap) noexcept {
94 return d.first_update_id <= snap && d.last_update_id >= snap;
95 }
96 static constexpr bool next_applies(const BookDeltaMsg& d, std::uint64_t prev_u) noexcept {
97 return d.prev_update_id == prev_u;
98 }
99 static constexpr bool is_snapshot_marker(const BookDeltaMsg&) noexcept { return false; }
100};
102// Bybit v5: the stream itself sends a snapshot first (kSnapshot flag), then deltas whose
103// `u` must be strictly increasing; `u == 1` means the venue restarted the book (treated as
104// a snapshot marker -> resubscribe). No REST snapshot, no buffering.
105struct BybitSyncTraits {
106 static constexpr bool kNeedsRestSnapshot = false;
107 static constexpr bool kBuffersDeltas = false;
108 static constexpr bool is_stale(const BookDeltaMsg& d, std::uint64_t snap) noexcept {
109 return d.last_update_id <= snap;
111 static constexpr bool first_applies(const BookDeltaMsg& d, std::uint64_t snap) noexcept {
112 return d.last_update_id > snap;
114 static constexpr bool next_applies(const BookDeltaMsg& d, std::uint64_t prev_u) noexcept {
115 return d.last_update_id > prev_u;
117 static constexpr bool is_snapshot_marker(const BookDeltaMsg& d) noexcept {
118 return d.last_update_id == 1 && !d.is_snapshot();
120};
121
122template <class Traits, class Sink>
123class BookSyncer {
124 public:
125 static constexpr std::size_t kMaxBuffered = 4096;
126 static constexpr std::size_t kDefaultBufferBytes = 8U << 20;
128 explicit BookSyncer(Sink& sink, std::size_t buffer_bytes = kDefaultBufferBytes)
129 : sink_(sink), buf_bytes_(Traits::kBuffersDeltas ? buffer_bytes : 0) {
130 if (buf_bytes_ > 0) {
131 buf_.reset(new std::byte[buf_bytes_]);
132 offsets_.reset(new std::uint32_t[kMaxBuffered]);
133 }
134 }
135
136 [[nodiscard]] SyncState state() const noexcept { return state_; }
137 [[nodiscard]] bool synced() const noexcept { return state_ == SyncState::Synced; }
138 [[nodiscard]] std::uint64_t last_update_id() const noexcept { return prev_u_; }
139 [[nodiscard]] std::uint32_t resync_count() const noexcept { return resyncs_; }
140 [[nodiscard]] std::size_t buffered() const noexcept { return count_; }
142 // Begin (or restart) synchronisation.
143 void start() noexcept {
144 reset_buffer();
145 awaiting_first_ = false;
146 state_ = SyncState::Buffering;
147 if constexpr (Traits::kNeedsRestSnapshot) sink_.request_snapshot();
148 }
149
150 // Force a resync (e.g. crossed book beyond grace, or a disconnect).
151 void resync(SyncReason reason) noexcept {
152 ++resyncs_;
154 sink_.on_resync(reason);
155 start();
156 }
157
158 // Delta from the stream. For venues whose stream carries snapshots (Bybit) a message
159 // flagged kSnapshot is routed to on_snapshot().
160 void on_delta(const BookDeltaMsg& d) noexcept {
161 if (d.is_snapshot()) {
163 return;
164 }
165 if (Traits::is_snapshot_marker(d)) {
167 return;
168 }
169 switch (state_) {
170 case SyncState::Idle:
172 return; // not started / waiting for start()
174 if constexpr (Traits::kBuffersDeltas) {
175 if (!buffer(d)) resync(SyncReason::BufferOverflow);
176 }
177 return;
179 if constexpr (Traits::kBuffersDeltas) {
180 // No buffered delta was newer than the snapshot, so the first live one must be checked
181 // with the first-delta rule: it may overlap the snapshot (U <= L+1 <= u on Binance
182 // spot) rather than follow it exactly, and must not be mistaken for a gap.
183 if (FASTMM_UNLIKELY(awaiting_first_)) {
184 if (Traits::is_stale(d, prev_u_)) return;
185 if (!Traits::first_applies(d, prev_u_)) {
186 resync_keeping(d);
187 return;
188 }
189 awaiting_first_ = false;
190 prev_u_ = d.last_update_id;
191 sink_.on_delta(d);
192 return;
193 }
194 }
195 if (FASTMM_LIKELY(Traits::next_applies(d, prev_u_))) {
196 prev_u_ = d.last_update_id;
197 sink_.on_delta(d);
198 } else if (!Traits::is_stale(d, prev_u_)) {
199 resync_keeping(d);
200 }
201 // stale duplicates are silently dropped
202 return;
203 }
204 }
205
206 // Snapshot (REST response or in-stream snapshot). Replays the buffer.
207 void on_snapshot(const BookDeltaMsg& snap) noexcept {
208 if (state_ == SyncState::Idle) return;
209 const std::uint64_t L = snap.last_update_id;
210 if constexpr (Traits::kBuffersDeltas) {
211 // Find the first non-stale buffered delta; it must bracket L.
212 std::size_t i = 0;
213 while (i < count_ && Traits::is_stale(*at(i), L)) ++i;
214 if (i < count_ && !Traits::first_applies(*at(i), L)) {
215 // Buffered stream starts after the snapshot: the snapshot is too old. Keep
216 // buffering and ask again.
217 ++resyncs_;
218 sink_.on_resync(SyncReason::SnapshotTooOld);
219 // keep only the deltas we still need
220 drop_front(i);
221 sink_.request_snapshot();
222 return;
223 }
224 sink_.on_snapshot(snap);
225 prev_u_ = L;
226 state_ = SyncState::Synced;
227 bool first = true;
228 for (; i < count_; ++i) {
229 const BookDeltaMsg& d = *at(i);
230 const bool ok = first ? Traits::first_applies(d, L) : Traits::next_applies(d, prev_u_);
231 if (!ok) {
232 // The stream itself has a gap before d: d and what follows it start the next attempt.
233 drop_front(i);
234 ++resyncs_;
235 state_ = SyncState::Resync;
236 sink_.on_resync(SyncReason::SequenceGap);
237 awaiting_first_ = false;
238 state_ = SyncState::Buffering;
239 sink_.request_snapshot();
240 return;
241 }
242 first = false;
243 prev_u_ = d.last_update_id;
244 sink_.on_delta(d);
245 }
246 awaiting_first_ = first; // nothing newer than the snapshot was buffered
247 reset_buffer();
248 } else {
249 sink_.on_snapshot(snap);
250 prev_u_ = L;
251 state_ = SyncState::Synced;
252 }
253 }
254
255 private:
256 // A gap revealed by d. d itself is the next update of the stream, so a venue that buffers for a
257 // REST snapshot keeps it: a snapshot taken inside d's range applies at once, where without d it
258 // would look older than the buffer and cost another request (SnapshotTooOld).
259 void resync_keeping(const BookDeltaMsg& d) noexcept {
261 if constexpr (Traits::kBuffersDeltas) {
262 if (state_ == SyncState::Buffering && !buffer(d)) resync(SyncReason::BufferOverflow);
263 }
264 }
265
266 bool buffer(const BookDeltaMsg& d) noexcept {
267 if (count_ >= kMaxBuffered || used_ + d.hdr.len > buf_bytes_) return false;
268 std::memcpy(buf_.get() + used_, &d, d.hdr.len);
269 offsets_[count_++] = static_cast<std::uint32_t>(used_);
270 used_ += d.hdr.len;
271 return true;
272 }
273 [[nodiscard]] const BookDeltaMsg* at(std::size_t i) const noexcept {
274 return reinterpret_cast<const BookDeltaMsg*>(buf_.get() + offsets_[i]);
275 }
276 void drop_front(std::size_t n) noexcept {
277 if (n == 0) return;
278 if (n >= count_) {
279 reset_buffer();
280 return;
281 }
282 const std::uint32_t base = offsets_[n];
283 std::memmove(buf_.get(), buf_.get() + base, used_ - base);
284 for (std::size_t i = n; i < count_; ++i) offsets_[i - n] = offsets_[i] - base;
285 count_ -= n;
286 used_ -= base;
287 }
288 void reset_buffer() noexcept {
289 count_ = 0;
290 used_ = 0;
291 }
292
293 Sink& sink_;
294 SyncState state_ = SyncState::Idle;
295 bool awaiting_first_ = false; // Synced, but the first-delta rule has not been applied yet
296 std::uint64_t prev_u_ = 0;
297 std::uint32_t resyncs_ = 0;
298 std::unique_ptr<std::byte[]> buf_;
299 std::unique_ptr<std::uint32_t[]> offsets_;
300 std::size_t buf_bytes_;
301 std::size_t used_ = 0;
302 std::size_t count_ = 0;
303};
304
305} // namespace fastmm
Definition book_syncer.hpp:125
static constexpr std::size_t kDefaultBufferBytes
Definition book_syncer.hpp:128
bool synced() const noexcept
Definition book_syncer.hpp:139
void resync(SyncReason reason) noexcept
Force a resync (e.g. crossed book beyond grace, or a disconnect).
Definition book_syncer.hpp:153
std::uint32_t resync_count() const noexcept
Definition book_syncer.hpp:141
std::uint64_t last_update_id() const noexcept
Definition book_syncer.hpp:140
void on_delta(const BookDeltaMsg &d) noexcept
Delta from the stream.
Definition book_syncer.hpp:162
SyncState state() const noexcept
Definition book_syncer.hpp:138
void start() noexcept
Begin (or restart) synchronisation.
Definition book_syncer.hpp:145
std::size_t buffered() const noexcept
Definition book_syncer.hpp:142
void on_snapshot(const BookDeltaMsg &snap) noexcept
Snapshot (REST response or in-stream snapshot). Replays the buffer.
Definition book_syncer.hpp:209
static constexpr std::size_t kMaxBuffered
Definition book_syncer.hpp:127
Event bus message structs (5.5).
Definition strategy.hpp:16
SyncState
Definition book_syncer.hpp:27
SyncReason
Definition book_syncer.hpp:28
@ ChecksumMismatch
the venue's book checksum disagrees with ours (OKX)
@ SnapshotTooOld
buffered deltas start after the snapshot; need a newer one
@ SnapshotMarker
venue signalled a reset (Bybit u == 1)
constexpr std::string_view to_string(ParamType t) noexcept
Definition params.hpp:63
Binance USDT-M futures: first applied delta must have U <= L and u >= L; afterwards each delta's pu m...
Definition book_syncer.hpp:89
static constexpr bool kBuffersDeltas
Definition book_syncer.hpp:91
static constexpr bool is_snapshot_marker(const BookDeltaMsg &) noexcept
Definition book_syncer.hpp:101
static constexpr bool kNeedsRestSnapshot
Definition book_syncer.hpp:90
static constexpr bool next_applies(const BookDeltaMsg &d, std::uint64_t prev_u) noexcept
Definition book_syncer.hpp:98
static constexpr bool first_applies(const BookDeltaMsg &d, std::uint64_t snap) noexcept
Definition book_syncer.hpp:95
static constexpr bool is_stale(const BookDeltaMsg &d, std::uint64_t snap) noexcept
Definition book_syncer.hpp:92
static constexpr bool kBuffersDeltas
Definition book_syncer.hpp:74
static constexpr bool next_applies(const BookDeltaMsg &d, std::uint64_t prev_u) noexcept
Definition book_syncer.hpp:81
static constexpr bool is_snapshot_marker(const BookDeltaMsg &) noexcept
Definition book_syncer.hpp:84
static constexpr bool kNeedsRestSnapshot
Definition book_syncer.hpp:73
static constexpr bool first_applies(const BookDeltaMsg &d, std::uint64_t snap) noexcept
Definition book_syncer.hpp:78
static constexpr bool is_stale(const BookDeltaMsg &d, std::uint64_t snap) noexcept
Definition book_syncer.hpp:75
Bids then asks follow the fixed part as a flexible array of Level (bid_count + ask_count).
Definition messages.hpp:99
std::uint64_t last_update_id
Binance u / Bybit u.
Definition messages.hpp:104
Bybit v5: the stream itself sends a snapshot first (kSnapshot flag), then deltas whose u must be stri...
Definition book_syncer.hpp:107
static constexpr bool next_applies(const BookDeltaMsg &d, std::uint64_t prev_u) noexcept
Definition book_syncer.hpp:116
static constexpr bool kBuffersDeltas
Definition book_syncer.hpp:109
static constexpr bool is_snapshot_marker(const BookDeltaMsg &d) noexcept
Definition book_syncer.hpp:119
static constexpr bool is_stale(const BookDeltaMsg &d, std::uint64_t snap) noexcept
Definition book_syncer.hpp:110
static constexpr bool first_applies(const BookDeltaMsg &d, std::uint64_t snap) noexcept
Definition book_syncer.hpp:113
static constexpr bool kNeedsRestSnapshot
Definition book_syncer.hpp:108