1use std::{
17 fmt::Display,
18 ops::{Deref, DerefMut},
19};
20
21use indexmap::IndexMap;
22use nautilus_core::{UUID4, UnixNanos};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{Order, OrderAny, OrderCore};
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)]
59 pub fn new(
60 trader_id: TraderId,
61 strategy_id: StrategyId,
62 instrument_id: InstrumentId,
63 client_order_id: ClientOrderId,
64 order_side: OrderSide,
65 quantity: Quantity,
66 price: Price,
67 time_in_force: TimeInForce,
68 expire_time: Option<UnixNanos>,
69 post_only: bool,
70 reduce_only: bool,
71 quote_quantity: bool,
72 display_qty: Option<Quantity>,
73 emulation_trigger: Option<TriggerType>,
74 trigger_instrument_id: Option<InstrumentId>,
75 contingency_type: Option<ContingencyType>,
76 order_list_id: Option<OrderListId>,
77 linked_order_ids: Option<Vec<ClientOrderId>>,
78 parent_order_id: Option<ClientOrderId>,
79 exec_algorithm_id: Option<ExecAlgorithmId>,
80 exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
81 exec_spawn_id: Option<ClientOrderId>,
82 tags: Option<Vec<Ustr>>,
83 init_id: UUID4,
84 ts_init: UnixNanos,
85 ) -> anyhow::Result<Self> {
86 check_positive_quantity(quantity, stringify!(quantity))?;
87 if time_in_force == TimeInForce::Gtd {
88 if expire_time.is_none() {
89 anyhow::bail!("Condition failed: `expire_time` is required for `GTD` order")
90 }
91 if let Some(time) = expire_time {
92 if time == 0 {
93 anyhow::bail!("`expire_time` for `GTD` Limit order should be higher then 0")
94 }
95 }
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: expire_time.or(Some(UnixNanos::default())),
137 is_post_only: post_only,
138 display_qty,
139 trigger_instrument_id,
140 })
141 }
142}
143
144impl Deref for LimitOrder {
145 type Target = OrderCore;
146
147 fn deref(&self) -> &Self::Target {
148 &self.core
149 }
150}
151
152impl DerefMut for LimitOrder {
153 fn deref_mut(&mut self) -> &mut Self::Target {
154 &mut self.core
155 }
156}
157
158impl PartialEq for LimitOrder {
159 fn eq(&self, other: &Self) -> bool {
160 self.client_order_id == other.client_order_id
161 }
162}
163
164impl Order for LimitOrder {
165 fn into_any(self) -> OrderAny {
166 OrderAny::Limit(self)
167 }
168
169 fn status(&self) -> OrderStatus {
170 self.status
171 }
172
173 fn trader_id(&self) -> TraderId {
174 self.trader_id
175 }
176
177 fn strategy_id(&self) -> StrategyId {
178 self.strategy_id
179 }
180
181 fn instrument_id(&self) -> InstrumentId {
182 self.instrument_id
183 }
184
185 fn symbol(&self) -> Symbol {
186 self.instrument_id.symbol
187 }
188
189 fn venue(&self) -> Venue {
190 self.instrument_id.venue
191 }
192
193 fn client_order_id(&self) -> ClientOrderId {
194 self.client_order_id
195 }
196
197 fn venue_order_id(&self) -> Option<VenueOrderId> {
198 self.venue_order_id
199 }
200
201 fn position_id(&self) -> Option<PositionId> {
202 self.position_id
203 }
204
205 fn account_id(&self) -> Option<AccountId> {
206 self.account_id
207 }
208
209 fn last_trade_id(&self) -> Option<TradeId> {
210 self.last_trade_id
211 }
212
213 fn order_side(&self) -> OrderSide {
214 self.side
215 }
216
217 fn order_type(&self) -> OrderType {
218 self.order_type
219 }
220
221 fn quantity(&self) -> Quantity {
222 self.quantity
223 }
224
225 fn time_in_force(&self) -> TimeInForce {
226 self.time_in_force
227 }
228
229 fn expire_time(&self) -> Option<UnixNanos> {
230 self.expire_time
231 }
232
233 fn price(&self) -> Option<Price> {
234 Some(self.price)
235 }
236
237 fn trigger_price(&self) -> Option<Price> {
238 None
239 }
240
241 fn trigger_type(&self) -> Option<TriggerType> {
242 None
243 }
244
245 fn liquidity_side(&self) -> Option<LiquiditySide> {
246 self.liquidity_side
247 }
248
249 fn is_post_only(&self) -> bool {
250 self.is_post_only
251 }
252
253 fn is_reduce_only(&self) -> bool {
254 self.is_reduce_only
255 }
256
257 fn is_quote_quantity(&self) -> bool {
258 self.is_quote_quantity
259 }
260
261 fn has_price(&self) -> bool {
262 true
263 }
264
265 fn display_qty(&self) -> Option<Quantity> {
266 self.display_qty
267 }
268
269 fn limit_offset(&self) -> Option<Decimal> {
270 None
271 }
272
273 fn trailing_offset(&self) -> Option<Decimal> {
274 None
275 }
276
277 fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
278 None
279 }
280
281 fn emulation_trigger(&self) -> Option<TriggerType> {
282 self.emulation_trigger
283 }
284
285 fn trigger_instrument_id(&self) -> Option<InstrumentId> {
286 self.trigger_instrument_id
287 }
288
289 fn contingency_type(&self) -> Option<ContingencyType> {
290 self.contingency_type
291 }
292
293 fn order_list_id(&self) -> Option<OrderListId> {
294 self.order_list_id
295 }
296
297 fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
298 self.linked_order_ids.as_deref()
299 }
300
301 fn parent_order_id(&self) -> Option<ClientOrderId> {
302 self.parent_order_id
303 }
304
305 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
306 self.exec_algorithm_id
307 }
308
309 fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
310 self.exec_algorithm_params.as_ref()
311 }
312
313 fn exec_spawn_id(&self) -> Option<ClientOrderId> {
314 self.exec_spawn_id
315 }
316
317 fn tags(&self) -> Option<&[Ustr]> {
318 self.tags.as_deref()
319 }
320
321 fn filled_qty(&self) -> Quantity {
322 self.filled_qty
323 }
324
325 fn leaves_qty(&self) -> Quantity {
326 self.leaves_qty
327 }
328
329 fn avg_px(&self) -> Option<f64> {
330 self.avg_px
331 }
332
333 fn slippage(&self) -> Option<f64> {
334 self.slippage
335 }
336
337 fn init_id(&self) -> UUID4 {
338 self.init_id
339 }
340
341 fn ts_init(&self) -> UnixNanos {
342 self.ts_init
343 }
344
345 fn ts_submitted(&self) -> Option<UnixNanos> {
346 self.ts_submitted
347 }
348
349 fn ts_accepted(&self) -> Option<UnixNanos> {
350 self.ts_accepted
351 }
352
353 fn ts_closed(&self) -> Option<UnixNanos> {
354 self.ts_closed
355 }
356
357 fn ts_last(&self) -> UnixNanos {
358 self.ts_last
359 }
360
361 fn events(&self) -> Vec<&OrderEventAny> {
362 self.events.iter().collect()
363 }
364
365 fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
366 self.venue_order_ids.iter().collect()
367 }
368
369 fn trade_ids(&self) -> Vec<&TradeId> {
370 self.trade_ids.iter().collect()
371 }
372
373 fn commissions(&self) -> &IndexMap<Currency, Money> {
374 &self.commissions
375 }
376
377 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
378 if let OrderEventAny::Updated(ref event) = event {
379 self.update(event);
380 };
381 let is_order_filled = matches!(event, OrderEventAny::Filled(_));
382
383 self.core.apply(event)?;
384
385 if is_order_filled {
386 self.core.set_slippage(self.price);
387 };
388
389 Ok(())
390 }
391
392 fn update(&mut self, event: &OrderUpdated) {
393 assert!(
394 event.trigger_price.is_none(),
395 "{}",
396 OrderError::InvalidOrderEvent
397 );
398
399 if let Some(price) = event.price {
400 self.price = price;
401 }
402
403 self.quantity = event.quantity;
404 self.leaves_qty = self.quantity - self.filled_qty;
405 }
406
407 fn is_triggered(&self) -> Option<bool> {
408 None
409 }
410
411 fn set_position_id(&mut self, position_id: Option<PositionId>) {
412 self.position_id = position_id;
413 }
414
415 fn set_quantity(&mut self, quantity: Quantity) {
416 self.quantity = quantity;
417 }
418
419 fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
420 self.leaves_qty = leaves_qty;
421 }
422
423 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
424 self.emulation_trigger = emulation_trigger;
425 }
426
427 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
428 self.is_quote_quantity = is_quote_quantity;
429 }
430
431 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
432 self.liquidity_side = Some(liquidity_side)
433 }
434
435 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
436 self.core.would_reduce_only(side, position_qty)
437 }
438
439 fn previous_status(&self) -> Option<OrderStatus> {
440 self.core.previous_status
441 }
442}
443
444impl Display for LimitOrder {
445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446 write!(
447 f,
448 "LimitOrder(\
449 {} {} {} {} @ {} {}, \
450 status={}, \
451 client_order_id={}, \
452 venue_order_id={}, \
453 position_id={}, \
454 exec_algorithm_id={}, \
455 exec_spawn_id={}, \
456 tags={:?}\
457 )",
458 self.side,
459 self.quantity.to_formatted_string(),
460 self.instrument_id,
461 self.order_type,
462 self.price,
463 self.time_in_force,
464 self.status,
465 self.client_order_id,
466 self.venue_order_id.map_or_else(
467 || "None".to_string(),
468 |venue_order_id| format!("{venue_order_id}")
469 ),
470 self.position_id.map_or_else(
471 || "None".to_string(),
472 |position_id| format!("{position_id}")
473 ),
474 self.exec_algorithm_id
475 .map_or_else(|| "None".to_string(), |id| format!("{id}")),
476 self.exec_spawn_id
477 .map_or_else(|| "None".to_string(), |id| format!("{id}")),
478 self.tags
479 )
480 }
481}
482
483impl From<OrderInitialized> for LimitOrder {
484 fn from(event: OrderInitialized) -> Self {
485 Self::new(
486 event.trader_id,
487 event.strategy_id,
488 event.instrument_id,
489 event.client_order_id,
490 event.order_side,
491 event.quantity,
492 event
493 .price .expect("Error initializing order: `price` was `None` for `LimitOrder"),
495 event.time_in_force,
496 event.expire_time,
497 event.post_only,
498 event.reduce_only,
499 event.quote_quantity,
500 event.display_qty,
501 event.emulation_trigger,
502 event.trigger_instrument_id,
503 event.contingency_type,
504 event.order_list_id,
505 event.linked_order_ids,
506 event.parent_order_id,
507 event.exec_algorithm_id,
508 event.exec_algorithm_params,
509 event.exec_spawn_id,
510 event.tags,
511 event.event_id,
512 event.ts_event,
513 )
514 .unwrap()
515 }
516}
517
518#[cfg(test)]
522mod tests {
523 use rstest::rstest;
524
525 use crate::{
526 enums::{OrderSide, OrderType, TimeInForce},
527 instruments::{CurrencyPair, stubs::*},
528 orders::OrderTestBuilder,
529 types::{Price, Quantity},
530 };
531
532 #[rstest]
533 fn test_display(audusd_sim: CurrencyPair) {
534 let order = OrderTestBuilder::new(OrderType::Limit)
535 .instrument_id(audusd_sim.id)
536 .side(OrderSide::Buy)
537 .price(Price::from("1.00000"))
538 .quantity(Quantity::from(100_000))
539 .build();
540
541 assert_eq!(
542 order.to_string(),
543 "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, \
544 status=INITIALIZED, client_order_id=O-19700101-000000-001-001-1, \
545 venue_order_id=None, position_id=None, exec_algorithm_id=None, \
546 exec_spawn_id=None, tags=None)"
547 );
548 }
549
550 #[rstest]
551 #[should_panic(
552 expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
553 )]
554 fn test_positive_quantity_condition(audusd_sim: CurrencyPair) {
555 let _ = OrderTestBuilder::new(OrderType::Limit)
556 .instrument_id(audusd_sim.id)
557 .side(OrderSide::Buy)
558 .price(Price::from("0.8"))
559 .quantity(Quantity::from(0))
560 .build();
561 }
562
563 #[rstest]
564 #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
565 fn test_correct_expiration_with_time_in_force_gtd(audusd_sim: CurrencyPair) {
566 let _ = OrderTestBuilder::new(OrderType::Limit)
567 .instrument_id(audusd_sim.id)
568 .side(OrderSide::Buy)
569 .price(Price::from("0.8"))
570 .quantity(Quantity::from(1))
571 .time_in_force(TimeInForce::Gtd)
572 .build();
573 }
574}