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