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