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