Skip to main content

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