1use std::{
17 fmt::Display,
18 ops::{Deref, DerefMut},
19};
20
21use indexmap::IndexMap;
22use nautilus_core::{UUID4, UnixNanos, correctness::FAILED};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{Order, OrderAny, OrderCore, OrderError, check_time_in_force};
28use crate::{
29 enums::{
30 ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
31 TimeInForce, TrailingOffsetType, TriggerType,
32 },
33 events::{OrderEventAny, OrderInitialized, OrderUpdated},
34 identifiers::{
35 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
36 StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
37 },
38 types::{Currency, Money, Price, Quantity, quantity::check_positive_quantity},
39};
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
45)]
46#[cfg_attr(
47 feature = "python",
48 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
49)]
50pub struct MarketIfTouchedOrder {
51 pub trigger_price: Price,
52 pub trigger_type: TriggerType,
53 pub expire_time: Option<UnixNanos>,
54 pub trigger_instrument_id: Option<InstrumentId>,
55 pub is_triggered: bool,
56 pub ts_triggered: Option<UnixNanos>,
57 core: OrderCore,
58}
59
60impl MarketIfTouchedOrder {
61 #[expect(clippy::too_many_arguments)]
69 pub fn new_checked(
70 trader_id: TraderId,
71 strategy_id: StrategyId,
72 instrument_id: InstrumentId,
73 client_order_id: ClientOrderId,
74 order_side: OrderSide,
75 quantity: Quantity,
76 trigger_price: Price,
77 trigger_type: TriggerType,
78 time_in_force: TimeInForce,
79 expire_time: Option<UnixNanos>,
80 reduce_only: bool,
81 quote_quantity: bool,
82 emulation_trigger: Option<TriggerType>,
83 trigger_instrument_id: Option<InstrumentId>,
84 contingency_type: Option<ContingencyType>,
85 order_list_id: Option<OrderListId>,
86 linked_order_ids: Option<Vec<ClientOrderId>>,
87 parent_order_id: Option<ClientOrderId>,
88 exec_algorithm_id: Option<ExecAlgorithmId>,
89 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
90 exec_spawn_id: Option<ClientOrderId>,
91 tags: Option<Vec<Ustr>>,
92 init_id: UUID4,
93 ts_init: UnixNanos,
94 ) -> Result<Self, OrderError> {
95 check_positive_quantity(quantity, stringify!(quantity))?;
96 check_time_in_force(time_in_force, expire_time)?;
97
98 let init_order = OrderInitialized::new(
99 trader_id,
100 strategy_id,
101 instrument_id,
102 client_order_id,
103 order_side,
104 OrderType::MarketIfTouched,
105 quantity,
106 time_in_force,
107 false,
108 reduce_only,
109 quote_quantity,
110 false,
111 init_id,
112 ts_init,
113 ts_init,
114 None,
115 Some(trigger_price),
116 Some(trigger_type),
117 None,
118 None,
119 None,
120 expire_time,
121 None,
122 emulation_trigger,
123 trigger_instrument_id,
124 contingency_type,
125 order_list_id,
126 linked_order_ids,
127 parent_order_id,
128 exec_algorithm_id,
129 exec_algorithm_params,
130 exec_spawn_id,
131 tags,
132 );
133
134 Ok(Self {
135 core: OrderCore::new(init_order),
136 trigger_price,
137 trigger_type,
138 expire_time,
139 trigger_instrument_id,
140 is_triggered: false,
141 ts_triggered: None,
142 })
143 }
144
145 #[expect(clippy::too_many_arguments)]
151 #[must_use]
152 pub fn new(
153 trader_id: TraderId,
154 strategy_id: StrategyId,
155 instrument_id: InstrumentId,
156 client_order_id: ClientOrderId,
157 order_side: OrderSide,
158 quantity: Quantity,
159 trigger_price: Price,
160 trigger_type: TriggerType,
161 time_in_force: TimeInForce,
162 expire_time: Option<UnixNanos>,
163 reduce_only: bool,
164 quote_quantity: bool,
165 emulation_trigger: Option<TriggerType>,
166 trigger_instrument_id: Option<InstrumentId>,
167 contingency_type: Option<ContingencyType>,
168 order_list_id: Option<OrderListId>,
169 linked_order_ids: Option<Vec<ClientOrderId>>,
170 parent_order_id: Option<ClientOrderId>,
171 exec_algorithm_id: Option<ExecAlgorithmId>,
172 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
173 exec_spawn_id: Option<ClientOrderId>,
174 tags: Option<Vec<Ustr>>,
175 init_id: UUID4,
176 ts_init: UnixNanos,
177 ) -> Self {
178 Self::new_checked(
179 trader_id,
180 strategy_id,
181 instrument_id,
182 client_order_id,
183 order_side,
184 quantity,
185 trigger_price,
186 trigger_type,
187 time_in_force,
188 expire_time,
189 reduce_only,
190 quote_quantity,
191 emulation_trigger,
192 trigger_instrument_id,
193 contingency_type,
194 order_list_id,
195 linked_order_ids,
196 parent_order_id,
197 exec_algorithm_id,
198 exec_algorithm_params,
199 exec_spawn_id,
200 tags,
201 init_id,
202 ts_init,
203 )
204 .unwrap_or_else(|e| panic!("{FAILED}: {e}"))
205 }
206}
207
208impl PartialEq for MarketIfTouchedOrder {
209 fn eq(&self, other: &Self) -> bool {
210 self.client_order_id == other.client_order_id
211 }
212}
213
214impl Deref for MarketIfTouchedOrder {
215 type Target = OrderCore;
216
217 fn deref(&self) -> &Self::Target {
218 &self.core
219 }
220}
221
222impl DerefMut for MarketIfTouchedOrder {
223 fn deref_mut(&mut self) -> &mut Self::Target {
224 &mut self.core
225 }
226}
227
228impl Order for MarketIfTouchedOrder {
229 fn into_any(self) -> OrderAny {
230 OrderAny::MarketIfTouched(self)
231 }
232
233 fn status(&self) -> OrderStatus {
234 self.status
235 }
236
237 fn trader_id(&self) -> TraderId {
238 self.trader_id
239 }
240
241 fn strategy_id(&self) -> StrategyId {
242 self.strategy_id
243 }
244
245 fn instrument_id(&self) -> InstrumentId {
246 self.instrument_id
247 }
248
249 fn symbol(&self) -> Symbol {
250 self.instrument_id.symbol
251 }
252
253 fn venue(&self) -> Venue {
254 self.instrument_id.venue
255 }
256
257 fn client_order_id(&self) -> ClientOrderId {
258 self.client_order_id
259 }
260
261 fn venue_order_id(&self) -> Option<VenueOrderId> {
262 self.venue_order_id
263 }
264
265 fn position_id(&self) -> Option<PositionId> {
266 self.position_id
267 }
268
269 fn account_id(&self) -> Option<AccountId> {
270 self.account_id
271 }
272
273 fn last_trade_id(&self) -> Option<TradeId> {
274 self.last_trade_id
275 }
276
277 fn order_side(&self) -> OrderSide {
278 self.side
279 }
280
281 fn order_type(&self) -> OrderType {
282 self.order_type
283 }
284
285 fn quantity(&self) -> Quantity {
286 self.quantity
287 }
288
289 fn time_in_force(&self) -> TimeInForce {
290 self.time_in_force
291 }
292
293 fn expire_time(&self) -> Option<UnixNanos> {
294 self.expire_time
295 }
296
297 fn price(&self) -> Option<Price> {
298 None
299 }
300
301 fn trigger_price(&self) -> Option<Price> {
302 Some(self.trigger_price)
303 }
304
305 fn trigger_type(&self) -> Option<TriggerType> {
306 Some(self.trigger_type)
307 }
308
309 fn liquidity_side(&self) -> Option<LiquiditySide> {
310 self.liquidity_side
311 }
312
313 fn is_post_only(&self) -> bool {
314 false
315 }
316
317 fn is_reduce_only(&self) -> bool {
318 self.is_reduce_only
319 }
320
321 fn is_quote_quantity(&self) -> bool {
322 self.is_quote_quantity
323 }
324
325 fn has_price(&self) -> bool {
326 false
327 }
328
329 fn display_qty(&self) -> Option<Quantity> {
330 None
331 }
332
333 fn limit_offset(&self) -> Option<Decimal> {
334 None
335 }
336
337 fn trailing_offset(&self) -> Option<Decimal> {
338 None
339 }
340
341 fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
342 None
343 }
344
345 fn emulation_trigger(&self) -> Option<TriggerType> {
346 self.emulation_trigger
347 }
348
349 fn trigger_instrument_id(&self) -> Option<InstrumentId> {
350 self.trigger_instrument_id
351 }
352
353 fn contingency_type(&self) -> Option<ContingencyType> {
354 self.contingency_type
355 }
356
357 fn order_list_id(&self) -> Option<OrderListId> {
358 self.order_list_id
359 }
360
361 fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
362 self.linked_order_ids.as_deref()
363 }
364
365 fn parent_order_id(&self) -> Option<ClientOrderId> {
366 self.parent_order_id
367 }
368
369 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
370 self.exec_algorithm_id
371 }
372
373 fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
374 self.exec_algorithm_params.as_ref()
375 }
376
377 fn exec_spawn_id(&self) -> Option<ClientOrderId> {
378 self.exec_spawn_id
379 }
380
381 fn tags(&self) -> Option<&[Ustr]> {
382 self.tags.as_deref()
383 }
384
385 fn filled_qty(&self) -> Quantity {
386 self.filled_qty
387 }
388
389 fn leaves_qty(&self) -> Quantity {
390 self.leaves_qty
391 }
392
393 fn overfill_qty(&self) -> Quantity {
394 self.overfill_qty
395 }
396
397 fn avg_px(&self) -> Option<f64> {
398 self.avg_px
399 }
400
401 fn slippage(&self) -> Option<f64> {
402 self.slippage
403 }
404
405 fn init_id(&self) -> UUID4 {
406 self.init_id
407 }
408
409 fn ts_init(&self) -> UnixNanos {
410 self.ts_init
411 }
412
413 fn ts_submitted(&self) -> Option<UnixNanos> {
414 self.ts_submitted
415 }
416
417 fn ts_accepted(&self) -> Option<UnixNanos> {
418 self.ts_accepted
419 }
420
421 fn ts_closed(&self) -> Option<UnixNanos> {
422 self.ts_closed
423 }
424
425 fn ts_last(&self) -> UnixNanos {
426 self.ts_last
427 }
428
429 fn events(&self) -> Vec<&OrderEventAny> {
430 self.events.iter().collect()
431 }
432
433 fn commissions(&self) -> &IndexMap<Currency, Money> {
434 &self.commissions
435 }
436
437 fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
438 self.venue_order_ids.iter().collect()
439 }
440
441 fn trade_ids(&self) -> Vec<&TradeId> {
442 self.trade_ids.iter().collect()
443 }
444
445 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
446 let is_order_filled = matches!(event, OrderEventAny::Filled(_));
447 let is_order_triggered = matches!(event, OrderEventAny::Triggered(_));
448 let ts_event = if is_order_triggered {
449 Some(event.ts_event())
450 } else {
451 None
452 };
453
454 self.core.apply(event.clone())?;
455
456 if let OrderEventAny::Updated(ref event) = event {
457 self.update(event);
458 }
459
460 if is_order_triggered {
461 self.is_triggered = true;
462 self.ts_triggered = ts_event;
463 }
464
465 if is_order_filled {
466 self.core.set_slippage(self.trigger_price);
467 }
468
469 Ok(())
470 }
471
472 fn update(&mut self, event: &OrderUpdated) {
473 assert!(event.price.is_none(), "{}", OrderError::InvalidOrderEvent);
474
475 if let Some(trigger_price) = event.trigger_price {
476 self.trigger_price = trigger_price;
477 }
478
479 self.quantity = event.quantity;
480 self.leaves_qty = self.quantity.saturating_sub(self.filled_qty);
481 }
482
483 fn is_triggered(&self) -> Option<bool> {
484 Some(self.is_triggered)
485 }
486
487 fn set_position_id(&mut self, position_id: Option<PositionId>) {
488 self.position_id = position_id;
489 }
490
491 fn set_quantity(&mut self, quantity: Quantity) {
492 self.quantity = quantity;
493 }
494
495 fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
496 self.leaves_qty = leaves_qty;
497 }
498
499 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
500 self.emulation_trigger = emulation_trigger;
501 }
502
503 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
504 self.is_quote_quantity = is_quote_quantity;
505 }
506
507 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
508 self.liquidity_side = Some(liquidity_side);
509 }
510
511 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
512 self.core.would_reduce_only(side, position_qty)
513 }
514
515 fn previous_status(&self) -> Option<OrderStatus> {
516 self.core.previous_status
517 }
518}
519
520impl From<OrderInitialized> for MarketIfTouchedOrder {
521 fn from(event: OrderInitialized) -> Self {
522 Self::new(
523 event.trader_id,
524 event.strategy_id,
525 event.instrument_id,
526 event.client_order_id,
527 event.order_side,
528 event.quantity,
529 event
530 .trigger_price .expect(
532 "Error initializing order: `trigger_price` was `None` for `MarketIfTouchedOrder`",
533 ),
534 event.trigger_type.expect(
535 "Error initializing order: `trigger_type` was `None` for `MarketIfTouchedOrder`",
536 ),
537 event.time_in_force,
538 event.expire_time,
539 event.reduce_only,
540 event.quote_quantity,
541 event.emulation_trigger,
542 event.trigger_instrument_id,
543 event.contingency_type,
544 event.order_list_id,
545 event.linked_order_ids,
546 event.parent_order_id,
547 event.exec_algorithm_id,
548 event.exec_algorithm_params,
549 event.exec_spawn_id,
550 event.tags,
551 event.event_id,
552 event.ts_event,
553 )
554 }
555}
556
557impl Display for MarketIfTouchedOrder {
558 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559 write!(
560 f,
561 "MarketIfTouchedOrder {{ \
562 side: {}, \
563 qty: {}, \
564 instrument: {}, \
565 tif: {}, \
566 trigger_price: {}, \
567 trigger_type: {}, \
568 status: {} \
569 }}",
570 self.side,
571 self.quantity,
572 self.instrument_id,
573 self.time_in_force,
574 self.trigger_price,
575 self.trigger_type,
576 self.status
577 )
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use rstest::rstest;
584
585 use super::*;
586 use crate::{
587 enums::{OrderSide, OrderType, TimeInForce, TriggerType},
588 events::order::spec::{OrderFilledSpec, OrderInitializedSpec},
589 identifiers::{InstrumentId, TradeId, VenueOrderId},
590 instruments::{CurrencyPair, stubs::*},
591 orders::{builder::OrderTestBuilder, stubs::TestOrderStubs},
592 types::{Price, Quantity},
593 };
594
595 #[rstest]
596 fn test_initialize(audusd_sim: CurrencyPair) {
597 let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
598 .instrument_id(audusd_sim.id)
599 .side(OrderSide::Buy)
600 .trigger_price(Price::from("0.68000"))
601 .quantity(Quantity::from(1))
602 .build();
603
604 assert_eq!(order.trigger_price(), Some(Price::from("0.68000")));
605 assert_eq!(order.price(), None);
606
607 assert_eq!(order.time_in_force(), TimeInForce::Gtc);
608
609 assert_eq!(order.is_triggered(), Some(false));
610 assert_eq!(order.filled_qty(), Quantity::from(0));
611 assert_eq!(order.leaves_qty(), Quantity::from(1));
612
613 assert_eq!(order.display_qty(), None);
614 assert_eq!(order.trigger_instrument_id(), None);
615 assert_eq!(order.order_list_id(), None);
616 }
617
618 #[rstest]
619 fn test_display(audusd_sim: CurrencyPair) {
620 let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
621 .instrument_id(audusd_sim.id)
622 .side(OrderSide::Buy)
623 .trigger_price(Price::from("30000"))
624 .trigger_type(TriggerType::LastPrice)
625 .quantity(Quantity::from(1))
626 .build();
627
628 assert_eq!(
629 order.to_string(),
630 "MarketIfTouchedOrder { \
631 side: BUY, \
632 qty: 1, \
633 instrument: AUD/USD.SIM, \
634 tif: GTC, \
635 trigger_price: 30000, \
636 trigger_type: LAST_PRICE, \
637 status: INITIALIZED \
638 }"
639 );
640 }
641
642 #[rstest]
643 #[should_panic(
644 expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
645 )]
646 fn test_quantity_zero(audusd_sim: CurrencyPair) {
647 let _ = OrderTestBuilder::new(OrderType::MarketIfTouched)
648 .instrument_id(audusd_sim.id)
649 .side(OrderSide::Buy)
650 .trigger_price(Price::from("30000"))
651 .trigger_type(TriggerType::LastPrice)
652 .quantity(Quantity::from(0))
653 .build();
654 }
655
656 #[rstest]
657 #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
658 fn test_gtd_without_expire(audusd_sim: CurrencyPair) {
659 let _ = OrderTestBuilder::new(OrderType::MarketIfTouched)
660 .instrument_id(audusd_sim.id)
661 .side(OrderSide::Buy)
662 .trigger_price(Price::from("30000"))
663 .trigger_type(TriggerType::LastPrice)
664 .quantity(Quantity::from(1))
665 .time_in_force(TimeInForce::Gtd)
666 .build();
667 }
668
669 #[rstest]
670 fn test_market_if_touched_order_update() {
671 let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
673 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
674 .quantity(Quantity::from(10))
675 .trigger_price(Price::new(100.0, 2))
676 .build();
677
678 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
679
680 let updated_trigger_price = Price::new(95.0, 2);
682 let updated_quantity = Quantity::from(5);
683
684 let event = OrderUpdated {
685 client_order_id: accepted_order.client_order_id(),
686 strategy_id: accepted_order.strategy_id(),
687 trigger_price: Some(updated_trigger_price),
688 quantity: updated_quantity,
689 ..Default::default()
690 };
691
692 accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
693
694 assert_eq!(accepted_order.quantity(), updated_quantity);
696 assert_eq!(accepted_order.trigger_price(), Some(updated_trigger_price));
697 }
698
699 #[rstest]
700 fn test_market_if_touched_order_from_order_initialized() {
701 let order_initialized = OrderInitializedSpec::builder()
703 .trigger_price(Price::new(100.0, 2))
704 .trigger_type(TriggerType::Default)
705 .order_type(OrderType::MarketIfTouched)
706 .build();
707
708 let order: MarketIfTouchedOrder = order_initialized.clone().into();
710
711 assert_eq!(order.trader_id(), order_initialized.trader_id);
713 assert_eq!(order.strategy_id(), order_initialized.strategy_id);
714 assert_eq!(order.instrument_id(), order_initialized.instrument_id);
715 assert_eq!(order.client_order_id(), order_initialized.client_order_id);
716 assert_eq!(order.order_side(), order_initialized.order_side);
717 assert_eq!(order.quantity(), order_initialized.quantity);
718
719 assert_eq!(
721 order.trigger_price,
722 order_initialized.trigger_price.unwrap()
723 );
724 assert_eq!(order.trigger_type, order_initialized.trigger_type.unwrap());
725 }
726
727 #[rstest]
728 fn test_market_if_touched_order_sets_slippage_when_filled() {
729 let order = OrderTestBuilder::new(OrderType::MarketIfTouched)
731 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
732 .quantity(Quantity::from(10))
733 .side(OrderSide::Buy) .trigger_price(Price::new(90.0, 2)) .build();
736
737 let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
739
740 let fill_quantity = accepted_order.quantity(); let fill_price = Price::new(98.50, 2); let order_filled_event = OrderFilledSpec::builder()
745 .client_order_id(accepted_order.client_order_id())
746 .strategy_id(accepted_order.strategy_id())
747 .instrument_id(accepted_order.instrument_id())
748 .order_side(accepted_order.order_side())
749 .last_qty(fill_quantity)
750 .last_px(fill_price)
751 .venue_order_id(VenueOrderId::from("TEST-001"))
752 .trade_id(TradeId::from("TRADE-001"))
753 .build();
754
755 accepted_order
757 .apply(OrderEventAny::Filled(order_filled_event))
758 .unwrap();
759
760 assert!(accepted_order.slippage().is_some());
762
763 let expected_slippage = 98.50 - 90.0; let actual_slippage = accepted_order.slippage().unwrap();
766
767 assert!(
768 (actual_slippage - expected_slippage).abs() < 0.001,
769 "Expected slippage around {expected_slippage}, was {actual_slippage}"
770 );
771 }
772}