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 leaves_qty(&self) -> Quantity {
415 self.leaves_qty
416 }
417
418 fn overfill_qty(&self) -> Quantity {
419 self.overfill_qty
420 }
421
422 fn avg_px(&self) -> Option<Decimal> {
423 self.avg_px
424 }
425
426 fn slippage(&self) -> Option<Decimal> {
427 self.slippage
428 }
429
430 fn init_id(&self) -> UUID4 {
431 self.init_id
432 }
433
434 fn ts_init(&self) -> UnixNanos {
435 self.ts_init
436 }
437
438 fn ts_submitted(&self) -> Option<UnixNanos> {
439 self.ts_submitted
440 }
441
442 fn ts_accepted(&self) -> Option<UnixNanos> {
443 self.ts_accepted
444 }
445
446 fn ts_closed(&self) -> Option<UnixNanos> {
447 self.ts_closed
448 }
449
450 fn ts_last(&self) -> UnixNanos {
451 self.ts_last
452 }
453
454 fn events(&self) -> Vec<&OrderEventAny> {
455 self.events.iter().collect()
456 }
457
458 fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
459 self.venue_order_ids.iter().collect()
460 }
461
462 fn commissions(&self) -> &IndexMap<Currency, Money> {
463 &self.commissions
464 }
465
466 fn trade_ids(&self) -> Vec<&TradeId> {
467 self.trade_ids.iter().collect()
468 }
469
470 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
471 let updates_slippage = matches!(
472 event,
473 OrderEventAny::Filled(_) | OrderEventAny::FillVoided(_),
474 );
475 let is_order_triggered = matches!(event, OrderEventAny::Triggered(_));
476 let ts_event = if is_order_triggered {
477 Some(event.ts_event())
478 } else {
479 None
480 };
481
482 self.core.apply(event.clone())?;
483
484 if let OrderEventAny::Updated(ref event) = event {
485 self.update(event);
486 }
487
488 if is_order_triggered {
489 self.is_triggered = true;
490 self.ts_triggered = ts_event;
491 }
492
493 if updates_slippage {
494 self.core.set_slippage(self.price);
495 }
496
497 Ok(())
498 }
499
500 fn update(&mut self, event: &OrderUpdated) {
501 if let Some(price) = event.price {
502 self.price = price;
503 }
504
505 if let Some(trigger_price) = event.trigger_price {
506 self.trigger_price = trigger_price;
507 }
508
509 self.quantity = event.quantity;
510 self.leaves_qty = self.quantity.saturating_sub(self.filled_qty);
511 }
512
513 fn is_triggered(&self) -> Option<bool> {
514 Some(self.is_triggered)
515 }
516
517 fn set_position_id(&mut self, position_id: Option<PositionId>) {
518 self.position_id = position_id;
519 }
520
521 fn set_quantity(&mut self, quantity: Quantity) {
522 self.quantity = quantity;
523 }
524
525 fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
526 self.leaves_qty = leaves_qty;
527 }
528
529 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
530 self.emulation_trigger = emulation_trigger;
531 }
532
533 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
534 self.is_quote_quantity = is_quote_quantity;
535 }
536
537 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
538 self.liquidity_side = Some(liquidity_side);
539 }
540
541 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
542 self.core.would_reduce_only(side, position_qty)
543 }
544
545 fn previous_status(&self) -> Option<OrderStatus> {
546 self.core.previous_status
547 }
548}
549
550impl TryFrom<OrderInitialized> for StopLimitOrder {
551 type Error = OrderError;
552
553 fn try_from(event: OrderInitialized) -> Result<Self, Self::Error> {
554 let price = event
555 .price
556 .ok_or_else(|| CorrectnessError::PredicateViolation {
557 message: "`price` is required for `StopLimitOrder` initialization".to_string(),
558 })?;
559 let trigger_price =
560 event
561 .trigger_price
562 .ok_or_else(|| CorrectnessError::PredicateViolation {
563 message: "`trigger_price` is required for `StopLimitOrder` initialization"
564 .to_string(),
565 })?;
566 let trigger_type =
567 event
568 .trigger_type
569 .ok_or_else(|| CorrectnessError::PredicateViolation {
570 message: "`trigger_type` is required for `StopLimitOrder` initialization"
571 .to_string(),
572 })?;
573 Self::new_checked(
574 event.trader_id,
575 event.strategy_id,
576 event.instrument_id,
577 event.client_order_id,
578 event.order_side,
579 event.quantity,
580 price,
581 trigger_price,
582 trigger_type,
583 event.time_in_force,
584 event.expire_time,
585 event.post_only,
586 event.reduce_only,
587 event.quote_quantity,
588 event.display_qty,
589 event.emulation_trigger,
590 event.trigger_instrument_id,
591 event.contingency_type,
592 event.order_list_id,
593 event.linked_order_ids,
594 event.parent_order_id,
595 event.exec_algorithm_id,
596 event.exec_algorithm_params,
597 event.exec_spawn_id,
598 event.tags,
599 event.event_id,
600 event.ts_event,
601 )
602 }
603}
604
605impl Display for StopLimitOrder {
606 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607 write!(
608 f,
609 "StopLimitOrder({} {} {} {} @ {}-STOP[{}] {}-LIMIT {}, status={}, client_order_id={}, venue_order_id={}, position_id={}, tags={})",
610 self.side,
611 self.quantity.to_formatted_string(),
612 self.instrument_id,
613 self.order_type,
614 self.trigger_price,
615 self.trigger_type,
616 self.price,
617 self.time_in_force,
618 self.status,
619 self.client_order_id,
620 self.venue_order_id
621 .map_or("None".to_string(), |venue_order_id| format!(
622 "{venue_order_id}"
623 )),
624 self.position_id
625 .map_or("None".to_string(), |position_id| format!("{position_id}")),
626 self.tags.clone().map_or("None".to_string(), |tags| tags
627 .iter()
628 .map(ToString::to_string)
629 .collect::<Vec<String>>()
630 .join(", ")),
631 )
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use nautilus_core::UnixNanos;
638 use rstest::rstest;
639
640 use super::*;
641 use crate::{
642 enums::{OrderSide, PositionSide, TimeInForce, TriggerType},
643 events::order::spec::OrderInitializedSpec,
644 identifiers::InstrumentId,
645 instruments::{CurrencyPair, stubs::*},
646 orders::{OrderTestBuilder, stubs::TestOrderStubs},
647 types::{Price, Quantity},
648 };
649
650 #[rstest]
651 fn test_initialize(audusd_sim: CurrencyPair) {
652 let order = OrderTestBuilder::new(OrderType::StopLimit)
654 .instrument_id(audusd_sim.id)
655 .side(OrderSide::Buy)
656 .trigger_price(Price::from("0.68000"))
657 .price(Price::from("0.68100"))
658 .trigger_type(TriggerType::LastPrice)
659 .quantity(Quantity::from(1))
660 .build();
661
662 assert_eq!(order.trigger_price(), Some(Price::from("0.68000")));
663 assert_eq!(order.price(), Some(Price::from("0.68100")));
664
665 assert_eq!(order.time_in_force(), TimeInForce::Gtc);
666
667 assert_eq!(order.is_triggered(), Some(false));
668 assert_eq!(order.filled_qty(), Quantity::from(0));
669 assert_eq!(order.leaves_qty(), Quantity::from(1));
670
671 assert_eq!(order.display_qty(), None);
672 assert_eq!(order.trigger_instrument_id(), None);
673 assert_eq!(order.order_list_id(), None);
674 }
675
676 #[rstest]
677 fn test_display(audusd_sim: CurrencyPair) {
678 let order = OrderTestBuilder::new(OrderType::MarketToLimit)
679 .instrument_id(audusd_sim.id)
680 .side(OrderSide::Buy)
681 .quantity(Quantity::from(1))
682 .build();
683
684 assert_eq!(
685 order.to_string(),
686 "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)"
687 );
688 }
689
690 #[rstest]
691 #[should_panic(expected = "display_qty` may not exceed `quantity")]
692 fn test_display_qty_gt_quantity_err(audusd_sim: CurrencyPair) {
693 let _ = OrderTestBuilder::new(OrderType::StopLimit)
694 .instrument_id(audusd_sim.id)
695 .side(OrderSide::Buy)
696 .trigger_price(Price::from("30300"))
697 .price(Price::from("30100"))
698 .trigger_type(TriggerType::LastPrice)
699 .quantity(Quantity::from(1))
700 .display_qty(Quantity::from(2))
701 .build();
702 }
703
704 #[rstest]
705 #[should_panic(expected = "Quantity must be non-negative")]
706 fn test_display_qty_negative_err(audusd_sim: CurrencyPair) {
707 let _ = OrderTestBuilder::new(OrderType::StopLimit)
708 .instrument_id(audusd_sim.id)
709 .side(OrderSide::Buy)
710 .trigger_price(Price::from("30300"))
711 .price(Price::from("30100"))
712 .trigger_type(TriggerType::LastPrice)
713 .quantity(Quantity::from(1))
714 .display_qty(Quantity::from("-1"))
715 .build();
716 }
717
718 #[rstest]
719 #[should_panic(expected = "expire_time` is required for `GTD` order")]
720 fn test_gtd_without_expire_time_err(audusd_sim: CurrencyPair) {
721 let _ = OrderTestBuilder::new(OrderType::StopLimit)
722 .instrument_id(audusd_sim.id)
723 .side(OrderSide::Buy)
724 .trigger_price(Price::from("30300"))
725 .price(Price::from("30100"))
726 .trigger_type(TriggerType::LastPrice)
727 .time_in_force(TimeInForce::Gtd)
728 .quantity(Quantity::from(1))
729 .build();
730 }
731 #[rstest]
732 fn test_stop_limit_order_update() {
733 let order = OrderTestBuilder::new(OrderType::StopLimit)
735 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
736 .quantity(Quantity::from(10))
737 .price(Price::new(100.0, 2))
738 .trigger_price(Price::new(95.0, 2))
739 .build();
740
741 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
742
743 let updated_price = Price::new(105.0, 2);
745 let updated_trigger_price = Price::new(90.0, 2);
746 let updated_quantity = Quantity::from(5);
747
748 let event = OrderUpdated {
749 client_order_id: accepted_order.client_order_id(),
750 strategy_id: accepted_order.strategy_id(),
751 price: Some(updated_price),
752 trigger_price: Some(updated_trigger_price),
753 quantity: updated_quantity,
754 ..Default::default()
755 };
756
757 accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
758
759 assert_eq!(accepted_order.quantity(), updated_quantity);
761 assert_eq!(accepted_order.price(), Some(updated_price));
762 assert_eq!(accepted_order.trigger_price(), Some(updated_trigger_price));
763 }
764
765 #[rstest]
766 fn test_stop_limit_order_expire_time() {
767 let expire_time = UnixNanos::from(1_234_567_890);
769 let order = OrderTestBuilder::new(OrderType::StopLimit)
770 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
771 .quantity(Quantity::from(10))
772 .price(Price::new(100.0, 2))
773 .trigger_price(Price::new(95.0, 2))
774 .expire_time(expire_time)
775 .build();
776
777 assert_eq!(order.expire_time(), Some(expire_time));
779 }
780
781 #[rstest]
782 fn test_stop_limit_order_post_only() {
783 let order = OrderTestBuilder::new(OrderType::StopLimit)
785 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
786 .quantity(Quantity::from(10))
787 .price(Price::new(100.0, 2))
788 .trigger_price(Price::new(95.0, 2))
789 .post_only(true)
790 .build();
791
792 assert!(order.is_post_only());
794 }
795
796 #[rstest]
797 fn test_stop_limit_order_reduce_only() {
798 let order = OrderTestBuilder::new(OrderType::StopLimit)
800 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
801 .quantity(Quantity::from(10))
802 .price(Price::new(100.0, 2))
803 .trigger_price(Price::new(95.0, 2))
804 .reduce_only(true)
805 .build();
806
807 assert!(order.is_reduce_only());
809 }
810
811 #[rstest]
812 fn test_stop_limit_order_trigger_instrument_id() {
813 let trigger_instrument_id = InstrumentId::from("ETH-USDT.BINANCE");
815 let order = OrderTestBuilder::new(OrderType::StopLimit)
816 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
817 .quantity(Quantity::from(10))
818 .price(Price::new(100.0, 2))
819 .trigger_price(Price::new(95.0, 2))
820 .trigger_instrument_id(trigger_instrument_id)
821 .build();
822
823 assert_eq!(order.trigger_instrument_id(), Some(trigger_instrument_id));
825 }
826
827 #[rstest]
828 fn test_stop_limit_order_would_reduce_only() {
829 let order = OrderTestBuilder::new(OrderType::StopLimit)
831 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
832 .side(OrderSide::Sell)
833 .quantity(Quantity::from(10))
834 .price(Price::new(100.0, 2))
835 .trigger_price(Price::new(95.0, 2))
836 .build();
837
838 assert!(order.would_reduce_only(PositionSide::Long, Quantity::from(15)));
840 assert!(!order.would_reduce_only(PositionSide::Short, Quantity::from(15)));
841 assert!(!order.would_reduce_only(PositionSide::Long, Quantity::from(5)));
842 }
843
844 #[rstest]
845 fn test_stop_limit_order_display_string() {
846 let order = OrderTestBuilder::new(OrderType::StopLimit)
848 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
849 .side(OrderSide::Buy)
850 .quantity(Quantity::from(10))
851 .price(Price::new(100.0, 2))
852 .trigger_price(Price::new(95.0, 2))
853 .client_order_id(ClientOrderId::from("ORDER-001"))
854 .build();
855
856 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)";
858
859 assert_eq!(order.to_string(), expected);
861 assert_eq!(format!("{order}"), expected);
862 }
863
864 #[rstest]
865 fn test_stop_limit_order_from_order_initialized() {
866 let order_initialized = OrderInitializedSpec::builder()
868 .order_type(OrderType::StopLimit)
869 .quantity(Quantity::from(10))
870 .price(Price::new(100.0, 2))
871 .trigger_price(Price::new(95.0, 2))
872 .trigger_type(TriggerType::Default)
873 .post_only(true)
874 .reduce_only(true)
875 .expire_time(UnixNanos::from(1_234_567_890))
876 .display_qty(Quantity::from(5))
877 .build();
878
879 let order: StopLimitOrder = order_initialized.clone().try_into().unwrap();
881
882 assert_eq!(order.trader_id(), order_initialized.trader_id);
884 assert_eq!(order.strategy_id(), order_initialized.strategy_id);
885 assert_eq!(order.instrument_id(), order_initialized.instrument_id);
886 assert_eq!(order.client_order_id(), order_initialized.client_order_id);
887 assert_eq!(order.order_side(), order_initialized.order_side);
888 assert_eq!(order.quantity(), order_initialized.quantity);
889
890 assert_eq!(order.price, order_initialized.price.unwrap());
892 assert_eq!(
893 order.trigger_price,
894 order_initialized.trigger_price.unwrap()
895 );
896 assert_eq!(order.trigger_type, order_initialized.trigger_type.unwrap());
897 assert_eq!(order.expire_time(), order_initialized.expire_time);
898 assert_eq!(order.is_post_only(), order_initialized.post_only);
899 assert_eq!(order.is_reduce_only(), order_initialized.reduce_only);
900 assert_eq!(order.display_qty(), order_initialized.display_qty);
901
902 assert_eq!(order.order_type(), OrderType::StopLimit);
904
905 assert_eq!(order.is_triggered(), Some(false));
907 }
908}