1use std::{
17 fmt::Display,
18 ops::{Deref, DerefMut},
19};
20
21use indexmap::IndexMap;
22use nautilus_core::{UUID4, UnixNanos, correctness::FAILED};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{Order, OrderAny, OrderCore, check_display_qty, check_time_in_force};
28use crate::{
29 enums::{
30 ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
31 TimeInForce, TrailingOffsetType, TriggerType,
32 },
33 events::{OrderEventAny, OrderInitialized, OrderUpdated},
34 identifiers::{
35 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
36 StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
37 },
38 orders::OrderError,
39 types::{Currency, Money, Price, Quantity, quantity::check_positive_quantity},
40};
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
46)]
47pub struct LimitIfTouchedOrder {
48 pub price: Price,
49 pub trigger_price: Price,
50 pub trigger_type: TriggerType,
51 pub expire_time: Option<UnixNanos>,
52 pub is_post_only: bool,
53 pub display_qty: Option<Quantity>,
54 pub trigger_instrument_id: Option<InstrumentId>,
55 pub is_triggered: bool,
56 pub ts_triggered: Option<UnixNanos>,
57 core: OrderCore,
58}
59
60#[allow(clippy::too_many_arguments)]
61impl LimitIfTouchedOrder {
62 #[allow(clippy::too_many_arguments)]
71 pub fn new_checked(
72 trader_id: TraderId,
73 strategy_id: StrategyId,
74 instrument_id: InstrumentId,
75 client_order_id: ClientOrderId,
76 order_side: OrderSide,
77 quantity: Quantity,
78 price: Price,
79 trigger_price: Price,
80 trigger_type: TriggerType,
81 time_in_force: TimeInForce,
82 expire_time: Option<UnixNanos>,
83 post_only: bool,
84 reduce_only: bool,
85 quote_quantity: bool,
86 display_qty: Option<Quantity>,
87 emulation_trigger: Option<TriggerType>,
88 trigger_instrument_id: Option<InstrumentId>,
89 contingency_type: Option<ContingencyType>,
90 order_list_id: Option<OrderListId>,
91 linked_order_ids: Option<Vec<ClientOrderId>>,
92 parent_order_id: Option<ClientOrderId>,
93 exec_algorithm_id: Option<ExecAlgorithmId>,
94 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
95 exec_spawn_id: Option<ClientOrderId>,
96 tags: Option<Vec<Ustr>>,
97 init_id: UUID4,
98 ts_init: UnixNanos,
99 ) -> anyhow::Result<Self> {
100 check_positive_quantity(quantity, stringify!(quantity))?;
101 check_display_qty(display_qty, quantity)?;
102 check_time_in_force(time_in_force, expire_time)?;
103
104 match order_side {
105 OrderSide::Buy if trigger_price > price => {
106 anyhow::bail!("BUY Limit-If-Touched must have `trigger_price` <= `price`")
107 }
108 OrderSide::Sell if trigger_price < price => {
109 anyhow::bail!("SELL Limit-If-Touched must have `trigger_price` >= `price`")
110 }
111 _ => {}
112 }
113
114 let init_order = OrderInitialized::new(
115 trader_id,
116 strategy_id,
117 instrument_id,
118 client_order_id,
119 order_side,
120 OrderType::LimitIfTouched,
121 quantity,
122 time_in_force,
123 post_only,
124 reduce_only,
125 quote_quantity,
126 false,
127 init_id,
128 ts_init,
129 ts_init,
130 Some(price),
131 Some(trigger_price),
132 Some(trigger_type),
133 None,
134 None,
135 None,
136 expire_time,
137 display_qty,
138 emulation_trigger,
139 trigger_instrument_id,
140 contingency_type,
141 order_list_id,
142 linked_order_ids,
143 parent_order_id,
144 exec_algorithm_id,
145 exec_algorithm_params,
146 exec_spawn_id,
147 tags,
148 );
149
150 Ok(Self {
151 price,
152 trigger_price,
153 trigger_type,
154 expire_time,
155 is_post_only: post_only,
156 display_qty,
157 trigger_instrument_id,
158 is_triggered: false,
159 ts_triggered: None,
160 core: OrderCore::new(init_order),
161 })
162 }
163
164 #[allow(clippy::too_many_arguments)]
170 pub fn new(
171 trader_id: TraderId,
172 strategy_id: StrategyId,
173 instrument_id: InstrumentId,
174 client_order_id: ClientOrderId,
175 order_side: OrderSide,
176 quantity: Quantity,
177 price: Price,
178 trigger_price: Price,
179 trigger_type: TriggerType,
180 time_in_force: TimeInForce,
181 expire_time: Option<UnixNanos>,
182 post_only: bool,
183 reduce_only: bool,
184 quote_quantity: bool,
185 display_qty: Option<Quantity>,
186 emulation_trigger: Option<TriggerType>,
187 trigger_instrument_id: Option<InstrumentId>,
188 contingency_type: Option<ContingencyType>,
189 order_list_id: Option<OrderListId>,
190 linked_order_ids: Option<Vec<ClientOrderId>>,
191 parent_order_id: Option<ClientOrderId>,
192 exec_algorithm_id: Option<ExecAlgorithmId>,
193 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
194 exec_spawn_id: Option<ClientOrderId>,
195 tags: Option<Vec<Ustr>>,
196 init_id: UUID4,
197 ts_init: UnixNanos,
198 ) -> Self {
199 Self::new_checked(
200 trader_id,
201 strategy_id,
202 instrument_id,
203 client_order_id,
204 order_side,
205 quantity,
206 price,
207 trigger_price,
208 trigger_type,
209 time_in_force,
210 expire_time,
211 post_only,
212 reduce_only,
213 quote_quantity,
214 display_qty,
215 emulation_trigger,
216 trigger_instrument_id,
217 contingency_type,
218 order_list_id,
219 linked_order_ids,
220 parent_order_id,
221 exec_algorithm_id,
222 exec_algorithm_params,
223 exec_spawn_id,
224 tags,
225 init_id,
226 ts_init,
227 )
228 .expect(FAILED)
229 }
230}
231
232impl Deref for LimitIfTouchedOrder {
233 type Target = OrderCore;
234
235 fn deref(&self) -> &Self::Target {
236 &self.core
237 }
238}
239
240impl DerefMut for LimitIfTouchedOrder {
241 fn deref_mut(&mut self) -> &mut Self::Target {
242 &mut self.core
243 }
244}
245
246impl Order for LimitIfTouchedOrder {
247 fn into_any(self) -> OrderAny {
248 OrderAny::LimitIfTouched(self)
249 }
250
251 fn status(&self) -> OrderStatus {
252 self.status
253 }
254
255 fn trader_id(&self) -> TraderId {
256 self.trader_id
257 }
258
259 fn strategy_id(&self) -> StrategyId {
260 self.strategy_id
261 }
262
263 fn instrument_id(&self) -> InstrumentId {
264 self.instrument_id
265 }
266
267 fn symbol(&self) -> Symbol {
268 self.instrument_id.symbol
269 }
270
271 fn venue(&self) -> Venue {
272 self.instrument_id.venue
273 }
274
275 fn client_order_id(&self) -> ClientOrderId {
276 self.client_order_id
277 }
278
279 fn venue_order_id(&self) -> Option<VenueOrderId> {
280 self.venue_order_id
281 }
282
283 fn position_id(&self) -> Option<PositionId> {
284 self.position_id
285 }
286
287 fn account_id(&self) -> Option<AccountId> {
288 self.account_id
289 }
290
291 fn last_trade_id(&self) -> Option<TradeId> {
292 self.last_trade_id
293 }
294
295 fn order_side(&self) -> OrderSide {
296 self.side
297 }
298
299 fn order_type(&self) -> OrderType {
300 self.order_type
301 }
302
303 fn quantity(&self) -> Quantity {
304 self.quantity
305 }
306
307 fn time_in_force(&self) -> TimeInForce {
308 self.time_in_force
309 }
310
311 fn expire_time(&self) -> Option<UnixNanos> {
312 self.expire_time
313 }
314
315 fn price(&self) -> Option<Price> {
316 Some(self.price)
317 }
318
319 fn trigger_price(&self) -> Option<Price> {
320 Some(self.trigger_price)
321 }
322
323 fn trigger_type(&self) -> Option<TriggerType> {
324 Some(self.trigger_type)
325 }
326
327 fn liquidity_side(&self) -> Option<LiquiditySide> {
328 self.liquidity_side
329 }
330
331 fn is_post_only(&self) -> bool {
332 self.is_post_only
333 }
334
335 fn is_reduce_only(&self) -> bool {
336 self.is_reduce_only
337 }
338
339 fn is_quote_quantity(&self) -> bool {
340 self.is_quote_quantity
341 }
342
343 fn has_price(&self) -> bool {
344 true
345 }
346
347 fn display_qty(&self) -> Option<Quantity> {
348 self.display_qty
349 }
350
351 fn limit_offset(&self) -> Option<Decimal> {
352 None
353 }
354
355 fn trailing_offset(&self) -> Option<Decimal> {
356 None
357 }
358
359 fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
360 None
361 }
362
363 fn emulation_trigger(&self) -> Option<TriggerType> {
364 self.emulation_trigger
365 }
366
367 fn trigger_instrument_id(&self) -> Option<InstrumentId> {
368 self.trigger_instrument_id
369 }
370
371 fn contingency_type(&self) -> Option<ContingencyType> {
372 self.contingency_type
373 }
374
375 fn order_list_id(&self) -> Option<OrderListId> {
376 self.order_list_id
377 }
378
379 fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
380 self.linked_order_ids.as_deref()
381 }
382
383 fn parent_order_id(&self) -> Option<ClientOrderId> {
384 self.parent_order_id
385 }
386
387 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
388 self.exec_algorithm_id
389 }
390
391 fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
392 self.exec_algorithm_params.as_ref()
393 }
394
395 fn exec_spawn_id(&self) -> Option<ClientOrderId> {
396 self.exec_spawn_id
397 }
398
399 fn tags(&self) -> Option<&[Ustr]> {
400 self.tags.as_deref()
401 }
402
403 fn filled_qty(&self) -> Quantity {
404 self.filled_qty
405 }
406
407 fn leaves_qty(&self) -> Quantity {
408 self.leaves_qty
409 }
410
411 fn overfill_qty(&self) -> Quantity {
412 self.overfill_qty
413 }
414
415 fn avg_px(&self) -> Option<f64> {
416 self.avg_px
417 }
418
419 fn slippage(&self) -> Option<f64> {
420 self.slippage
421 }
422
423 fn init_id(&self) -> UUID4 {
424 self.init_id
425 }
426
427 fn ts_init(&self) -> UnixNanos {
428 self.ts_init
429 }
430
431 fn ts_submitted(&self) -> Option<UnixNanos> {
432 self.ts_submitted
433 }
434
435 fn ts_accepted(&self) -> Option<UnixNanos> {
436 self.ts_accepted
437 }
438
439 fn ts_closed(&self) -> Option<UnixNanos> {
440 self.ts_closed
441 }
442
443 fn ts_last(&self) -> UnixNanos {
444 self.ts_last
445 }
446
447 fn commissions(&self) -> &IndexMap<Currency, Money> {
448 &self.commissions
449 }
450
451 fn events(&self) -> Vec<&OrderEventAny> {
452 self.events.iter().collect()
453 }
454
455 fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
456 self.venue_order_ids.iter().collect()
457 }
458
459 fn trade_ids(&self) -> Vec<&TradeId> {
460 self.trade_ids.iter().collect()
461 }
462
463 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
464 if let OrderEventAny::Updated(ref event) = event {
465 self.update(event);
466 };
467
468 let is_order_filled = matches!(event, OrderEventAny::Filled(_));
469 let is_order_triggered = matches!(event, OrderEventAny::Triggered(_));
470 let ts_event = if is_order_triggered {
471 Some(event.ts_event())
472 } else {
473 None
474 };
475
476 self.core.apply(event)?;
477
478 if is_order_triggered {
479 self.is_triggered = true;
480 self.ts_triggered = ts_event;
481 }
482
483 if is_order_filled {
484 self.core.set_slippage(self.price);
485 };
486
487 Ok(())
488 }
489
490 fn update(&mut self, event: &OrderUpdated) {
491 if let Some(price) = event.price {
492 self.price = price;
493 }
494
495 if let Some(trigger_price) = event.trigger_price {
496 self.trigger_price = trigger_price;
497 }
498
499 self.quantity = event.quantity;
500 self.leaves_qty = self.quantity.saturating_sub(self.filled_qty);
501 }
502
503 fn is_triggered(&self) -> Option<bool> {
504 Some(self.is_triggered)
505 }
506
507 fn set_position_id(&mut self, position_id: Option<PositionId>) {
508 self.position_id = position_id;
509 }
510
511 fn set_quantity(&mut self, quantity: Quantity) {
512 self.quantity = quantity;
513 }
514
515 fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
516 self.leaves_qty = leaves_qty;
517 }
518
519 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
520 self.emulation_trigger = emulation_trigger;
521 }
522
523 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
524 self.is_quote_quantity = is_quote_quantity;
525 }
526
527 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
528 self.liquidity_side = Some(liquidity_side);
529 }
530
531 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
532 self.core.would_reduce_only(side, position_qty)
533 }
534
535 fn previous_status(&self) -> Option<OrderStatus> {
536 self.core.previous_status
537 }
538}
539
540impl Display for LimitIfTouchedOrder {
541 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542 write!(
543 f,
544 "LimitIfTouchedOrder({} {} {} @ {} / trigger {} ({:?}) {}, status={})",
545 self.side,
546 self.quantity.to_formatted_string(),
547 self.instrument_id,
548 self.price,
549 self.trigger_price,
550 self.trigger_type,
551 self.time_in_force,
552 self.status
553 )
554 }
555}
556
557impl From<OrderInitialized> for LimitIfTouchedOrder {
558 fn from(event: OrderInitialized) -> Self {
559 Self::new(
560 event.trader_id,
561 event.strategy_id,
562 event.instrument_id,
563 event.client_order_id,
564 event.order_side,
565 event.quantity,
566 event
567 .price .expect("Error initializing order: `price` was `None` for `LimitIfTouchedOrder"),
569 event
570 .trigger_price .expect(
572 "Error initializing order: `trigger_price` was `None` for `LimitIfTouchedOrder",
573 ),
574 event
575 .trigger_type
576 .expect("Error initializing order: `trigger_type` was `None`"),
577 event.time_in_force,
578 event.expire_time,
579 event.post_only,
580 event.reduce_only,
581 event.quote_quantity,
582 event.display_qty,
583 event.emulation_trigger,
584 event.trigger_instrument_id,
585 event.contingency_type,
586 event.order_list_id,
587 event.linked_order_ids,
588 event.parent_order_id,
589 event.exec_algorithm_id,
590 event.exec_algorithm_params,
591 event.exec_spawn_id,
592 event.tags,
593 event.event_id,
594 event.ts_event,
595 )
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use rstest::rstest;
602
603 use super::*;
604 use crate::{
605 enums::{TimeInForce, TriggerType},
606 events::order::{filled::OrderFilledBuilder, initialized::OrderInitializedBuilder},
607 identifiers::InstrumentId,
608 instruments::{CurrencyPair, stubs::*},
609 orders::{builder::OrderTestBuilder, stubs::TestOrderStubs},
610 types::{Price, Quantity},
611 };
612
613 #[rstest]
614 fn test_initialize(_audusd_sim: CurrencyPair) {
615 let order = OrderTestBuilder::new(OrderType::LimitIfTouched)
616 .instrument_id(_audusd_sim.id)
617 .side(OrderSide::Buy)
618 .price(Price::from("0.68000"))
619 .trigger_price(Price::from("0.68000"))
620 .trigger_type(TriggerType::LastPrice)
621 .quantity(Quantity::from(1))
622 .build();
623
624 assert_eq!(order.trigger_price(), Some(Price::from("0.68000")));
625 assert_eq!(order.price(), Some(Price::from("0.68000")));
626
627 assert_eq!(order.time_in_force(), TimeInForce::Gtc);
628
629 assert_eq!(order.is_triggered(), Some(false));
630 assert_eq!(order.filled_qty(), Quantity::from(0));
631 assert_eq!(order.leaves_qty(), Quantity::from(1));
632
633 assert_eq!(order.display_qty(), None);
634 assert_eq!(order.trigger_instrument_id(), None);
635 assert_eq!(order.order_list_id(), None);
636 }
637
638 #[rstest]
639 fn test_display(audusd_sim: CurrencyPair) {
640 let order = OrderTestBuilder::new(OrderType::LimitIfTouched)
641 .instrument_id(audusd_sim.id)
642 .side(OrderSide::Buy)
643 .trigger_price(Price::from("30200"))
644 .price(Price::from("30200"))
645 .trigger_type(TriggerType::LastPrice)
646 .quantity(Quantity::from(1))
647 .build();
648
649 assert_eq!(
650 order.to_string(),
651 "LimitIfTouchedOrder(BUY 1 AUD/USD.SIM @ 30200 / trigger 30200 (LastPrice) GTC, status=INITIALIZED)"
652 );
653 }
654
655 #[rstest]
656 #[should_panic(
657 expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
658 )]
659 fn test_quantity_zero(audusd_sim: CurrencyPair) {
660 let _ = OrderTestBuilder::new(OrderType::LimitIfTouched)
661 .instrument_id(audusd_sim.id)
662 .side(OrderSide::Buy)
663 .price(Price::from("30000"))
664 .trigger_price(Price::from("30200"))
665 .trigger_type(TriggerType::LastPrice)
666 .quantity(Quantity::from(0))
667 .build();
668 }
669
670 #[rstest]
671 #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
672 fn test_gtd_without_expire(audusd_sim: CurrencyPair) {
673 let _ = OrderTestBuilder::new(OrderType::LimitIfTouched)
674 .instrument_id(audusd_sim.id)
675 .side(OrderSide::Buy)
676 .price(Price::from("30000"))
677 .trigger_price(Price::from("30200"))
678 .trigger_type(TriggerType::LastPrice)
679 .quantity(Quantity::from(1))
680 .time_in_force(TimeInForce::Gtd)
681 .build();
682 }
683
684 #[rstest]
685 #[should_panic(expected = "BUY Limit-If-Touched must have `trigger_price` <= `price`")]
686 fn test_buy_trigger_gt_price(audusd_sim: CurrencyPair) {
687 OrderTestBuilder::new(OrderType::LimitIfTouched)
688 .instrument_id(audusd_sim.id)
689 .side(OrderSide::Buy)
690 .trigger_price(Price::from("30300")) .price(Price::from("30200"))
692 .trigger_type(TriggerType::LastPrice)
693 .quantity(Quantity::from(1))
694 .build();
695 }
696
697 #[rstest]
698 #[should_panic(expected = "SELL Limit-If-Touched must have `trigger_price` >= `price`")]
699 fn test_sell_trigger_lt_price(audusd_sim: CurrencyPair) {
700 OrderTestBuilder::new(OrderType::LimitIfTouched)
701 .instrument_id(audusd_sim.id)
702 .side(OrderSide::Sell)
703 .trigger_price(Price::from("30100")) .price(Price::from("30200"))
705 .trigger_type(TriggerType::LastPrice)
706 .quantity(Quantity::from(1))
707 .build();
708 }
709
710 #[rstest]
711 fn test_limit_if_touched_order_update() {
712 let order = OrderTestBuilder::new(OrderType::LimitIfTouched)
714 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
715 .quantity(Quantity::from(10))
716 .price(Price::new(100.0, 2))
717 .trigger_price(Price::new(95.0, 2))
718 .trigger_type(TriggerType::Default)
719 .build();
720
721 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
722
723 let updated_price = Price::new(105.0, 2);
725 let updated_trigger_price = Price::new(97.0, 2);
726 let updated_quantity = Quantity::from(5);
727
728 let event = OrderUpdated {
729 client_order_id: accepted_order.client_order_id(),
730 strategy_id: accepted_order.strategy_id(),
731 price: Some(updated_price),
732 trigger_price: Some(updated_trigger_price),
733 quantity: updated_quantity,
734 ..Default::default()
735 };
736
737 accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
738
739 assert_eq!(accepted_order.price(), Some(updated_price));
741 assert_eq!(accepted_order.trigger_price(), Some(updated_trigger_price));
742 assert_eq!(accepted_order.quantity(), updated_quantity);
743 }
744
745 #[rstest]
746 fn test_limit_if_touched_order_from_order_initialized() {
747 let order_initialized = OrderInitializedBuilder::default()
749 .price(Some(Price::new(100.0, 2)))
750 .trigger_price(Some(Price::new(95.0, 2)))
751 .trigger_type(Some(TriggerType::Default))
752 .order_type(OrderType::LimitIfTouched)
753 .build()
754 .unwrap();
755
756 let order: LimitIfTouchedOrder = order_initialized.clone().into();
758
759 assert_eq!(order.trader_id(), order_initialized.trader_id);
761 assert_eq!(order.strategy_id(), order_initialized.strategy_id);
762 assert_eq!(order.instrument_id(), order_initialized.instrument_id);
763 assert_eq!(order.client_order_id(), order_initialized.client_order_id);
764 assert_eq!(order.order_side(), order_initialized.order_side);
765 assert_eq!(order.quantity(), order_initialized.quantity);
766
767 assert_eq!(order.price, order_initialized.price.unwrap());
769 assert_eq!(
770 order.trigger_price,
771 order_initialized.trigger_price.unwrap()
772 );
773 assert_eq!(order.trigger_type, order_initialized.trigger_type.unwrap());
774 }
775
776 #[rstest]
777 fn test_limit_if_touched_order_sets_slippage_when_filled() {
778 let order = OrderTestBuilder::new(OrderType::LimitIfTouched)
780 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
781 .quantity(Quantity::from(10))
782 .side(OrderSide::Buy) .price(Price::new(95.0, 2)) .trigger_price(Price::new(90.0, 2)) .trigger_type(TriggerType::Default)
786 .build();
787
788 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
790
791 let fill_quantity = accepted_order.quantity(); let fill_price = Price::new(98.50, 2); let order_filled_event = OrderFilledBuilder::default()
796 .client_order_id(accepted_order.client_order_id())
797 .strategy_id(accepted_order.strategy_id())
798 .instrument_id(accepted_order.instrument_id())
799 .order_side(accepted_order.order_side())
800 .last_qty(fill_quantity)
801 .last_px(fill_price)
802 .venue_order_id(VenueOrderId::from("TEST-001"))
803 .trade_id(TradeId::from("TRADE-001"))
804 .build()
805 .unwrap();
806
807 accepted_order
809 .apply(OrderEventAny::Filled(order_filled_event))
810 .unwrap();
811
812 print!("Slippageee: {:?}", accepted_order.slippage());
814 assert!(accepted_order.slippage().is_some());
815
816 let expected_slippage = 98.50 - 95.0;
818 let actual_slippage = accepted_order.slippage().unwrap();
819
820 assert!(
821 (actual_slippage - expected_slippage).abs() < 0.001,
822 "Expected slippage around {expected_slippage}, was {actual_slippage}"
823 );
824 }
825}