1use std::collections::HashMap as StdHashMap;
18use std::num::NonZeroU64;
19
20use crate::account::{AccountManager, Balance};
21use crate::exchange::Exchange;
22use crate::orderbook::OrderBook;
23use crate::scheduler::{ScheduledTask, ScheduledTaskHeap, ScheduledTaskKind};
24use crate::types::{
25 AccountId, CircuitBreakerConfig, CurrencyId, FeeSchedule, InstrumentSpec, OrderId, Price,
26 Quantity, ReservationSlot, RiskLimits, Side, Symbol, TimeInForce,
27};
28
29use crate::le;
30
31#[derive(Debug)]
36pub enum SnapshotDecodeError {
37 Truncated,
39 Corrupt { reason: &'static str },
43}
44
45impl std::fmt::Display for SnapshotDecodeError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::Truncated => write!(f, "truncated snapshot payload"),
49 Self::Corrupt { reason } => write!(f, "corrupt snapshot payload: {reason}"),
50 }
51 }
52}
53
54impl std::error::Error for SnapshotDecodeError {}
55
56type RestingLevels = Vec<(Price, Vec<RestingOrderSnapshot>)>;
58
59type StopLevels = Vec<(Price, Vec<PendingStopSnapshot>)>;
61
62pub const PAYLOAD_VERSION: u16 = 18;
102
103pub fn encode_exchange_payload(exchange: &Exchange) -> Vec<u8> {
107 let state = exchange.snapshot_state();
108 let mut buf = Vec::with_capacity(64 * 1024);
112 encode_exchange_state(&state, &mut buf);
113 buf
114}
115
116pub fn decode_exchange_payload(buf: &[u8]) -> Result<Exchange, SnapshotDecodeError> {
122 let (_consumed, state) = decode_exchange_state(buf, PAYLOAD_VERSION)?;
123 Ok(Exchange::restore_state(state))
124}
125
126#[derive(Debug)]
131pub(crate) struct ExchangeSnapshot {
132 pub(crate) instruments: Vec<InstrumentSpec>,
133 pub(crate) balances: Vec<((AccountId, CurrencyId), Balance)>,
134 pub(crate) reservations: Vec<(OrderId, AccountId, CurrencyId, u64)>,
135 pub(crate) order_sides: Vec<((AccountId, OrderId), Side)>,
136 pub(crate) books: Vec<(Symbol, BookSnapshot)>,
137 pub(crate) risk_limits: Vec<(Symbol, RiskLimits)>,
139 pub(crate) circuit_breakers: Vec<(Symbol, CircuitBreakerConfig)>,
141 pub(crate) fee_schedules: Vec<(Symbol, FeeSchedule)>,
143 pub(crate) key_hwm: Vec<(u64, u64)>,
145 pub(crate) disabled_instruments: Vec<Symbol>,
147 pub(crate) fee_account_deficits: Vec<(CurrencyId, u64)>,
152 pub(crate) order_buckets: Vec<(AccountId, u64, u64)>,
159}
160
161#[derive(Debug)]
164pub(crate) struct BookSnapshot {
165 pub(crate) bids: Vec<(Price, Vec<RestingOrderSnapshot>)>,
166 pub(crate) asks: Vec<(Price, Vec<RestingOrderSnapshot>)>,
167 pub(crate) order_index: Vec<(OrderId, AccountId, Side, Price)>,
168 pub(crate) stop_buys: Vec<(Price, Vec<PendingStopSnapshot>)>,
169 pub(crate) stop_sells: Vec<(Price, Vec<PendingStopSnapshot>)>,
170 pub(crate) stop_index: Vec<(OrderId, AccountId, Side, Price)>,
171 pub(crate) last_trade_price: Option<Price>,
172}
173
174#[derive(Debug)]
176pub(crate) struct RestingOrderSnapshot {
177 pub(crate) id: OrderId,
178 pub(crate) account: AccountId,
179 pub(crate) remaining: Quantity,
180 pub(crate) time_in_force: TimeInForce,
181 pub(crate) expiry_ns: u64,
182}
183
184#[derive(Debug)]
186pub(crate) struct PendingStopSnapshot {
187 pub(crate) id: OrderId,
188 pub(crate) account: AccountId,
189 pub(crate) side: Side,
190 pub(crate) trigger_price: Price,
191 pub(crate) quantity: Quantity,
192 pub(crate) time_in_force: crate::types::TimeInForce,
193 pub(crate) limit_price: Option<Price>,
194 pub(crate) quote_budget: Option<u64>,
196 pub(crate) stp: crate::types::SelfTradeProtection,
198 pub(crate) expiry_ns: u64,
200}
201
202fn encode_opt_nz_u64(buf: &mut Vec<u8>, v: Option<NonZeroU64>) {
207 match v {
208 Some(n) => {
209 buf.push(1);
210 le::push_u64(buf, n.get());
211 }
212 None => buf.push(0),
213 }
214}
215
216fn encode_instruments(buf: &mut Vec<u8>, instruments: &[InstrumentSpec]) {
221 le::push_u32(buf, instruments.len() as u32);
222 for spec in instruments {
223 le::push_u32(buf, spec.symbol.0);
224 le::push_u32(buf, spec.base.0);
225 le::push_u32(buf, spec.quote.0);
226 }
227}
228
229fn encode_balances(buf: &mut Vec<u8>, balances: &[BalanceEntry]) {
230 le::push_u32(buf, balances.len() as u32);
231 for ((account, currency), balance) in balances {
232 le::push_u32(buf, account.0);
233 le::push_u32(buf, currency.0);
234 le::push_u64(buf, balance.available);
235 le::push_u64(buf, balance.reserved);
236 }
237}
238
239fn encode_reservations(buf: &mut Vec<u8>, reservations: &[ReservationEntry]) {
240 le::push_u32(buf, reservations.len() as u32);
241 for (order_id, account, currency, remaining) in reservations {
242 le::push_u64(buf, order_id.0);
243 le::push_u32(buf, account.0);
244 le::push_u32(buf, currency.0);
245 le::push_u64(buf, *remaining);
246 }
247}
248
249fn encode_order_sides(buf: &mut Vec<u8>, order_sides: &[OrderSideEntry]) {
251 le::push_u32(buf, order_sides.len() as u32);
252 for ((account, order_id), side) in order_sides {
253 le::push_u32(buf, account.0);
254 le::push_u64(buf, order_id.0);
255 buf.push(le::encode_side(*side));
256 }
257}
258
259fn encode_books(buf: &mut Vec<u8>, books: &[(Symbol, BookSnapshot)]) {
260 le::push_u32(buf, books.len() as u32);
261 for (symbol, book) in books {
262 le::push_u32(buf, symbol.0);
263 encode_book_snapshot(book, buf);
264 }
265}
266
267fn encode_risk_limits(buf: &mut Vec<u8>, risk_limits: &[(Symbol, RiskLimits)]) {
268 le::push_u32(buf, risk_limits.len() as u32);
269 for (symbol, limits) in risk_limits {
270 le::push_u32(buf, symbol.0);
271 encode_opt_nz_u64(buf, limits.max_order_qty.map(|q| q.0));
272 match limits.max_order_notional {
273 Some(notional) => {
274 buf.push(1);
275 le::push_u64(buf, notional);
276 }
277 None => buf.push(0),
278 }
279 }
280}
281
282fn encode_circuit_breakers(buf: &mut Vec<u8>, circuit_breakers: &[(Symbol, CircuitBreakerConfig)]) {
283 le::push_u32(buf, circuit_breakers.len() as u32);
284 for (symbol, config) in circuit_breakers {
285 le::push_u32(buf, symbol.0);
286 encode_opt_nz_u64(buf, config.price_band_lower.map(|p| p.0));
287 encode_opt_nz_u64(buf, config.price_band_upper.map(|p| p.0));
288 buf.push(u8::from(config.halted));
289 }
290}
291
292fn encode_fee_schedules(buf: &mut Vec<u8>, fee_schedules: &[(Symbol, FeeSchedule)]) {
293 le::push_u32(buf, fee_schedules.len() as u32);
294 for (symbol, schedule) in fee_schedules {
295 le::push_u32(buf, symbol.0);
296 le::push_i16(buf, schedule.maker_fee_bps);
297 le::push_i16(buf, schedule.taker_fee_bps);
298 }
299}
300
301fn encode_key_hwm(buf: &mut Vec<u8>, key_hwm: &[(u64, u64)]) {
302 le::push_u32(buf, key_hwm.len() as u32);
303 for (key_hash, hwm) in key_hwm {
304 le::push_u64(buf, *key_hash);
305 le::push_u64(buf, *hwm);
306 }
307}
308
309fn encode_disabled_instruments(buf: &mut Vec<u8>, disabled: &[Symbol]) {
310 le::push_u32(buf, disabled.len() as u32);
311 for symbol in disabled {
312 le::push_u32(buf, symbol.0);
313 }
314}
315
316fn encode_fee_account_deficits(buf: &mut Vec<u8>, deficits: &[(CurrencyId, u64)]) {
317 le::push_u32(buf, deficits.len() as u32);
318 for (currency, amount) in deficits {
319 le::push_u32(buf, currency.0);
320 le::push_u64(buf, *amount);
321 }
322}
323
324fn encode_order_buckets(buf: &mut Vec<u8>, buckets: &[OrderBucketEntry]) {
327 le::push_u32(buf, buckets.len() as u32);
328 for (account, tokens, last_refill_ns) in buckets {
329 le::push_u32(buf, account.0);
330 le::push_u64(buf, *tokens);
331 le::push_u64(buf, *last_refill_ns);
332 }
333}
334
335fn encode_exchange_state(state: &ExchangeSnapshot, buf: &mut Vec<u8>) {
336 let ExchangeSnapshot {
341 instruments,
342 balances,
343 reservations,
344 order_sides,
345 books,
346 risk_limits,
347 circuit_breakers,
348 fee_schedules,
349 key_hwm,
350 disabled_instruments,
351 fee_account_deficits,
352 order_buckets,
353 } = state;
354 encode_instruments(buf, instruments);
355 encode_balances(buf, balances);
356 encode_reservations(buf, reservations);
357 encode_order_sides(buf, order_sides);
358 encode_books(buf, books);
359 encode_risk_limits(buf, risk_limits);
360 encode_circuit_breakers(buf, circuit_breakers);
361 encode_fee_schedules(buf, fee_schedules);
362 encode_key_hwm(buf, key_hwm);
363 encode_disabled_instruments(buf, disabled_instruments);
364 encode_fee_account_deficits(buf, fee_account_deficits);
365 encode_order_buckets(buf, order_buckets);
366}
367
368fn encode_book_snapshot(book: &BookSnapshot, buf: &mut Vec<u8>) {
369 encode_book_side(&book.bids, buf);
371 encode_book_side(&book.asks, buf);
373
374 le::push_u32(buf, book.order_index.len() as u32);
376 for (order_id, account, side, price) in &book.order_index {
377 le::push_u64(buf, order_id.0);
378 le::push_u32(buf, account.0);
379 buf.push(le::encode_side(*side));
380 le::push_u64(buf, price.get());
381 }
382
383 encode_stop_side(&book.stop_buys, buf);
385 encode_stop_side(&book.stop_sells, buf);
387
388 le::push_u32(buf, book.stop_index.len() as u32);
390 for (order_id, account, side, price) in &book.stop_index {
391 le::push_u64(buf, order_id.0);
392 le::push_u32(buf, account.0);
393 buf.push(le::encode_side(*side));
394 le::push_u64(buf, price.get());
395 }
396
397 match book.last_trade_price {
399 Some(p) => {
400 buf.push(1);
401 le::push_u64(buf, p.get());
402 }
403 None => buf.push(0),
404 }
405}
406
407fn encode_book_side(levels: &[(Price, Vec<RestingOrderSnapshot>)], buf: &mut Vec<u8>) {
408 le::push_u32(buf, levels.len() as u32);
409 for (price, orders) in levels {
410 le::push_u64(buf, price.get());
411 le::push_u32(buf, orders.len() as u32);
412 for order in orders {
413 le::push_u64(buf, order.id.0);
414 le::push_u32(buf, order.account.0);
415 le::push_u64(buf, order.remaining.get());
416 buf.push(le::encode_tif(order.time_in_force));
417 le::push_u64(buf, order.expiry_ns);
419 }
420 }
421}
422
423fn encode_stop_side(levels: &[(Price, Vec<PendingStopSnapshot>)], buf: &mut Vec<u8>) {
424 le::push_u32(buf, levels.len() as u32);
425 for (trigger_price, stops) in levels {
426 le::push_u64(buf, trigger_price.get());
427 le::push_u32(buf, stops.len() as u32);
428 for stop in stops {
429 le::push_u64(buf, stop.id.0);
430 le::push_u32(buf, stop.account.0);
431 buf.push(le::encode_side(stop.side));
432 le::push_u64(buf, stop.trigger_price.get());
433 le::push_u64(buf, stop.quantity.get());
434 buf.push(le::encode_tif(stop.time_in_force));
435 match stop.limit_price {
436 Some(p) => {
437 buf.push(1);
438 le::push_u64(buf, p.get());
439 }
440 None => buf.push(0),
441 }
442 match stop.quote_budget {
443 Some(budget) => {
444 buf.push(1);
445 le::push_u64(buf, budget);
446 }
447 None => buf.push(0),
448 }
449 buf.push(le::encode_stp(stop.stp));
450 le::push_u64(buf, stop.expiry_ns);
452 }
453 }
454}
455
456fn validate_count(remaining: usize, n: usize, item_size: usize) -> Result<(), SnapshotDecodeError> {
462 let needed = n.saturating_mul(item_size);
463 if needed > remaining {
464 Err(SnapshotDecodeError::Corrupt {
465 reason: "count exceeds remaining buffer",
466 })
467 } else {
468 Ok(())
469 }
470}
471
472type BalanceEntry = ((AccountId, CurrencyId), Balance);
476type ReservationEntry = (OrderId, AccountId, CurrencyId, u64);
477type OrderSideEntry = ((AccountId, OrderId), Side);
478type OrderBucketEntry = (AccountId, u64, u64);
479
480fn corrupt(reason: &'static str) -> SnapshotDecodeError {
482 SnapshotDecodeError::Corrupt { reason }
483}
484
485fn check(buf: &[u8], pos: usize, need: usize) -> Result<(), SnapshotDecodeError> {
487 if pos + need > buf.len() {
488 Err(SnapshotDecodeError::Truncated)
489 } else {
490 Ok(())
491 }
492}
493
494fn read_section_len(buf: &[u8]) -> Result<usize, SnapshotDecodeError> {
498 check(buf, 0, 4)?;
499 Ok(le::get_u32(buf) as usize)
500}
501
502fn decode_instruments(buf: &[u8]) -> Result<(usize, Vec<InstrumentSpec>), SnapshotDecodeError> {
503 let n = read_section_len(buf)?;
504 let mut pos = 4;
505 validate_count(buf.len() - pos, n, 12)?;
506 let mut out = Vec::with_capacity(n);
507 for _ in 0..n {
508 check(buf, pos, 12)?;
509 out.push(InstrumentSpec {
510 symbol: Symbol(le::get_u32(&buf[pos..])),
511 base: CurrencyId(le::get_u32(&buf[pos + 4..])),
512 quote: CurrencyId(le::get_u32(&buf[pos + 8..])),
513 });
514 pos += 12;
515 }
516 Ok((pos, out))
517}
518
519fn decode_balances(buf: &[u8]) -> Result<(usize, Vec<BalanceEntry>), SnapshotDecodeError> {
520 let n = read_section_len(buf)?;
521 let mut pos = 4;
522 validate_count(buf.len() - pos, n, 24)?;
523 let mut out = Vec::with_capacity(n);
524 for _ in 0..n {
525 check(buf, pos, 24)?;
526 let account = AccountId(le::get_u32(&buf[pos..]));
527 let currency = CurrencyId(le::get_u32(&buf[pos + 4..]));
528 let available = le::get_u64(&buf[pos + 8..]);
529 let reserved = le::get_u64(&buf[pos + 16..]);
530 out.push((
531 (account, currency),
532 Balance {
533 available,
534 reserved,
535 },
536 ));
537 pos += 24;
538 }
539 Ok((pos, out))
540}
541
542fn decode_reservations(buf: &[u8]) -> Result<(usize, Vec<ReservationEntry>), SnapshotDecodeError> {
543 let n = read_section_len(buf)?;
544 let mut pos = 4;
545 validate_count(buf.len() - pos, n, 24)?;
546 let mut out = Vec::with_capacity(n);
547 for _ in 0..n {
548 check(buf, pos, 24)?;
549 let order_id = OrderId(le::get_u64(&buf[pos..]));
550 let account = AccountId(le::get_u32(&buf[pos + 8..]));
551 let currency = CurrencyId(le::get_u32(&buf[pos + 12..]));
552 let remaining = le::get_u64(&buf[pos + 16..]);
553 out.push((order_id, account, currency, remaining));
554 pos += 24;
555 }
556 Ok((pos, out))
557}
558
559fn decode_order_sides(
564 buf: &[u8],
565 version: u16,
566) -> Result<(usize, Vec<OrderSideEntry>), SnapshotDecodeError> {
567 let n = read_section_len(buf)?;
568 let mut pos = 4;
569 let mut out = Vec::with_capacity(n);
570 if version >= 7 {
571 validate_count(buf.len() - pos, n, 13)?;
572 for _ in 0..n {
573 check(buf, pos, 13)?;
574 let account = AccountId(le::get_u32(&buf[pos..]));
575 let order_id = OrderId(le::get_u64(&buf[pos + 4..]));
576 let side = le::decode_side(buf[pos + 12]).ok_or(corrupt("invalid side in snapshot"))?;
577 out.push(((account, order_id), side));
578 pos += 13;
579 }
580 } else {
581 validate_count(buf.len() - pos, n, 9)?;
582 for _ in 0..n {
583 check(buf, pos, 9)?;
584 let order_id = OrderId(le::get_u64(&buf[pos..]));
585 let side = le::decode_side(buf[pos + 8]).ok_or(corrupt("invalid side in snapshot"))?;
586 out.push(((AccountId(0), order_id), side));
587 pos += 9;
588 }
589 }
590 Ok((pos, out))
591}
592
593fn decode_books(
594 buf: &[u8],
595 version: u16,
596) -> Result<(usize, Vec<(Symbol, BookSnapshot)>), SnapshotDecodeError> {
597 let n = read_section_len(buf)?;
598 let mut pos = 4;
599 validate_count(buf.len() - pos, n, 4)?;
601 let mut out = Vec::with_capacity(n);
602 for _ in 0..n {
603 check(buf, pos, 4)?;
604 let symbol = Symbol(le::get_u32(&buf[pos..]));
605 pos += 4;
606 let (consumed, book) = decode_book_snapshot(&buf[pos..], version)?;
607 pos += consumed;
608 out.push((symbol, book));
609 }
610 Ok((pos, out))
611}
612
613fn decode_opt_nz_u64(
616 buf: &[u8],
617 mut pos: usize,
618 invalid_tag_reason: &'static str,
619 zero_value_reason: &'static str,
620) -> Result<(usize, Option<NonZeroU64>), SnapshotDecodeError> {
621 check(buf, pos, 1)?;
622 match buf[pos] {
623 1 => {
624 pos += 1;
625 check(buf, pos, 8)?;
626 let v = NonZeroU64::new(le::get_u64(&buf[pos..])).ok_or(corrupt(zero_value_reason))?;
627 pos += 8;
628 Ok((pos, Some(v)))
629 }
630 0 => Ok((pos + 1, None)),
631 _ => Err(corrupt(invalid_tag_reason)),
632 }
633}
634
635fn decode_risk_limits(
636 buf: &[u8],
637) -> Result<(usize, Vec<(Symbol, RiskLimits)>), SnapshotDecodeError> {
638 let n = read_section_len(buf)?;
639 let mut pos = 4;
640 validate_count(buf.len() - pos, n, 6)?;
642 let mut out = Vec::with_capacity(n);
643 for _ in 0..n {
644 check(buf, pos, 6)?;
645 let symbol = Symbol(le::get_u32(&buf[pos..]));
646 pos += 4;
647 let (new_pos, max_order_qty) = decode_opt_nz_u64(
648 buf,
649 pos,
650 "invalid max_order_qty tag in risk limits",
651 "zero max_order_qty in risk limits",
652 )?;
653 pos = new_pos;
654 let max_order_qty = max_order_qty.map(Quantity);
655 check(buf, pos, 1)?;
656 let max_order_notional = match buf[pos] {
657 1 => {
658 pos += 1;
659 check(buf, pos, 8)?;
660 let v = le::get_u64(&buf[pos..]);
661 pos += 8;
662 Some(v)
663 }
664 0 => {
665 pos += 1;
666 None
667 }
668 _ => return Err(corrupt("invalid max_order_notional tag in risk limits")),
669 };
670 out.push((
671 symbol,
672 RiskLimits {
673 max_order_qty,
674 max_order_notional,
675 },
676 ));
677 }
678 Ok((pos, out))
679}
680
681fn decode_circuit_breakers(
682 buf: &[u8],
683) -> Result<(usize, Vec<(Symbol, CircuitBreakerConfig)>), SnapshotDecodeError> {
684 let n = read_section_len(buf)?;
685 let mut pos = 4;
686 validate_count(buf.len() - pos, n, 7)?;
688 let mut out = Vec::with_capacity(n);
689 for _ in 0..n {
690 check(buf, pos, 7)?;
691 let symbol = Symbol(le::get_u32(&buf[pos..]));
692 pos += 4;
693 let (new_pos, lower) = decode_opt_nz_u64(
694 buf,
695 pos,
696 "invalid price_band_lower tag in circuit breaker",
697 "zero price_band_lower in circuit breaker",
698 )?;
699 pos = new_pos;
700 let (new_pos, upper) = decode_opt_nz_u64(
701 buf,
702 pos,
703 "invalid price_band_upper tag in circuit breaker",
704 "zero price_band_upper in circuit breaker",
705 )?;
706 pos = new_pos;
707 check(buf, pos, 1)?;
708 let halted = buf[pos] != 0;
709 pos += 1;
710 out.push((
711 symbol,
712 CircuitBreakerConfig {
713 price_band_lower: lower.map(Price),
714 price_band_upper: upper.map(Price),
715 halted,
716 },
717 ));
718 }
719 Ok((pos, out))
720}
721
722fn decode_fee_schedules(
723 buf: &[u8],
724) -> Result<(usize, Vec<(Symbol, FeeSchedule)>), SnapshotDecodeError> {
725 let n = read_section_len(buf)?;
726 let mut pos = 4;
727 validate_count(buf.len() - pos, n, 8)?;
729 let mut out = Vec::with_capacity(n);
730 for _ in 0..n {
731 check(buf, pos, 8)?;
732 let symbol = Symbol(le::get_u32(&buf[pos..]));
733 pos += 4;
734 let maker_fee_bps = le::get_i16(&buf[pos..]);
735 pos += 2;
736 let taker_fee_bps = le::get_i16(&buf[pos..]);
737 pos += 2;
738 out.push((
739 symbol,
740 FeeSchedule {
741 maker_fee_bps,
742 taker_fee_bps,
743 },
744 ));
745 }
746 Ok((pos, out))
747}
748
749fn decode_key_hwm(buf: &[u8]) -> Result<(usize, Vec<(u64, u64)>), SnapshotDecodeError> {
750 let n = read_section_len(buf)?;
751 let mut pos = 4;
752 validate_count(buf.len() - pos, n, 16)?;
754 let mut out = Vec::with_capacity(n);
755 for _ in 0..n {
756 check(buf, pos, 16)?;
757 let key_hash = le::get_u64(&buf[pos..]);
758 let hwm = le::get_u64(&buf[pos + 8..]);
759 out.push((key_hash, hwm));
760 pos += 16;
761 }
762 Ok((pos, out))
763}
764
765fn decode_disabled_instruments(buf: &[u8]) -> Result<(usize, Vec<Symbol>), SnapshotDecodeError> {
766 let n = read_section_len(buf)?;
767 let mut pos = 4;
768 validate_count(buf.len() - pos, n, 4)?;
770 let mut out = Vec::with_capacity(n);
771 for _ in 0..n {
772 check(buf, pos, 4)?;
773 out.push(Symbol(le::get_u32(&buf[pos..])));
774 pos += 4;
775 }
776 Ok((pos, out))
777}
778
779fn decode_fee_account_deficits(
780 buf: &[u8],
781) -> Result<(usize, Vec<(CurrencyId, u64)>), SnapshotDecodeError> {
782 let n = read_section_len(buf)?;
783 let mut pos = 4;
784 validate_count(buf.len() - pos, n, 12)?;
786 let mut out = Vec::with_capacity(n);
787 for _ in 0..n {
788 check(buf, pos, 12)?;
789 let currency = CurrencyId(le::get_u32(&buf[pos..]));
790 let amount = le::get_u64(&buf[pos + 4..]);
791 out.push((currency, amount));
792 pos += 12;
793 }
794 Ok((pos, out))
795}
796
797fn decode_order_buckets(buf: &[u8]) -> Result<(usize, Vec<OrderBucketEntry>), SnapshotDecodeError> {
800 let n = read_section_len(buf)?;
801 let mut pos = 4;
802 validate_count(buf.len() - pos, n, 20)?;
803 let mut out = Vec::with_capacity(n);
804 let mut seen: std::collections::HashSet<AccountId> =
811 std::collections::HashSet::with_capacity(n);
812 for _ in 0..n {
813 check(buf, pos, 20)?;
814 let account = AccountId(le::get_u32(&buf[pos..]));
815 let tokens = le::get_u64(&buf[pos + 4..]);
816 let last_refill_ns = le::get_u64(&buf[pos + 12..]);
817 if !seen.insert(account) {
818 return Err(corrupt("duplicate account in order_buckets section"));
819 }
820 out.push((account, tokens, last_refill_ns));
821 pos += 20;
822 }
823 Ok((pos, out))
824}
825
826fn decode_exchange_state(
827 buf: &[u8],
828 version: u16,
829) -> Result<(usize, ExchangeSnapshot), SnapshotDecodeError> {
830 let mut pos = 0;
831
832 let (consumed, instruments) = decode_instruments(&buf[pos..])?;
833 pos += consumed;
834 let (consumed, balances) = decode_balances(&buf[pos..])?;
835 pos += consumed;
836 let (consumed, reservations) = decode_reservations(&buf[pos..])?;
837 pos += consumed;
838 let (consumed, order_sides) = decode_order_sides(&buf[pos..], version)?;
839 pos += consumed;
840 let (consumed, books) = decode_books(&buf[pos..], version)?;
841 pos += consumed;
842 let (consumed, risk_limits) = decode_risk_limits(&buf[pos..])?;
843 pos += consumed;
844 let (consumed, circuit_breakers) = decode_circuit_breakers(&buf[pos..])?;
845 pos += consumed;
846
847 let fee_schedules = if version >= 7 && pos < buf.len() {
854 let (consumed, v) = decode_fee_schedules(&buf[pos..])?;
855 pos += consumed;
856 v
857 } else {
858 Vec::new()
859 };
860
861 let key_hwm = if version >= 9 && pos < buf.len() {
862 let (consumed, v) = decode_key_hwm(&buf[pos..])?;
863 pos += consumed;
864 v
865 } else {
866 Vec::new()
867 };
868
869 let disabled_instruments = if version >= 12 {
870 let (consumed, v) = decode_disabled_instruments(&buf[pos..])?;
871 pos += consumed;
872 v
873 } else {
874 Vec::new()
875 };
876
877 let fee_account_deficits = if version >= 16 {
878 let (consumed, v) = decode_fee_account_deficits(&buf[pos..])?;
879 pos += consumed;
880 v
881 } else {
882 Vec::new()
883 };
884
885 let order_buckets = if version >= 18 {
886 let (consumed, v) = decode_order_buckets(&buf[pos..])?;
887 pos += consumed;
888 v
889 } else {
890 Vec::new()
891 };
892
893 Ok((
894 pos,
895 ExchangeSnapshot {
896 instruments,
897 balances,
898 reservations,
899 order_sides,
900 books,
901 risk_limits,
902 circuit_breakers,
903 fee_schedules,
904 key_hwm,
905 disabled_instruments,
906 fee_account_deficits,
907 order_buckets,
908 },
909 ))
910}
911
912fn decode_book_snapshot(
913 buf: &[u8],
914 version: u16,
915) -> Result<(usize, BookSnapshot), SnapshotDecodeError> {
916 let corrupt = |reason: &'static str| SnapshotDecodeError::Corrupt { reason };
917 let mut pos = 0;
918
919 let check = |pos: usize, need: usize| -> Result<(), SnapshotDecodeError> {
920 if pos + need > buf.len() {
921 Err(SnapshotDecodeError::Truncated)
922 } else {
923 Ok(())
924 }
925 };
926
927 let (consumed, bids) = decode_book_side_levels(&buf[pos..], version)?;
929 pos += consumed;
930
931 let (consumed, asks) = decode_book_side_levels(&buf[pos..], version)?;
933 pos += consumed;
934
935 check(pos, 4)?;
938 let n_order_index = le::get_u32(&buf[pos..]) as usize;
939 pos += 4;
940 let mut order_index = Vec::with_capacity(n_order_index);
941 if version >= 8 {
942 validate_count(buf.len() - pos, n_order_index, 21)?;
943 for _ in 0..n_order_index {
944 check(pos, 21)?;
945 let order_id = OrderId(le::get_u64(&buf[pos..]));
946 let account = AccountId(le::get_u32(&buf[pos + 8..]));
947 let side = le::decode_side(buf[pos + 12]).ok_or(corrupt("invalid side"))?;
948 let price_val = NonZeroU64::new(le::get_u64(&buf[pos + 13..]))
949 .ok_or(corrupt("zero price in index"))?;
950 order_index.push((order_id, account, side, Price(price_val)));
951 pos += 21;
952 }
953 } else {
954 validate_count(buf.len() - pos, n_order_index, 17)?;
955 for _ in 0..n_order_index {
956 check(pos, 17)?;
957 let order_id = OrderId(le::get_u64(&buf[pos..]));
958 let side = le::decode_side(buf[pos + 8]).ok_or(corrupt("invalid side"))?;
959 let price_val = NonZeroU64::new(le::get_u64(&buf[pos + 9..]))
960 .ok_or(corrupt("zero price in index"))?;
961 order_index.push((order_id, AccountId(0), side, Price(price_val)));
964 pos += 17;
965 }
966 }
967
968 let (consumed, stop_buys) = decode_stop_side_levels(&buf[pos..], version)?;
970 pos += consumed;
971
972 let (consumed, stop_sells) = decode_stop_side_levels(&buf[pos..], version)?;
974 pos += consumed;
975
976 check(pos, 4)?;
979 let n_stop_index = le::get_u32(&buf[pos..]) as usize;
980 pos += 4;
981 let mut stop_index = Vec::with_capacity(n_stop_index);
982 if version >= 8 {
983 validate_count(buf.len() - pos, n_stop_index, 21)?;
984 for _ in 0..n_stop_index {
985 check(pos, 21)?;
986 let order_id = OrderId(le::get_u64(&buf[pos..]));
987 let account = AccountId(le::get_u32(&buf[pos + 8..]));
988 let side = le::decode_side(buf[pos + 12]).ok_or(corrupt("invalid side"))?;
989 let price_val = NonZeroU64::new(le::get_u64(&buf[pos + 13..]))
990 .ok_or(corrupt("zero price in stop index"))?;
991 stop_index.push((order_id, account, side, Price(price_val)));
992 pos += 21;
993 }
994 } else {
995 validate_count(buf.len() - pos, n_stop_index, 17)?;
996 for _ in 0..n_stop_index {
997 check(pos, 17)?;
998 let order_id = OrderId(le::get_u64(&buf[pos..]));
999 let side = le::decode_side(buf[pos + 8]).ok_or(corrupt("invalid side"))?;
1000 let price_val = NonZeroU64::new(le::get_u64(&buf[pos + 9..]))
1001 .ok_or(corrupt("zero price in stop index"))?;
1002 stop_index.push((order_id, AccountId(0), side, Price(price_val)));
1004 pos += 17;
1005 }
1006 }
1007
1008 check(pos, 1)?;
1010 let last_trade_price = match buf[pos] {
1011 1 => {
1012 pos += 1;
1013 check(pos, 8)?;
1014 let p = NonZeroU64::new(le::get_u64(&buf[pos..]))
1015 .ok_or(corrupt("zero last trade price"))?;
1016 pos += 8;
1017 Some(Price(p))
1018 }
1019 0 => {
1020 pos += 1;
1021 None
1022 }
1023 _ => return Err(corrupt("invalid last_trade_price tag")),
1024 };
1025
1026 Ok((
1027 pos,
1028 BookSnapshot {
1029 bids,
1030 asks,
1031 order_index,
1032 stop_buys,
1033 stop_sells,
1034 stop_index,
1035 last_trade_price,
1036 },
1037 ))
1038}
1039
1040fn decode_book_side_levels(
1041 buf: &[u8],
1042 version: u16,
1043) -> Result<(usize, RestingLevels), SnapshotDecodeError> {
1044 let corrupt = |reason: &'static str| SnapshotDecodeError::Corrupt { reason };
1045 let mut pos = 0;
1046
1047 if buf.len() < 4 {
1048 return Err(SnapshotDecodeError::Truncated);
1049 }
1050 let n_levels = le::get_u32(&buf[pos..]) as usize;
1051 pos += 4;
1052 validate_count(buf.len() - pos, n_levels, 12)?;
1054
1055 let order_size: usize = if version >= 11 { 29 } else { 21 };
1057
1058 let mut levels = Vec::with_capacity(n_levels);
1059 for _ in 0..n_levels {
1060 if pos + 12 > buf.len() {
1061 return Err(SnapshotDecodeError::Truncated);
1062 }
1063 let price_val =
1064 NonZeroU64::new(le::get_u64(&buf[pos..])).ok_or(corrupt("zero price in book level"))?;
1065 pos += 8;
1066 let n_orders = le::get_u32(&buf[pos..]) as usize;
1067 pos += 4;
1068
1069 validate_count(buf.len() - pos, n_orders, order_size)?;
1071 let mut orders = Vec::with_capacity(n_orders);
1072 for _ in 0..n_orders {
1073 if pos + order_size > buf.len() {
1074 return Err(SnapshotDecodeError::Truncated);
1075 }
1076 let id = OrderId(le::get_u64(&buf[pos..]));
1077 let account = AccountId(le::get_u32(&buf[pos + 8..]));
1078 let remaining_val = NonZeroU64::new(le::get_u64(&buf[pos + 12..]))
1079 .ok_or(corrupt("zero remaining quantity"))?;
1080 let time_in_force = le::decode_tif(buf[pos + 20])
1081 .ok_or(corrupt("invalid time-in-force on resting order"))?;
1082 pos += 21;
1083 let expiry_ns = if version >= 11 {
1084 let v = le::get_u64(&buf[pos..]);
1085 pos += 8;
1086 v
1087 } else {
1088 0
1089 };
1090 orders.push(RestingOrderSnapshot {
1091 id,
1092 account,
1093 remaining: Quantity(remaining_val),
1094 time_in_force,
1095 expiry_ns,
1096 });
1097 }
1098 levels.push((Price(price_val), orders));
1099 }
1100
1101 Ok((pos, levels))
1102}
1103
1104fn decode_stop_side_levels(
1105 buf: &[u8],
1106 version: u16,
1107) -> Result<(usize, StopLevels), SnapshotDecodeError> {
1108 let corrupt = |reason: &'static str| SnapshotDecodeError::Corrupt { reason };
1109 let mut pos = 0;
1110
1111 if buf.len() < 4 {
1112 return Err(SnapshotDecodeError::Truncated);
1113 }
1114 let n_levels = le::get_u32(&buf[pos..]) as usize;
1115 pos += 4;
1116 validate_count(buf.len() - pos, n_levels, 12)?;
1118
1119 let mut levels = Vec::with_capacity(n_levels);
1120 for _ in 0..n_levels {
1121 if pos + 12 > buf.len() {
1122 return Err(SnapshotDecodeError::Truncated);
1123 }
1124 let trigger_val = NonZeroU64::new(le::get_u64(&buf[pos..]))
1125 .ok_or(corrupt("zero trigger price in stop level"))?;
1126 pos += 8;
1127 let n_stops = le::get_u32(&buf[pos..]) as usize;
1128 pos += 4;
1129
1130 validate_count(buf.len() - pos, n_stops, 31)?;
1132 let mut stops = Vec::with_capacity(n_stops);
1133 for _ in 0..n_stops {
1134 if pos + 31 > buf.len() {
1136 return Err(SnapshotDecodeError::Truncated);
1137 }
1138 let id = OrderId(le::get_u64(&buf[pos..]));
1139 pos += 8;
1140 let account = AccountId(le::get_u32(&buf[pos..]));
1141 pos += 4;
1142 let side = le::decode_side(buf[pos]).ok_or(corrupt("invalid side in stop"))?;
1143 pos += 1;
1144 let tp = NonZeroU64::new(le::get_u64(&buf[pos..]))
1145 .ok_or(corrupt("zero trigger price in stop"))?;
1146 pos += 8;
1147 let qty = NonZeroU64::new(le::get_u64(&buf[pos..]))
1148 .ok_or(corrupt("zero quantity in stop"))?;
1149 pos += 8;
1150 let tif = le::decode_tif(buf[pos]).ok_or(corrupt("invalid tif in stop"))?;
1151 pos += 1;
1152
1153 let limit_price = match buf[pos] {
1154 1 => {
1155 pos += 1;
1156 if pos + 8 > buf.len() {
1157 return Err(SnapshotDecodeError::Truncated);
1158 }
1159 let lp = NonZeroU64::new(le::get_u64(&buf[pos..]))
1160 .ok_or(corrupt("zero limit price in stop"))?;
1161 pos += 8;
1162 Some(Price(lp))
1163 }
1164 0 => {
1165 pos += 1;
1166 None
1167 }
1168 _ => return Err(corrupt("invalid limit_price tag in stop")),
1169 };
1170
1171 if pos >= buf.len() {
1173 return Err(SnapshotDecodeError::Truncated);
1174 }
1175 let quote_budget = match buf[pos] {
1176 1 => {
1177 pos += 1;
1178 if pos + 8 > buf.len() {
1179 return Err(SnapshotDecodeError::Truncated);
1180 }
1181 let budget = le::get_u64(&buf[pos..]);
1182 pos += 8;
1183 Some(budget)
1184 }
1185 0 => {
1186 pos += 1;
1187 None
1188 }
1189 _ => return Err(corrupt("invalid quote_budget tag in stop")),
1190 };
1191
1192 if pos >= buf.len() {
1193 return Err(SnapshotDecodeError::Truncated);
1194 }
1195 let stp = le::decode_stp(buf[pos]).ok_or(corrupt("invalid stp in stop"))?;
1196 pos += 1;
1197
1198 let expiry_ns = if version >= 11 {
1200 if pos + 8 > buf.len() {
1201 return Err(SnapshotDecodeError::Truncated);
1202 }
1203 let v = le::get_u64(&buf[pos..]);
1204 pos += 8;
1205 v
1206 } else {
1207 0
1208 };
1209
1210 stops.push(PendingStopSnapshot {
1211 id,
1212 account,
1213 side,
1214 trigger_price: Price(tp),
1215 quantity: Quantity(qty),
1216 time_in_force: tif,
1217 limit_price,
1218 quote_budget,
1219 stp,
1220 expiry_ns,
1221 });
1222 }
1223 levels.push((Price(trigger_val), stops));
1224 }
1225
1226 Ok((pos, levels))
1227}
1228
1229fn rebuild_scheduler_heap(
1236 instruments: &[Option<Box<crate::exchange::InstrumentState>>],
1237) -> ScheduledTaskHeap {
1238 let mut heap = ScheduledTaskHeap::new();
1239 for inst in instruments.iter().flatten() {
1240 let symbol = inst.spec.symbol;
1241 for (account, order_id, expiry_ns) in inst.book.iter_gtd_orders() {
1242 heap.push(ScheduledTask {
1243 fire_ns: expiry_ns,
1244 kind: ScheduledTaskKind::ExpireOrder {
1245 symbol,
1246 account,
1247 order_id,
1248 },
1249 });
1250 }
1251 }
1252 heap
1253}
1254
1255fn build_indexed_instruments(
1264 specs: Vec<InstrumentSpec>,
1265 books: Vec<(Symbol, BookSnapshot)>,
1266 risk_limits: Vec<(Symbol, RiskLimits)>,
1267 circuit_breakers: Vec<(Symbol, CircuitBreakerConfig)>,
1268 fee_schedules: Vec<(Symbol, FeeSchedule)>,
1269 disabled_instruments: Vec<Symbol>,
1270) -> Vec<Option<Box<crate::exchange::InstrumentState>>> {
1271 use crate::exchange::InstrumentState;
1272
1273 let mut books_map: StdHashMap<Symbol, OrderBook> = StdHashMap::new();
1274 for (symbol, book_snap) in books {
1275 books_map.insert(symbol, OrderBook::restore(symbol, book_snap));
1276 }
1277 let risk_map: StdHashMap<Symbol, RiskLimits> = risk_limits.into_iter().collect();
1278 let cb_map: StdHashMap<Symbol, CircuitBreakerConfig> = circuit_breakers.into_iter().collect();
1279 let fee_map: StdHashMap<Symbol, FeeSchedule> = fee_schedules.into_iter().collect();
1280 let disabled_set: std::collections::HashSet<Symbol> =
1281 disabled_instruments.into_iter().collect();
1282
1283 let max_sym = specs.iter().map(|s| s.symbol.0 as usize).max().unwrap_or(0);
1284 let mut instruments: Vec<Option<Box<InstrumentState>>> = Vec::new();
1285 instruments.resize_with(max_sym + 1, || None);
1286 for spec in &specs {
1287 let idx = spec.symbol.0 as usize;
1288 let book = books_map
1289 .remove(&spec.symbol)
1290 .unwrap_or_else(|| OrderBook::new(spec.symbol));
1291 instruments[idx] = Some(Box::new(InstrumentState {
1292 spec: *spec,
1293 book,
1294 risk_limits: risk_map.get(&spec.symbol).copied().unwrap_or_default(),
1295 circuit_breaker: cb_map.get(&spec.symbol).copied().unwrap_or_default(),
1296 fee_schedule: fee_map.get(&spec.symbol).copied().unwrap_or_default(),
1297 disabled: disabled_set.contains(&spec.symbol),
1298 }));
1299 }
1300 instruments
1301}
1302
1303fn inject_reservation_slots_into_instruments(
1308 instruments: &mut [Option<Box<crate::exchange::InstrumentState>>],
1309 slot_assignments: &[((AccountId, OrderId), ReservationSlot)],
1310) {
1311 for inst in instruments {
1312 if let Some(inst) = inst.as_deref_mut() {
1313 inst.book.inject_reservation_slots(slot_assignments);
1314 }
1315 }
1316}
1317
1318impl Exchange {
1319 pub(crate) fn snapshot_state(&self) -> ExchangeSnapshot {
1321 let instruments: Vec<InstrumentSpec> = self.instrument_specs().copied().collect();
1322 let balances = self.accounts().snapshot_balances();
1323 let reservations = self.snapshot_reservations();
1324 let order_sides: Vec<((AccountId, OrderId), Side)> = self.snapshot_order_sides();
1325
1326 let books: Vec<(Symbol, BookSnapshot)> = self
1327 .books()
1328 .map(|(symbol, book)| (symbol, book.snapshot()))
1329 .collect();
1330
1331 let risk_limits = self.snapshot_risk_limits();
1332 let circuit_breakers = self.snapshot_circuit_breakers();
1333 let fee_schedules = self.snapshot_fee_schedules();
1334 let key_hwm = self.snapshot_key_hwm();
1335 let disabled_instruments = self.snapshot_disabled_instruments();
1336 let fee_account_deficits = self.accounts().snapshot_fee_deficits();
1337 let order_buckets = self.snapshot_order_buckets();
1338
1339 ExchangeSnapshot {
1340 instruments,
1341 balances,
1342 reservations,
1343 order_sides,
1344 books,
1345 risk_limits,
1346 circuit_breakers,
1347 fee_schedules,
1348 key_hwm,
1349 disabled_instruments,
1350 fee_account_deficits,
1351 order_buckets,
1352 }
1353 }
1354
1355 pub(crate) fn restore_state(state: ExchangeSnapshot) -> Self {
1357 let ExchangeSnapshot {
1370 instruments: instrument_specs,
1371 balances,
1372 reservations,
1373 order_sides: snapshot_order_sides,
1374 books,
1375 risk_limits,
1376 circuit_breakers,
1377 fee_schedules,
1378 key_hwm: key_hwm_entries,
1379 disabled_instruments,
1380 fee_account_deficits,
1381 order_buckets,
1382 } = state;
1383
1384 let mut instruments = build_indexed_instruments(
1385 instrument_specs,
1386 books,
1387 risk_limits,
1388 circuit_breakers,
1389 fee_schedules,
1390 disabled_instruments,
1391 );
1392
1393 let (accounts, slot_assignments) =
1394 AccountManager::from_parts(balances, reservations, fee_account_deficits);
1395 inject_reservation_slots_into_instruments(&mut instruments, &slot_assignments);
1396
1397 let mut key_hwm: crate::types::HashMap<u64, u64> =
1402 crate::types::HashMap::with_capacity_and_hasher(
1403 key_hwm_entries.len(),
1404 Default::default(),
1405 );
1406 for (key_hash, hwm) in key_hwm_entries {
1407 key_hwm.insert(key_hash, hwm);
1408 }
1409
1410 let scheduled_tasks = rebuild_scheduler_heap(&instruments);
1416
1417 let mut exchange = Self::from_parts(instruments, accounts, key_hwm, scheduled_tasks);
1418 exchange.restore_order_buckets(order_buckets);
1426
1427 let mut regenerated = exchange.snapshot_order_sides();
1436 let mut from_snapshot = snapshot_order_sides;
1437 regenerated.sort_unstable_by_key(|(k, _)| *k);
1442 from_snapshot.sort_unstable_by_key(|(k, _)| *k);
1443 if regenerated != from_snapshot {
1444 let diff = regenerated
1448 .iter()
1449 .zip(from_snapshot.iter())
1450 .position(|(a, b)| a != b);
1451 match diff {
1452 Some(i) => panic!(
1453 "snapshot corruption: order_sides mismatch at sorted index {i} — \
1454 books regenerated {:?}, snapshot had {:?}",
1455 regenerated[i], from_snapshot[i],
1456 ),
1457 None => panic!(
1458 "snapshot corruption: order_sides length mismatch — \
1459 books regenerated {} entries, snapshot had {}",
1460 regenerated.len(),
1461 from_snapshot.len(),
1462 ),
1463 }
1464 }
1465
1466 exchange
1467 }
1468
1469 pub fn clone_via_snapshot(&self) -> Self {
1475 let mut cloned = Self::restore_state(self.snapshot_state());
1476 cloned.set_max_open_orders_per_account(self.max_open_orders_per_account());
1482 let (rate, burst) = self.max_orders_per_second();
1491 cloned.set_max_orders_per_second(rate, burst);
1492 cloned
1493 }
1494}
1495
1496impl OrderBook {
1497 pub(crate) fn snapshot(&self) -> BookSnapshot {
1499 let snapshot_side =
1500 |side: &crate::orderbook::BookSide| -> Vec<(Price, Vec<RestingOrderSnapshot>)> {
1501 side.levels_snapshot()
1502 .into_iter()
1503 .map(|(price, orders)| {
1504 let snaps = orders
1505 .into_iter()
1506 .map(|o| RestingOrderSnapshot {
1507 id: o.id(),
1508 account: o.account(),
1509 remaining: o.remaining(),
1510 time_in_force: o.time_in_force(),
1511 expiry_ns: o.expiry_ns(),
1512 })
1513 .collect();
1514 (price, snaps)
1515 })
1516 .collect()
1517 };
1518
1519 let snapshot_stops = |stops: &crate::orderbook::StopSide| {
1520 stops
1521 .levels_snapshot()
1522 .into_iter()
1523 .map(|(trigger_price, pending)| {
1524 let snaps = pending
1525 .into_iter()
1526 .map(|s| PendingStopSnapshot {
1527 id: s.id(),
1528 account: s.account(),
1529 side: s.side(),
1530 trigger_price: s.trigger_price(),
1531 quantity: s.quantity(),
1532 time_in_force: s.time_in_force(),
1533 limit_price: s.limit_price(),
1534 quote_budget: s.quote_budget(),
1535 stp: s.stp(),
1536 expiry_ns: s.expiry_ns(),
1537 })
1538 .collect();
1539 (trigger_price, snaps)
1540 })
1541 .collect()
1542 };
1543
1544 BookSnapshot {
1545 bids: snapshot_side(self.bids()),
1546 asks: snapshot_side(self.asks()),
1547 order_index: self.snapshot_order_index(),
1548 stop_buys: snapshot_stops(self.stop_buys()),
1549 stop_sells: snapshot_stops(self.stop_sells()),
1550 stop_index: self.snapshot_stop_index(),
1551 last_trade_price: self.last_trade_price(),
1552 }
1553 }
1554
1555 pub(crate) fn restore(symbol: Symbol, snap: BookSnapshot) -> Self {
1557 let restore_side = |levels: Vec<(Price, Vec<RestingOrderSnapshot>)>, side: Side| {
1560 let materialized: Vec<(Price, Vec<crate::orderbook::RestingOrder>)> = levels
1561 .into_iter()
1562 .map(|(price, orders)| {
1563 let restored = orders
1564 .into_iter()
1565 .map(|o| {
1566 crate::orderbook::RestingOrder::new(
1567 o.id,
1568 o.account,
1569 o.remaining,
1570 o.time_in_force,
1571 o.expiry_ns,
1572 side,
1573 ReservationSlot::DUMMY,
1574 )
1575 })
1576 .collect();
1577 (price, restored)
1578 })
1579 .collect();
1580 crate::orderbook::BookSide::from_levels_snapshot(side, materialized)
1581 };
1582
1583 let restore_stops = |levels: Vec<(Price, Vec<PendingStopSnapshot>)>| {
1584 let materialized: Vec<(Price, Vec<crate::orderbook::PendingStop>)> = levels
1585 .into_iter()
1586 .map(|(trigger_price, stops)| {
1587 let pending = stops
1588 .into_iter()
1589 .map(|s| {
1590 crate::orderbook::PendingStop::new(
1591 s.id,
1592 s.account,
1593 s.side,
1594 s.trigger_price,
1595 s.quantity,
1596 s.time_in_force,
1597 s.limit_price,
1598 s.quote_budget,
1599 s.stp,
1600 s.expiry_ns,
1601 ReservationSlot::DUMMY,
1602 )
1603 })
1604 .collect();
1605 (trigger_price, pending)
1606 })
1607 .collect();
1608 crate::orderbook::StopSide::from_levels_snapshot(materialized)
1609 };
1610
1611 let (bids, bid_node_idx) = restore_side(snap.bids, Side::Buy);
1614 let (asks, ask_node_idx) = restore_side(snap.asks, Side::Sell);
1615
1616 let mut node_for: std::collections::HashMap<(AccountId, OrderId), u32> =
1621 std::collections::HashMap::with_capacity(bid_node_idx.len() + ask_node_idx.len());
1622 node_for.extend(bid_node_idx);
1623 node_for.extend(ask_node_idx);
1624
1625 let order_index: crate::slab_map::SlabMap<(Side, Price, ReservationSlot, u32)> = snap
1626 .order_index
1627 .into_iter()
1628 .map(|(id, account, side, price)| {
1629 let node_idx = node_for
1630 .get(&(account, id))
1631 .copied()
1632 .expect("snapshot order_index references missing book entry");
1637 (
1638 (account, id),
1639 (side, price, ReservationSlot::DUMMY, node_idx),
1640 )
1641 })
1642 .collect();
1643
1644 let (stop_buys, buy_stop_idx) = restore_stops(snap.stop_buys);
1649 let (stop_sells, sell_stop_idx) = restore_stops(snap.stop_sells);
1650 let mut stop_node_for: std::collections::HashMap<(AccountId, OrderId), u32> =
1651 std::collections::HashMap::with_capacity(buy_stop_idx.len() + sell_stop_idx.len());
1652 stop_node_for.extend(buy_stop_idx);
1653 stop_node_for.extend(sell_stop_idx);
1654
1655 let stop_index: crate::slab_map::SlabMap<(Side, Price, u32)> = snap
1656 .stop_index
1657 .into_iter()
1658 .map(|(id, account, side, price)| {
1659 let node_idx = stop_node_for
1660 .get(&(account, id))
1661 .copied()
1662 .expect("snapshot stop_index references missing stop entry");
1663 ((account, id), (side, price, node_idx))
1664 })
1665 .collect();
1666
1667 Self::from_parts(
1668 symbol,
1669 bids,
1670 asks,
1671 order_index,
1672 stop_buys,
1673 stop_sells,
1674 stop_index,
1675 snap.last_trade_price,
1676 )
1677 }
1678}
1679
1680#[cfg(test)]
1681mod tests {
1682 use std::num::NonZeroU64;
1683 use std::path::Path;
1684
1685 use super::*;
1686 use crate::exchange::Exchange;
1687 use crate::types::*;
1688
1689 type SnapResult<T> = std::io::Result<T>;
1698
1699 fn save(exchange: &Exchange, seq: u64, chain_hash: [u8; 32], path: &Path) -> SnapResult<()> {
1700 let payload = encode_exchange_payload(exchange);
1701 let mut framed = Vec::with_capacity(40 + payload.len());
1702 framed.extend_from_slice(&seq.to_le_bytes());
1703 framed.extend_from_slice(&chain_hash);
1704 framed.extend_from_slice(&payload);
1705 std::fs::write(path, framed)
1706 }
1707
1708 fn load(path: &Path) -> SnapResult<(Exchange, u64, [u8; 32])> {
1709 let bytes = std::fs::read(path)?;
1710 if bytes.len() < 40 {
1711 return Err(std::io::Error::other("truncated test snapshot header"));
1712 }
1713 let seq = u64::from_le_bytes(bytes[..8].try_into().unwrap());
1714 let mut hash = [0u8; 32];
1715 hash.copy_from_slice(&bytes[8..40]);
1716 let exchange = decode_exchange_payload(&bytes[40..])
1717 .map_err(|e| std::io::Error::other(e.to_string()))?;
1718 Ok((exchange, seq, hash))
1719 }
1720
1721 const ACCT_A: AccountId = AccountId(1);
1722 const ACCT_B: AccountId = AccountId(2);
1723 const BTC: CurrencyId = CurrencyId(0);
1724 const USD: CurrencyId = CurrencyId(1);
1725
1726 fn btc_usd_spec() -> InstrumentSpec {
1727 InstrumentSpec {
1728 symbol: Symbol(1),
1729 base: BTC,
1730 quote: USD,
1731 }
1732 }
1733
1734 fn qty(n: u64) -> Quantity {
1735 Quantity(NonZeroU64::new(n).unwrap())
1736 }
1737
1738 fn price_val(n: u64) -> Price {
1739 Price(NonZeroU64::new(n).unwrap())
1740 }
1741
1742 fn limit_order(id: u64, account: AccountId, side: Side, p: u64, q: u64) -> Order {
1743 Order {
1744 id: OrderId(id),
1745 account,
1746 side,
1747 order_type: OrderType::Limit {
1748 price: price_val(p),
1749 post_only: false,
1750 },
1751 time_in_force: TimeInForce::GTC,
1752 quantity: qty(q),
1753 stp: SelfTradeProtection::Allow,
1754 expiry_ns: 0,
1755 }
1756 }
1757
1758 #[test]
1765 fn snapshot_save_load_round_trip() {
1766 let dir = tempfile::tempdir().unwrap();
1767 let path = dir.path().join("test.snapshot");
1768
1769 let mut exchange = Exchange::new();
1770 exchange.add_instrument(btc_usd_spec());
1771 exchange.deposit(ACCT_A, USD, 100_000);
1772 exchange.deposit(ACCT_B, BTC, 500);
1773
1774 let mut reports = Vec::new();
1775 exchange.execute(
1776 Symbol(1),
1777 limit_order(1, ACCT_B, Side::Sell, 100, 50),
1778 &mut reports,
1779 );
1780 exchange.execute(
1781 Symbol(1),
1782 limit_order(2, ACCT_A, Side::Buy, 100, 30),
1783 &mut reports,
1784 );
1785
1786 save(&exchange, 42, [0u8; 32], &path).unwrap();
1787
1788 let (restored, seq, _chain_hash) = load(&path).unwrap();
1789 assert_eq!(seq, 42);
1790 assert_eq!(
1791 restored.accounts().balance(ACCT_A, USD).available,
1792 exchange.accounts().balance(ACCT_A, USD).available
1793 );
1794 assert_eq!(
1795 restored.accounts().balance(ACCT_A, USD).reserved,
1796 exchange.accounts().balance(ACCT_A, USD).reserved
1797 );
1798 assert_eq!(
1799 restored.accounts().balance(ACCT_A, BTC).available,
1800 exchange.accounts().balance(ACCT_A, BTC).available
1801 );
1802 assert_eq!(
1803 restored.accounts().balance(ACCT_B, USD).available,
1804 exchange.accounts().balance(ACCT_B, USD).available
1805 );
1806 assert_eq!(
1807 restored.accounts().balance(ACCT_B, BTC).available,
1808 exchange.accounts().balance(ACCT_B, BTC).available
1809 );
1810 assert_eq!(
1811 restored.accounts().balance(ACCT_B, BTC).reserved,
1812 exchange.accounts().balance(ACCT_B, BTC).reserved
1813 );
1814 }
1815
1816 #[test]
1817 fn snapshot_with_resting_orders_replays_correctly() {
1818 let dir = tempfile::tempdir().unwrap();
1819 let path = dir.path().join("resting.snapshot");
1820
1821 let mut exchange = Exchange::new();
1822 exchange.add_instrument(btc_usd_spec());
1823 exchange.deposit(ACCT_A, USD, 100_000);
1824 exchange.deposit(ACCT_B, BTC, 500);
1825
1826 let mut reports = Vec::new();
1827 exchange.execute(
1829 Symbol(1),
1830 limit_order(1, ACCT_B, Side::Sell, 100, 50),
1831 &mut reports,
1832 );
1833 reports.clear();
1834
1835 save(&exchange, 10, [0u8; 32], &path).unwrap();
1836
1837 let (mut restored, _seq, _chain_hash) = load(&path).unwrap();
1838
1839 let mut new_reports = Vec::new();
1841 restored.execute(
1842 Symbol(1),
1843 limit_order(2, ACCT_A, Side::Buy, 100, 20),
1844 &mut new_reports,
1845 );
1846
1847 assert!(matches!(new_reports[0], ExecutionReport::Fill { .. }));
1848 assert_eq!(restored.accounts().balance(ACCT_A, BTC).available, 20);
1849 }
1850
1851 #[test]
1852 fn snapshot_preserves_circuit_breaker_state() {
1853 let dir = tempfile::tempdir().unwrap();
1854 let path = dir.path().join("cb.snapshot");
1855
1856 let mut exchange = Exchange::new();
1857 exchange.add_instrument(btc_usd_spec());
1858 exchange.deposit(ACCT_A, USD, 100_000);
1859
1860 exchange.set_circuit_breaker(
1862 Symbol(1),
1863 CircuitBreakerConfig {
1864 price_band_lower: Some(price_val(90)),
1865 price_band_upper: Some(price_val(110)),
1866 halted: true,
1867 },
1868 );
1869
1870 save(&exchange, 5, [0u8; 32], &path).unwrap();
1871 let (mut restored, _, _) = load(&path).unwrap();
1872
1873 let mut reports = Vec::new();
1875 restored.execute(
1876 Symbol(1),
1877 limit_order(1, ACCT_A, Side::Buy, 100, 10),
1878 &mut reports,
1879 );
1880 assert!(matches!(
1881 reports[0],
1882 ExecutionReport::Rejected {
1883 reason: RejectReason::TradingHalted,
1884 ..
1885 }
1886 ));
1887
1888 restored.set_circuit_breaker(
1890 Symbol(1),
1891 CircuitBreakerConfig {
1892 price_band_lower: Some(price_val(90)),
1893 price_band_upper: Some(price_val(110)),
1894 halted: false,
1895 },
1896 );
1897
1898 reports.clear();
1899 restored.execute(
1900 Symbol(1),
1901 limit_order(2, ACCT_A, Side::Buy, 80, 10),
1902 &mut reports,
1903 );
1904 assert!(matches!(
1905 reports[0],
1906 ExecutionReport::Rejected {
1907 reason: RejectReason::OutsidePriceBand,
1908 ..
1909 }
1910 ));
1911
1912 reports.clear();
1914 restored.execute(
1915 Symbol(1),
1916 limit_order(3, ACCT_A, Side::Buy, 100, 10),
1917 &mut reports,
1918 );
1919 assert!(matches!(reports[0], ExecutionReport::Placed { .. }));
1920 }
1921
1922 #[test]
1923 fn snapshot_preserves_gtd_expiry() {
1924 let dir = tempfile::tempdir().unwrap();
1925 let path = dir.path().join("gtd.snapshot");
1926
1927 let mut exchange = Exchange::new();
1928 exchange.add_instrument(btc_usd_spec());
1929 exchange.deposit(ACCT_A, USD, 100_000);
1930
1931 let mut reports = Vec::new();
1932
1933 exchange.execute(
1935 Symbol(1),
1936 Order {
1937 id: OrderId(1),
1938 account: ACCT_A,
1939 side: Side::Buy,
1940 order_type: OrderType::Limit {
1941 price: price_val(100),
1942 post_only: false,
1943 },
1944 time_in_force: TimeInForce::GTD,
1945 quantity: qty(10),
1946 stp: SelfTradeProtection::Allow,
1947 expiry_ns: 5_000_000,
1948 },
1949 &mut reports,
1950 );
1951 assert!(matches!(reports[0], ExecutionReport::Placed { .. }));
1952 reports.clear();
1953
1954 save(&exchange, 20, [0u8; 32], &path).unwrap();
1955 let (mut restored, _, _) = load(&path).unwrap();
1956
1957 restored.drain_due_scheduled_tasks(4_999_999, &mut reports);
1961 assert!(reports.is_empty(), "should not expire before timestamp");
1962
1963 restored.drain_due_scheduled_tasks(5_000_000, &mut reports);
1964 assert_eq!(reports.len(), 1);
1965 assert!(matches!(
1966 reports[0],
1967 ExecutionReport::Cancelled {
1968 order_id: OrderId(1),
1969 ..
1970 }
1971 ));
1972 }
1973
1974 #[test]
1975 fn clone_via_snapshot_produces_identical_state() {
1976 let mut exchange = Exchange::new();
1977 exchange.add_instrument(btc_usd_spec());
1978 exchange.deposit(ACCT_A, USD, 100_000);
1979 exchange.deposit(ACCT_B, BTC, 500);
1980
1981 let mut reports = Vec::new();
1982 exchange.execute(
1983 Symbol(1),
1984 limit_order(1, ACCT_B, Side::Sell, 100, 50),
1985 &mut reports,
1986 );
1987 reports.clear();
1988
1989 let cloned = exchange.clone_via_snapshot();
1990
1991 assert_eq!(
1993 cloned.accounts().balance(ACCT_A, USD).available,
1994 exchange.accounts().balance(ACCT_A, USD).available,
1995 );
1996 assert_eq!(
1997 cloned.accounts().balance(ACCT_B, BTC).reserved,
1998 exchange.accounts().balance(ACCT_B, BTC).reserved,
1999 );
2000
2001 let mut clone_reports = Vec::new();
2003 let mut mutable_clone = cloned;
2004 mutable_clone.execute(
2005 Symbol(1),
2006 limit_order(2, ACCT_A, Side::Buy, 100, 10),
2007 &mut clone_reports,
2008 );
2009 assert!(matches!(clone_reports[0], ExecutionReport::Fill { .. }));
2010 }
2011
2012 #[test]
2013 #[should_panic(expected = "snapshot corruption: order_sides mismatch")]
2014 fn restore_detects_order_sides_mismatch() {
2015 let mut exchange = Exchange::new();
2018 exchange.add_instrument(btc_usd_spec());
2019 exchange.deposit(ACCT_B, BTC, 500);
2020 let mut reports = Vec::new();
2021 exchange.execute(
2022 Symbol(1),
2023 limit_order(1, ACCT_B, Side::Sell, 100, 50),
2024 &mut reports,
2025 );
2026
2027 let mut state = exchange.snapshot_state();
2032 assert!(!state.order_sides.is_empty(), "test prerequisite");
2033 state.order_sides[0].1 = Side::Buy;
2034
2035 let _ = Exchange::restore_state(state);
2038 }
2039
2040 #[test]
2041 fn snapshot_rebuilds_scheduler_heap_from_gtd_orders() {
2042 let dir = tempfile::tempdir().unwrap();
2043 let path = dir.path().join("rebuild.snapshot");
2044
2045 let mut exchange = Exchange::new();
2046 exchange.add_instrument(btc_usd_spec());
2047 exchange.deposit(ACCT_A, USD, 10_000_000);
2048
2049 let mut reports = Vec::new();
2052 exchange.execute(
2054 Symbol(1),
2055 Order {
2056 id: OrderId(1),
2057 account: ACCT_A,
2058 side: Side::Buy,
2059 order_type: OrderType::Limit {
2060 price: price_val(100),
2061 post_only: false,
2062 },
2063 time_in_force: TimeInForce::GTD,
2064 quantity: qty(1),
2065 stp: SelfTradeProtection::Allow,
2066 expiry_ns: 5_000,
2067 },
2068 &mut reports,
2069 );
2070 exchange.execute(
2074 Symbol(1),
2075 Order {
2076 id: OrderId(2),
2077 account: ACCT_A,
2078 side: Side::Buy,
2079 order_type: OrderType::StopLimit {
2080 trigger_price: price_val(200),
2081 limit_price: price_val(200),
2082 },
2083 time_in_force: TimeInForce::GTD,
2084 quantity: qty(1),
2085 stp: SelfTradeProtection::Allow,
2086 expiry_ns: 6_000,
2087 },
2088 &mut reports,
2089 );
2090 exchange.execute(
2092 Symbol(1),
2093 Order {
2094 id: OrderId(3),
2095 account: ACCT_A,
2096 side: Side::Buy,
2097 order_type: OrderType::Limit {
2098 price: price_val(101),
2099 post_only: false,
2100 },
2101 time_in_force: TimeInForce::GTD,
2102 quantity: qty(1),
2103 stp: SelfTradeProtection::Allow,
2104 expiry_ns: 8_000,
2105 },
2106 &mut reports,
2107 );
2108 reports.clear();
2109
2110 assert_eq!(exchange.scheduled_task_count(), 3, "pre-snapshot heap");
2112
2113 save(&exchange, 7, [0u8; 32], &path).unwrap();
2114 let (mut restored, _, _) = load(&path).unwrap();
2115
2116 assert_eq!(restored.scheduled_task_count(), 3, "post-restore heap");
2118
2119 restored.drain_due_scheduled_tasks(4_999, &mut reports);
2121 assert!(reports.is_empty());
2122
2123 restored.drain_due_scheduled_tasks(5_000, &mut reports);
2125 assert_eq!(reports.len(), 1);
2126 assert!(matches!(
2127 reports[0],
2128 ExecutionReport::Cancelled {
2129 order_id: OrderId(1),
2130 ..
2131 }
2132 ));
2133 reports.clear();
2134
2135 restored.drain_due_scheduled_tasks(6_000, &mut reports);
2137 assert_eq!(reports.len(), 1);
2138 assert!(matches!(
2139 reports[0],
2140 ExecutionReport::Cancelled {
2141 order_id: OrderId(2),
2142 ..
2143 }
2144 ));
2145 reports.clear();
2146
2147 restored.drain_due_scheduled_tasks(8_000, &mut reports);
2149 assert_eq!(reports.len(), 1);
2150 assert!(matches!(
2151 reports[0],
2152 ExecutionReport::Cancelled {
2153 order_id: OrderId(3),
2154 ..
2155 }
2156 ));
2157 }
2158
2159 #[test]
2167 fn snapshot_round_trip_preserves_rate_limit_buckets() {
2168 let dir = tempfile::tempdir().unwrap();
2169 let path = dir.path().join("rate_limit.snapshot");
2170
2171 let mut exchange = Exchange::new();
2172 exchange.set_max_orders_per_second(1_000, 5);
2173 exchange.add_instrument(btc_usd_spec());
2174 exchange.deposit(ACCT_A, USD, 1_000_000);
2175 exchange.deposit(ACCT_B, USD, 1_000_000);
2176
2177 let mut reports = Vec::new();
2183 for i in 0..3u64 {
2184 exchange.set_current_event_ts_ns(1_000_000_000);
2185 exchange.execute(
2186 Symbol(1),
2187 limit_order(i + 1, ACCT_A, Side::Buy, 100, 1),
2188 &mut reports,
2189 );
2190 }
2191 exchange.set_current_event_ts_ns(2_000_000_000);
2192 exchange.execute(
2193 Symbol(1),
2194 limit_order(100, ACCT_B, Side::Buy, 101, 1),
2195 &mut reports,
2196 );
2197
2198 let pre = exchange.snapshot_order_buckets();
2199 assert_eq!(pre.len(), 2, "two buckets should be populated");
2200
2201 save(&exchange, 1, [0u8; 32], &path).unwrap();
2202 let (mut restored, _seq, _hash) = load(&path).unwrap();
2203 restored.set_max_orders_per_second(1_000, 5);
2206
2207 let post = restored.snapshot_order_buckets();
2208 let mut pre_sorted = pre.clone();
2211 let mut post_sorted = post;
2212 pre_sorted.sort_by_key(|(a, _, _)| a.0);
2213 post_sorted.sort_by_key(|(a, _, _)| a.0);
2214 assert_eq!(pre_sorted, post_sorted);
2215
2216 let mut after = Vec::new();
2221 for i in 0..2u64 {
2222 restored.set_current_event_ts_ns(1_000_000_000 + 1 + i);
2223 restored.execute(
2224 Symbol(1),
2225 limit_order(200 + i, ACCT_A, Side::Buy, 102 + i, 1),
2226 &mut after,
2227 );
2228 }
2229 assert!(
2230 !after
2231 .iter()
2232 .any(|r| matches!(r, ExecutionReport::Rejected { .. })),
2233 "two more orders should fit in the restored bucket: {after:?}",
2234 );
2235 after.clear();
2236 restored.set_current_event_ts_ns(1_000_000_000 + 10);
2239 restored.execute(
2240 Symbol(1),
2241 limit_order(999, ACCT_A, Side::Buy, 200, 1),
2242 &mut after,
2243 );
2244 assert!(
2245 matches!(
2246 after[0],
2247 ExecutionReport::Rejected {
2248 reason: RejectReason::ExceedsOrderRate,
2249 ..
2250 }
2251 ),
2252 "restored bucket lost throttle state: {after:?}",
2253 );
2254 }
2255
2256 #[test]
2263 fn truncated_v18_snapshot_payload_errors_instead_of_emptying_buckets() {
2264 let mut exchange = Exchange::new();
2265 exchange.set_max_orders_per_second(1_000, 5);
2266 exchange.add_instrument(btc_usd_spec());
2267 exchange.deposit(ACCT_A, USD, 1_000_000);
2268 let mut reports = Vec::new();
2269 exchange.set_current_event_ts_ns(1_000_000_000);
2270 exchange.execute(
2271 Symbol(1),
2272 limit_order(1, ACCT_A, Side::Buy, 100, 1),
2273 &mut reports,
2274 );
2275
2276 let full = encode_exchange_payload(&exchange);
2281 let truncated = &full[..full.len() - 24];
2282 match decode_exchange_payload(truncated) {
2283 Err(SnapshotDecodeError::Truncated) => {}
2284 Err(other) => panic!("expected TruncatedEntry, got {other:?}"),
2285 Ok(_) => panic!("truncated v18 payload must not decode silently as empty"),
2286 }
2287 }
2288
2289 #[test]
2296 fn duplicate_account_in_v18_bucket_section_rejected() {
2297 let mut exchange = Exchange::new();
2298 exchange.set_max_orders_per_second(1_000, 5);
2299 exchange.add_instrument(btc_usd_spec());
2300 exchange.deposit(ACCT_A, USD, 1_000_000);
2301 let mut reports = Vec::new();
2302 exchange.set_current_event_ts_ns(1_000_000_000);
2303 exchange.execute(
2304 Symbol(1),
2305 limit_order(1, ACCT_A, Side::Buy, 100, 1),
2306 &mut reports,
2307 );
2308
2309 let mut payload = encode_exchange_payload(&exchange);
2310 let entry_start = payload.len() - 20;
2314 let dup_entry = payload[entry_start..].to_vec();
2315 let count_pos = entry_start - 4;
2316 let count = le::get_u32(&payload[count_pos..]);
2317 let new_count = count
2320 .checked_add(1)
2321 .expect("test fixture must keep count within u32");
2322 payload[count_pos..count_pos + 4].copy_from_slice(&new_count.to_le_bytes());
2323 payload.extend_from_slice(&dup_entry);
2324
2325 match decode_exchange_payload(&payload) {
2326 Err(SnapshotDecodeError::Corrupt { reason, .. }) => {
2327 assert!(
2328 reason.contains("duplicate account"),
2329 "expected duplicate-account corruption, got: {reason}",
2330 );
2331 }
2332 Err(other) => panic!("expected CorruptEntry, got {other:?}"),
2333 Ok(_) => panic!("duplicate-account payload must not decode silently"),
2334 }
2335 }
2336
2337 #[test]
2343 fn rate_limit_set_idempotent_preserves_buckets() {
2344 let mut exchange = Exchange::new();
2345 exchange.set_max_orders_per_second(500, 3);
2346 exchange.add_instrument(btc_usd_spec());
2347 exchange.deposit(ACCT_A, USD, 1_000_000);
2348 let mut reports = Vec::new();
2349 exchange.set_current_event_ts_ns(1_000);
2350 exchange.execute(
2351 Symbol(1),
2352 limit_order(1, ACCT_A, Side::Buy, 100, 1),
2353 &mut reports,
2354 );
2355 let before = exchange.snapshot_order_buckets();
2356 assert_eq!(before.len(), 1);
2357 exchange.set_max_orders_per_second(500, 3);
2359 let after = exchange.snapshot_order_buckets();
2360 assert_eq!(before, after, "same-config call must not clear");
2361 exchange.set_max_orders_per_second(500, 4);
2363 assert!(
2364 exchange.snapshot_order_buckets().is_empty(),
2365 "changed-config call must clear",
2366 );
2367 }
2368}