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