1use std::{
17 fmt::Display,
18 ops::{Deref, DerefMut},
19};
20
21use indexmap::IndexMap;
22use nautilus_core::{UnixNanos, UUID4};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{
28 any::OrderAny,
29 base::{Order, OrderCore},
30};
31use crate::{
32 enums::{
33 ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce,
34 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,
42 types::{quantity::check_quantity_positive, Price, Quantity},
43};
44
45#[derive(Clone, Debug, Serialize, Deserialize)]
46#[cfg_attr(
47 feature = "python",
48 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
49)]
50pub struct LimitOrder {
51 core: OrderCore,
52 pub price: Price,
53 pub expire_time: Option<UnixNanos>,
54 pub is_post_only: bool,
55 pub display_qty: Option<Quantity>,
56 pub trigger_instrument_id: Option<InstrumentId>,
57}
58
59impl LimitOrder {
60 #[allow(clippy::too_many_arguments)]
62 pub fn new(
63 trader_id: TraderId,
64 strategy_id: StrategyId,
65 instrument_id: InstrumentId,
66 client_order_id: ClientOrderId,
67 order_side: OrderSide,
68 quantity: Quantity,
69 price: Price,
70 time_in_force: TimeInForce,
71 expire_time: Option<UnixNanos>,
72 post_only: bool,
73 reduce_only: bool,
74 quote_quantity: bool,
75 display_qty: Option<Quantity>,
76 emulation_trigger: Option<TriggerType>,
77 trigger_instrument_id: Option<InstrumentId>,
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_quantity_positive(quantity)?;
90 if time_in_force == TimeInForce::Gtd {
91 if expire_time.is_none() {
92 anyhow::bail!("Condition failed: `expire_time` is required for `GTD` order")
93 }
94 if let Some(time) = expire_time {
95 if time == 0 {
96 anyhow::bail!("`expire_time` for `GTD` Limit order should be higher then 0")
97 }
98 }
99 }
100 let init_order = OrderInitialized::new(
101 trader_id,
102 strategy_id,
103 instrument_id,
104 client_order_id,
105 order_side,
106 OrderType::Limit,
107 quantity,
108 time_in_force,
109 post_only,
110 reduce_only,
111 quote_quantity,
112 false,
113 init_id,
114 ts_init, ts_init,
116 Some(price),
117 None,
118 None,
119 None,
120 None,
121 None,
122 expire_time,
123 display_qty,
124 emulation_trigger,
125 trigger_instrument_id,
126 contingency_type,
127 order_list_id,
128 linked_order_ids,
129 parent_order_id,
130 exec_algorithm_id,
131 exec_algorithm_params,
132 exec_spawn_id,
133 tags,
134 );
135
136 Ok(Self {
137 core: OrderCore::new(init_order),
138 price,
139 expire_time: expire_time.or(Some(UnixNanos::default())),
140 is_post_only: post_only,
141 display_qty,
142 trigger_instrument_id,
143 })
144 }
145}
146
147impl Deref for LimitOrder {
148 type Target = OrderCore;
149
150 fn deref(&self) -> &Self::Target {
151 &self.core
152 }
153}
154
155impl DerefMut for LimitOrder {
156 fn deref_mut(&mut self) -> &mut Self::Target {
157 &mut self.core
158 }
159}
160
161impl PartialEq for LimitOrder {
162 fn eq(&self, other: &Self) -> bool {
163 self.client_order_id == other.client_order_id
164 }
165}
166
167impl Order for LimitOrder {
168 fn into_any(self) -> OrderAny {
169 OrderAny::Limit(self)
170 }
171
172 fn status(&self) -> OrderStatus {
173 self.status
174 }
175
176 fn trader_id(&self) -> TraderId {
177 self.trader_id
178 }
179
180 fn strategy_id(&self) -> StrategyId {
181 self.strategy_id
182 }
183
184 fn instrument_id(&self) -> InstrumentId {
185 self.instrument_id
186 }
187
188 fn symbol(&self) -> Symbol {
189 self.instrument_id.symbol
190 }
191
192 fn venue(&self) -> Venue {
193 self.instrument_id.venue
194 }
195
196 fn client_order_id(&self) -> ClientOrderId {
197 self.client_order_id
198 }
199
200 fn venue_order_id(&self) -> Option<VenueOrderId> {
201 self.venue_order_id
202 }
203
204 fn position_id(&self) -> Option<PositionId> {
205 self.position_id
206 }
207
208 fn account_id(&self) -> Option<AccountId> {
209 self.account_id
210 }
211
212 fn last_trade_id(&self) -> Option<TradeId> {
213 self.last_trade_id
214 }
215
216 fn side(&self) -> OrderSide {
217 self.side
218 }
219
220 fn order_type(&self) -> OrderType {
221 self.order_type
222 }
223
224 fn quantity(&self) -> Quantity {
225 self.quantity
226 }
227
228 fn time_in_force(&self) -> TimeInForce {
229 self.time_in_force
230 }
231
232 fn expire_time(&self) -> Option<UnixNanos> {
233 self.expire_time
234 }
235
236 fn price(&self) -> Option<Price> {
237 Some(self.price)
238 }
239
240 fn trigger_price(&self) -> Option<Price> {
241 None
242 }
243
244 fn trigger_type(&self) -> Option<TriggerType> {
245 None
246 }
247
248 fn liquidity_side(&self) -> Option<LiquiditySide> {
249 self.liquidity_side
250 }
251
252 fn is_post_only(&self) -> bool {
253 self.is_post_only
254 }
255
256 fn is_reduce_only(&self) -> bool {
257 self.is_reduce_only
258 }
259
260 fn is_quote_quantity(&self) -> bool {
261 self.is_quote_quantity
262 }
263
264 fn display_qty(&self) -> Option<Quantity> {
265 self.display_qty
266 }
267
268 fn limit_offset(&self) -> Option<Decimal> {
269 None
270 }
271
272 fn trailing_offset(&self) -> Option<Decimal> {
273 None
274 }
275
276 fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
277 None
278 }
279
280 fn emulation_trigger(&self) -> Option<TriggerType> {
281 self.emulation_trigger
282 }
283
284 fn trigger_instrument_id(&self) -> Option<InstrumentId> {
285 self.trigger_instrument_id
286 }
287
288 fn contingency_type(&self) -> Option<ContingencyType> {
289 self.contingency_type
290 }
291
292 fn order_list_id(&self) -> Option<OrderListId> {
293 self.order_list_id
294 }
295
296 fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
297 self.linked_order_ids.as_deref()
298 }
299
300 fn parent_order_id(&self) -> Option<ClientOrderId> {
301 self.parent_order_id
302 }
303
304 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
305 self.exec_algorithm_id
306 }
307
308 fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
309 self.exec_algorithm_params.as_ref()
310 }
311
312 fn exec_spawn_id(&self) -> Option<ClientOrderId> {
313 self.exec_spawn_id
314 }
315
316 fn tags(&self) -> Option<&[Ustr]> {
317 self.tags.as_deref()
318 }
319
320 fn filled_qty(&self) -> Quantity {
321 self.filled_qty
322 }
323
324 fn leaves_qty(&self) -> Quantity {
325 self.leaves_qty
326 }
327
328 fn avg_px(&self) -> Option<f64> {
329 self.avg_px
330 }
331
332 fn slippage(&self) -> Option<f64> {
333 self.slippage
334 }
335
336 fn init_id(&self) -> UUID4 {
337 self.init_id
338 }
339
340 fn ts_init(&self) -> UnixNanos {
341 self.ts_init
342 }
343
344 fn ts_last(&self) -> UnixNanos {
345 self.ts_last
346 }
347
348 fn events(&self) -> Vec<&OrderEventAny> {
349 self.events.iter().collect()
350 }
351
352 fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
353 self.venue_order_ids.iter().collect()
354 }
355
356 fn trade_ids(&self) -> Vec<&TradeId> {
357 self.trade_ids.iter().collect()
358 }
359
360 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
361 if let OrderEventAny::Updated(ref event) = event {
362 self.update(event);
363 };
364 let is_order_filled = matches!(event, OrderEventAny::Filled(_));
365
366 self.core.apply(event)?;
367
368 if is_order_filled {
369 self.core.set_slippage(self.price);
370 };
371
372 Ok(())
373 }
374
375 fn update(&mut self, event: &OrderUpdated) {
376 assert!(
377 event.trigger_price.is_none(),
378 "{}",
379 OrderError::InvalidOrderEvent
380 );
381
382 if let Some(price) = event.price {
383 self.price = price;
384 }
385
386 self.quantity = event.quantity;
387 self.leaves_qty = self.quantity - self.filled_qty;
388 }
389}
390
391impl Display for LimitOrder {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 write!(
394 f,
395 "LimitOrder(\
396 {} {} {} {} @ {} {}, \
397 status={}, \
398 client_order_id={}, \
399 venue_order_id={}, \
400 position_id={}, \
401 exec_algorithm_id={}, \
402 exec_spawn_id={}, \
403 tags={:?}\
404 )",
405 self.side,
406 self.quantity.to_formatted_string(),
407 self.instrument_id,
408 self.order_type,
409 self.price,
410 self.time_in_force,
411 self.status,
412 self.client_order_id,
413 self.venue_order_id.map_or_else(
414 || "None".to_string(),
415 |venue_order_id| format!("{venue_order_id}")
416 ),
417 self.position_id.map_or_else(
418 || "None".to_string(),
419 |position_id| format!("{position_id}")
420 ),
421 self.exec_algorithm_id
422 .map_or_else(|| "None".to_string(), |id| format!("{id}")),
423 self.exec_spawn_id
424 .map_or_else(|| "None".to_string(), |id| format!("{id}")),
425 self.tags
426 )
427 }
428}
429
430impl From<OrderAny> for LimitOrder {
431 fn from(order: OrderAny) -> LimitOrder {
432 match order {
433 OrderAny::Limit(order) => order,
434 _ => panic!(
435 "Invalid `OrderAny` not `{}`, was {order:?}",
436 stringify!(LimitOrder),
437 ),
438 }
439 }
440}
441
442impl From<OrderInitialized> for LimitOrder {
443 fn from(event: OrderInitialized) -> Self {
444 Self::new(
445 event.trader_id,
446 event.strategy_id,
447 event.instrument_id,
448 event.client_order_id,
449 event.order_side,
450 event.quantity,
451 event
452 .price .expect("Error initializing order: `price` was `None` for `LimitOrder"),
454 event.time_in_force,
455 event.expire_time,
456 event.post_only,
457 event.reduce_only,
458 event.quote_quantity,
459 event.display_qty,
460 event.emulation_trigger,
461 event.trigger_instrument_id,
462 event.contingency_type,
463 event.order_list_id,
464 event.linked_order_ids,
465 event.parent_order_id,
466 event.exec_algorithm_id,
467 event.exec_algorithm_params,
468 event.exec_spawn_id,
469 event.tags,
470 event.event_id,
471 event.ts_event,
472 )
473 .unwrap()
474 }
475}
476
477#[cfg(test)]
481mod tests {
482 use rstest::rstest;
483
484 use crate::{
485 enums::{OrderSide, OrderType, TimeInForce},
486 instruments::{stubs::*, CurrencyPair},
487 orders::OrderTestBuilder,
488 types::{Price, Quantity},
489 };
490
491 #[rstest]
492 fn test_display(audusd_sim: CurrencyPair) {
493 let order = OrderTestBuilder::new(OrderType::Limit)
494 .instrument_id(audusd_sim.id)
495 .side(OrderSide::Buy)
496 .price(Price::from("1.00000"))
497 .quantity(Quantity::from(100_000))
498 .build();
499
500 assert_eq!(
501 order.to_string(),
502 "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, \
503 status=INITIALIZED, client_order_id=O-19700101-000000-001-001-1, \
504 venue_order_id=None, position_id=None, exec_algorithm_id=None, \
505 exec_spawn_id=None, tags=None)"
506 );
507 }
508
509 #[rstest]
510 #[should_panic(expected = "Condition failed: invalid `Quantity`, should be positive and was 0")]
511 fn test_positive_quantity_condition(audusd_sim: CurrencyPair) {
512 let _ = OrderTestBuilder::new(OrderType::Limit)
513 .instrument_id(audusd_sim.id)
514 .side(OrderSide::Buy)
515 .price(Price::from("0.8"))
516 .quantity(Quantity::from(0))
517 .build();
518 }
519
520 #[rstest]
521 #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
522 fn test_correct_expiration_with_time_in_force_gtd(audusd_sim: CurrencyPair) {
523 let _ = OrderTestBuilder::new(OrderType::Limit)
524 .instrument_id(audusd_sim.id)
525 .side(OrderSide::Buy)
526 .price(Price::from("0.8"))
527 .quantity(Quantity::from(1))
528 .time_in_force(TimeInForce::Gtd)
529 .build();
530 }
531}