Skip to main content

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