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