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