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