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