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 venue_order_ids(&self) -> Vec<&VenueOrderId> {
457 self.venue_order_ids.iter().collect()
458 }
459
460 fn trade_ids(&self) -> Vec<&TradeId> {
461 self.trade_ids.iter().collect()
462 }
463
464 fn commissions(&self) -> &IndexMap<Currency, Money> {
465 &self.commissions
466 }
467
468 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
469 let updates_slippage = matches!(
470 event,
471 OrderEventAny::Filled(_) | OrderEventAny::FillVoided(_),
472 );
473 let is_order_triggered = matches!(event, OrderEventAny::Triggered(_));
474 let ts_event = if is_order_triggered {
475 Some(event.ts_event())
476 } else {
477 None
478 };
479
480 self.core.apply(event.clone())?;
481
482 if let OrderEventAny::Updated(ref event) = event {
483 self.update(event);
484 }
485
486 if is_order_triggered {
487 self.is_triggered = true;
488 self.ts_triggered = ts_event;
489 }
490
491 if updates_slippage {
492 self.core.set_slippage(self.trigger_price);
493 }
494
495 Ok(())
496 }
497
498 fn update(&mut self, event: &OrderUpdated) {
499 assert!(event.price.is_none(), "{}", OrderError::InvalidOrderEvent);
500
501 if let Some(trigger_price) = event.trigger_price {
502 self.trigger_price = trigger_price;
503 }
504
505 if let Some(protection_price) = event.protection_price {
506 self.protection_price = Some(protection_price);
507 }
508 self.core.apply_updated_quantity(event.quantity);
509 }
510
511 fn is_triggered(&self) -> Option<bool> {
512 Some(self.is_triggered)
513 }
514
515 fn set_position_id(&mut self, position_id: Option<PositionId>) {
516 self.position_id = position_id;
517 }
518
519 fn set_quantity(&mut self, quantity: Quantity) {
520 self.quantity = quantity;
521 }
522
523 fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
524 self.leaves_qty = leaves_qty;
525 }
526
527 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
528 self.emulation_trigger = emulation_trigger;
529 }
530
531 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
532 self.is_quote_quantity = is_quote_quantity;
533 }
534
535 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
536 self.liquidity_side = Some(liquidity_side);
537 }
538
539 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
540 self.core.would_reduce_only(side, position_qty)
541 }
542
543 fn previous_status(&self) -> Option<OrderStatus> {
544 self.core.previous_status
545 }
546}
547
548impl Display for StopMarketOrder {
549 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
550 write!(
551 f,
552 "StopMarketOrder(\
553 {} {} {} {} {}, \
554 status={}, \
555 client_order_id={}, \
556 venue_order_id={}, \
557 position_id={}, \
558 exec_algorithm_id={}, \
559 exec_spawn_id={}, \
560 tags={:?}\
561 )",
562 self.side,
563 self.quantity.to_formatted_string(),
564 self.instrument_id,
565 self.order_type,
566 self.time_in_force,
567 self.status,
568 self.client_order_id,
569 self.venue_order_id.map_or_else(
570 || "None".to_string(),
571 |venue_order_id| format!("{venue_order_id}")
572 ),
573 self.position_id.map_or_else(
574 || "None".to_string(),
575 |position_id| format!("{position_id}")
576 ),
577 self.exec_algorithm_id
578 .map_or_else(|| "None".to_string(), |id| format!("{id}")),
579 self.exec_spawn_id
580 .map_or_else(|| "None".to_string(), |id| format!("{id}")),
581 self.tags
582 )
583 }
584}
585
586impl TryFrom<OrderInitialized> for StopMarketOrder {
587 type Error = OrderError;
588
589 fn try_from(event: OrderInitialized) -> Result<Self, Self::Error> {
590 let trigger_price =
591 event
592 .trigger_price
593 .ok_or_else(|| CorrectnessError::PredicateViolation {
594 message: "`trigger_price` is required for `StopMarketOrder` initialization"
595 .to_string(),
596 })?;
597 let trigger_type =
598 event
599 .trigger_type
600 .ok_or_else(|| CorrectnessError::PredicateViolation {
601 message: "`trigger_type` is required for `StopMarketOrder` initialization"
602 .to_string(),
603 })?;
604 Self::new_checked(
605 event.trader_id,
606 event.strategy_id,
607 event.instrument_id,
608 event.client_order_id,
609 event.order_side,
610 event.quantity,
611 trigger_price,
612 trigger_type,
613 event.time_in_force,
614 event.expire_time,
615 event.reduce_only,
616 event.quote_quantity,
617 event.display_qty,
618 event.emulation_trigger,
619 event.trigger_instrument_id,
620 event.contingency_type,
621 event.order_list_id,
622 event.linked_order_ids,
623 event.parent_order_id,
624 event.exec_algorithm_id,
625 event.exec_algorithm_params,
626 event.exec_spawn_id,
627 event.tags,
628 event.event_id,
629 event.ts_event,
630 )
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use rstest::rstest;
637
638 use super::*;
639 use crate::{
640 enums::{TimeInForce, TriggerType},
641 events::order::spec::OrderInitializedSpec,
642 identifiers::InstrumentId,
643 instruments::{CurrencyPair, stubs::*},
644 orders::{builder::OrderTestBuilder, stubs::TestOrderStubs},
645 types::{Price, Quantity},
646 };
647
648 #[rstest]
649 fn test_initialize(audusd_sim: CurrencyPair) {
650 let order = OrderTestBuilder::new(OrderType::StopMarket)
651 .instrument_id(audusd_sim.id)
652 .side(OrderSide::Buy)
653 .trigger_price(Price::from("0.68000"))
654 .trigger_type(TriggerType::LastPrice)
655 .quantity(Quantity::from(1))
656 .build();
657
658 assert_eq!(order.trigger_price(), Some(Price::from("0.68000")));
659 assert_eq!(order.price(), None);
660
661 assert_eq!(order.time_in_force(), TimeInForce::Gtc);
662
663 assert_eq!(order.is_triggered(), Some(false));
664 assert_eq!(order.filled_qty(), Quantity::from(0));
665 assert_eq!(order.leaves_qty(), Quantity::from(1));
666
667 assert_eq!(order.display_qty(), None);
668 assert_eq!(order.trigger_instrument_id(), None);
669 assert_eq!(order.order_list_id(), None);
670 }
671
672 #[rstest]
673 fn test_display(audusd_sim: CurrencyPair) {
674 let order = OrderTestBuilder::new(OrderType::StopMarket)
675 .instrument_id(audusd_sim.id)
676 .side(OrderSide::Buy)
677 .trigger_price(Price::from("0.68000"))
678 .trigger_type(TriggerType::LastPrice)
679 .quantity(Quantity::from(1))
680 .build();
681
682 assert_eq!(
683 order.to_string(),
684 "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)"
685 );
686 }
687
688 #[rstest]
689 #[should_panic(expected = "Condition failed: `display_qty` may not exceed `quantity`")]
690 fn test_display_qty_gt_quantity_err(audusd_sim: CurrencyPair) {
691 let _ = OrderTestBuilder::new(OrderType::StopMarket)
692 .instrument_id(audusd_sim.id)
693 .side(OrderSide::Buy)
694 .trigger_price(Price::from("0.68000"))
695 .trigger_type(TriggerType::LastPrice)
696 .quantity(Quantity::from(1))
697 .display_qty(Quantity::from(2))
698 .build();
699 }
700
701 #[rstest]
702 #[should_panic(
703 expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
704 )]
705 fn test_quantity_zero_err(audusd_sim: CurrencyPair) {
706 let _ = OrderTestBuilder::new(OrderType::StopMarket)
707 .instrument_id(audusd_sim.id)
708 .side(OrderSide::Buy)
709 .trigger_price(Price::from("0.68000"))
710 .trigger_type(TriggerType::LastPrice)
711 .quantity(Quantity::from(0))
712 .build();
713 }
714
715 #[rstest]
716 #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
717 fn test_gtd_without_expire_err(audusd_sim: CurrencyPair) {
718 let _ = OrderTestBuilder::new(OrderType::StopMarket)
719 .instrument_id(audusd_sim.id)
720 .side(OrderSide::Buy)
721 .trigger_price(Price::from("0.68000"))
722 .trigger_type(TriggerType::LastPrice)
723 .time_in_force(TimeInForce::Gtd)
724 .quantity(Quantity::from(1))
725 .build();
726 }
727
728 #[rstest]
729 fn test_stop_market_order_update() {
730 let order = OrderTestBuilder::new(OrderType::StopMarket)
732 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
733 .quantity(Quantity::from(10))
734 .trigger_price(Price::new(100.0, 2))
735 .build();
736
737 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
738
739 let updated_trigger_price = Price::new(95.0, 2);
741 let updated_quantity = Quantity::from(5);
742
743 let event = OrderUpdated {
744 client_order_id: accepted_order.client_order_id(),
745 strategy_id: accepted_order.strategy_id(),
746 trigger_price: Some(updated_trigger_price),
747 quantity: updated_quantity,
748 ..Default::default()
749 };
750
751 accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
752
753 assert_eq!(accepted_order.quantity(), updated_quantity);
755 assert_eq!(accepted_order.trigger_price(), Some(updated_trigger_price));
756 }
757
758 #[rstest]
759 fn test_stop_market_order_rejects_invalid_update_atomically() {
760 let order = OrderTestBuilder::new(OrderType::StopMarket)
761 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
762 .quantity(Quantity::from(10))
763 .trigger_price(Price::new(100.0, 2))
764 .build();
765 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
766 let state = (
767 accepted_order.status(),
768 accepted_order.previous_status(),
769 accepted_order.ts_last(),
770 accepted_order.events().len(),
771 );
772 let event = OrderUpdated {
773 client_order_id: accepted_order.client_order_id(),
774 strategy_id: accepted_order.strategy_id(),
775 price: Some(Price::new(95.0, 2)),
776 ..Default::default()
777 };
778
779 let result = accepted_order.apply(OrderEventAny::Updated(event));
780
781 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
782 assert_eq!(accepted_order.status(), state.0);
783 assert_eq!(accepted_order.previous_status(), state.1);
784 assert_eq!(accepted_order.ts_last(), state.2);
785 assert_eq!(accepted_order.events().len(), state.3);
786 }
787
788 #[rstest]
789 fn test_stop_market_order_expire_time() {
790 let expire_time = UnixNanos::from(1_234_567_890);
792 let order = OrderTestBuilder::new(OrderType::StopMarket)
793 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
794 .quantity(Quantity::from(10))
795 .trigger_price(Price::new(100.0, 2))
796 .expire_time(expire_time)
797 .build();
798
799 assert_eq!(order.expire_time(), Some(expire_time));
801 }
802
803 #[rstest]
804 fn test_stop_market_order_trigger_instrument_id() {
805 let trigger_instrument_id = InstrumentId::from("ETH-USDT.BINANCE");
807 let order = OrderTestBuilder::new(OrderType::StopMarket)
808 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
809 .quantity(Quantity::from(10))
810 .trigger_price(Price::new(100.0, 2))
811 .trigger_instrument_id(trigger_instrument_id)
812 .build();
813
814 assert_eq!(order.trigger_instrument_id(), Some(trigger_instrument_id));
816 }
817
818 #[rstest]
819 fn test_stop_market_order_from_order_initialized() {
820 let order_initialized = OrderInitializedSpec::builder()
822 .order_type(OrderType::StopMarket)
823 .quantity(Quantity::from(10))
824 .trigger_price(Price::new(100.0, 2))
825 .trigger_type(TriggerType::Default)
826 .build();
827
828 let order: StopMarketOrder = order_initialized.clone().try_into().unwrap();
830
831 assert_eq!(order.trader_id(), order_initialized.trader_id);
833 assert_eq!(order.strategy_id(), order_initialized.strategy_id);
834 assert_eq!(order.instrument_id(), order_initialized.instrument_id);
835 assert_eq!(order.client_order_id(), order_initialized.client_order_id);
836 assert_eq!(order.quantity(), order_initialized.quantity);
837 assert_eq!(order.trigger_price(), order_initialized.trigger_price);
838 assert_eq!(order.trigger_type(), order_initialized.trigger_type);
839 }
840
841 #[rstest]
842 fn test_stop_market_order_is_triggered() {
843 let order = OrderTestBuilder::new(OrderType::StopMarket)
845 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
846 .quantity(Quantity::from(10))
847 .trigger_price(Price::new(100.0, 2))
848 .build();
849
850 assert_eq!(order.is_triggered(), Some(false));
852 }
853
854 #[rstest]
855 fn test_stop_market_order_protection_price_update() {
856 let order = OrderTestBuilder::new(OrderType::StopMarket)
858 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
859 .quantity(Quantity::from(10))
860 .trigger_price(Price::new(100.0, 2))
861 .build();
862
863 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
864
865 let calculated_protection_price = Price::new(95.0, 2);
867
868 let event = OrderUpdated {
869 client_order_id: accepted_order.client_order_id(),
870 strategy_id: accepted_order.strategy_id(),
871 protection_price: Some(calculated_protection_price),
872 ..Default::default()
873 };
874
875 assert_eq!(accepted_order.price(), None);
876 assert!(!accepted_order.has_price());
877
878 accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
879
880 assert_eq!(accepted_order.price(), Some(calculated_protection_price));
882 assert!(accepted_order.has_price());
883 }
884
885 #[rstest]
886 fn test_stop_market_order_update_preserves_protection_price_when_omitted() {
887 let order = OrderTestBuilder::new(OrderType::StopMarket)
888 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
889 .quantity(Quantity::from(10))
890 .trigger_price(Price::new(100.0, 2))
891 .build();
892 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
893 let protection_price = Price::new(95.0, 2);
894
895 let set_protection_event = OrderUpdated {
896 client_order_id: accepted_order.client_order_id(),
897 strategy_id: accepted_order.strategy_id(),
898 quantity: accepted_order.quantity(),
899 protection_price: Some(protection_price),
900 ..Default::default()
901 };
902 accepted_order
903 .apply(OrderEventAny::Updated(set_protection_event))
904 .unwrap();
905
906 let updated_quantity = Quantity::from(5);
907 let updated_trigger_price = Price::new(105.0, 2);
908 let omitted_protection_event = OrderUpdated {
909 client_order_id: accepted_order.client_order_id(),
910 strategy_id: accepted_order.strategy_id(),
911 quantity: updated_quantity,
912 trigger_price: Some(updated_trigger_price),
913 protection_price: None,
914 ..Default::default()
915 };
916 accepted_order
917 .apply(OrderEventAny::Updated(omitted_protection_event))
918 .unwrap();
919
920 assert_eq!(accepted_order.quantity(), updated_quantity);
921 assert_eq!(accepted_order.trigger_price(), Some(updated_trigger_price));
922 assert_eq!(accepted_order.price(), Some(protection_price));
923 assert!(accepted_order.has_price());
924
925 let updated_protection_price = Price::new(90.0, 2);
926 let overwrite_protection_event = OrderUpdated {
927 client_order_id: accepted_order.client_order_id(),
928 strategy_id: accepted_order.strategy_id(),
929 quantity: accepted_order.quantity(),
930 protection_price: Some(updated_protection_price),
931 ..Default::default()
932 };
933 accepted_order
934 .apply(OrderEventAny::Updated(overwrite_protection_event))
935 .unwrap();
936
937 assert_eq!(accepted_order.price(), Some(updated_protection_price));
938 assert!(accepted_order.has_price());
939 }
940}