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