nautilus_model/python/orders/
stop_limit.rs

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