Skip to main content

nautilus_model/python/orders/
limit.rs

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