From 1173782f6dc64e7929c3e574dd81077f55dd6498 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Mon, 11 Dec 2023 02:59:52 +0100 Subject: [PATCH] Implement event OrderCancelRejected in Rust (#1402) --- .../model/src/events/order/cancel_rejected.rs | 84 +++++++++- nautilus_core/model/src/events/order/stubs.rs | 36 ++++- .../python/events/order/cancel_rejected.rs | 143 ++++++++++++++++++ .../model/src/python/events/order/mod.rs | 1 + nautilus_core/model/src/python/mod.rs | 1 + nautilus_trader/core/nautilus_pyo3.pyi | 19 +++ nautilus_trader/test_kit/rust/events_pyo3.py | 17 +++ tests/unit_tests/model/test_events_pyo3.py | 19 +++ 8 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 nautilus_core/model/src/python/events/order/cancel_rejected.rs diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index 968de2e3f295..594b96e8f522 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -13,8 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +use std::fmt::Display; + +use anyhow::Result; use derive_builder::{self, Builder}; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use pyo3::prelude::*; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -27,16 +31,90 @@ use crate::identifiers::{ #[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] +#[cfg_attr( + feature = "python", + pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") +)] pub struct OrderCancelRejected { pub trader_id: TraderId, pub strategy_id: StrategyId, pub instrument_id: InstrumentId, pub client_order_id: ClientOrderId, - pub venue_order_id: Option, - pub account_id: Option, pub reason: Ustr, pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: bool, + pub reconciliation: u8, + pub venue_order_id: Option, + pub account_id: Option, +} + +impl OrderCancelRejected { + #[allow(clippy::too_many_arguments)] + pub fn new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + reason: Ustr, + event_id: UUID4, + ts_event: UnixNanos, + ts_init: UnixNanos, + reconciliation: bool, + venue_order_id: Option, + account_id: Option, + ) -> Result { + Ok(OrderCancelRejected { + trader_id, + strategy_id, + instrument_id, + client_order_id, + reason, + event_id, + ts_event, + ts_init, + reconciliation: reconciliation as u8, + venue_order_id, + account_id, + }) + } +} + +impl Display for OrderCancelRejected { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "OrderCancelRejected(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason={}, ts_event={})", + self.instrument_id, + self.client_order_id, + self.venue_order_id + .map(|venue_order_id| format!("{}", venue_order_id)) + .unwrap_or_else(|| "None".to_string()), + self.account_id + .map(|account_id| format!("{}", account_id)) + .unwrap_or_else(|| "None".to_string()), + self.reason, + self.ts_event + ) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + + use rstest::rstest; + + use crate::events::order::{cancel_rejected::OrderCancelRejected, stubs::*}; + + #[rstest] + fn test_order_cancel_rejected(order_cancel_rejected: OrderCancelRejected) { + let display = format!("{}", order_cancel_rejected); + assert_eq!( + display, + "OrderCancelRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, venue_order_id=001, account_id=SIM-001, reason=ORDER_DOES_NOT_EXISTS, ts_event=0)" + ); + } } diff --git a/nautilus_core/model/src/events/order/stubs.rs b/nautilus_core/model/src/events/order/stubs.rs index 8d16828cdcc0..8a79cd094125 100644 --- a/nautilus_core/model/src/events/order/stubs.rs +++ b/nautilus_core/model/src/events/order/stubs.rs @@ -22,11 +22,11 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TriggerType}, events::order::{ - accepted::OrderAccepted, denied::OrderDenied, emulated::OrderEmulated, filled::OrderFilled, - initialized::OrderInitialized, modify_rejected::OrderModifyRejected, - pending_cancel::OrderPendingCancel, pending_update::OrderPendingUpdate, - rejected::OrderRejected, released::OrderReleased, submitted::OrderSubmitted, - triggered::OrderTriggered, updated::OrderUpdated, + accepted::OrderAccepted, cancel_rejected::OrderCancelRejected, denied::OrderDenied, + emulated::OrderEmulated, filled::OrderFilled, initialized::OrderInitialized, + modify_rejected::OrderModifyRejected, pending_cancel::OrderPendingCancel, + pending_update::OrderPendingUpdate, rejected::OrderRejected, released::OrderReleased, + submitted::OrderSubmitted, triggered::OrderTriggered, updated::OrderUpdated, }, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, @@ -377,3 +377,29 @@ pub fn order_accepted( ) .unwrap() } + +#[fixture] +pub fn order_cancel_rejected( + trader_id: TraderId, + strategy_id_ema_cross: StrategyId, + instrument_id_btc_usdt: InstrumentId, + client_order_id: ClientOrderId, + venue_order_id: VenueOrderId, + account_id: AccountId, + uuid4: UUID4, +) -> OrderCancelRejected { + OrderCancelRejected::new( + trader_id, + strategy_id_ema_cross, + instrument_id_btc_usdt, + client_order_id, + Ustr::from("ORDER_DOES_NOT_EXISTS"), + uuid4, + 0, + 0, + false, + Some(venue_order_id), + Some(account_id), + ) + .unwrap() +} diff --git a/nautilus_core/model/src/python/events/order/cancel_rejected.rs b/nautilus_core/model/src/python/events/order/cancel_rejected.rs new file mode 100644 index 000000000000..6b7981b2ec3f --- /dev/null +++ b/nautilus_core/model/src/python/events/order/cancel_rejected.rs @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +use std::str::FromStr; + +use nautilus_core::{ + python::{serialization::from_dict_pyo3, to_pyvalue_err}, + time::UnixNanos, + uuid::UUID4, +}; +use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; +use rust_decimal::prelude::ToPrimitive; +use ustr::Ustr; + +use crate::{ + events::order::cancel_rejected::OrderCancelRejected, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, + strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, + }, +}; + +#[pymethods] +impl OrderCancelRejected { + #[allow(clippy::too_many_arguments)] + #[new] + fn py_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + reason: &str, + event_id: UUID4, + ts_event: UnixNanos, + ts_init: UnixNanos, + reconciliation: bool, + venue_order_id: Option, + account_id: Option, + ) -> PyResult { + let reason = Ustr::from_str(reason).unwrap(); + Self::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + reason, + event_id, + ts_event, + ts_init, + reconciliation, + venue_order_id, + account_id, + ) + .map_err(to_pyvalue_err) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } + } + + fn __repr__(&self) -> String { + format!( + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderCancelRejected), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id + .map(|venue_order_id| format!("{}", venue_order_id)) + .unwrap_or_else(|| "None".to_string()), + self.account_id + .map(|account_id| format!("{}", account_id)) + .unwrap_or_else(|| "None".to_string()), + self.reason, + self.event_id, + self.ts_event, + self.ts_init + ) + } + + fn __str__(&self) -> String { + format!( + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason={}, ts_event={})", + stringify!(OrderCancelRejected), + self.instrument_id, + self.client_order_id, + self.venue_order_id + .map(|venue_order_id| format!("{}", venue_order_id)) + .unwrap_or_else(|| "None".to_string()), + self.account_id + .map(|account_id| format!("{}", account_id)) + .unwrap_or_else(|| "None".to_string()), + self.reason, + self.ts_event, + ) + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("trader_id", self.trader_id.to_string())?; + dict.set_item("strategy_id", self.strategy_id.to_string())?; + dict.set_item("instrument_id", self.instrument_id.to_string())?; + dict.set_item("client_order_id", self.client_order_id.to_string())?; + dict.set_item("reason", self.reason.as_str())?; + dict.set_item("event_id", self.event_id.to_string())?; + dict.set_item("ts_event", self.ts_event.to_u64())?; + dict.set_item("ts_init", self.ts_init.to_u64())?; + dict.set_item("reconciliation", self.reconciliation)?; + match self.venue_order_id { + Some(venue_order_id) => dict.set_item("venue_order_id", venue_order_id.to_string())?, + None => dict.set_item("venue_order_id", py.None())?, + } + match self.account_id { + Some(account_id) => dict.set_item("account_id", account_id.to_string())?, + None => dict.set_item("account_id", py.None())?, + } + Ok(dict.into()) + } +} diff --git a/nautilus_core/model/src/python/events/order/mod.rs b/nautilus_core/model/src/python/events/order/mod.rs index 2aef6a66866c..cc363733bded 100644 --- a/nautilus_core/model/src/python/events/order/mod.rs +++ b/nautilus_core/model/src/python/events/order/mod.rs @@ -14,6 +14,7 @@ // ------------------------------------------------------------------------------------------------- pub mod accepted; +pub mod cancel_rejected; pub mod denied; pub mod emulated; pub mod filled; diff --git a/nautilus_core/model/src/python/mod.rs b/nautilus_core/model/src/python/mod.rs index bd00fee704d2..30d4a544ecf1 100644 --- a/nautilus_core/model/src/python/mod.rs +++ b/nautilus_core/model/src/python/mod.rs @@ -306,5 +306,6 @@ pub fn model(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 271237c99af5..f15b85cb7a82 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1028,6 +1028,25 @@ class OrderAccepted: def to_dict(self) -> dict[str, str]: ... +class OrderCancelRejected: + def __init__( + self, + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + reason: str, + event_id: UUID4, + ts_event: int, + ts_init: int, + reconciliation: bool, + venue_order_id: VenueOrderId | None = None, + account_id: AccountId | None = None, + )-> None: ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> OrderCancelRejected: ... + def to_dict(self) -> dict[str, str]: ... + ################################################################################################### # Infrastructure ################################################################################################### diff --git a/nautilus_trader/test_kit/rust/events_pyo3.py b/nautilus_trader/test_kit/rust/events_pyo3.py index 9ba02f9e3554..41f98b47c557 100644 --- a/nautilus_trader/test_kit/rust/events_pyo3.py +++ b/nautilus_trader/test_kit/rust/events_pyo3.py @@ -20,6 +20,7 @@ from nautilus_trader.core.nautilus_pyo3 import LiquiditySide from nautilus_trader.core.nautilus_pyo3 import Money from nautilus_trader.core.nautilus_pyo3 import OrderAccepted +from nautilus_trader.core.nautilus_pyo3 import OrderCancelRejected from nautilus_trader.core.nautilus_pyo3 import OrderDenied from nautilus_trader.core.nautilus_pyo3 import OrderEmulated from nautilus_trader.core.nautilus_pyo3 import OrderFilled @@ -262,3 +263,19 @@ def order_accepted() -> OrderAccepted: ts_event=0, reconciliation=False, ) + + @staticmethod + def order_cancel_rejected() -> OrderCancelRejected: + return OrderCancelRejected( + trader_id=TestIdProviderPyo3.trader_id(), + strategy_id=TestIdProviderPyo3.strategy_id(), + instrument_id=TestIdProviderPyo3.ethusdt_binance_id(), + client_order_id=TestIdProviderPyo3.client_order_id(), + account_id=TestIdProviderPyo3.account_id(), + venue_order_id=TestIdProviderPyo3.venue_order_id(), + reason="ORDER_DOES_NOT_EXIST", + event_id=UUID4(uuid), + ts_init=0, + ts_event=0, + reconciliation=False, + ) diff --git a/tests/unit_tests/model/test_events_pyo3.py b/tests/unit_tests/model/test_events_pyo3.py index 0267a82bafb5..885edef8dbcc 100644 --- a/tests/unit_tests/model/test_events_pyo3.py +++ b/tests/unit_tests/model/test_events_pyo3.py @@ -15,6 +15,7 @@ from nautilus_trader.core.nautilus_pyo3 import OrderAccepted +from nautilus_trader.core.nautilus_pyo3 import OrderCancelRejected from nautilus_trader.core.nautilus_pyo3 import OrderDenied from nautilus_trader.core.nautilus_pyo3 import OrderEmulated from nautilus_trader.core.nautilus_pyo3 import OrderFilled @@ -266,3 +267,21 @@ def test_order_accepted(): == "OrderAccepted(trader_id=TESTER-001, strategy_id=S-001, instrument_id=ETHUSDT.BINANCE, client_order_id=O-20210410-022422-001-001-1, " + "venue_order_id=123456, account_id=SIM-000, event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" ) + + +def test_order_cancel_rejected(): + event = TestEventsProviderPyo3.order_cancel_rejected() + result_dict = OrderCancelRejected.to_dict(event) + order_cancel_rejected = OrderCancelRejected.from_dict(result_dict) + assert order_cancel_rejected == event + assert ( + str(event) + == "OrderCancelRejected(instrument_id=ETHUSDT.BINANCE, client_order_id=O-20210410-022422-001-001-1, venue_order_id=123456, " + + "account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, ts_event=0)" + ) + assert ( + repr(event) + == "OrderCancelRejected(trader_id=TESTER-001, strategy_id=S-001, instrument_id=ETHUSDT.BINANCE, " + + "client_order_id=O-20210410-022422-001-001-1, venue_order_id=123456, account_id=SIM-000, " + + "reason=ORDER_DOES_NOT_EXIST, event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" + )