1use crate::{
13 order::{
14 id::{ClientOrderId, OrderId},
15 request::ReduceOnly,
16 },
17 trade::Trade,
18};
19use rustrade_integration::collection::FnvIndexMap;
20use serde::{Deserialize, Deserializer, Serialize};
21use std::fmt;
22use tracing::debug;
23
24#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
26struct SentOrder {
27 reduce_only: ReduceOnly,
28 venue_id: Option<OrderId>,
29}
30
31struct RetiredOrder(SentOrder);
35
36impl<'de> Deserialize<'de> for RetiredOrder {
37 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
38 struct RetiredOrderVisitor;
39
40 impl<'de> serde::de::Visitor<'de> for RetiredOrderVisitor {
41 type Value = SentOrder;
42
43 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter.write_str("a retired order, or its venue order id alone, or null")
45 }
46
47 fn visit_map<A: serde::de::MapAccess<'de>>(
49 self,
50 map: A,
51 ) -> Result<SentOrder, A::Error> {
52 SentOrder::deserialize(serde::de::value::MapAccessDeserializer::new(map))
53 }
54
55 fn visit_str<E: serde::de::Error>(self, venue_id: &str) -> Result<SentOrder, E> {
56 Ok(bare(Some(OrderId::new(venue_id))))
57 }
58
59 fn visit_unit<E: serde::de::Error>(self) -> Result<SentOrder, E> {
60 Ok(bare(None))
61 }
62
63 fn visit_none<E: serde::de::Error>(self) -> Result<SentOrder, E> {
64 Ok(bare(None))
65 }
66
67 fn visit_some<D: Deserializer<'de>>(self, inner: D) -> Result<SentOrder, D::Error> {
68 inner.deserialize_any(self)
69 }
70 }
71
72 fn bare(venue_id: Option<OrderId>) -> SentOrder {
74 SentOrder {
75 reduce_only: ReduceOnly::Position,
76 venue_id,
77 }
78 }
79
80 if deserializer.is_human_readable() {
81 deserializer.deserialize_any(RetiredOrderVisitor)
82 } else {
83 SentOrder::deserialize(deserializer)
84 }
85 .map(Self)
86 }
87}
88
89#[non_exhaustive]
91#[derive(Debug, Clone, Copy, Eq, PartialEq)]
92pub struct BookedOrder<'a> {
93 pub cid: &'a ClientOrderId,
95 pub reduce_only: ReduceOnly,
97}
98
99#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
125#[serde(from = "BookParts")]
126pub struct ReduceOnlyBook {
127 live: FnvIndexMap<ClientOrderId, SentOrder>,
129 retired: FnvIndexMap<ClientOrderId, SentOrder>,
131 #[serde(skip)]
134 live_reduce_only: usize,
135}
136
137#[derive(Deserialize)]
139struct BookParts {
140 #[serde(default)]
141 live: FnvIndexMap<ClientOrderId, SentOrder>,
142 #[serde(default)]
143 retired: FnvIndexMap<ClientOrderId, RetiredOrder>,
144}
145
146impl From<BookParts> for ReduceOnlyBook {
147 fn from(parts: BookParts) -> Self {
148 let BookParts { live, retired } = parts;
149 let mut retired: FnvIndexMap<ClientOrderId, SentOrder> = retired
151 .into_iter()
152 .map(|(cid, RetiredOrder(order))| (cid, order))
153 .filter(|(_, order)| order.reduce_only.is_reduce_only())
154 .collect();
155 retired.retain(|cid, _| !live.contains_key(cid));
157 let excess = retired.len().saturating_sub(Self::RETIRED_CAPACITY);
158 retired.drain(..excess);
159 let live_reduce_only = live
160 .values()
161 .filter(|order| order.reduce_only.is_reduce_only())
162 .count();
163 Self {
164 live,
165 retired,
166 live_reduce_only,
167 }
168 }
169}
170
171impl ReduceOnlyBook {
172 pub const RETIRED_CAPACITY: usize = 64;
174
175 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn is_empty(&self) -> bool {
182 self.live.is_empty() && self.retired.is_empty()
183 }
184
185 pub fn live_len(&self) -> usize {
188 self.live.len()
189 }
190
191 pub fn has_reduce_only(&self) -> bool {
194 self.live_reduce_only > 0 || !self.retired.is_empty()
195 }
196
197 pub fn register(&mut self, cid: &ClientOrderId, reduce_only: ReduceOnly) {
204 self.retired.shift_remove(cid);
205 if let Some(earlier) = self.live.shift_remove(cid) {
206 self.live_reduce_only -= usize::from(earlier.reduce_only.is_reduce_only());
207 }
208 self.live_reduce_only += usize::from(reduce_only.is_reduce_only());
209 self.live.insert(
210 cid.clone(),
211 SentOrder {
212 reduce_only,
213 venue_id: None,
214 },
215 );
216 }
217
218 pub fn acknowledge(&mut self, cid: &ClientOrderId, venue_id: &OrderId) {
225 if let Some(order) = self.live.get_mut(cid).or_else(|| self.retired.get_mut(cid)) {
226 order.venue_id = Some(venue_id.clone());
227 }
228 }
229
230 pub fn retire(&mut self, cid: &ClientOrderId) {
235 if let Some(order) = self.live.shift_remove(cid)
236 && order.reduce_only.is_reduce_only()
237 {
238 self.live_reduce_only -= 1;
239 self.insert_retired(cid.clone(), order);
240 }
241 }
242
243 pub fn retire_untracked(&mut self, mut is_tracked: impl FnMut(&ClientOrderId) -> bool) {
249 let mut ended: Vec<(ClientOrderId, SentOrder)> = Vec::new();
250 self.live.retain(|cid, order| {
251 let tracked = is_tracked(cid);
252 if !tracked && order.reduce_only.is_reduce_only() {
253 ended.push((
254 cid.clone(),
255 SentOrder {
256 reduce_only: order.reduce_only,
257 venue_id: order.venue_id.take(),
258 },
259 ));
260 }
261 tracked
262 });
263 self.live_reduce_only -= ended.len();
264 for (cid, order) in ended {
265 self.insert_retired(cid, order);
266 }
267 }
268
269 pub fn lookup<AssetKey, InstrumentKey>(
275 &self,
276 trade: &Trade<AssetKey, InstrumentKey>,
277 ) -> Option<BookedOrder<'_>> {
278 self.lookup_ids(trade.client_order_id.as_ref(), &trade.order_id)
279 }
280
281 pub fn lookup_ids(
285 &self,
286 cid: Option<&ClientOrderId>,
287 order_id: &OrderId,
288 ) -> Option<BookedOrder<'_>> {
289 fn booked<'a>((cid, order): (&'a ClientOrderId, &'a SentOrder)) -> BookedOrder<'a> {
290 BookedOrder {
291 cid,
292 reduce_only: order.reduce_only,
293 }
294 }
295 if let Some(cid) = cid {
296 return self
297 .live
298 .get_key_value(cid)
299 .or_else(|| self.retired.get_key_value(cid))
300 .map(booked);
301 }
302 let by_venue_id =
305 |(_, order): &(&ClientOrderId, &SentOrder)| order.venue_id.as_ref() == Some(order_id);
306 self.live
307 .iter()
308 .find(by_venue_id)
309 .or_else(|| self.retired.iter().find(by_venue_id))
310 .map(booked)
311 }
312
313 pub fn is_reduce_only<AssetKey, InstrumentKey>(
316 &self,
317 trade: &Trade<AssetKey, InstrumentKey>,
318 ) -> bool {
319 self.lookup(trade)
320 .is_some_and(|order| order.reduce_only.is_reduce_only())
321 }
322
323 pub fn clear(&mut self) {
325 self.live.clear();
326 self.retired.clear();
327 self.live_reduce_only = 0;
328 }
329
330 fn insert_retired(&mut self, cid: ClientOrderId, order: SentOrder) {
331 if self.retired.len() >= Self::RETIRED_CAPACITY
332 && !self.retired.contains_key(&cid)
333 && let Some((evicted, _)) = self.retired.shift_remove_index(0)
334 {
335 debug!(
336 evicted_cid = %evicted,
337 capacity = Self::RETIRED_CAPACITY,
338 "Retired reduce-only orders at capacity: evicting the oldest. A late fill of the \
339 evicted order is now not recognised as reduce-only."
340 );
341 }
342 self.retired.insert(cid, order);
343 }
344}
345
346#[cfg(test)]
347#[allow(clippy::unwrap_used)] mod tests {
349 use super::*;
350 use crate::{order::id::StrategyId, trade::AssetFees};
351 use ReduceOnly::{No, Position, PositionAndFreeBase};
352 use chrono::DateTime;
353 use rust_decimal_macros::dec;
354 use rustrade_instrument::Side;
355 use rustrade_instrument::asset::QuoteAsset;
356
357 fn cid(id: &str) -> ClientOrderId {
358 ClientOrderId::new(id)
359 }
360
361 fn venue(id: &str) -> OrderId {
362 OrderId::new(id)
363 }
364
365 fn fill(cid: Option<&str>, venue_id: &str) -> Trade<QuoteAsset, ()> {
366 let trade = Trade::new(
367 crate::trade::TradeId::new("t"),
368 venue(venue_id),
369 (),
370 StrategyId::new("s"),
371 DateTime::UNIX_EPOCH,
372 Side::Sell,
373 dec!(1),
374 dec!(1),
375 None,
376 AssetFees::quote_fees(dec!(0)),
377 );
378 match cid {
379 Some(id) => trade.with_client_order_id(self::cid(id)),
380 None => trade,
381 }
382 }
383
384 fn flag(book: &ReduceOnlyBook, trade: &Trade<QuoteAsset, ()>) -> Option<ReduceOnly> {
385 book.lookup(trade).map(|order| order.reduce_only)
386 }
387
388 #[test]
389 fn a_fill_finds_its_order_by_client_order_id() {
390 let mut book = ReduceOnlyBook::new();
391 book.register(&cid("ro"), Position);
392 book.register(&cid("plain"), No);
393 assert_eq!(flag(&book, &fill(Some("ro"), "v")), Some(Position));
394 assert_eq!(flag(&book, &fill(Some("plain"), "v")), Some(No));
395 assert_eq!(flag(&book, &fill(Some("other"), "v")), None);
396 assert!(book.is_reduce_only(&fill(Some("ro"), "v")));
397 assert!(!book.is_reduce_only(&fill(Some("plain"), "v")));
398 assert!(!book.is_reduce_only(&fill(Some("other"), "v")));
399 }
400
401 #[test]
402 fn a_fill_naming_only_the_venue_id_finds_its_order_once_acknowledged() {
403 let mut book = ReduceOnlyBook::new();
404 book.register(&cid("ro"), Position);
405 assert_eq!(flag(&book, &fill(None, "v1")), None, "not yet acknowledged");
406 book.acknowledge(&cid("ro"), &venue("v1"));
407 let trade = fill(None, "v1");
408 let found = book.lookup(&trade).unwrap();
409 assert_eq!((found.cid, found.reduce_only), (&cid("ro"), Position));
410 }
411
412 #[test]
413 fn a_client_order_id_the_book_lacks_does_not_fall_back_to_the_venue_id() {
414 let mut book = ReduceOnlyBook::new();
415 book.register(&cid("ro"), Position);
416 book.acknowledge(&cid("ro"), &venue("v1"));
417 assert_eq!(flag(&book, &fill(Some("other"), "v1")), None);
418 }
419
420 #[test]
421 fn a_retired_reduce_only_order_is_still_found_and_an_ordinary_one_is_forgotten() {
422 let mut book = ReduceOnlyBook::new();
423 book.register(&cid("ro"), Position);
424 book.register(&cid("plain"), No);
425 book.acknowledge(&cid("ro"), &venue("v1"));
426 book.acknowledge(&cid("plain"), &venue("v2"));
427 book.retire(&cid("ro"));
428 book.retire(&cid("plain"));
429 assert_eq!(flag(&book, &fill(Some("ro"), "x")), Some(Position));
430 assert_eq!(flag(&book, &fill(None, "v1")), Some(Position));
431 assert_eq!(flag(&book, &fill(Some("plain"), "x")), None);
432 assert_eq!(flag(&book, &fill(None, "v2")), None);
433 }
434
435 #[test]
436 fn an_order_retired_before_its_acknowledgement_takes_the_venue_id_after() {
437 let mut book = ReduceOnlyBook::new();
438 book.register(&cid("ro"), Position);
439 book.retire(&cid("ro"));
440 assert_eq!(flag(&book, &fill(None, "v1")), None);
441 book.acknowledge(&cid("ro"), &venue("v1"));
442 assert_eq!(flag(&book, &fill(None, "v1")), Some(Position));
443 }
444
445 #[test]
446 fn a_live_order_outranks_a_retired_one_under_the_same_venue_id() {
447 let mut book = ReduceOnlyBook::new();
448 book.register(&cid("old"), Position);
449 book.acknowledge(&cid("old"), &venue("v1"));
450 book.retire(&cid("old"));
451 book.register(&cid("new"), No);
452 book.acknowledge(&cid("new"), &venue("v1"));
453 let trade = fill(None, "v1");
454 let found = book.lookup(&trade).unwrap();
455 assert_eq!((found.cid, found.reduce_only), (&cid("new"), No));
456 }
457
458 #[test]
459 fn registering_a_reused_client_order_id_drops_what_the_earlier_order_left() {
460 for retire_first in [false, true] {
461 let mut book = ReduceOnlyBook::new();
462 book.register(&cid("c"), Position);
463 book.acknowledge(&cid("c"), &venue("v1"));
464 if retire_first {
465 book.retire(&cid("c"));
466 }
467 book.register(&cid("c"), No);
468 assert_eq!(flag(&book, &fill(Some("c"), "x")), Some(No));
469 assert_eq!(flag(&book, &fill(None, "v1")), None, "{retire_first}");
470 }
471 }
472
473 #[test]
474 fn the_oldest_retirement_is_dropped_at_capacity() {
475 let mut book = ReduceOnlyBook::new();
476 let cids: Vec<_> = (0..=ReduceOnlyBook::RETIRED_CAPACITY)
477 .map(|i| cid(&format!("c{i}")))
478 .collect();
479 for c in &cids {
480 book.register(c, Position);
481 book.retire(c);
482 }
483 assert_eq!(book.retired.len(), ReduceOnlyBook::RETIRED_CAPACITY);
484 assert_eq!(flag(&book, &fill(Some("c0"), "x")), None);
485 assert_eq!(flag(&book, &fill(Some("c1"), "x")), Some(Position));
486 assert_eq!(flag(&book, &fill(Some("c64"), "x")), Some(Position));
487 }
488
489 #[test]
490 fn ordinary_orders_do_not_push_out_retired_reduce_only_ones() {
491 let mut book = ReduceOnlyBook::new();
492 book.register(&cid("ro"), Position);
493 book.retire(&cid("ro"));
494 for i in 0..ReduceOnlyBook::RETIRED_CAPACITY * 2 {
495 let c = cid(&format!("p{i}"));
496 book.register(&c, No);
497 book.retire(&c);
498 }
499 assert_eq!(flag(&book, &fill(Some("ro"), "x")), Some(Position));
500 }
501
502 #[test]
503 fn retire_untracked_retires_in_the_order_registered() {
504 let mut book = ReduceOnlyBook::new();
505 let cids: Vec<_> = (0..5).map(|i| cid(&format!("c{i}"))).collect();
506 for c in &cids {
507 book.register(c, Position);
508 }
509 let kept = cid("c2");
510 book.retire_untracked(|c| *c == kept);
511 assert!(
512 book.retired
513 .keys()
514 .eq([&cids[0], &cids[1], &cids[3], &cids[4]])
515 );
516 assert!(book.live.keys().eq([&kept]));
517 }
518
519 #[test]
520 fn retire_untracked_keeps_the_reduce_only_count_and_forgets_ordinary_orders() {
521 let mut book = ReduceOnlyBook::new();
522 book.register(&cid("ro1"), Position);
523 book.register(&cid("gone"), No);
524 book.register(&cid("ro2"), Position);
525 book.register(&cid("kept"), No);
526 book.retire_untracked(|c| *c == cid("kept"));
527 assert_eq!(book.live_reduce_only, 0);
528 assert!(book.live.keys().eq([&cid("kept")]));
529 assert!(book.retired.keys().eq([&cid("ro1"), &cid("ro2")]));
530 assert!(book.has_reduce_only(), "retired ones are still held");
531 book.retired.clear();
532 assert!(!book.has_reduce_only());
533 }
534
535 #[test]
536 fn retiring_or_acknowledging_an_unknown_order_does_nothing() {
537 let mut book = ReduceOnlyBook::new();
538 book.retire(&cid("none"));
539 book.acknowledge(&cid("none"), &venue("v"));
540 assert!(book.is_empty());
541 }
542
543 #[test]
544 fn clear_forgets_everything() {
545 let mut book = ReduceOnlyBook::new();
546 book.register(&cid("a"), Position);
547 book.register(&cid("b"), Position);
548 book.retire(&cid("b"));
549 book.clear();
550 assert!(book.is_empty());
551 }
552
553 #[test]
554 fn is_empty_and_live_len_count_live_and_retired_orders() {
555 let mut book = ReduceOnlyBook::new();
556 assert_eq!((book.is_empty(), book.live_len()), (true, 0));
557 book.register(&cid("plain"), No);
558 book.register(&cid("ro"), Position);
559 assert_eq!((book.is_empty(), book.live_len()), (false, 2), "live only");
560 book.retire(&cid("ro"));
561 assert_eq!((book.is_empty(), book.live_len()), (false, 1));
562 book.retire(&cid("plain"));
563 assert_eq!(
564 (book.is_empty(), book.live_len()),
565 (false, 0),
566 "retired only"
567 );
568 }
569
570 #[test]
571 fn has_reduce_only_follows_the_orders_held() {
572 let mut book = ReduceOnlyBook::new();
573 assert!(!book.has_reduce_only());
574 book.register(&cid("plain"), No);
575 assert!(!book.has_reduce_only());
576 book.register(&cid("ro"), Position);
577 assert!(book.has_reduce_only());
578 book.register(&cid("ro"), No);
579 assert!(!book.has_reduce_only(), "replaced by an ordinary order");
580 book.register(&cid("ro"), Position);
581 book.retire(&cid("ro"));
582 assert!(book.has_reduce_only(), "retired but still held");
583 book.clear();
584 assert!(!book.has_reduce_only());
585 book.register(&cid("a"), Position);
586 book.register(&cid("b"), Position);
587 book.retire_untracked(|c| *c == cid("a"));
588 assert_eq!((book.live_len(), book.has_reduce_only()), (1, true));
589 }
590
591 #[test]
592 fn the_serialised_form_is_pinned_and_keeps_the_retirement_order() {
593 let mut book = ReduceOnlyBook::new();
594 book.register(&cid("b"), Position);
595 book.acknowledge(&cid("b"), &venue("v"));
596 book.retire(&cid("b"));
597 book.register(&cid("a"), Position);
598 book.retire(&cid("a"));
599 book.register(&cid("c"), No);
600 let json = serde_json::to_string(&book).unwrap();
601 assert_eq!(
602 json,
603 r#"{"live":{"c":{"reduce_only":"no","venue_id":null}},"retired":{"b":{"reduce_only":"position","venue_id":"v"},"a":{"reduce_only":"position","venue_id":null}}}"#
604 );
605 let loaded: ReduceOnlyBook = serde_json::from_str(&json).unwrap();
606 assert!(loaded.retired.keys().eq(book.retired.keys()));
607 assert_eq!(loaded.has_reduce_only(), book.has_reduce_only());
608 }
609
610 #[test]
611 fn a_loaded_book_keeps_its_bound_and_counts_its_live_reduce_only_orders() {
612 let retired: Vec<String> = (0..100).map(|i| format!(r#""c{i}":null"#)).collect();
614 let json = format!(
615 r#"{{"live":{{"l":{{"reduce_only":true,"venue_id":null}},"c99":{{"reduce_only":false,"venue_id":null}}}},"retired":{{{}}}}}"#,
616 retired.join(",")
617 );
618 let mut book: ReduceOnlyBook = serde_json::from_str(&json).unwrap();
619 assert_eq!(book.retired.len(), ReduceOnlyBook::RETIRED_CAPACITY);
620 assert!(book.retired.keys().next().is_some_and(|c| *c == cid("c35")));
621 assert!(!book.retired.contains_key(&cid("c99")), "live wins");
622 book.retire(&cid("l"));
623 assert_eq!(book.retired.len(), ReduceOnlyBook::RETIRED_CAPACITY);
624 book.retire(&cid("c99"));
625 assert!(book.live.is_empty() && book.live_reduce_only == 0);
626 }
627
628 #[test]
629 fn a_book_serialised_with_a_reduce_only_flag_and_bare_retirements_loads() {
630 let json = r#"{"live":{"l":{"reduce_only":true,"venue_id":"v0"},"p":{"reduce_only":false,"venue_id":null}},"retired":{"b":"v1","a":null}}"#;
633 let book: ReduceOnlyBook = serde_json::from_str(json).unwrap();
634 assert_eq!(flag(&book, &fill(Some("l"), "x")), Some(Position));
635 assert_eq!(flag(&book, &fill(Some("p"), "x")), Some(No));
636 assert_eq!(flag(&book, &fill(Some("a"), "x")), Some(Position));
637 assert_eq!(flag(&book, &fill(None, "v1")), Some(Position));
638 assert_eq!(book.live_reduce_only, 1);
639 }
640
641 #[test]
642 fn a_loaded_retirement_that_was_not_reduce_only_is_dropped() {
643 for not_reduce_only in [r#""no""#, "false"] {
644 let json = format!(
645 r#"{{"retired":{{"p":{{"reduce_only":{not_reduce_only},"venue_id":"v"}}}}}}"#
646 );
647 let book: ReduceOnlyBook = serde_json::from_str(&json).unwrap();
648 assert!(
649 book.is_empty() && !book.has_reduce_only(),
650 "{not_reduce_only}"
651 );
652 }
653 }
654
655 #[test]
656 fn a_loaded_retirement_with_a_reduce_only_flag_is_kept_as_position() {
657 let json = r#"{"retired":{"r":{"reduce_only":true,"venue_id":"v"}}}"#;
658 let book: ReduceOnlyBook = serde_json::from_str(json).unwrap();
659 assert_eq!(flag(&book, &fill(None, "v")), Some(Position));
660 }
661
662 #[test]
664 fn a_malformed_retirement_keeps_the_error_inside_it() {
665 let json = r#"{"retired":{"r":{"reduce_only":"sometimes","venue_id":null}}}"#;
666 let error = serde_json::from_str::<ReduceOnlyBook>(json).unwrap_err();
667 assert!(error.to_string().contains("sometimes"), "{error}");
668
669 let error = serde_json::from_str::<ReduceOnlyBook>(r#"{"retired":{"r":7}}"#).unwrap_err();
670 assert!(
671 error
672 .to_string()
673 .contains("a retired order, or its venue order id alone, or null"),
674 "{error}"
675 );
676 }
677
678 #[test]
679 fn an_order_keeps_what_it_was_sent_as_once_retired() {
680 let mut book = ReduceOnlyBook::new();
681 book.register(&cid("sweep"), PositionAndFreeBase);
682 book.register(&cid("close"), Position);
683 book.acknowledge(&cid("sweep"), &venue("v1"));
684 book.retire(&cid("sweep"));
685 book.retire_untracked(|_| false);
686 assert_eq!(
687 flag(&book, &fill(Some("sweep"), "x")),
688 Some(PositionAndFreeBase)
689 );
690 assert_eq!(flag(&book, &fill(None, "v1")), Some(PositionAndFreeBase));
691 assert_eq!(flag(&book, &fill(Some("close"), "x")), Some(Position));
692 assert!(book.is_reduce_only(&fill(Some("sweep"), "x")));
693 let json = serde_json::to_string(&book).unwrap();
694 assert_eq!(serde_json::from_str::<ReduceOnlyBook>(&json).unwrap(), book);
695 }
696
697 #[test]
698 fn a_book_round_trips_through_json_and_loads_from_nothing() {
699 let mut book = ReduceOnlyBook::new();
700 book.register(&cid("live"), Position);
701 book.acknowledge(&cid("live"), &venue("v1"));
702 book.register(&cid("gone"), Position);
703 book.retire(&cid("gone"));
704 let json = serde_json::to_string(&book).unwrap();
705 assert_eq!(serde_json::from_str::<ReduceOnlyBook>(&json).unwrap(), book);
706 assert_eq!(
707 serde_json::from_str::<ReduceOnlyBook>("{}").unwrap(),
708 ReduceOnlyBook::new()
709 );
710 }
711}