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