nautilus_model/python/orders/
stop_limit.rs

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