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