Skip to main content

nautilus_model/orders/
market_if_touched.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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, OrderError, 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    types::{Currency, Money, Price, Quantity, quantity::check_positive_quantity},
39};
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
42#[cfg_attr(
43    feature = "python",
44    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
45)]
46#[cfg_attr(
47    feature = "python",
48    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
49)]
50pub struct MarketIfTouchedOrder {
51    pub trigger_price: Price,
52    pub trigger_type: TriggerType,
53    pub expire_time: Option<UnixNanos>,
54    pub trigger_instrument_id: Option<InstrumentId>,
55    pub is_triggered: bool,
56    pub ts_triggered: Option<UnixNanos>,
57    core: OrderCore,
58}
59
60impl MarketIfTouchedOrder {
61    /// Creates a new [`MarketIfTouchedOrder`] instance.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if:
66    /// - The `quantity` is not positive.
67    /// - The `time_in_force` is GTD and the `expire_time` is `None` or zero.
68    #[expect(clippy::too_many_arguments)]
69    pub fn new_checked(
70        trader_id: TraderId,
71        strategy_id: StrategyId,
72        instrument_id: InstrumentId,
73        client_order_id: ClientOrderId,
74        order_side: OrderSide,
75        quantity: Quantity,
76        trigger_price: Price,
77        trigger_type: TriggerType,
78        time_in_force: TimeInForce,
79        expire_time: Option<UnixNanos>,
80        reduce_only: bool,
81        quote_quantity: bool,
82        emulation_trigger: Option<TriggerType>,
83        trigger_instrument_id: Option<InstrumentId>,
84        contingency_type: Option<ContingencyType>,
85        order_list_id: Option<OrderListId>,
86        linked_order_ids: Option<Vec<ClientOrderId>>,
87        parent_order_id: Option<ClientOrderId>,
88        exec_algorithm_id: Option<ExecAlgorithmId>,
89        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
90        exec_spawn_id: Option<ClientOrderId>,
91        tags: Option<Vec<Ustr>>,
92        init_id: UUID4,
93        ts_init: UnixNanos,
94    ) -> Result<Self, OrderError> {
95        check_positive_quantity(quantity, stringify!(quantity))?;
96        check_time_in_force(time_in_force, expire_time)?;
97
98        let init_order = OrderInitialized::new(
99            trader_id,
100            strategy_id,
101            instrument_id,
102            client_order_id,
103            order_side,
104            OrderType::MarketIfTouched,
105            quantity,
106            time_in_force,
107            false,
108            reduce_only,
109            quote_quantity,
110            false,
111            init_id,
112            ts_init,
113            ts_init,
114            None,
115            Some(trigger_price),
116            Some(trigger_type),
117            None,
118            None,
119            None,
120            expire_time,
121            None,
122            emulation_trigger,
123            trigger_instrument_id,
124            contingency_type,
125            order_list_id,
126            linked_order_ids,
127            parent_order_id,
128            exec_algorithm_id,
129            exec_algorithm_params,
130            exec_spawn_id,
131            tags,
132        );
133
134        Ok(Self {
135            core: OrderCore::new(init_order),
136            trigger_price,
137            trigger_type,
138            expire_time,
139            trigger_instrument_id,
140            is_triggered: false,
141            ts_triggered: None,
142        })
143    }
144
145    /// Creates a new [`MarketIfTouchedOrder`] instance.
146    ///
147    /// # Panics
148    ///
149    /// Panics if any order validation fails (see [`MarketIfTouchedOrder::new_checked`]).
150    #[expect(clippy::too_many_arguments)]
151    #[must_use]
152    pub fn new(
153        trader_id: TraderId,
154        strategy_id: StrategyId,
155        instrument_id: InstrumentId,
156        client_order_id: ClientOrderId,
157        order_side: OrderSide,
158        quantity: Quantity,
159        trigger_price: Price,
160        trigger_type: TriggerType,
161        time_in_force: TimeInForce,
162        expire_time: Option<UnixNanos>,
163        reduce_only: bool,
164        quote_quantity: bool,
165        emulation_trigger: Option<TriggerType>,
166        trigger_instrument_id: Option<InstrumentId>,
167        contingency_type: Option<ContingencyType>,
168        order_list_id: Option<OrderListId>,
169        linked_order_ids: Option<Vec<ClientOrderId>>,
170        parent_order_id: Option<ClientOrderId>,
171        exec_algorithm_id: Option<ExecAlgorithmId>,
172        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
173        exec_spawn_id: Option<ClientOrderId>,
174        tags: Option<Vec<Ustr>>,
175        init_id: UUID4,
176        ts_init: UnixNanos,
177    ) -> Self {
178        Self::new_checked(
179            trader_id,
180            strategy_id,
181            instrument_id,
182            client_order_id,
183            order_side,
184            quantity,
185            trigger_price,
186            trigger_type,
187            time_in_force,
188            expire_time,
189            reduce_only,
190            quote_quantity,
191            emulation_trigger,
192            trigger_instrument_id,
193            contingency_type,
194            order_list_id,
195            linked_order_ids,
196            parent_order_id,
197            exec_algorithm_id,
198            exec_algorithm_params,
199            exec_spawn_id,
200            tags,
201            init_id,
202            ts_init,
203        )
204        .unwrap_or_else(|e| panic!("{FAILED}: {e}"))
205    }
206}
207
208impl PartialEq for MarketIfTouchedOrder {
209    fn eq(&self, other: &Self) -> bool {
210        self.client_order_id == other.client_order_id
211    }
212}
213
214impl Deref for MarketIfTouchedOrder {
215    type Target = OrderCore;
216
217    fn deref(&self) -> &Self::Target {
218        &self.core
219    }
220}
221
222impl DerefMut for MarketIfTouchedOrder {
223    fn deref_mut(&mut self) -> &mut Self::Target {
224        &mut self.core
225    }
226}
227
228impl Order for MarketIfTouchedOrder {
229    fn into_any(self) -> OrderAny {
230        OrderAny::MarketIfTouched(self)
231    }
232
233    fn status(&self) -> OrderStatus {
234        self.status
235    }
236
237    fn trader_id(&self) -> TraderId {
238        self.trader_id
239    }
240
241    fn strategy_id(&self) -> StrategyId {
242        self.strategy_id
243    }
244
245    fn instrument_id(&self) -> InstrumentId {
246        self.instrument_id
247    }
248
249    fn symbol(&self) -> Symbol {
250        self.instrument_id.symbol
251    }
252
253    fn venue(&self) -> Venue {
254        self.instrument_id.venue
255    }
256
257    fn client_order_id(&self) -> ClientOrderId {
258        self.client_order_id
259    }
260
261    fn venue_order_id(&self) -> Option<VenueOrderId> {
262        self.venue_order_id
263    }
264
265    fn position_id(&self) -> Option<PositionId> {
266        self.position_id
267    }
268
269    fn account_id(&self) -> Option<AccountId> {
270        self.account_id
271    }
272
273    fn last_trade_id(&self) -> Option<TradeId> {
274        self.last_trade_id
275    }
276
277    fn order_side(&self) -> OrderSide {
278        self.side
279    }
280
281    fn order_type(&self) -> OrderType {
282        self.order_type
283    }
284
285    fn quantity(&self) -> Quantity {
286        self.quantity
287    }
288
289    fn time_in_force(&self) -> TimeInForce {
290        self.time_in_force
291    }
292
293    fn expire_time(&self) -> Option<UnixNanos> {
294        self.expire_time
295    }
296
297    fn price(&self) -> Option<Price> {
298        None
299    }
300
301    fn trigger_price(&self) -> Option<Price> {
302        Some(self.trigger_price)
303    }
304
305    fn trigger_type(&self) -> Option<TriggerType> {
306        Some(self.trigger_type)
307    }
308
309    fn liquidity_side(&self) -> Option<LiquiditySide> {
310        self.liquidity_side
311    }
312
313    fn is_post_only(&self) -> bool {
314        false
315    }
316
317    fn is_reduce_only(&self) -> bool {
318        self.is_reduce_only
319    }
320
321    fn is_quote_quantity(&self) -> bool {
322        self.is_quote_quantity
323    }
324
325    fn has_price(&self) -> bool {
326        false
327    }
328
329    fn display_qty(&self) -> Option<Quantity> {
330        None
331    }
332
333    fn limit_offset(&self) -> Option<Decimal> {
334        None
335    }
336
337    fn trailing_offset(&self) -> Option<Decimal> {
338        None
339    }
340
341    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
342        None
343    }
344
345    fn emulation_trigger(&self) -> Option<TriggerType> {
346        self.emulation_trigger
347    }
348
349    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
350        self.trigger_instrument_id
351    }
352
353    fn contingency_type(&self) -> Option<ContingencyType> {
354        self.contingency_type
355    }
356
357    fn order_list_id(&self) -> Option<OrderListId> {
358        self.order_list_id
359    }
360
361    fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
362        self.linked_order_ids.as_deref()
363    }
364
365    fn parent_order_id(&self) -> Option<ClientOrderId> {
366        self.parent_order_id
367    }
368
369    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
370        self.exec_algorithm_id
371    }
372
373    fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
374        self.exec_algorithm_params.as_ref()
375    }
376
377    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
378        self.exec_spawn_id
379    }
380
381    fn tags(&self) -> Option<&[Ustr]> {
382        self.tags.as_deref()
383    }
384
385    fn filled_qty(&self) -> Quantity {
386        self.filled_qty
387    }
388
389    fn leaves_qty(&self) -> Quantity {
390        self.leaves_qty
391    }
392
393    fn overfill_qty(&self) -> Quantity {
394        self.overfill_qty
395    }
396
397    fn avg_px(&self) -> Option<f64> {
398        self.avg_px
399    }
400
401    fn slippage(&self) -> Option<f64> {
402        self.slippage
403    }
404
405    fn init_id(&self) -> UUID4 {
406        self.init_id
407    }
408
409    fn ts_init(&self) -> UnixNanos {
410        self.ts_init
411    }
412
413    fn ts_submitted(&self) -> Option<UnixNanos> {
414        self.ts_submitted
415    }
416
417    fn ts_accepted(&self) -> Option<UnixNanos> {
418        self.ts_accepted
419    }
420
421    fn ts_closed(&self) -> Option<UnixNanos> {
422        self.ts_closed
423    }
424
425    fn ts_last(&self) -> UnixNanos {
426        self.ts_last
427    }
428
429    fn events(&self) -> Vec<&OrderEventAny> {
430        self.events.iter().collect()
431    }
432
433    fn commissions(&self) -> &IndexMap<Currency, Money> {
434        &self.commissions
435    }
436
437    fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
438        self.venue_order_ids.iter().collect()
439    }
440
441    fn trade_ids(&self) -> Vec<&TradeId> {
442        self.trade_ids.iter().collect()
443    }
444
445    fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
446        let is_order_filled = matches!(event, OrderEventAny::Filled(_));
447        let is_order_triggered = matches!(event, OrderEventAny::Triggered(_));
448        let ts_event = if is_order_triggered {
449            Some(event.ts_event())
450        } else {
451            None
452        };
453
454        self.core.apply(event.clone())?;
455
456        if let OrderEventAny::Updated(ref event) = event {
457            self.update(event);
458        }
459
460        if is_order_triggered {
461            self.is_triggered = true;
462            self.ts_triggered = ts_event;
463        }
464
465        if is_order_filled {
466            self.core.set_slippage(self.trigger_price);
467        }
468
469        Ok(())
470    }
471
472    fn update(&mut self, event: &OrderUpdated) {
473        assert!(event.price.is_none(), "{}", OrderError::InvalidOrderEvent);
474
475        if let Some(trigger_price) = event.trigger_price {
476            self.trigger_price = trigger_price;
477        }
478
479        self.quantity = event.quantity;
480        self.leaves_qty = self.quantity.saturating_sub(self.filled_qty);
481    }
482
483    fn is_triggered(&self) -> Option<bool> {
484        Some(self.is_triggered)
485    }
486
487    fn set_position_id(&mut self, position_id: Option<PositionId>) {
488        self.position_id = position_id;
489    }
490
491    fn set_quantity(&mut self, quantity: Quantity) {
492        self.quantity = quantity;
493    }
494
495    fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
496        self.leaves_qty = leaves_qty;
497    }
498
499    fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
500        self.emulation_trigger = emulation_trigger;
501    }
502
503    fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
504        self.is_quote_quantity = is_quote_quantity;
505    }
506
507    fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
508        self.liquidity_side = Some(liquidity_side);
509    }
510
511    fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
512        self.core.would_reduce_only(side, position_qty)
513    }
514
515    fn previous_status(&self) -> Option<OrderStatus> {
516        self.core.previous_status
517    }
518}
519
520impl From<OrderInitialized> for MarketIfTouchedOrder {
521    fn from(event: OrderInitialized) -> Self {
522        Self::new(
523            event.trader_id,
524            event.strategy_id,
525            event.instrument_id,
526            event.client_order_id,
527            event.order_side,
528            event.quantity,
529            event
530            .trigger_price // TODO: Improve this error, model order domain errors
531            .expect(
532                "Error initializing order: `trigger_price` was `None` for `MarketIfTouchedOrder`",
533            ),
534            event.trigger_type.expect(
535                "Error initializing order: `trigger_type` was `None` for `MarketIfTouchedOrder`",
536            ),
537            event.time_in_force,
538            event.expire_time,
539            event.reduce_only,
540            event.quote_quantity,
541            event.emulation_trigger,
542            event.trigger_instrument_id,
543            event.contingency_type,
544            event.order_list_id,
545            event.linked_order_ids,
546            event.parent_order_id,
547            event.exec_algorithm_id,
548            event.exec_algorithm_params,
549            event.exec_spawn_id,
550            event.tags,
551            event.event_id,
552            event.ts_event,
553        )
554    }
555}
556
557impl Display for MarketIfTouchedOrder {
558    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559        write!(
560            f,
561            "MarketIfTouchedOrder {{ \
562                side: {}, \
563                qty: {}, \
564                instrument: {}, \
565                tif: {}, \
566                trigger_price: {}, \
567                trigger_type: {}, \
568                status: {} \
569            }}",
570            self.side,
571            self.quantity,
572            self.instrument_id,
573            self.time_in_force,
574            self.trigger_price,
575            self.trigger_type,
576            self.status
577        )
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use rstest::rstest;
584
585    use super::*;
586    use crate::{
587        enums::{OrderSide, OrderType, TimeInForce, TriggerType},
588        events::order::spec::{OrderFilledSpec, OrderInitializedSpec},
589        identifiers::{InstrumentId, TradeId, VenueOrderId},
590        instruments::{CurrencyPair, stubs::*},
591        orders::{builder::OrderTestBuilder, stubs::TestOrderStubs},
592        types::{Price, Quantity},
593    };
594
595    #[rstest]
596    fn test_initialize(audusd_sim: CurrencyPair) {
597        let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
598            .instrument_id(audusd_sim.id)
599            .side(OrderSide::Buy)
600            .trigger_price(Price::from("0.68000"))
601            .quantity(Quantity::from(1))
602            .build();
603
604        assert_eq!(order.trigger_price(), Some(Price::from("0.68000")));
605        assert_eq!(order.price(), None);
606
607        assert_eq!(order.time_in_force(), TimeInForce::Gtc);
608
609        assert_eq!(order.is_triggered(), Some(false));
610        assert_eq!(order.filled_qty(), Quantity::from(0));
611        assert_eq!(order.leaves_qty(), Quantity::from(1));
612
613        assert_eq!(order.display_qty(), None);
614        assert_eq!(order.trigger_instrument_id(), None);
615        assert_eq!(order.order_list_id(), None);
616    }
617
618    #[rstest]
619    fn test_display(audusd_sim: CurrencyPair) {
620        let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
621            .instrument_id(audusd_sim.id)
622            .side(OrderSide::Buy)
623            .trigger_price(Price::from("30000"))
624            .trigger_type(TriggerType::LastPrice)
625            .quantity(Quantity::from(1))
626            .build();
627
628        assert_eq!(
629            order.to_string(),
630            "MarketIfTouchedOrder { \
631                side: BUY, \
632                qty: 1, \
633                instrument: AUD/USD.SIM, \
634                tif: GTC, \
635                trigger_price: 30000, \
636                trigger_type: LAST_PRICE, \
637                status: INITIALIZED \
638            }"
639        );
640    }
641
642    #[rstest]
643    #[should_panic(
644        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
645    )]
646    fn test_quantity_zero(audusd_sim: CurrencyPair) {
647        let _ = OrderTestBuilder::new(OrderType::MarketIfTouched)
648            .instrument_id(audusd_sim.id)
649            .side(OrderSide::Buy)
650            .trigger_price(Price::from("30000"))
651            .trigger_type(TriggerType::LastPrice)
652            .quantity(Quantity::from(0))
653            .build();
654    }
655
656    #[rstest]
657    #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
658    fn test_gtd_without_expire(audusd_sim: CurrencyPair) {
659        let _ = OrderTestBuilder::new(OrderType::MarketIfTouched)
660            .instrument_id(audusd_sim.id)
661            .side(OrderSide::Buy)
662            .trigger_price(Price::from("30000"))
663            .trigger_type(TriggerType::LastPrice)
664            .quantity(Quantity::from(1))
665            .time_in_force(TimeInForce::Gtd)
666            .build();
667    }
668
669    #[rstest]
670    fn test_market_if_touched_order_update() {
671        // Create and accept a basic MarketIfTouchedOrder
672        let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
673            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
674            .quantity(Quantity::from(10))
675            .trigger_price(Price::new(100.0, 2))
676            .build();
677
678        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
679
680        // Update with new values
681        let updated_trigger_price = Price::new(95.0, 2);
682        let updated_quantity = Quantity::from(5);
683
684        let event = OrderUpdated {
685            client_order_id: accepted_order.client_order_id(),
686            strategy_id: accepted_order.strategy_id(),
687            trigger_price: Some(updated_trigger_price),
688            quantity: updated_quantity,
689            ..Default::default()
690        };
691
692        accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
693
694        // Verify updates were applied correctly
695        assert_eq!(accepted_order.quantity(), updated_quantity);
696        assert_eq!(accepted_order.trigger_price(), Some(updated_trigger_price));
697    }
698
699    #[rstest]
700    fn test_market_if_touched_order_from_order_initialized() {
701        // Create an OrderInitialized event with all required fields for a MarketIfTouchedOrder
702        let order_initialized = OrderInitializedSpec::builder()
703            .trigger_price(Price::new(100.0, 2))
704            .trigger_type(TriggerType::Default)
705            .order_type(OrderType::MarketIfTouched)
706            .build();
707
708        // Convert the OrderInitialized event into a MarketIfTouchedOrder
709        let order: MarketIfTouchedOrder = order_initialized.clone().into();
710
711        // Assert essential fields match the OrderInitialized fields
712        assert_eq!(order.trader_id(), order_initialized.trader_id);
713        assert_eq!(order.strategy_id(), order_initialized.strategy_id);
714        assert_eq!(order.instrument_id(), order_initialized.instrument_id);
715        assert_eq!(order.client_order_id(), order_initialized.client_order_id);
716        assert_eq!(order.order_side(), order_initialized.order_side);
717        assert_eq!(order.quantity(), order_initialized.quantity);
718
719        // Assert specific fields for MarketIfTouchedOrder
720        assert_eq!(
721            order.trigger_price,
722            order_initialized.trigger_price.unwrap()
723        );
724        assert_eq!(order.trigger_type, order_initialized.trigger_type.unwrap());
725    }
726
727    #[rstest]
728    fn test_market_if_touched_order_sets_slippage_when_filled() {
729        // Create a MarketIfTouchedOrder
730        let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
731            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
732            .quantity(Quantity::from(10))
733            .side(OrderSide::Buy) // Explicitly setting Buy side
734            .trigger_price(Price::new(90.0, 2)) // Trigger price LOWER than fill price
735            .build();
736
737        // Accept the order first
738        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
739
740        // Create a filled event with the correct quantity
741        let fill_quantity = accepted_order.quantity(); // Use the same quantity as the order
742        let fill_price = Price::new(98.50, 2); // Use a price HIGHER than trigger price
743
744        let order_filled_event = OrderFilledSpec::builder()
745            .client_order_id(accepted_order.client_order_id())
746            .strategy_id(accepted_order.strategy_id())
747            .instrument_id(accepted_order.instrument_id())
748            .order_side(accepted_order.order_side())
749            .last_qty(fill_quantity)
750            .last_px(fill_price)
751            .venue_order_id(VenueOrderId::from("TEST-001"))
752            .trade_id(TradeId::from("TRADE-001"))
753            .build();
754
755        // Apply the fill event
756        accepted_order
757            .apply(OrderEventAny::Filled(order_filled_event))
758            .unwrap();
759
760        // The slippage calculation should be triggered by the filled event
761        assert!(accepted_order.slippage().is_some());
762
763        // We can also check the actual slippage value
764        let expected_slippage = 98.50 - 90.0; // For buy order: execution price - trigger price
765        let actual_slippage = accepted_order.slippage().unwrap();
766
767        assert!(
768            (actual_slippage - expected_slippage).abs() < 0.001,
769            "Expected slippage around {expected_slippage}, was {actual_slippage}"
770        );
771    }
772}