FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
oms.hpp
Go to the documentation of this file.
1#pragma once
2// Order management (5.8): a Pool<Order> + OpenHashMap<ClientOrderId, Handle> + a ring of
3// recently terminal ids used to classify late venue messages.
4//
5// State machine (terminal states free the slot and go to the recently-terminal ring):
6// submit -> PendingNew
7// ack PendingNew -> Live; PendingReplace -> Live/PartiallyFilled (replace ok)
8// reject PendingNew -> Rejected; PendingReplace -> back to previous working state
9// cancel request Live/PartiallyFilled -> PendingCancel
10// cancel ack any open -> Canceled (unsolicited if we did not ask); Filled when the
11// venue's cum_qty covers the order
12// cancel reject PendingCancel -> back; >3 rejects -> ReconcileNeeded;
13// VenueUnknownOrder -> Canceled
14// fill cum >= qty -> Filled, else PartiallyFilled (pending states keep pending)
15// expired any open -> Expired; Filled when the venue's cum_qty covers the order
16// A cum_qty a venue reports that no fill message covered (a cancel ack, an expiry or a
17// reconciliation snapshot) becomes OmsUpdate::missed_qty for the engine to book. That booking is an
18// estimate - the order's own price, no fee - so it is remembered here until an execution names it:
19// one from the venue's trade history (OrderFillMsg::kReplayed), or a live one that arrives after
20// the cum_qty covering it (its cum_qty - qty lies below the booked cum_qty; order and execution
21// topics are not ordered on every venue). The execution then corrects the price and the fee
22// (OmsUpdate::corrected_qty) instead of the quantity being counted twice.
23// Races: fill after cancel ack -> LateFill (position still updated from the terminal record's
24// instrument and side; the record takes the fill's quantity, and a Canceled or Expired order the
25// fill completes becomes Filled, so the order's last update agrees with its fills); cancel-reject
26// after fill -> ignored; ack for unknown id -> CancelUnknown
27// (never leave an unknown live order); ack for an order reconciliation marked cancelled ->
28// CancelUnknown; duplicate exec_id -> Duplicate; duplicate ack -> Ignored (also for the original
29// id while a replace to a new id is pending: only the new id's ack completes the replace).
30// Reconciliation (per venue): a cancel or replace still in flight stays pending; orders sent after
31// the snapshot was requested (above the Begin's sent watermark) are not marked cancelled.
33#include "fastmm/core/config_macros.hpp"
34#include "fastmm/core/containers/open_hash_map.hpp"
35#include "fastmm/core/containers/pool.hpp"
36#include "fastmm/core/containers/recent_map.hpp"
37#include "fastmm/core/containers/ring_buffer.hpp"
38#include "fastmm/core/containers/static_vector.hpp"
39#include "fastmm/core/enums.hpp"
42#include "fastmm/core/order.hpp"
44
45#include <cstdint>
46#include <limits>
47#include <memory>
48#include <optional>
49
50namespace fastmm {
51
52inline constexpr std::size_t kMaxOpenOrders = 4096;
53inline constexpr std::size_t kRecentlyTerminal = 4096;
54// Fills booked from a cumulative quantity and never named by an execution. One entry per order, so
55// this is the number of orders that can be waiting for an execution-history replay at once; a
56// private-stream outage covers a handful, and the oldest entry is dropped (and counted) past that.
57inline constexpr std::size_t kMaxSyntheticFills = 64;
58// Client order ids per session: the wire form is 48 bits, epoch(16) << 32 | seq(32).
59inline constexpr std::uint64_t kMaxSeq = 0xFFFF'FFFFULL;
60
61enum class OmsAction : std::uint8_t {
62 None = 0,
63 Ignored, // message did not apply (duplicate ack, late cancel-reject, ...)
64 Duplicate, // duplicate exec_id
65 CancelUnknown, // ack/fill for an id we do not know: engine must cancel it
66 LateFill, // fill for a recently terminal order: position, and the terminal record's
67 // cum_qty and state (changed = true when they moved)
68 UnknownFill, // fill for a completely unknown id: update position, alert
69 ReconcileNeeded, // too many cancel rejects
70};
71
72struct OmsUpdate {
73 Order order{}; // snapshot after the transition; for a recently terminal id only the terminal
74 // record's fields (id, instrument, venue, side, state, price, qty, cum_qty);
75 // empty if unknown
76 Handle<Order> handle{}; // invalid once terminal (slot freed) or unknown
79 bool known = false; // the id mapped to an order (open or recently terminal)
80 bool changed = false; // state or quantities changed
81 bool terminal = false; // the order reached a terminal state in this update
82 Qty fill_qty{}; // for fills
84 // Quantity the venue reports as filled that we never saw a fill message for (a private-stream
85 // outage covered by a cancel ack, an expiry or a reconciliation snapshot). The engine books it
86 // as a synthetic fill at the order's own price; see Engine::book_missed_fill.
88 // A cancel-replace to a new id completed: the order carries on under order.cl_ord_id and this
89 // one is gone. It never produces an update of its own (the OMS renames the record in place), so
90 // anything that tracks orders by id closes it here.
92 // Quantity that was still working when a reconciliation snapshot failed to mention the order.
93 // The venue does not say whether it was cancelled or filled, so nothing can be booked; it is
94 // reported so the engine can say the position may be wrong instead of assuming it is not.
96 // Part of a replayed execution (OrderFillMsg::kReplayed) whose quantity a synthetic fill already
97 // put in the position: the execution names what it was, so its price and its fee replace the
98 // estimate rather than the quantity being counted twice. `synthetic_px` is the price the estimate
99 // used. See Engine::on_fill and PositionTracker::correct_fill.
102 // The order's times (Oms::times), and the pool slot it had: set on every update of an open
103 // order, the terminal one included (`handle` is invalid then, the slot already free).
108struct OmsStats {
109 std::uint32_t open = 0;
110 std::uint64_t submitted = 0;
111 std::uint64_t acked = 0;
112 std::uint64_t rejected = 0;
113 std::uint64_t canceled = 0;
114 std::uint64_t filled = 0;
115 std::uint64_t fills = 0;
116 std::uint64_t expired = 0;
117 std::uint64_t unknown_ids = 0;
118 std::uint64_t duplicates = 0;
119 std::uint64_t late_fills = 0;
120 std::uint64_t unsolicited_cancels = 0;
121 std::uint64_t missed_fills = 0; // cum_qty jumps a fill message never reported
122 // Missed fills a replayed execution later named, so their price and fee stopped being estimates.
123 std::uint64_t corrected_fills = 0;
124 // Missed fills whose entry was evicted before an execution named them: their price and fee stay
125 // estimates, and an execution for them would now be counted twice.
126 std::uint64_t synthetic_forgotten = 0;
127 std::uint64_t ack_timeouts = 0; // PendingNew orders swept by sweep_pending()
128 // Orders a reconciliation snapshot did not report that still had working quantity: the venue
129 // ended them and never said how, so the position may be short by up to that much.
130 std::uint64_t reconcile_unresolved = 0;
131};
133enum class OrderClass : std::uint8_t { Open, RecentlyTerminal, Unknown };
134
135class Oms {
136 public:
137 // `max_seq` is the highest client order id sequence this session may issue; only tests lower it
138 // from kMaxSeq (reaching it takes 4.3 billion orders).
139 explicit Oms(std::uint16_t session_epoch = 1, std::uint64_t max_seq = kMaxSeq) noexcept
140 : epoch_(session_epoch), max_seq_(max_seq) {
141 for (auto& per_inst : best_own_) per_inst[0] = per_inst[1] = Price{};
142 for (auto& per_inst : open_qty_) per_inst[0] = per_inst[1] = Qty{};
143 }
144 Oms(const Oms&) = delete;
145 Oms& operator=(const Oms&) = delete;
147 [[nodiscard]] std::uint16_t session_epoch() const noexcept { return epoch_; }
148 // An invalid id once the session's 32-bit sequence is used up: the wire form carries 48 bits
149 // (epoch 16 + seq 32), so reusing a sequence would repeat a ClientOrderId within the session and
150 // let a late venue message match the wrong order. The engine turns that into a kill switch.
151 [[nodiscard]] ClientOrderId next_cl_ord_id() noexcept {
152 if (FASTMM_UNLIKELY(seq_ >= max_seq_)) return ClientOrderId{};
153 return make_cl_ord_id(epoch_, static_cast<std::uint32_t>(++seq_));
154 }
155 // Client order ids left in this session.
156 [[nodiscard]] std::uint64_t ids_left() const noexcept { return max_seq_ - seq_; }
157 [[nodiscard]] const OmsStats& stats() const noexcept { return stats_; }
158 [[nodiscard]] std::uint32_t open_count() const noexcept { return stats_.open; }
159 [[nodiscard]] std::uint32_t open_count(InstrumentId id) const noexcept {
160 return open_per_inst_[id.value];
162 // Sum of leaves of open orders on (instrument, side): "in-flight same-side exposure".
163 [[nodiscard]] Qty open_qty(InstrumentId id, Side s) const noexcept {
164 return open_qty_[id.value][static_cast<std::size_t>(s)];
166 // Best price among our own open orders on (instrument, side); Price{} if none. For STP.
167 [[nodiscard]] Price best_own_px(InstrumentId id, Side s) const noexcept {
168 return best_own_[id.value][static_cast<std::size_t>(s)];
170
171 // ---- outbound ---------------------------------------------------------------------------
172
175 Timestamp now) noexcept {
176 if (FASTMM_UNLIKELY(req.instrument.value >= kMaxInstruments))
178 if (FASTMM_UNLIKELY(by_id_.contains(id))) return fail(RejectReason::DuplicateId);
179 const Handle<Order> h = pool_.allocate();
180 if (FASTMM_UNLIKELY(!h.valid())) return fail(RejectReason::PoolExhausted);
181 if (FASTMM_UNLIKELY(by_id_.insert(id, h).first == nullptr)) {
182 pool_.free(h);
184 }
185 live_pos_[h.idx] = static_cast<std::uint32_t>(live_.size());
186 static_cast<void>(live_.push_back(h.idx)); // cannot fail: one entry per pool slot
187 Order& o = pool_.get(h);
188 o = Order{};
189 o.cl_ord_id = id;
190 o.instrument = req.instrument;
191 o.venue = req.venue;
192 o.side = req.side;
193 o.type = req.type;
194 o.tif = req.tif;
195 o.flags = static_cast<std::uint8_t>((req.post_only ? Order::kPostOnly : 0) |
196 (req.reduce_only ? Order::kReduceOnly : 0));
198 o.price = req.price;
199 o.qty = req.qty;
200 o.created = now;
201 o.user_tag = req.user_tag;
202 times_[h.idx] = TimesSlot{OrderTimes{now, {}, {}}, {}};
203 ++stats_.submitted;
204 ++stats_.open;
205 ++open_per_inst_[o.instrument.value];
206 open_qty_[o.instrument.value][static_cast<std::size_t>(o.side)] += o.qty;
207 Price& best = best_own_[o.instrument.value][static_cast<std::size_t>(o.side)];
208 if (best.is_zero() || better(o.side, o.price, best)) best = o.price;
209 return h;
210 }
211
213 if (!pool_.is_live(h)) return fail(RejectReason::UnknownOrder);
214 Order& o = pool_.get(h);
217 return {};
218 }
219
220 // Cancel of an order that is still waiting for its ack (the ack-timeout sweep only). A venue
221 // that never saw the order answers VenueUnknownOrder, and on_cancel_reject ends it.
222 Result<void, RejectReason> request_cancel_unacked(Handle<Order> h) noexcept {
223 if (!pool_.is_live(h)) return fail(RejectReason::UnknownOrder);
224 Order& o = pool_.get(h);
227 return {};
228 }
229
230 // new_id may equal the current id for venues that amend in place. `now`: the replace's send
231 // time, which becomes OrderTimes::sent once it is acknowledged.
232 Result<void, RejectReason> request_replace(
233 Handle<Order> h, ClientOrderId new_id, Price px, Qty qty, Timestamp now = {}) noexcept {
234 if (!pool_.is_live(h)) return fail(RejectReason::UnknownOrder);
235 Order& o = pool_.get(h);
237 if (new_id != o.cl_ord_id) {
238 if (by_id_.contains(new_id)) return fail(RejectReason::DuplicateId);
239 if (by_id_.insert(new_id, h).first == nullptr) return fail(RejectReason::PoolExhausted);
240 }
241 o.pending_cl_ord_id = new_id;
242 o.pending_price = px;
243 o.pending_qty = qty;
245 times_[h.idx].replace_sent = now;
246 return {};
247 }
248
249 // ---- inbound ----------------------------------------------------------------------------
250
251 OmsUpdate on_ack(const OrderAckMsg& m) noexcept {
252 OmsUpdate u;
253 Handle<Order> h = lookup(m.cl_ord_id, u);
254 if (!h.valid()) {
255 if (!u.known) {
256 u.action = OmsAction::CancelUnknown; // a live order we do not know: cancel it
257 ++stats_.unknown_ids;
258 } else if (u.order.has(Order::kReconciled)) {
259 // Reconciliation took it for gone, yet the venue has it (the snapshot missed an order
260 // still in flight). Nothing tracks it any more: cancel it rather than leave it live.
261 u.action = OmsAction::CancelUnknown;
262 } else {
263 u.action = OmsAction::Ignored; // late ack for a terminal order
264 }
265 return u;
266 }
267 Order& o = pool_.get(h);
268 u.prev = o.state;
269 switch (o.state) {
272 o.venue_order_id = m.venue_order_id;
273 ++stats_.acked;
274 u.changed = true;
275 times_[h.idx].t.venue_ack = m.hdr.exch_ts;
276 times_[h.idx].t.local_ack = m.hdr.recv_ts;
277 break;
279 // Only the replacement's ack completes it (the same id for an in-place amend). An ack that
280 // repeats the current id while a replace to a new id is in flight is a duplicate of the
281 // original ack (Binance sends one from the WS API response and one from the user stream):
282 // applying it would take the replace for confirmed, leave the new id mapped after the order
283 // ends and let the new leg's ack read a freed slot while that order stays live untracked.
284 if (m.cl_ord_id == o.pending_cl_ord_id) {
285 const ClientOrderId old_id = o.cl_ord_id;
286 const bool rekey = o.pending_cl_ord_id != o.cl_ord_id;
287 // An in-place amend keeps the venue's order even when it is given a new client id
288 // (Binance Spot keepPriority), so the fills booked against it stay.
289 const bool new_venue_order = rekey && (m.flags & OrderAckMsg::kAmendedInPlace) == 0;
290 apply_replace(o, rekey, new_venue_order);
291 if (o.cl_ord_id != old_id) u.replaced_cl_ord_id = old_id;
292 o.venue_order_id = m.venue_order_id;
293 u.changed = true;
294 TimesSlot& t = times_[h.idx];
295 t.t = OrderTimes{t.replace_sent, m.hdr.exch_ts, m.hdr.recv_ts};
296 } else {
297 u.action = OmsAction::Ignored;
298 }
299 break;
300 default:
301 u.action = OmsAction::Ignored; // duplicate ack
302 // A venue that answers twice (API response and user stream) may put its time on either.
303 if (m.cl_ord_id == o.cl_ord_id && !times_[h.idx].t.venue_ack.valid())
304 times_[h.idx].t.venue_ack = m.hdr.exch_ts;
305 break;
306 }
307 finish_update(u, h, o);
308 return u;
309 }
310
311 OmsUpdate on_reject(const OrderRejectMsg& m) noexcept {
312 OmsUpdate u;
313 Handle<Order> h = lookup(m.cl_ord_id, u);
314 if (!h.valid()) {
315 u.action = OmsAction::Ignored;
316 return u;
317 }
318 Order& o = pool_.get(h);
319 u.prev = o.state;
320 if (o.state == OrderState::PendingNew) {
321 o.reject_reason = m.reason;
322 ++stats_.rejected;
323 terminate(u, h, o, OrderState::Rejected);
324 return u;
325 }
326 if (o.state == OrderState::PendingReplace && m.cl_ord_id == o.pending_cl_ord_id) {
327 // Replace failed. If the venue already cancelled the original (cancel-then-new
328 // style replace) the order is gone; otherwise it keeps working unchanged.
330 clear_pending(o);
331 terminate(u, h, o, OrderState::Canceled);
332 return u;
333 }
334 clear_pending(o);
336 u.changed = true;
337 finish_update(u, h, o);
338 return u;
339 }
340 u.action = OmsAction::Ignored;
341 finish_update(u, h, o);
342 return u;
343 }
344
346 OmsUpdate u;
347 Handle<Order> h = lookup(m.cl_ord_id, u);
348 if (!h.valid()) {
349 u.action = OmsAction::Ignored;
350 return u;
351 }
352 Order& o = pool_.get(h);
353 u.prev = o.state;
354 absorb_cum(o, m.cum_qty, u);
355 if (o.state == OrderState::PendingReplace && m.cl_ord_id == o.cl_ord_id) {
356 // Old leg of a cancel-then-new replace: wait for the new leg's ack/reject.
358 finish_update(u, h, o);
359 return u;
360 }
361 // The cancel raced a fill that finished the order: it is Filled, not Canceled.
362 if (o.cum_qty >= o.qty) {
363 ++stats_.filled;
364 terminate(u, h, o, OrderState::Filled);
365 return u;
366 }
369 ++stats_.unsolicited_cancels;
370 }
371 terminate(u, h, o, OrderState::Canceled);
372 return u;
373 }
374
376 OmsUpdate u;
377 Handle<Order> h = lookup(m.cl_ord_id, u);
378 if (!h.valid()) {
379 u.action = OmsAction::Ignored; // e.g. already filled: ignore
380 return u;
381 }
382 Order& o = pool_.get(h);
383 u.prev = o.state;
385 u.action = OmsAction::Ignored;
386 finish_update(u, h, o);
387 return u;
388 }
389 if (m.reason == RejectReason::VenueUnknownOrder) {
390 // The venue has no such order: it is gone (filled/cancelled elsewhere).
391 terminate(u, h, o, OrderState::Canceled);
392 return u;
393 }
395 o.cancel_attempts = static_cast<std::uint8_t>(o.cancel_attempts + 1);
396 if (o.cancel_attempts > 3) u.action = OmsAction::ReconcileNeeded;
397 u.changed = true;
398 finish_update(u, h, o);
399 return u;
400 }
401
402 OmsUpdate on_fill(const OrderFillMsg& m) noexcept {
403 OmsUpdate u;
404 u.fill_qty = m.qty;
405 u.fill_px = m.price;
406 if (!m.exec_id.empty() && !remember_exec(m)) {
407 u.action = OmsAction::Duplicate;
408 ++stats_.duplicates;
409 return u;
410 }
411 // A replayed execution may be one the venue already counted into a cum_qty the engine booked as
412 // a synthetic fill. Taking that quantity out of the pool here is what keeps it from being
413 // booked twice, whether the order is still open, recently terminal or gone.
414 const bool replayed = (m.flags & OrderFillMsg::kReplayed) != 0;
415 if (FASTMM_UNLIKELY(replayed)) take_synthetic(m.cl_ord_id, m.qty, u);
416 Handle<Order> h = lookup(m.cl_ord_id, u);
417 // A live execution can also arrive after the cum_qty that covers it: venues that publish order
418 // states and executions on separate topics (Bybit's `order` and `execution`) may end an order
419 // with its cumulative quantity first. The quantity was booked then as a synthetic fill; the
420 // part of this execution at or below that cum_qty (this execution spans cum_qty - qty to
421 // cum_qty on the venue's count) is the estimate being named, not new quantity.
422 // Without a cum_qty (Deribit's trades) only an order that has ended can tell: the venue's
423 // final cum_qty was all it will ever fill, so the execution is within the estimate.
424 if (FASTMM_UNLIKELY(!replayed && !synthetic_.empty()) && u.known) {
425 if (m.cum_qty.is_positive()) {
426 const Qty booked = h.valid() ? pool_.get(h).cum_qty : u.order.cum_qty;
427 Qty covered = booked - (m.cum_qty - m.qty);
428 if (covered > m.qty) covered = m.qty;
429 if (covered.is_positive()) take_synthetic(m.cl_ord_id, covered, u);
430 } else if (!h.valid()) {
431 take_synthetic(m.cl_ord_id, m.qty, u);
432 }
433 }
434 ++stats_.fills;
435 if (!h.valid()) {
436 if (u.known) {
437 u.action = OmsAction::LateFill;
438 ++stats_.late_fills;
439 late_fill(m, u);
440 } else {
441 u.action = OmsAction::UnknownFill;
442 ++stats_.unknown_ids;
443 }
444 return u;
445 }
446 Order& o = pool_.get(h);
447 u.prev = o.state;
448 const Qty before = o.cum_qty;
449 // Only the part the position does not already hold moves the order forward: a replayed
450 // execution carries no cumulative quantity of its own.
451 const Qty fresh = m.qty - u.corrected_qty;
452 Qty cum = m.cum_qty.is_positive() ? m.cum_qty : o.cum_qty + fresh;
453 if (cum < before) cum = before; // out-of-order cum: never go backwards
454 if (cum > o.qty) cum = o.qty;
455 open_qty_[o.instrument.value][static_cast<std::size_t>(o.side)] -= (cum - before);
456 o.cum_qty = cum;
457 if (o.venue_order_id.empty()) o.venue_order_id = m.venue_order_id;
458 u.changed = true;
459 if (o.cum_qty >= o.qty) {
460 ++stats_.filled;
461 terminate(u, h, o, OrderState::Filled);
462 return u;
463 }
466 finish_update(u, h, o);
467 return u;
468 }
469
471 OmsUpdate u;
472 Handle<Order> h = lookup(m.cl_ord_id, u);
473 if (!h.valid()) {
474 u.action = OmsAction::Ignored;
475 return u;
476 }
477 Order& o = pool_.get(h);
478 u.prev = o.state;
479 absorb_cum(o, m.cum_qty, u);
480 if (o.cum_qty >= o.qty) { // an IOC that filled completely is Filled, not Expired
481 ++stats_.filled;
482 terminate(u, h, o, OrderState::Filled);
483 return u;
484 }
485 ++stats_.expired;
486 terminate(u, h, o, OrderState::Expired);
487 return u;
488 }
489
490 // ---- reconciliation ---------------------------------------------------------------------
491
492 // Starts reconciling `venue`'s orders (an invalid venue: every venue). `sent_watermark` is the
493 // highest id the venue had sent when it requested the snapshot: orders above it cannot be in the
494 // snapshot, and reconcile_end() leaves them alone. Without it every unreported order ends.
496 std::optional<ClientOrderId> sent_watermark = std::nullopt) noexcept {
497 ReconcileScope& sc = recon_scope_[venue.value];
498 sc.bounded = sent_watermark.has_value();
499 sc.watermark = sent_watermark.value_or(ClientOrderId{});
500 pool_.for_each([&](Handle<Order>, Order& o) {
501 if (in_scope(o, venue)) o.flags &= static_cast<std::uint8_t>(~Order::kSeenInReconcile);
502 });
503 }
504 // Venue reports an open order. Unknown -> CancelUnknown (engine cancels by venue id).
505 OmsUpdate reconcile_open_order(const ReconcileMsg& m) noexcept {
506 OmsUpdate u;
507 Handle<Order> h = lookup(m.cl_ord_id, u);
508 if (!h.valid()) {
509 u.action = OmsAction::CancelUnknown;
510 ++stats_.unknown_ids;
511 return u;
512 }
513 Order& o = pool_.get(h);
514 u.prev = o.state;
516 if (!o.venue_order_id.empty() || !m.venue_order_id.empty()) o.venue_order_id = m.venue_order_id;
517 absorb_cum(o, m.cum_qty, u);
518 // The venue has it, so a new order was accepted. A cancel or replace is left pending: its ack
519 // or reject is still on the way and settles the order (clearing it made that cancel ack look
520 // unsolicited while the quote manager kept the order as a working quote).
521 if (o.state == OrderState::PendingNew) {
523 u.changed = true;
524 }
525 finish_update(u, h, o);
526 return u;
527 }
528 // Every open order of `venue` (invalid: every venue) the snapshot did not report is marked
529 // Canceled, except orders sent after the snapshot was requested (see reconcile_begin).
530 // F(const OmsUpdate&) runs after each order ends and outside the pool iteration, so it may place
531 // or cancel orders.
532 template <class F>
533 void reconcile_end(F&& f, VenueId venue = VenueId::invalid()) noexcept {
534 const ReconcileScope& sc = recon_scope_[venue.value];
535 StaticVector<Handle<Order>, kMaxOpenOrders> unseen;
536 pool_.for_each([&](Handle<Order> h, const Order& o) {
537 if (!in_scope(o, venue) || o.has(Order::kSeenInReconcile)) return;
538 if (sc.bounded && o.cl_ord_id.value > sc.watermark.value) return; // not sent yet
539 static_cast<void>(unseen.push_back(h));
540 });
541 for (const Handle<Order> h : unseen) {
542 if (!pool_.is_live(h)) continue;
543 Order& o = pool_.get(h);
544 OmsUpdate u;
545 u.prev = o.state;
546 u.known = true;
548 // Working quantity nobody accounted for: the snapshot proves the order is over, not how it
549 // ended. Treating it as cancelled is a guess, so report it instead of hiding it.
550 if (o.leaves_qty().is_positive()) {
551 u.unresolved_qty = o.leaves_qty();
552 ++stats_.reconcile_unresolved;
553 }
554 terminate(u, h, o, OrderState::Canceled, /*by_reconcile=*/true);
555 f(u);
556 }
557 }
558
559 // ---- ack timeout ------------------------------------------------------------------------
560
561 // Orders still PendingNew `timeout` after they were submitted: the request or its ack was lost,
562 // and nothing else ever frees them. They hold a pool slot, a max_open_orders slot and same-side
563 // open quantity that feeds max_position, so the engine force-cancels them (the venue may know
564 // the order even though we never saw its ack). F(Handle<Order>, const Order&) runs outside the
565 // pool iteration, so it may send. Returns the number reported.
566 template <class F>
567 std::size_t sweep_pending(Timestamp now, Duration timeout, F&& f) noexcept {
568 if (timeout.ns <= 0) return 0;
569 StaticVector<Handle<Order>, kMaxOpenOrders> stale;
570 pool_.for_each([&](Handle<Order> h, const Order& o) {
571 if (o.state != OrderState::PendingNew || !o.created.valid()) return;
572 if (now - o.created < timeout) return;
573 static_cast<void>(stale.push_back(h));
574 });
575 for (const Handle<Order> h : stale) {
576 if (!pool_.is_live(h)) continue;
577 ++stats_.ack_timeouts;
578 f(h, pool_.get(h));
579 }
580 return stale.size();
581 }
583 // ---- queries ----------------------------------------------------------------------------
585 [[nodiscard]] const Order& get(Handle<Order> h) const noexcept { return pool_.get(h); }
586 [[nodiscard]] Order& get(Handle<Order> h) noexcept { return pool_.get(h); }
587 [[nodiscard]] bool is_live(Handle<Order> h) const noexcept { return pool_.is_live(h); }
588 // Send and ack times of an open order.
589 [[nodiscard]] const OrderTimes& times(Handle<Order> h) const noexcept { return times_[h.idx].t; }
590 [[nodiscard]] Handle<Order> find(ClientOrderId id) const noexcept {
591 const Handle<Order>* p = by_id_.find(id);
592 return p == nullptr ? Handle<Order>{} : *p;
593 }
594 [[nodiscard]] OrderClass classify(ClientOrderId id) const noexcept {
595 if (by_id_.contains(id)) return OrderClass::Open;
596 if (recently_terminal_.find_if([&](const TerminalRecord& r) { return r.cl_ord_id == id; }) !=
597 nullptr) {
599 }
600 return OrderClass::Unknown;
602 // Ascending handle order (deterministic). F(Handle<Order>, const Order&).
603 template <class F>
604 void for_each_open_order(F&& f) const noexcept {
605 pool_.for_each([&](Handle<Order> h, const Order& o) { f(h, o); });
606 }
607 template <class F>
608 void for_each_open_order(InstrumentId id, F&& f) const noexcept {
609 pool_.for_each([&](Handle<Order> h, const Order& o) {
610 if (o.instrument == id) f(h, o);
611 });
612 }
613 void warm_up() noexcept { pool_.warm_up(); }
614
615 private:
616 struct TerminalRecord {
617 ClientOrderId cl_ord_id;
618 Price price;
619 Qty qty;
620 Qty cum_qty;
621 InstrumentId instrument;
622 VenueId venue;
623 OrderState state;
624 Side side;
625 bool by_reconcile; // marked Canceled by reconcile_end(), not reported by the venue
626 };
627 struct ReconcileScope {
628 ClientOrderId watermark{};
629 bool bounded = false;
630 };
631 // Quantity booked at `price` because a venue message reported it as cumulative and no execution
632 // ever named it (Engine::book_missed_fill). It stays until an execution does.
633 struct SyntheticFill {
634 ClientOrderId cl_ord_id;
635 Qty qty;
636 Price price;
637 };
638 struct TimesSlot {
639 OrderTimes t;
640 Timestamp replace_sent; // a replace in flight: its send time
641 };
642
643 [[nodiscard]] static bool in_scope(const Order& o, VenueId venue) noexcept {
644 return !venue.valid() || o.venue == venue;
645 }
646
647 // A venue message carries more cumulative quantity than the fills we booked: the difference
648 // traded while we were not listening. Advance cum_qty (and the open-quantity accounting) and
649 // report the difference so the caller can book it; without that the position, the PnL and the
650 // max-loss budget silently lose those fills.
651 void absorb_cum(Order& o, Qty reported, OmsUpdate& u) noexcept {
652 Qty cum = reported;
653 if (cum > o.qty) cum = o.qty;
654 if (cum <= o.cum_qty) return;
655 const Qty diff = cum - o.cum_qty;
656 open_qty_[o.instrument.value][static_cast<std::size_t>(o.side)] -= diff;
657 o.cum_qty = cum;
658 u.missed_qty += diff;
659 u.changed = true;
660 ++stats_.missed_fills;
661 note_synthetic(o.cl_ord_id, diff, o.price);
662 }
663
664 // ---- synthetic fills waiting to be named ------------------------------------------------
665
666 // The engine is about to book `qty` at `px` without knowing what it really traded at. Remember
667 // it against the order so a later execution-history replay corrects it instead of adding it
668 // again. The pool is empty in normal operation, which is the only cost on_fill pays for this.
669 void note_synthetic(ClientOrderId id, Qty qty, Price px) noexcept {
670 for (SyntheticFill& f : synthetic_) {
671 if (f.cl_ord_id == id) {
672 f.qty += qty;
673 f.price = px;
674 return;
675 }
676 }
677 if (synthetic_.full()) {
678 ++stats_.synthetic_forgotten;
679 synthetic_.erase_front();
680 }
681 static_cast<void>(synthetic_.push_back(SyntheticFill{id, qty, px}));
682 }
683
684 // Takes up to `qty` of the order's outstanding estimate into `u`.
685 void take_synthetic(ClientOrderId id, Qty qty, OmsUpdate& u) noexcept {
686 if (FASTMM_LIKELY(synthetic_.empty())) return;
687 for (std::size_t i = 0; i < synthetic_.size(); ++i) {
688 SyntheticFill& f = synthetic_[i];
689 if (f.cl_ord_id != id) continue;
690 u.corrected_qty = qty < f.qty ? qty : f.qty;
691 u.synthetic_px = f.price;
692 f.qty -= u.corrected_qty;
693 ++stats_.corrected_fills;
694 if (!f.qty.is_positive()) synthetic_.erase_at(i);
695 return;
696 }
697 }
698
699 // A fill for an order that already ended: a cancel ack (or a cancel reject for an order the venue
700 // no longer has) overtook the execution. The terminal record takes the quantity, so a later
701 // duplicate or replay of the order reads the right cum_qty, and an order the fill completes is
702 // Filled, not Canceled. u.changed tells the engine to write the corrected order.
703 void late_fill(const OrderFillMsg& m, OmsUpdate& u) noexcept {
704 TerminalRecord* r = recently_terminal_.find_if(
705 [&](const TerminalRecord& t) { return t.cl_ord_id == m.cl_ord_id; });
706 if (r == nullptr) return;
707 u.prev = r->state;
708 const Qty fresh = m.qty - u.corrected_qty;
709 Qty cum = m.cum_qty.is_positive() ? m.cum_qty : r->cum_qty + fresh;
710 if (cum > r->qty) cum = r->qty;
711 if (cum <= r->cum_qty) return;
712 r->cum_qty = cum;
713 if (cum >= r->qty && (r->state == OrderState::Canceled || r->state == OrderState::Expired)) {
714 r->state = OrderState::Filled;
715 ++stats_.filled;
716 }
717 u.order.cum_qty = r->cum_qty;
718 u.order.state = r->state;
719 u.changed = true;
720 }
721
722 // Finds an open order; sets u.known if the id is open or recently terminal.
723 Handle<Order> lookup(ClientOrderId id, OmsUpdate& u) noexcept {
724 const Handle<Order>* p = by_id_.find(id);
725 if (p != nullptr) {
726 u.known = true;
727 u.handle = *p;
728 return *p;
729 }
730 const TerminalRecord* r =
731 recently_terminal_.find_if([&](const TerminalRecord& t) { return t.cl_ord_id == id; });
732 if (r != nullptr) {
733 // The slot is gone; a late fill still needs to know what it was for.
734 u.known = true;
735 u.order.cl_ord_id = r->cl_ord_id;
736 u.order.instrument = r->instrument;
737 u.order.venue = r->venue;
738 u.order.side = r->side;
739 u.order.state = r->state;
740 u.order.price = r->price;
741 u.order.qty = r->qty;
742 u.order.cum_qty = r->cum_qty;
743 if (r->by_reconcile) u.order.flags |= Order::kReconciled;
744 }
745 return Handle<Order>{};
746 }
747
748 void finish_update(OmsUpdate& u, Handle<Order> h, const Order& o) noexcept {
749 u.order = o;
750 u.handle = h;
751 u.times = times_[h.idx].t;
752 u.slot = h;
753 }
754
755 // `rekey`: the replacement carries a new client id. `new_venue_order`: it is a different order
756 // on the venue (cancel-replace), so its cumulative quantity starts at zero; an in-place amend
757 // rekeys without that.
758 void apply_replace(Order& o, bool rekey, bool new_venue_order) noexcept {
759 const std::size_t s = static_cast<std::size_t>(o.side);
760 Qty& open = open_qty_[o.instrument.value][s];
761 open -= o.leaves_qty();
762 if (rekey) {
763 by_id_.erase(o.cl_ord_id);
764 o.cl_ord_id = o.pending_cl_ord_id;
765 }
766 if (new_venue_order) o.cum_qty = Qty{};
767 const Price old_px = o.price;
768 o.price = o.pending_price;
769 o.qty = o.pending_qty;
770 if (o.cum_qty > o.qty) o.cum_qty = o.qty;
771 open += o.leaves_qty();
772 o.pending_cl_ord_id = ClientOrderId{};
773 o.pending_price = Price{};
774 o.pending_qty = Qty{};
775 o.flags &= static_cast<std::uint8_t>(~Order::kReplaceOldCanceled);
776 o.state = o.cum_qty.is_zero() ? OrderState::Live : OrderState::PartiallyFilled;
777 Price& best = best_own_[o.instrument.value][s];
778 if (best.is_zero() || better(o.side, o.price, best)) {
779 best = o.price;
780 } else if (old_px == best) {
781 recompute_best_own(o.instrument, o.side);
782 }
783 }
784 void clear_pending(Order& o) noexcept {
785 if (o.pending_cl_ord_id.valid() && o.pending_cl_ord_id != o.cl_ord_id)
786 by_id_.erase(o.pending_cl_ord_id);
787 o.pending_cl_ord_id = ClientOrderId{};
788 o.pending_price = Price{};
789 o.pending_qty = Qty{};
790 o.flags &= static_cast<std::uint8_t>(~Order::kReplaceOldCanceled);
791 }
792
793 void terminate(OmsUpdate& u,
794 Handle<Order> h,
795 Order& o,
796 OrderState final_state,
797 bool by_reconcile = false) noexcept {
798 o.state = final_state;
799 if (final_state == OrderState::Canceled) ++stats_.canceled;
800 --stats_.open;
801 --open_per_inst_[o.instrument.value];
802 const std::size_t s = static_cast<std::size_t>(o.side);
803 open_qty_[o.instrument.value][s] -= o.leaves_qty();
804 by_id_.erase(o.cl_ord_id);
805 if (o.pending_cl_ord_id.valid() && o.pending_cl_ord_id != o.cl_ord_id)
806 by_id_.erase(o.pending_cl_ord_id);
807 recently_terminal_.push(TerminalRecord{o.cl_ord_id,
808 o.price,
809 o.qty,
810 o.cum_qty,
811 o.instrument,
812 o.venue,
813 final_state,
814 o.side,
815 by_reconcile});
816 u.order = o;
817 u.handle = Handle<Order>{};
818 u.times = times_[h.idx].t;
819 u.slot = h;
820 u.changed = true;
821 u.terminal = true;
822 const InstrumentId inst = o.instrument;
823 const Side side = o.side;
824 const Price px = o.price;
825 // Swap-remove from the dense list of live orders.
826 const std::uint32_t pos = live_pos_[h.idx];
827 const std::uint32_t last = live_.back();
828 live_[pos] = last;
829 live_pos_[last] = pos;
830 live_.pop_back();
831 pool_.free(h);
832 if (px == best_own_[inst.value][s]) recompute_best_own(inst, side);
833 }
834
835 // Scans the live orders (live_), not the kMaxOpenOrders-slot pool.
836 void recompute_best_own(InstrumentId inst, Side side) noexcept {
837 Price best{};
838 for (const std::uint32_t idx : live_) {
839 const Order& o = pool_.get(Handle<Order>{idx});
840 if (o.instrument != inst || o.side != side) continue;
841 if (best.is_zero() || better(side, o.price, best)) best = o.price;
842 }
843 best_own_[inst.value][static_cast<std::size_t>(side)] = best;
844 }
845
846 // An execution is what the venue says it is: its id on that instrument and side (the two halves
847 // of a self-trade share an id). Not the order it names: the private stream names it and a
848 // replayed trade history may not (a venue order id the connector no longer maps), and keying on
849 // the order booked such an execution twice.
850 bool remember_exec(const OrderFillMsg& m) noexcept {
851 const std::uint64_t key = m.exec_id.hash() ^
852 (static_cast<std::uint64_t>(m.hdr.instrument.value) << 1U) ^
853 (static_cast<std::uint64_t>(m.side) * 0x9E3779B97F4A7C15ULL);
854 return exec_seen_.assign(key, 1);
855 }
856
857 std::uint16_t epoch_;
858 std::uint64_t max_seq_;
859 std::uint64_t seq_ = 0; // 64-bit so the comparison with max_seq_ cannot itself wrap
860 OmsStats stats_{};
861 Pool<Order, kMaxOpenOrders> pool_;
862 // Pool slots of the live orders in no particular order, and each live slot's position in it.
863 // Only order-independent scans use it (recompute_best_own); iteration that produces messages
864 // keeps the pool's slot order.
865 StaticVector<std::uint32_t, kMaxOpenOrders> live_;
866 std::uint32_t live_pos_[kMaxOpenOrders] = {};
867 OpenHashMap<ClientOrderId, Handle<Order>, kMaxOpenOrders * 4> by_id_; // ids + pending ids
868 RingBuffer<TerminalRecord, kRecentlyTerminal> recently_terminal_;
869 RecentMap<std::uint64_t, std::uint8_t, kRecentlyTerminal> exec_seen_;
870 Price best_own_[kMaxInstruments][2];
871 Qty open_qty_[kMaxInstruments][2];
872 std::uint32_t open_per_inst_[kMaxInstruments] = {};
873 ReconcileScope recon_scope_[std::numeric_limits<VenueId::rep_type>::max() + 1U] = {};
874 StaticVector<SyntheticFill, kMaxSyntheticFills> synthetic_;
875 // OrderTimes by pool slot, apart from Order so the order record keeps its 128-byte layout.
876 std::unique_ptr<TimesSlot[]> times_ = std::make_unique<TimesSlot[]>(kMaxOpenOrders);
877};
878
879} // namespace fastmm
BookView: the read interface every book implementation (L2Book, L3Book::to_l2 view,...
Definition oms.hpp:137
void reconcile_begin(VenueId venue=VenueId::invalid(), std::optional< ClientOrderId > sent_watermark=std::nullopt) noexcept
Starts reconciling venue's orders (an invalid venue: every venue).
Definition oms.hpp:494
Result< void, RejectReason > request_cancel_unacked(Handle< Order > h) noexcept
Cancel of an order that is still waiting for its ack (the ack-timeout sweep only).
Definition oms.hpp:223
Result< void, RejectReason > request_cancel(Handle< Order > h) noexcept
Definition oms.hpp:213
const OrderTimes & times(Handle< Order > h) const noexcept
Send and ack times of an open order.
Definition oms.hpp:586
std::size_t sweep_pending(Timestamp now, Duration timeout, F &&f) noexcept
Orders still PendingNew timeout after they were submitted: the request or its ack was lost,...
Definition oms.hpp:565
OrderClass classify(ClientOrderId id) const noexcept
Definition oms.hpp:591
std::uint64_t ids_left() const noexcept
Client order ids left in this session.
Definition oms.hpp:158
Result< Handle< Order >, RejectReason > submit(const NewOrderRequest &req, ClientOrderId id, Timestamp now) noexcept
Definition oms.hpp:174
Result< void, RejectReason > request_replace(Handle< Order > h, ClientOrderId new_id, Price px, Qty qty, Timestamp now={}) noexcept
new_id may equal the current id for venues that amend in place.
Definition oms.hpp:233
OmsUpdate on_ack(const OrderAckMsg &m) noexcept
Definition oms.hpp:251
Price best_own_px(InstrumentId id, Side s) const noexcept
Best price among our own open orders on (instrument, side); Price{} if none. For STP.
Definition oms.hpp:169
void reconcile_end(F &&f, VenueId venue=VenueId::invalid()) noexcept
Every open order of venue (invalid: every venue) the snapshot did not report is marked Canceled,...
Definition oms.hpp:532
Qty open_qty(InstrumentId id, Side s) const noexcept
Sum of leaves of open orders on (instrument, side): "in-flight same-side exposure".
Definition oms.hpp:165
OmsUpdate on_expired(const OrderExpiredMsg &m) noexcept
Definition oms.hpp:470
OmsUpdate on_reject(const OrderRejectMsg &m) noexcept
Definition oms.hpp:311
Oms & operator=(const Oms &)=delete
OmsUpdate on_fill(const OrderFillMsg &m) noexcept
Definition oms.hpp:402
bool is_live(Handle< Order > h) const noexcept
Definition oms.hpp:584
std::uint16_t session_epoch() const noexcept
Definition oms.hpp:149
ClientOrderId next_cl_ord_id() noexcept
An invalid id once the session's 32-bit sequence is used up: the wire form carries 48 bits (epoch 16 ...
Definition oms.hpp:153
OmsUpdate on_cancel_ack(const OrderCancelAckMsg &m) noexcept
Definition oms.hpp:345
void warm_up() noexcept
Definition oms.hpp:610
OmsUpdate on_cancel_reject(const OrderCancelRejectMsg &m) noexcept
Definition oms.hpp:375
OmsUpdate reconcile_open_order(const ReconcileMsg &m) noexcept
Venue reports an open order. Unknown -> CancelUnknown (engine cancels by venue id).
Definition oms.hpp:504
Handle< Order > find(ClientOrderId id) const noexcept
Definition oms.hpp:587
const OmsStats & stats() const noexcept
Definition oms.hpp:159
void for_each_open_order(F &&f) const noexcept
Ascending handle order (deterministic). F(Handle<Order>, const Order&).
Definition oms.hpp:601
std::uint32_t open_count() const noexcept
Definition oms.hpp:160
const Order & get(Handle< Order > h) const noexcept
Definition oms.hpp:582
Definition result.hpp:30
Minimal expected<T, E>: the hot path never throws, it returns Result.
All engine enumerations.
Instrument: the single definition of a tradable contract (5.3 / 5.15).
Event bus message structs (5.5).
Definition strategy.hpp:16
StrongId< struct ClientOrderTag, std::uint64_t, 0 > ClientOrderId
48-bit: session_epoch(16) << 32 | seq(32). 0 is never issued.
Definition strong_id.hpp:41
constexpr std::size_t kMaxSyntheticFills
Fills booked from a cumulative quantity and never named by an execution.
Definition oms.hpp:59
OrderState
Definition enums.hpp:79
constexpr ClientOrderId make_cl_ord_id(std::uint16_t epoch, std::uint32_t seq) noexcept
Definition strong_id.hpp:103
RejectReason
Ordered exactly like RiskEngine::check_new evaluates them (5.7), followed by OMS / transport / venue ...
Definition enums.hpp:123
@ PoolExhausted
OMS / transport.
OrderClass
Definition oms.hpp:135
Side
Definition enums.hpp:12
Fixed< struct QtyTag > Qty
Definition fixed_point.hpp:333
constexpr std::size_t kMaxOpenOrders
Definition oms.hpp:54
constexpr Unexpected< std::decay_t< E > > fail(E &&e) noexcept
Definition result.hpp:25
constexpr std::uint64_t kMaxSeq
Client order ids per session: the wire form is 48 bits, epoch(16) << 32 | seq(32).
Definition oms.hpp:61
StrongId< struct InstrumentTag, std::uint32_t > InstrumentId
Dense index into the engine's instrument table.
Definition strong_id.hpp:38
constexpr std::size_t kRecentlyTerminal
Definition oms.hpp:55
Fixed< struct PriceTag > Price
Definition fixed_point.hpp:332
constexpr std::size_t kMaxInstruments
Definition instrument.hpp:26
constexpr bool better(Side s, Price p, Price q) noexcept
Definition book_view.hpp:48
OmsAction
Definition oms.hpp:63
@ LateFill
fill for a recently terminal order: position, and the terminal record's cum_qty and state (changed = ...
@ CancelUnknown
ack/fill for an id we do not know: engine must cancel it
@ UnknownFill
fill for a completely unknown id: update position, alert
@ Ignored
message did not apply (duplicate ack, late cancel-reject, ...)
@ ReconcileNeeded
too many cancel rejects
@ Duplicate
duplicate exec_id
StrongId< struct VenueTag, std::uint8_t > VenueId
Definition strong_id.hpp:39
Order: the OMS's 128-byte record for one order across its lifetime.
Definition time.hpp:22
constexpr bool is_zero() const noexcept
Definition fixed_point.hpp:301
constexpr bool is_positive() const noexcept
Definition fixed_point.hpp:302
Definition strong_id.hpp:54
constexpr bool valid() const noexcept
Definition strong_id.hpp:58
std::uint32_t idx
Definition strong_id.hpp:55
Definition order.hpp:66
Definition oms.hpp:110
std::uint64_t late_fills
Definition oms.hpp:121
std::uint64_t corrected_fills
Missed fills a replayed execution later named, so their price and fee stopped being estimates.
Definition oms.hpp:125
std::uint32_t open
Definition oms.hpp:111
std::uint64_t duplicates
Definition oms.hpp:120
std::uint64_t synthetic_forgotten
Missed fills whose entry was evicted before an execution named them: their price and fee stay estimat...
Definition oms.hpp:128
std::uint64_t reconcile_unresolved
Orders a reconciliation snapshot did not report that still had working quantity: the venue ended them...
Definition oms.hpp:132
std::uint64_t unsolicited_cancels
Definition oms.hpp:122
std::uint64_t unknown_ids
Definition oms.hpp:119
std::uint64_t filled
Definition oms.hpp:116
std::uint64_t missed_fills
cum_qty jumps a fill message never reported
Definition oms.hpp:123
std::uint64_t rejected
Definition oms.hpp:114
std::uint64_t canceled
Definition oms.hpp:115
std::uint64_t fills
Definition oms.hpp:117
std::uint64_t expired
Definition oms.hpp:118
std::uint64_t ack_timeouts
PendingNew orders swept by sweep_pending()
Definition oms.hpp:129
std::uint64_t submitted
Definition oms.hpp:112
std::uint64_t acked
Definition oms.hpp:113
Definition oms.hpp:74
OrderTimes times
The order's times (Oms::times), and the pool slot it had: set on every update of an open order,...
Definition oms.hpp:106
Price fill_px
Definition oms.hpp:85
bool known
the id mapped to an order (open or recently terminal)
Definition oms.hpp:81
Price synthetic_px
Definition oms.hpp:103
Qty corrected_qty
Part of a replayed execution (OrderFillMsg::kReplayed) whose quantity a synthetic fill already put in...
Definition oms.hpp:102
Qty unresolved_qty
Quantity that was still working when a reconciliation snapshot failed to mention the order.
Definition oms.hpp:97
Qty fill_qty
for fills
Definition oms.hpp:84
Handle< Order > slot
Definition oms.hpp:107
Handle< Order > handle
invalid once terminal (slot freed) or unknown
Definition oms.hpp:78
Order order
snapshot after the transition; for a recently terminal id only the terminal record's fields (id,...
Definition oms.hpp:75
bool terminal
the order reached a terminal state in this update
Definition oms.hpp:83
OrderState prev
Definition oms.hpp:79
bool changed
state or quantities changed
Definition oms.hpp:82
ClientOrderId replaced_cl_ord_id
A cancel-replace to a new id completed: the order carries on under order.cl_ord_id and this one is go...
Definition oms.hpp:93
Qty missed_qty
Quantity the venue reports as filled that we never saw a fill message for (a private-stream outage co...
Definition oms.hpp:89
OmsAction action
Definition oms.hpp:80
Definition messages.hpp:188
static constexpr std::uint8_t kAmendedInPlace
The venue amended the resting order in place (Binance Spot order.amend.keepPriority,...
Definition messages.hpp:192
Definition messages.hpp:213
Definition messages.hpp:222
Definition messages.hpp:261
Definition messages.hpp:233
static constexpr std::uint8_t kReplayed
The execution comes from the venue's trade history (Venue::request_executions), not from the private ...
Definition messages.hpp:237
Definition messages.hpp:202
When the order's working leg (the order, or its latest replacement once acknowledged) was sent and ac...
Definition order.hpp:58
Definition order.hpp:15
OrderState state
1
Definition order.hpp:34
TimeInForce tif
1 -> 64
Definition order.hpp:33
VenueOrderId venue_order_id
41 -> 49
Definition order.hpp:27
ClientOrderId cl_ord_id
8
Definition order.hpp:26
Qty qty
8 -> 88
Definition order.hpp:40
constexpr Qty leaves_qty() const noexcept
Definition order.hpp:47
std::uint8_t flags
1
Definition order.hpp:36
Price pending_price
8 -> 120
Definition order.hpp:44
constexpr bool has(Flags f) const noexcept
Definition order.hpp:52
std::uint8_t cancel_attempts
1
Definition order.hpp:37
ClientOrderId pending_cl_ord_id
8 -> 112 replace: id of the replacement order
Definition order.hpp:43
Timestamp created
8 -> 104
Definition order.hpp:42
VenueId venue
1
Definition order.hpp:30
constexpr bool is_working() const noexcept
Definition order.hpp:49
Qty pending_qty
8 -> 128
Definition order.hpp:45
InstrumentId instrument
4 -> 60
Definition order.hpp:29
Side side
1
Definition order.hpp:31
std::uint32_t user_tag
4 -> 72 (QuoteManager slot id etc.)
Definition order.hpp:38
RejectReason reject_reason
1
Definition order.hpp:35
Qty cum_qty
8 -> 96
Definition order.hpp:41
OrderType type
1
Definition order.hpp:32
Price price
8 -> 80
Definition order.hpp:39
@ kReplaceOldCanceled
cancel-then-new replace: old leg already cancelled
Definition order.hpp:23
@ kReconciled
state fixed up by reconciliation
Definition order.hpp:20
@ kReduceOnly
Definition order.hpp:18
@ kPostOnly
Definition order.hpp:17
@ kSeenInReconcile
scratch flag during a reconcile pass
Definition order.hpp:21
@ kUnsolicitedCancel
venue cancelled it (not requested by us)
Definition order.hpp:19
Control thread injects the venue's view after a reconnect (5.8).
Definition messages.hpp:469
static constexpr StrongId invalid() noexcept
Definition strong_id.hpp:30
Rep value
Definition strong_id.hpp:26
Definition time.hpp:45
constexpr bool valid() const noexcept
Definition time.hpp:55