Skip to main content

nautilus_model/python/reports/
order.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 nautilus_core::{
17    UUID4,
18    python::{IntoPyObjectNautilusExt, serialization::from_dict_pyo3},
19};
20use pyo3::{
21    Py,
22    basic::CompareOp,
23    prelude::*,
24    types::{PyDict, PyList},
25};
26use rust_decimal::Decimal;
27
28use crate::{
29    enums::{
30        ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType,
31        TriggerType,
32    },
33    identifiers::{AccountId, ClientOrderId, InstrumentId, OrderListId, PositionId, VenueOrderId},
34    reports::order::OrderStatusReport,
35    types::{Price, Quantity},
36};
37
38#[pymethods]
39impl OrderStatusReport {
40    #[new]
41    #[allow(clippy::too_many_arguments)]
42    #[pyo3(signature = (
43        account_id,
44        instrument_id,
45        venue_order_id,
46        order_side,
47        order_type,
48        time_in_force,
49        order_status,
50        quantity,
51        filled_qty,
52        ts_accepted,
53        ts_last,
54        ts_init,
55        client_order_id=None,
56        report_id=None,
57        order_list_id=None,
58        venue_position_id=None,
59        linked_order_ids=None,
60        parent_order_id=None,
61        contingency_type=None,
62        expire_time=None,
63        price=None,
64        trigger_price=None,
65        trigger_type=None,
66        limit_offset=None,
67        trailing_offset=None,
68        trailing_offset_type=None,
69        avg_px=None,
70        display_qty=None,
71        post_only=false,
72        reduce_only=false,
73        cancel_reason=None,
74        ts_triggered=None,
75    ))]
76    fn py_new(
77        account_id: AccountId,
78        instrument_id: InstrumentId,
79        venue_order_id: VenueOrderId,
80        order_side: OrderSide,
81        order_type: OrderType,
82        time_in_force: TimeInForce,
83        order_status: OrderStatus,
84        quantity: Quantity,
85        filled_qty: Quantity,
86        ts_accepted: u64,
87        ts_last: u64,
88        ts_init: u64,
89        client_order_id: Option<ClientOrderId>,
90        report_id: Option<UUID4>,
91        order_list_id: Option<OrderListId>,
92        venue_position_id: Option<PositionId>,
93        linked_order_ids: Option<Vec<ClientOrderId>>,
94        parent_order_id: Option<ClientOrderId>,
95        contingency_type: Option<ContingencyType>,
96        expire_time: Option<u64>,
97        price: Option<Price>,
98        trigger_price: Option<Price>,
99        trigger_type: Option<TriggerType>,
100        limit_offset: Option<Decimal>,
101        trailing_offset: Option<Decimal>,
102        trailing_offset_type: Option<TrailingOffsetType>,
103        avg_px: Option<Decimal>,
104        display_qty: Option<Quantity>,
105        post_only: bool,
106        reduce_only: bool,
107        cancel_reason: Option<String>,
108        ts_triggered: Option<u64>,
109    ) -> PyResult<Self> {
110        let mut report = Self::new(
111            account_id,
112            instrument_id,
113            client_order_id,
114            venue_order_id,
115            order_side,
116            order_type,
117            time_in_force,
118            order_status,
119            quantity,
120            filled_qty,
121            ts_accepted.into(),
122            ts_last.into(),
123            ts_init.into(),
124            report_id,
125        );
126
127        if let Some(order_list_id) = order_list_id {
128            report = report.with_order_list_id(order_list_id);
129        }
130
131        if let Some(venue_position_id) = venue_position_id {
132            report = report.with_venue_position_id(venue_position_id);
133        }
134
135        if let Some(linked_order_ids) = linked_order_ids {
136            report = report.with_linked_order_ids(linked_order_ids);
137        }
138
139        if let Some(parent_order_id) = parent_order_id {
140            report = report.with_parent_order_id(parent_order_id);
141        }
142
143        if let Some(contingency_type) = contingency_type {
144            report = report.with_contingency_type(contingency_type);
145        }
146
147        if let Some(expire_time) = expire_time {
148            report = report.with_expire_time(expire_time.into());
149        }
150
151        if let Some(price) = price {
152            report = report.with_price(price);
153        }
154
155        if let Some(trigger_price) = trigger_price {
156            report = report.with_trigger_price(trigger_price);
157        }
158
159        if let Some(trigger_type) = trigger_type {
160            report = report.with_trigger_type(trigger_type);
161        }
162
163        if let Some(limit_offset) = limit_offset {
164            report = report.with_limit_offset(limit_offset);
165        }
166
167        if let Some(trailing_offset) = trailing_offset {
168            report = report.with_trailing_offset(trailing_offset);
169        }
170
171        if let Some(trailing_offset_type) = trailing_offset_type {
172            report = report.with_trailing_offset_type(trailing_offset_type);
173        }
174
175        if let Some(avg_px) = avg_px {
176            report.avg_px = Some(avg_px);
177        }
178
179        if let Some(display_qty) = display_qty {
180            report = report.with_display_qty(display_qty);
181        }
182
183        if post_only {
184            report = report.with_post_only(post_only);
185        }
186
187        if reduce_only {
188            report = report.with_reduce_only(reduce_only);
189        }
190
191        if let Some(cancel_reason) = cancel_reason {
192            report = report.with_cancel_reason(cancel_reason);
193        }
194
195        if let Some(ts_triggered) = ts_triggered {
196            report = report.with_ts_triggered(ts_triggered.into());
197        }
198
199        Ok(report)
200    }
201
202    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
203        match op {
204            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
205            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
206            _ => py.NotImplemented(),
207        }
208    }
209
210    fn __repr__(&self) -> String {
211        self.to_string()
212    }
213
214    fn __str__(&self) -> String {
215        self.to_string()
216    }
217
218    #[getter]
219    #[pyo3(name = "account_id")]
220    const fn py_account_id(&self) -> AccountId {
221        self.account_id
222    }
223
224    #[getter]
225    #[pyo3(name = "instrument_id")]
226    const fn py_instrument_id(&self) -> InstrumentId {
227        self.instrument_id
228    }
229
230    #[getter]
231    #[pyo3(name = "venue_order_id")]
232    const fn py_venue_order_id(&self) -> VenueOrderId {
233        self.venue_order_id
234    }
235
236    #[getter]
237    #[pyo3(name = "order_side")]
238    const fn py_order_side(&self) -> OrderSide {
239        self.order_side
240    }
241
242    #[getter]
243    #[pyo3(name = "order_type")]
244    const fn py_order_type(&self) -> OrderType {
245        self.order_type
246    }
247
248    #[getter]
249    #[pyo3(name = "time_in_force")]
250    const fn py_time_in_force(&self) -> TimeInForce {
251        self.time_in_force
252    }
253
254    #[getter]
255    #[pyo3(name = "order_status")]
256    const fn py_order_status(&self) -> OrderStatus {
257        self.order_status
258    }
259
260    #[getter]
261    #[pyo3(name = "quantity")]
262    const fn py_quantity(&self) -> Quantity {
263        self.quantity
264    }
265
266    #[getter]
267    #[pyo3(name = "filled_qty")]
268    const fn py_filled_qty(&self) -> Quantity {
269        self.filled_qty
270    }
271
272    #[getter]
273    #[pyo3(name = "report_id")]
274    const fn py_report_id(&self) -> UUID4 {
275        self.report_id
276    }
277
278    #[getter]
279    #[pyo3(name = "ts_accepted")]
280    const fn py_ts_accepted(&self) -> u64 {
281        self.ts_accepted.as_u64()
282    }
283
284    #[getter]
285    #[pyo3(name = "ts_last")]
286    const fn py_ts_last(&self) -> u64 {
287        self.ts_last.as_u64()
288    }
289
290    #[getter]
291    #[pyo3(name = "ts_init")]
292    const fn py_ts_init(&self) -> u64 {
293        self.ts_init.as_u64()
294    }
295
296    #[getter]
297    #[pyo3(name = "client_order_id")]
298    const fn py_client_order_id(&self) -> Option<ClientOrderId> {
299        self.client_order_id
300    }
301
302    #[getter]
303    #[pyo3(name = "order_list_id")]
304    const fn py_order_list_id(&self) -> Option<OrderListId> {
305        self.order_list_id
306    }
307
308    #[getter]
309    #[pyo3(name = "venue_position_id")]
310    const fn py_venue_position_id(&self) -> Option<PositionId> {
311        self.venue_position_id
312    }
313
314    #[getter]
315    #[pyo3(name = "linked_order_ids")]
316    fn py_linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
317        self.linked_order_ids.clone()
318    }
319
320    #[getter]
321    #[pyo3(name = "parent_order_id")]
322    fn py_parent_order_id(&self) -> Option<ClientOrderId> {
323        self.parent_order_id
324    }
325
326    #[getter]
327    #[pyo3(name = "contingency_type")]
328    const fn py_contingency_type(&self) -> ContingencyType {
329        self.contingency_type
330    }
331
332    #[getter]
333    #[pyo3(name = "expire_time")]
334    fn py_expire_time(&self) -> Option<u64> {
335        self.expire_time.map(|t| t.as_u64())
336    }
337
338    #[getter]
339    #[pyo3(name = "price")]
340    const fn py_price(&self) -> Option<Price> {
341        self.price
342    }
343
344    #[getter]
345    #[pyo3(name = "trigger_price")]
346    const fn py_trigger_price(&self) -> Option<Price> {
347        self.trigger_price
348    }
349
350    #[getter]
351    #[pyo3(name = "trigger_type")]
352    const fn py_trigger_type(&self) -> Option<TriggerType> {
353        self.trigger_type
354    }
355
356    #[getter]
357    #[pyo3(name = "limit_offset")]
358    const fn py_limit_offset(&self) -> Option<Decimal> {
359        self.limit_offset
360    }
361
362    #[getter]
363    #[pyo3(name = "trailing_offset")]
364    const fn py_trailing_offset(&self) -> Option<Decimal> {
365        self.trailing_offset
366    }
367
368    #[getter]
369    #[pyo3(name = "trailing_offset_type")]
370    const fn py_trailing_offset_type(&self) -> TrailingOffsetType {
371        self.trailing_offset_type
372    }
373
374    #[getter]
375    #[pyo3(name = "avg_px")]
376    fn py_avg_px(&self) -> Option<Decimal> {
377        self.avg_px
378    }
379
380    #[getter]
381    #[pyo3(name = "display_qty")]
382    const fn py_display_qty(&self) -> Option<Quantity> {
383        self.display_qty
384    }
385
386    #[getter]
387    #[pyo3(name = "post_only")]
388    const fn py_post_only(&self) -> bool {
389        self.post_only
390    }
391
392    #[getter]
393    #[pyo3(name = "reduce_only")]
394    const fn py_reduce_only(&self) -> bool {
395        self.reduce_only
396    }
397
398    #[getter]
399    #[pyo3(name = "cancel_reason")]
400    fn py_cancel_reason(&self) -> Option<String> {
401        self.cancel_reason.clone()
402    }
403
404    #[getter]
405    #[pyo3(name = "ts_triggered")]
406    fn py_ts_triggered(&self) -> Option<u64> {
407        self.ts_triggered.map(|t| t.as_u64())
408    }
409
410    #[getter]
411    #[pyo3(name = "is_open")]
412    fn py_is_open(&self) -> bool {
413        matches!(
414            self.order_status,
415            OrderStatus::Accepted
416                | OrderStatus::Triggered
417                | OrderStatus::PendingCancel
418                | OrderStatus::PendingUpdate
419                | OrderStatus::PartiallyFilled
420        )
421    }
422
423    /// Creates an `OrderStatusReport` from a Python dictionary.
424    ///
425    /// # Errors
426    ///
427    /// Returns a Python exception if conversion from dict fails.
428    #[staticmethod]
429    #[pyo3(name = "from_dict")]
430    pub fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
431        from_dict_pyo3(py, values)
432    }
433
434    /// Converts the `OrderStatusReport` to a Python dictionary.
435    ///
436    /// # Errors
437    ///
438    /// Returns a Python exception if conversion to dict fails.
439    #[pyo3(name = "to_dict")]
440    pub fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
441        let dict = PyDict::new(py);
442        dict.set_item("type", stringify!(OrderStatusReport))?;
443        dict.set_item("account_id", self.account_id.to_string())?;
444        dict.set_item("instrument_id", self.instrument_id.to_string())?;
445        dict.set_item("venue_order_id", self.venue_order_id.to_string())?;
446        dict.set_item("order_side", self.order_side.to_string())?;
447        dict.set_item("order_type", self.order_type.to_string())?;
448        dict.set_item("time_in_force", self.time_in_force.to_string())?;
449        dict.set_item("order_status", self.order_status.to_string())?;
450        dict.set_item("quantity", self.quantity.to_string())?;
451        dict.set_item("filled_qty", self.filled_qty.to_string())?;
452        dict.set_item("report_id", self.report_id.to_string())?;
453        dict.set_item("ts_accepted", self.ts_accepted.as_u64())?;
454        dict.set_item("ts_last", self.ts_last.as_u64())?;
455        dict.set_item("ts_init", self.ts_init.as_u64())?;
456        dict.set_item("contingency_type", self.contingency_type.to_string())?;
457        dict.set_item(
458            "trailing_offset_type",
459            self.trailing_offset_type.to_string(),
460        )?;
461        dict.set_item("post_only", self.post_only)?;
462        dict.set_item("reduce_only", self.reduce_only)?;
463
464        match &self.client_order_id {
465            Some(id) => dict.set_item("client_order_id", id.to_string())?,
466            None => dict.set_item("client_order_id", py.None())?,
467        }
468        match &self.order_list_id {
469            Some(id) => dict.set_item("order_list_id", id.to_string())?,
470            None => dict.set_item("order_list_id", py.None())?,
471        }
472        match &self.venue_position_id {
473            Some(id) => dict.set_item("venue_position_id", id.to_string())?,
474            None => dict.set_item("venue_position_id", py.None())?,
475        }
476        match &self.linked_order_ids {
477            Some(ids) => {
478                let py_list = PyList::new(py, ids.iter().map(|id| id.to_string()))?;
479                dict.set_item("linked_order_ids", py_list)?;
480            }
481            None => dict.set_item("linked_order_ids", py.None())?,
482        }
483        match &self.parent_order_id {
484            Some(id) => dict.set_item("parent_order_id", id.to_string())?,
485            None => dict.set_item("parent_order_id", py.None())?,
486        }
487        match &self.expire_time {
488            Some(t) => dict.set_item("expire_time", t.as_u64())?,
489            None => dict.set_item("expire_time", py.None())?,
490        }
491        match &self.price {
492            Some(p) => dict.set_item("price", p.to_string())?,
493            None => dict.set_item("price", py.None())?,
494        }
495        match &self.trigger_price {
496            Some(p) => dict.set_item("trigger_price", p.to_string())?,
497            None => dict.set_item("trigger_price", py.None())?,
498        }
499        match &self.trigger_type {
500            Some(t) => dict.set_item("trigger_type", t.to_string())?,
501            None => dict.set_item("trigger_type", py.None())?,
502        }
503        match &self.limit_offset {
504            Some(o) => dict.set_item("limit_offset", o.to_string())?,
505            None => dict.set_item("limit_offset", py.None())?,
506        }
507        match &self.trailing_offset {
508            Some(o) => dict.set_item("trailing_offset", o.to_string())?,
509            None => dict.set_item("trailing_offset", py.None())?,
510        }
511        match &self.avg_px {
512            Some(p) => dict.set_item("avg_px", p)?,
513            None => dict.set_item("avg_px", py.None())?,
514        }
515        match &self.display_qty {
516            Some(q) => dict.set_item("display_qty", q.to_string())?,
517            None => dict.set_item("display_qty", py.None())?,
518        }
519        match &self.cancel_reason {
520            Some(r) => dict.set_item("cancel_reason", r)?,
521            None => dict.set_item("cancel_reason", py.None())?,
522        }
523        match &self.ts_triggered {
524            Some(t) => dict.set_item("ts_triggered", t.as_u64())?,
525            None => dict.set_item("ts_triggered", py.None())?,
526        }
527
528        Ok(dict.into())
529    }
530}