1use indexmap::IndexMap;
17use nautilus_core::{
18 UUID4, UnixNanos,
19 python::{
20 IntoPyObjectNautilusExt,
21 parsing::{
22 get_optional, get_optional_parsed, get_required, get_required_parsed,
23 get_required_string,
24 },
25 to_pyruntime_err, to_pyvalue_err,
26 },
27};
28use pyo3::{
29 basic::CompareOp,
30 prelude::*,
31 types::{PyDict, PyList},
32};
33use rust_decimal::Decimal;
34use ustr::Ustr;
35
36use crate::{
37 enums::{
38 ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
39 TimeInForce, TriggerType,
40 },
41 events::order::initialized::OrderInitialized,
42 identifiers::{
43 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
44 StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
45 },
46 orders::{LimitOrder, Order, OrderCore, str_indexmap_to_ustr},
47 python::{
48 common::commissions_from_indexmap,
49 events::order::{order_event_to_pyobject, pyobject_to_order_event},
50 },
51 types::{Currency, Money, Price, Quantity},
52};
53
54#[pymethods]
55#[pyo3_stub_gen::derive::gen_stub_pymethods]
56impl LimitOrder {
57 #[new]
59 #[expect(clippy::too_many_arguments)]
60 #[pyo3(signature = (trader_id, strategy_id, instrument_id, client_order_id, order_side, quantity, price, time_in_force, post_only, reduce_only, quote_quantity, init_id, ts_init, expire_time=None, display_qty=None, emulation_trigger=None, trigger_instrument_id=None, contingency_type=None, order_list_id=None, linked_order_ids=None, parent_order_id=None, exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, tags=None))]
61 fn py_new(
62 trader_id: TraderId,
63 strategy_id: StrategyId,
64 instrument_id: InstrumentId,
65 client_order_id: ClientOrderId,
66 order_side: OrderSide,
67 quantity: Quantity,
68 price: Price,
69 time_in_force: TimeInForce,
70 post_only: bool,
71 reduce_only: bool,
72 quote_quantity: bool,
73 init_id: UUID4,
74 ts_init: u64,
75 expire_time: Option<u64>,
76 display_qty: Option<Quantity>,
77 emulation_trigger: Option<TriggerType>,
78 trigger_instrument_id: Option<InstrumentId>,
79 contingency_type: Option<ContingencyType>,
80 order_list_id: Option<OrderListId>,
81 linked_order_ids: Option<Vec<ClientOrderId>>,
82 parent_order_id: Option<ClientOrderId>,
83 exec_algorithm_id: Option<ExecAlgorithmId>,
84 exec_algorithm_params: Option<IndexMap<String, String>>,
85 exec_spawn_id: Option<ClientOrderId>,
86 tags: Option<Vec<String>>,
87 ) -> PyResult<Self> {
88 Self::new_checked(
89 trader_id,
90 strategy_id,
91 instrument_id,
92 client_order_id,
93 order_side,
94 quantity,
95 price,
96 time_in_force,
97 expire_time.map(UnixNanos::from),
98 post_only,
99 reduce_only,
100 quote_quantity,
101 display_qty,
102 emulation_trigger,
103 trigger_instrument_id,
104 contingency_type,
105 order_list_id,
106 linked_order_ids,
107 parent_order_id,
108 exec_algorithm_id,
109 exec_algorithm_params.map(str_indexmap_to_ustr),
110 exec_spawn_id,
111 tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()),
112 init_id,
113 ts_init.into(),
114 )
115 .map_err(to_pyvalue_err)
116 }
117
118 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
119 match op {
120 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
121 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
122 _ => py.NotImplemented(),
123 }
124 }
125
126 fn __repr__(&self) -> String {
127 self.to_string()
128 }
129
130 fn __str__(&self) -> String {
131 self.to_string()
132 }
133
134 #[staticmethod]
135 #[pyo3(name = "create")]
136 fn py_create(init: OrderInitialized) -> Self {
137 Self::from(init)
138 }
139
140 #[staticmethod]
141 #[pyo3(name = "opposite_side")]
142 fn py_opposite_side(side: OrderSide) -> OrderSide {
143 OrderCore::opposite_side(side)
144 }
145
146 #[staticmethod]
147 #[pyo3(name = "closing_side")]
148 fn py_closing_side(side: PositionSide) -> OrderSide {
149 OrderCore::closing_side(side)
150 }
151
152 #[getter]
153 #[pyo3(name = "status")]
154 fn py_status(&self) -> OrderStatus {
155 self.status
156 }
157
158 #[getter]
159 #[pyo3(name = "trader_id")]
160 fn py_trader_id(&self) -> TraderId {
161 self.trader_id
162 }
163
164 #[getter]
165 #[pyo3(name = "strategy_id")]
166 fn py_strategy_id(&self) -> StrategyId {
167 self.strategy_id
168 }
169
170 #[getter]
171 #[pyo3(name = "instrument_id")]
172 fn py_instrument_id(&self) -> InstrumentId {
173 self.instrument_id
174 }
175
176 #[getter]
177 #[pyo3(name = "symbol")]
178 fn py_symbol(&self) -> Symbol {
179 self.symbol()
180 }
181
182 #[getter]
183 #[pyo3(name = "venue")]
184 fn py_venue(&self) -> Venue {
185 self.venue()
186 }
187
188 #[getter]
189 #[pyo3(name = "client_order_id")]
190 fn py_client_order_id(&self) -> ClientOrderId {
191 self.client_order_id
192 }
193
194 #[getter]
195 #[pyo3(name = "venue_order_id")]
196 fn py_venue_order_id(&self) -> Option<VenueOrderId> {
197 self.venue_order_id
198 }
199
200 #[getter]
201 #[pyo3(name = "position_id")]
202 fn py_position_id(&self) -> Option<PositionId> {
203 self.position_id
204 }
205
206 #[getter]
207 #[pyo3(name = "account_id")]
208 fn py_account_id(&self) -> Option<AccountId> {
209 self.account_id
210 }
211
212 #[getter]
213 #[pyo3(name = "last_trade_id")]
214 fn py_last_trade_id(&self) -> Option<TradeId> {
215 self.last_trade_id
216 }
217
218 #[getter]
219 #[pyo3(name = "side")]
220 fn py_side(&self) -> OrderSide {
221 self.side
222 }
223
224 #[getter]
225 #[pyo3(name = "order_type")]
226 fn py_order_type(&self) -> OrderType {
227 self.order_type
228 }
229
230 #[getter]
231 #[pyo3(name = "quantity")]
232 fn py_quantity(&self) -> Quantity {
233 self.quantity
234 }
235
236 #[getter]
237 #[pyo3(name = "time_in_force")]
238 fn py_time_in_force(&self) -> TimeInForce {
239 self.time_in_force
240 }
241
242 #[getter]
243 #[pyo3(name = "expire_time")]
244 fn py_expire_time(&self) -> Option<u64> {
245 self.expire_time.map(std::convert::Into::into)
246 }
247
248 #[getter]
249 #[pyo3(name = "price")]
250 fn py_price(&self) -> Price {
251 self.price
252 }
253
254 #[getter]
255 #[pyo3(name = "is_post_only")]
256 fn py_is_post_only(&self) -> bool {
257 self.is_post_only
258 }
259
260 #[getter]
261 #[pyo3(name = "is_reduce_only")]
262 fn py_is_reduce_only(&self) -> bool {
263 self.is_reduce_only
264 }
265
266 #[getter]
267 #[pyo3(name = "is_quote_quantity")]
268 fn py_is_quote_quantity(&self) -> bool {
269 self.is_quote_quantity
270 }
271
272 #[pyo3(name = "commission")]
273 fn py_commission(&self, currency: &Currency) -> Option<Money> {
274 self.commission(currency)
275 }
276
277 #[pyo3(name = "commissions")]
278 fn py_commissions(&self) -> IndexMap<Currency, Money> {
279 self.commissions().clone()
280 }
281
282 #[getter]
283 #[pyo3(name = "has_price")]
284 fn py_has_price(&self) -> bool {
285 true
286 }
287
288 #[getter]
289 #[pyo3(name = "has_trigger_price")]
290 fn py_trigger_price(&self) -> bool {
291 false
292 }
293
294 #[getter]
295 #[pyo3(name = "is_passive")]
296 fn py_is_passive(&self) -> bool {
297 true
298 }
299
300 #[getter]
301 #[pyo3(name = "is_open")]
302 fn py_is_open(&self) -> bool {
303 self.is_open()
304 }
305
306 #[getter]
307 #[pyo3(name = "is_closed")]
308 fn py_is_closed(&self) -> bool {
309 self.is_closed()
310 }
311
312 #[getter]
313 #[pyo3(name = "is_aggressive")]
314 fn py_is_aggressive(&self) -> bool {
315 self.is_aggressive()
316 }
317
318 #[getter]
319 #[pyo3(name = "is_emulated")]
320 fn py_is_emulated(&self) -> bool {
321 self.is_emulated()
322 }
323
324 #[getter]
325 #[pyo3(name = "is_active_local")]
326 fn py_is_active_local(&self) -> bool {
327 self.is_active_local()
328 }
329
330 #[getter]
331 #[pyo3(name = "is_primary")]
332 fn py_is_primary(&self) -> bool {
333 self.is_primary()
334 }
335
336 #[getter]
337 #[pyo3(name = "is_spawned")]
338 fn py_is_spawned(&self) -> bool {
339 self.is_spawned()
340 }
341
342 #[getter]
343 #[pyo3(name = "liquidity_side")]
344 fn py_liquidity_side(&self) -> Option<LiquiditySide> {
345 self.liquidity_side
346 }
347
348 #[getter]
349 #[pyo3(name = "filled_qty")]
350 fn py_filled_qty(&self) -> Quantity {
351 self.filled_qty
352 }
353
354 #[getter]
355 #[pyo3(name = "trigger_instrument_id")]
356 fn py_trigger_instrument_id(&self) -> Option<InstrumentId> {
357 self.trigger_instrument_id
358 }
359
360 #[getter]
361 #[pyo3(name = "contingency_type")]
362 fn py_contingency_type(&self) -> Option<ContingencyType> {
363 self.contingency_type
364 }
365
366 #[getter]
367 #[pyo3(name = "order_list_id")]
368 fn py_order_list_id(&self) -> Option<OrderListId> {
369 self.order_list_id
370 }
371
372 #[getter]
373 #[pyo3(name = "linked_order_ids")]
374 fn py_linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
375 self.linked_order_ids.clone()
376 }
377
378 #[getter]
379 #[pyo3(name = "parent_order_id")]
380 fn py_parent_order_id(&self) -> Option<ClientOrderId> {
381 self.parent_order_id
382 }
383
384 #[getter]
385 #[pyo3(name = "exec_algorithm_id")]
386 fn py_exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
387 self.exec_algorithm_id
388 }
389
390 #[getter]
391 #[pyo3(name = "exec_algorithm_params")]
392 fn py_exec_algorithm_params(&self) -> Option<IndexMap<&str, &str>> {
393 self.exec_algorithm_params
394 .as_ref()
395 .map(|x| x.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect())
396 }
397
398 #[getter]
399 #[pyo3(name = "tags")]
400 fn py_tags(&self) -> Option<Vec<&str>> {
401 self.tags
402 .as_ref()
403 .map(|vec| vec.iter().map(|s| s.as_str()).collect())
404 }
405
406 #[getter]
407 #[pyo3(name = "emulation_trigger")]
408 fn py_emulation_trigger(&self) -> Option<TriggerType> {
409 self.emulation_trigger
410 }
411
412 #[getter]
413 #[pyo3(name = "expire_time_ns")]
414 fn py_expire_time_ns(&self) -> Option<u64> {
415 self.expire_time.map(std::convert::Into::into)
416 }
417
418 #[getter]
419 #[pyo3(name = "exec_spawn_id")]
420 fn py_exec_spawn_id(&self) -> Option<ClientOrderId> {
421 self.exec_spawn_id
422 }
423
424 #[getter]
425 #[pyo3(name = "display_qty")]
426 fn py_display_qty(&self) -> Option<Quantity> {
427 self.display_qty
428 }
429
430 #[getter]
431 #[pyo3(name = "init_id")]
432 fn py_init_id(&self) -> UUID4 {
433 self.init_id
434 }
435
436 #[getter]
437 #[pyo3(name = "ts_init")]
438 fn py_ts_init(&self) -> u64 {
439 self.ts_init.as_u64()
440 }
441
442 #[getter]
443 #[pyo3(name = "ts_last")]
444 fn py_ts_last(&self) -> u64 {
445 self.ts_last.as_u64()
446 }
447
448 #[pyo3(name = "events")]
449 fn py_events(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
450 self.events()
451 .into_iter()
452 .map(|event| order_event_to_pyobject(py, event.clone()))
453 .collect()
454 }
455
456 #[pyo3(name = "signed_decimal_qty")]
457 fn py_signed_decimal_qty(&self) -> Decimal {
458 self.signed_decimal_qty()
459 }
460
461 #[pyo3(name = "would_reduce_only")]
462 fn py_would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
463 self.would_reduce_only(side, position_qty)
464 }
465
466 #[pyo3(name = "apply")]
467 fn py_apply(&mut self, event: Py<PyAny>, py: Python<'_>) -> PyResult<()> {
468 let event_any = pyobject_to_order_event(py, event).unwrap();
469 self.apply(event_any).map_err(to_pyruntime_err)
470 }
471
472 #[staticmethod]
473 #[pyo3(name = "from_dict")]
474 fn py_from_dict(values: &Bound<'_, PyDict>) -> PyResult<Self> {
475 let trader_id = TraderId::from(get_required_string(values, "trader_id")?.as_str());
476 let strategy_id = StrategyId::from(get_required_string(values, "strategy_id")?.as_str());
477 let instrument_id = InstrumentId::from(get_required_string(values, "instrument_id")?);
478 let client_order_id =
479 ClientOrderId::from(get_required_string(values, "client_order_id")?.as_str());
480 let order_side = get_required_parsed(values, "side", |s| {
481 s.parse::<OrderSide>().map_err(|e| e.to_string())
482 })?;
483 let quantity = Quantity::from(get_required_string(values, "quantity")?.as_str());
484 let price = Price::from(get_required_string(values, "price")?.as_str());
485 let time_in_force = get_required_parsed(values, "time_in_force", |s| {
486 s.parse::<TimeInForce>().map_err(|e| e.to_string())
487 })?;
488 let expire_time = get_optional::<u64>(values, "expire_time_ns")?.map(UnixNanos::from);
489 let is_post_only = get_required::<bool>(values, "is_post_only")?;
490 let is_reduce_only = get_required::<bool>(values, "is_reduce_only")?;
491 let is_quote_quantity = get_required::<bool>(values, "is_quote_quantity")?;
492 let display_qty =
493 get_optional_parsed(values, "display_qty", |s| Ok(Quantity::from(s.as_str())))?;
494 let emulation_trigger = get_optional_parsed(values, "emulation_trigger", |s| {
495 s.parse::<TriggerType>().map_err(|e| e.to_string())
496 })?;
497 let trigger_instrument_id = get_optional_parsed(values, "trigger_instrument_id", |s| {
498 s.parse::<InstrumentId>().map_err(|e| e.to_string())
499 })?;
500 let contingency_type = get_optional_parsed(values, "contingency_type", |s| {
501 s.parse::<ContingencyType>().map_err(|e| e.to_string())
502 })?;
503 let order_list_id = get_optional_parsed(values, "order_list_id", |s| {
504 Ok(OrderListId::from(s.as_str()))
505 })?;
506 let linked_order_ids =
507 get_optional::<Vec<String>>(values, "linked_order_ids")?.map(|vec| {
508 vec.iter()
509 .map(|s| ClientOrderId::from(s.as_str()))
510 .collect()
511 });
512 let parent_order_id = get_optional_parsed(values, "parent_order_id", |s| {
513 Ok(ClientOrderId::from(s.as_str()))
514 })?;
515 let exec_algorithm_id = get_optional_parsed(values, "exec_algorithm_id", |s| {
516 Ok(ExecAlgorithmId::from(s.as_str()))
517 })?;
518 let exec_algorithm_params =
519 get_optional::<IndexMap<String, String>>(values, "exec_algorithm_params")?
520 .map(str_indexmap_to_ustr);
521 let exec_spawn_id = get_optional_parsed(values, "exec_spawn_id", |s| {
522 Ok(ClientOrderId::from(s.as_str()))
523 })?;
524 let tags = get_optional::<Vec<String>>(values, "tags")?
525 .map(|vec| vec.iter().map(|s| Ustr::from(s)).collect());
526 let init_id = get_required_parsed(values, "init_id", |s| s.parse::<UUID4>())?;
527 let ts_init = get_required::<u64>(values, "ts_init")?;
528
529 Self::new_checked(
530 trader_id,
531 strategy_id,
532 instrument_id,
533 client_order_id,
534 order_side,
535 quantity,
536 price,
537 time_in_force,
538 expire_time,
539 is_post_only,
540 is_reduce_only,
541 is_quote_quantity,
542 display_qty,
543 emulation_trigger,
544 trigger_instrument_id,
545 contingency_type,
546 order_list_id,
547 linked_order_ids,
548 parent_order_id,
549 exec_algorithm_id,
550 exec_algorithm_params,
551 exec_spawn_id,
552 tags,
553 init_id,
554 ts_init.into(),
555 )
556 .map_err(to_pyvalue_err)
557 }
558
559 #[pyo3(name = "to_dict")]
560 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
561 let dict = PyDict::new(py);
562 dict.set_item("trader_id", self.trader_id.to_string())?;
563 dict.set_item("strategy_id", self.strategy_id.to_string())?;
564 dict.set_item("instrument_id", self.instrument_id.to_string())?;
565 dict.set_item("client_order_id", self.client_order_id.to_string())?;
566 dict.set_item("side", self.side.to_string())?;
567 dict.set_item("type", self.order_type.to_string())?;
568 dict.set_item("quantity", self.quantity.to_string())?;
569 dict.set_item("price", self.price.to_string())?;
570 dict.set_item("status", self.status.to_string())?;
571 dict.set_item("time_in_force", self.time_in_force.to_string())?;
572 dict.set_item(
573 "expire_time_ns",
574 self.expire_time.filter(|&t| t != 0).map(|t| t.as_u64()),
575 )?;
576 dict.set_item("is_post_only", self.is_post_only)?;
577 dict.set_item("is_reduce_only", self.is_reduce_only)?;
578 dict.set_item("is_quote_quantity", self.is_quote_quantity)?;
579 dict.set_item("filled_qty", self.filled_qty.to_string())?;
580 dict.set_item("init_id", self.init_id.to_string())?;
581 dict.set_item("ts_init", self.ts_init.as_u64())?;
582 dict.set_item("ts_last", self.ts_last.as_u64())?;
583 dict.set_item(
584 "commissions",
585 commissions_from_indexmap(py, self.commissions())?,
586 )?;
587 self.venue_order_id.map_or_else(
588 || dict.set_item("venue_order_id", py.None()),
589 |x| dict.set_item("venue_order_id", x.to_string()),
590 )?;
591 self.display_qty.map_or_else(
592 || dict.set_item("display_qty", py.None()),
593 |x| dict.set_item("display_qty", x.to_string()),
594 )?;
595 self.emulation_trigger.map_or_else(
596 || dict.set_item("emulation_trigger", py.None()),
597 |x| dict.set_item("emulation_trigger", x.to_string()),
598 )?;
599 self.trigger_instrument_id.map_or_else(
600 || dict.set_item("trigger_instrument_id", py.None()),
601 |x| dict.set_item("trigger_instrument_id", x.to_string()),
602 )?;
603 self.contingency_type.map_or_else(
604 || dict.set_item("contingency_type", py.None()),
605 |x| dict.set_item("contingency_type", x.to_string()),
606 )?;
607 self.order_list_id.map_or_else(
608 || dict.set_item("order_list_id", py.None()),
609 |x| dict.set_item("order_list_id", x.to_string()),
610 )?;
611 self.linked_order_ids.clone().map_or_else(
612 || dict.set_item("linked_order_ids", py.None()),
613 |linked_order_ids| {
614 let linked_order_ids_list =
615 PyList::new(py, linked_order_ids.iter().map(ToString::to_string))
616 .expect("Invalid `ExactSizeIterator`");
617 dict.set_item("linked_order_ids", linked_order_ids_list)
618 },
619 )?;
620 self.parent_order_id.map_or_else(
621 || dict.set_item("parent_order_id", py.None()),
622 |x| dict.set_item("parent_order_id", x.to_string()),
623 )?;
624 self.exec_algorithm_id.map_or_else(
625 || dict.set_item("exec_algorithm_id", py.None()),
626 |x| dict.set_item("exec_algorithm_id", x.to_string()),
627 )?;
628
629 match &self.exec_algorithm_params {
630 Some(exec_algorithm_params) => {
631 let py_exec_algorithm_params = PyDict::new(py);
632 for (key, value) in exec_algorithm_params {
633 py_exec_algorithm_params.set_item(key.to_string(), value.to_string())?;
634 }
635 dict.set_item("exec_algorithm_params", py_exec_algorithm_params)?;
636 }
637 None => dict.set_item("exec_algorithm_params", py.None())?,
638 }
639 self.exec_spawn_id.map_or_else(
640 || dict.set_item("exec_spawn_id", py.None()),
641 |x| dict.set_item("exec_spawn_id", x.to_string()),
642 )?;
643 self.tags.clone().map_or_else(
644 || dict.set_item("tags", py.None()),
645 |x| {
646 dict.set_item(
647 "tags",
648 x.iter().map(|x| x.to_string()).collect::<Vec<String>>(),
649 )
650 },
651 )?;
652 self.account_id.map_or_else(
653 || dict.set_item("account_id", py.None()),
654 |x| dict.set_item("account_id", x.to_string()),
655 )?;
656 self.slippage.map_or_else(
657 || dict.set_item("slippage", py.None()),
658 |x| dict.set_item("slippage", x.to_string()),
659 )?;
660 self.position_id.map_or_else(
661 || dict.set_item("position_id", py.None()),
662 |x| dict.set_item("position_id", x.to_string()),
663 )?;
664 self.liquidity_side.map_or_else(
665 || dict.set_item("liquidity_side", py.None()),
666 |x| dict.set_item("liquidity_side", x.to_string()),
667 )?;
668 self.last_trade_id.map_or_else(
669 || dict.set_item("last_trade_id", py.None()),
670 |x| dict.set_item("last_trade_id", x.to_string()),
671 )?;
672 self.avg_px.map_or_else(
673 || dict.set_item("avg_px", py.None()),
674 |x| dict.set_item("avg_px", x),
675 )?;
676 Ok(dict.into())
677 }
678}