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