Skip to main content

nautilus_model/python/orders/
stop_market.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Creates a new `StopMarketOrder` instance.
49    #[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}