1use std::{
17 fmt::Display,
18 ops::{Deref, DerefMut},
19};
20
21use indexmap::IndexMap;
22use nautilus_core::{UUID4, UnixNanos, correctness::FAILED};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{Order, OrderAny, OrderCore, 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 #[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 #[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 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}