nautilus_model/orders/
limit_if_touched.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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, 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    /// Creates a new [`LimitIfTouchedOrder`] instance.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if:
67    /// - The `quantity` is not positive.
68    /// - The `display_qty` (when provided) exceeds `quantity`.
69    /// - The `time_in_force` is GTD and the `expire_time` is `None` or zero.
70    #[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    /// Creates a new [`LimitIfTouchedOrder`] instance.
165    ///
166    /// # Panics
167    ///
168    /// Panics if any order validation fails (see [`LimitIfTouchedOrder::new_checked`]).
169    #[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 // TODO: Improve this error, model order domain errors
568                .expect("Error initializing order: `price` was `None` for `LimitIfTouchedOrder"),
569            event
570                .trigger_price // TODO: Improve this error, model order domain errors
571                .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")) // Invalid trigger > price
691            .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")) // Invalid trigger < price
704            .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        // Create and accept a basic limit-if-touched order
713        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        // Update with new values
724        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        // Verify updates were applied correctly
740        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        // Create an OrderInitialized event with all required fields for a LimitIfTouchedOrder
748        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        // Convert the OrderInitialized event into a LimitIfTouchedOrder
757        let order: LimitIfTouchedOrder = order_initialized.clone().into();
758
759        // Assert essential fields match the OrderInitialized fields
760        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 specific fields for LimitIfTouchedOrder
768        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        // Create a limit-if-touched order
779        let order = OrderTestBuilder::new(OrderType::LimitIfTouched)
780            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
781            .quantity(Quantity::from(10))
782            .side(OrderSide::Buy) // Explicitly setting Buy side
783            .price(Price::new(95.0, 2)) // Limit price
784            .trigger_price(Price::new(90.0, 2)) // Trigger price LOWER than fill price
785            .trigger_type(TriggerType::Default)
786            .build();
787
788        // Accept the order first
789        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
790
791        // Create a filled event with the correct quantity
792        let fill_quantity = accepted_order.quantity(); // Use the same quantity as the order
793        let fill_price = Price::new(98.50, 2); // Use a price LOWER than limit price
794
795        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        // Apply the fill event
808        accepted_order
809            .apply(OrderEventAny::Filled(order_filled_event))
810            .unwrap();
811
812        // The slippage calculation should be triggered by the filled event
813        print!("Slippageee: {:?}", accepted_order.slippage());
814        assert!(accepted_order.slippage().is_some());
815
816        // We can also check the actual slippage value
817        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}