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