Skip to main content

nautilus_model/python/events/order/
fill_voided.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// -------------------------------------------------------------------------------------------------
8
9use indexmap::IndexMap;
10use nautilus_core::{
11    UUID4,
12    python::{
13        IntoPyObjectNautilusExt,
14        serialization::{from_dict_pyo3, to_dict_pyo3},
15    },
16};
17use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
18use ustr::Ustr;
19
20use crate::{
21    enums::{LiquiditySide, OrderSide, OrderType},
22    events::OrderFillVoided,
23    identifiers::{
24        AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
25        VenueOrderId,
26    },
27    orders::str_indexmap_to_ustr,
28    types::{Currency, Money, Price, Quantity},
29};
30
31#[pymethods]
32#[pyo3_stub_gen::derive::gen_stub_pymethods]
33impl OrderFillVoided {
34    /// Records that a cumulative fill quantity no longer has economic effect.
35    ///
36    /// The correction identity, voided quantity, and commission are cumulative for the referenced
37    /// trade. `is_reopened` records positive evidence that the corrected order is executable again
38    /// and therefore requires the referenced fill to have been applied locally. Without a local fill,
39    /// a non-reopened correction is an authoritative terminal order void.
40    #[expect(clippy::too_many_arguments)]
41    #[new]
42    #[pyo3(signature = (trader_id, strategy_id, instrument_id, client_order_id, venue_order_id, account_id, correction_id, trade_id, voided_qty, order_side, order_type, last_px, currency, liquidity_side, event_id, ts_event, ts_init, reconciliation, is_reopened=false, commission_voided=None, position_id=None, reason=None, info=None))]
43    fn py_new(
44        trader_id: TraderId,
45        strategy_id: StrategyId,
46        instrument_id: InstrumentId,
47        client_order_id: ClientOrderId,
48        venue_order_id: VenueOrderId,
49        account_id: AccountId,
50        correction_id: &str,
51        trade_id: TradeId,
52        voided_qty: Quantity,
53        order_side: OrderSide,
54        order_type: OrderType,
55        last_px: Price,
56        currency: Currency,
57        liquidity_side: LiquiditySide,
58        event_id: UUID4,
59        ts_event: u64,
60        ts_init: u64,
61        reconciliation: bool,
62        is_reopened: bool,
63        commission_voided: Option<Money>,
64        position_id: Option<PositionId>,
65        reason: Option<&str>,
66        info: Option<IndexMap<String, String>>,
67    ) -> Self {
68        Self::new(
69            trader_id,
70            strategy_id,
71            instrument_id,
72            client_order_id,
73            venue_order_id,
74            account_id,
75            Ustr::from(correction_id),
76            trade_id,
77            voided_qty,
78            commission_voided,
79            order_side,
80            order_type,
81            last_px,
82            currency,
83            liquidity_side,
84            position_id,
85            reason.map(Ustr::from),
86            info.map(str_indexmap_to_ustr),
87            event_id,
88            ts_event.into(),
89            ts_init.into(),
90            reconciliation,
91            is_reopened,
92        )
93    }
94
95    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
96        match op {
97            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
98            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
99            _ => py.NotImplemented(),
100        }
101    }
102
103    fn __repr__(&self) -> String {
104        format!("{self:?}")
105    }
106    fn __str__(&self) -> String {
107        self.to_string()
108    }
109
110    #[getter]
111    fn trader_id(&self) -> TraderId {
112        self.trader_id
113    }
114    #[getter]
115    fn strategy_id(&self) -> StrategyId {
116        self.strategy_id
117    }
118    #[getter]
119    fn instrument_id(&self) -> InstrumentId {
120        self.instrument_id
121    }
122    #[getter]
123    fn client_order_id(&self) -> ClientOrderId {
124        self.client_order_id
125    }
126    #[getter]
127    fn venue_order_id(&self) -> VenueOrderId {
128        self.venue_order_id
129    }
130    #[getter]
131    fn account_id(&self) -> AccountId {
132        self.account_id
133    }
134    #[getter]
135    fn correction_id(&self) -> &str {
136        self.correction_id.as_str()
137    }
138    #[getter]
139    fn trade_id(&self) -> TradeId {
140        self.trade_id
141    }
142    #[getter]
143    fn voided_qty(&self) -> Quantity {
144        self.voided_qty
145    }
146    #[getter]
147    fn commission_voided(&self) -> Option<Money> {
148        self.commission_voided
149    }
150    #[getter]
151    fn order_side(&self) -> OrderSide {
152        self.order_side
153    }
154    #[getter]
155    fn order_type(&self) -> OrderType {
156        self.order_type
157    }
158    #[getter]
159    fn last_px(&self) -> Price {
160        self.last_px
161    }
162    #[getter]
163    fn currency(&self) -> Currency {
164        self.currency
165    }
166    #[getter]
167    fn liquidity_side(&self) -> LiquiditySide {
168        self.liquidity_side
169    }
170    #[getter]
171    fn position_id(&self) -> Option<PositionId> {
172        self.position_id
173    }
174    #[getter]
175    fn reason(&self) -> Option<&str> {
176        self.reason.map(|value| value.as_str())
177    }
178    #[getter]
179    fn event_id(&self) -> UUID4 {
180        self.event_id
181    }
182    #[getter]
183    fn ts_event(&self) -> u64 {
184        self.ts_event.as_u64()
185    }
186    #[getter]
187    fn ts_init(&self) -> u64 {
188        self.ts_init.as_u64()
189    }
190    #[getter]
191    fn reconciliation(&self) -> bool {
192        self.reconciliation
193    }
194    #[getter]
195    fn is_reopened(&self) -> bool {
196        self.is_reopened
197    }
198    #[getter]
199    fn causation_id(&self) -> Option<UUID4> {
200        self.causation_id
201    }
202
203    #[getter]
204    fn info(&self) -> Option<IndexMap<&str, &str>> {
205        self.info.as_ref().map(|info| {
206            info.iter()
207                .map(|(key, value)| (key.as_str(), value.as_str()))
208                .collect()
209        })
210    }
211
212    #[staticmethod]
213    #[pyo3(name = "from_dict")]
214    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
215        from_dict_pyo3(py, values)
216    }
217
218    #[pyo3(name = "to_dict")]
219    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
220        to_dict_pyo3(py, self)
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use nautilus_core::UUID4;
227    use rstest::rstest;
228
229    use super::*;
230    use crate::events::order::spec::OrderFillVoidedSpec;
231
232    #[rstest]
233    fn test_order_fill_voided_python_dict_round_trip() {
234        let mut event = OrderFillVoidedSpec::builder()
235            .correction_id(Ustr::from("CORRECTION-PYTHON"))
236            .voided_qty(Quantity::from("0.561000"))
237            .commission_voided(Money::from("12.20000000 USDT"))
238            .position_id(PositionId::from("P-001"))
239            .reason(Ustr::from("VENUE_VOID"))
240            .info(IndexMap::from([(Ustr::from("source"), Ustr::from("test"))]))
241            .reconciliation(true)
242            .is_reopened(true)
243            .build();
244        event.causation_id = Some(UUID4::new());
245
246        Python::initialize();
247        Python::attach(|py| {
248            let values = event.py_to_dict(py).unwrap();
249            let restored = OrderFillVoided::py_from_dict(py, values).unwrap();
250
251            assert_eq!(restored, event);
252        });
253    }
254}