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