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