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