From 180c5df910fff761143efb49828de131a1fc6d44 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 20 Apr 2024 11:41:33 +1000 Subject: [PATCH 001/193] Bump version --- README.md | 8 ++++---- nautilus_core/Cargo.lock | 24 ++++++++++++------------ nautilus_core/Cargo.toml | 4 ++-- nautilus_core/rust-toolchain.toml | 2 +- pyproject.toml | 2 +- version.json | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 776b68ad14c5..6a5ade00169a 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ | Platform | Rust | Python | | :----------------- | :------ | :----- | -| `Linux (x86_64)` | 1.77.1+ | 3.10+ | -| `macOS (x86_64)` | 1.77.1+ | 3.10+ | -| `macOS (arm64)` | 1.77.1+ | 3.10+ | -| `Windows (x86_64)` | 1.77.1+ | 3.10+ | +| `Linux (x86_64)` | 1.77.2+ | 3.10+ | +| `macOS (x86_64)` | 1.77.2+ | 3.10+ | +| `macOS (arm64)` | 1.77.2+ | 3.10+ | +| `Windows (x86_64)` | 1.77.2+ | 3.10+ | - **Website:** https://nautilustrader.io - **Docs:** https://docs.nautilustrader.io diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 673858a77798..c8def8558528 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2479,7 +2479,7 @@ dependencies = [ [[package]] name = "nautilus-accounting" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "cbindgen", @@ -2495,7 +2495,7 @@ dependencies = [ [[package]] name = "nautilus-adapters" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "chrono", @@ -2526,7 +2526,7 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "cbindgen", @@ -2543,7 +2543,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "cbindgen", @@ -2571,7 +2571,7 @@ dependencies = [ [[package]] name = "nautilus-core" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "cbindgen", @@ -2590,7 +2590,7 @@ dependencies = [ [[package]] name = "nautilus-execution" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "criterion", @@ -2615,7 +2615,7 @@ dependencies = [ [[package]] name = "nautilus-indicators" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "nautilus-core", @@ -2627,7 +2627,7 @@ dependencies = [ [[package]] name = "nautilus-infrastructure" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "nautilus-common", @@ -2643,7 +2643,7 @@ dependencies = [ [[package]] name = "nautilus-model" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "cbindgen", @@ -2671,7 +2671,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "axum", @@ -2696,7 +2696,7 @@ dependencies = [ [[package]] name = "nautilus-persistence" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "binary-heap-plus", @@ -2720,7 +2720,7 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.21.0" +version = "0.22.0" dependencies = [ "nautilus-accounting", "nautilus-adapters", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 9b8cf2f39019..28d128c81c93 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -17,8 +17,8 @@ members = [ ] [workspace.package] -rust-version = "1.77.1" -version = "0.21.0" +rust-version = "1.77.2" +version = "0.22.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" diff --git a/nautilus_core/rust-toolchain.toml b/nautilus_core/rust-toolchain.toml index d2af5ff31167..e8adc29550cf 100644 --- a/nautilus_core/rust-toolchain.toml +++ b/nautilus_core/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -version = "1.77.1" +version = "1.77.2" channel = "stable" diff --git a/pyproject.toml b/pyproject.toml index 6c2eb7224f25..dc5bc2939d76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.191.0" +version = "1.192.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" diff --git a/version.json b/version.json index 1f85a5e6161b..5695ec227447 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.191.0", + "message": "v1.192.0", "color": "orange" } From b8ce7add1ec627c965ed596e0fc837c535a70418 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 20 Apr 2024 12:18:27 +1000 Subject: [PATCH 002/193] Fix docstring typos --- nautilus_core/model/src/data/quote.rs | 2 +- nautilus_core/model/src/enums.rs | 2 +- nautilus_trader/core/includes/model.h | 4 ++-- nautilus_trader/core/rust/model.pxd | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 939e85312072..0d82d2bbdbd8 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -30,7 +30,7 @@ use crate::{ types::{fixed::FIXED_PRECISION, price::Price, quantity::Quantity}, }; -/// Represents a single quote tick in market. +/// Represents a single quote tick in a market. #[repr(C)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(tag = "type")] diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index 4473cdc863c0..5ec1ae7c421a 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -873,7 +873,7 @@ pub enum PositionSide { Short = 3, } -/// The type of price for an instrument in market. +/// The type of price for an instrument in a market. #[repr(C)] #[derive( Copy, diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 214c715049a3..bf73015a0fdf 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -521,7 +521,7 @@ typedef enum PositionSide { } PositionSide; /** - * The type of price for an instrument in market. + * The type of price for an instrument in a market. */ typedef enum PriceType { /** @@ -888,7 +888,7 @@ typedef struct OrderBookDepth10_t { } OrderBookDepth10_t; /** - * Represents a single quote tick in market. + * Represents a single quote tick in a market. */ typedef struct QuoteTick_t { /** diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 98b21290661b..4dd425b87c7a 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -283,7 +283,7 @@ cdef extern from "../includes/model.h": # A short position in the market, typically acquired through one or many SELL orders. SHORT # = 3, - # The type of price for an instrument in market. + # The type of price for an instrument in a market. cpdef enum PriceType: # A quoted order price where a buyer is willing to buy a quantity of an instrument. BID # = 1, @@ -491,7 +491,7 @@ cdef extern from "../includes/model.h": # The UNIX timestamp (nanoseconds) when the struct was initialized. uint64_t ts_init; - # Represents a single quote tick in market. + # Represents a single quote tick in a market. cdef struct QuoteTick_t: # The quotes instrument ID. InstrumentId_t instrument_id; From 8dd3c5f12078d5f38794870554078cc7ea882d90 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 20 Apr 2024 13:13:42 +1000 Subject: [PATCH 003/193] Add additional Rust docs --- nautilus_core/accounting/src/lib.rs | 13 +++++++++++++ nautilus_core/accounting/src/python/mod.rs | 2 ++ .../adapters/src/databento/python/mod.rs | 2 ++ nautilus_core/adapters/src/lib.rs | 15 +++++++++++++++ nautilus_core/backtest/src/lib.rs | 14 ++++++++++++++ nautilus_core/common/src/ffi/mod.rs | 2 ++ nautilus_core/common/src/lib.rs | 15 +++++++++++++++ nautilus_core/common/src/python/mod.rs | 2 ++ nautilus_core/core/src/correctness.rs | 2 +- nautilus_core/core/src/datetime.rs | 2 ++ nautilus_core/core/src/ffi/mod.rs | 2 ++ nautilus_core/core/src/lib.rs | 14 ++++++++++++++ nautilus_core/core/src/message.rs | 2 ++ nautilus_core/core/src/nanos.rs | 2 ++ nautilus_core/core/src/parsing.rs | 2 ++ nautilus_core/core/src/python/mod.rs | 2 ++ nautilus_core/core/src/serialization.rs | 2 ++ nautilus_core/core/src/time.rs | 2 ++ nautilus_core/core/src/uuid.rs | 3 +++ nautilus_core/execution/src/lib.rs | 14 ++++++++++++++ nautilus_core/indicators/src/lib.rs | 13 +++++++++++++ nautilus_core/indicators/src/python/mod.rs | 2 ++ nautilus_core/infrastructure/src/lib.rs | 14 ++++++++++++++ nautilus_core/infrastructure/src/python/mod.rs | 2 ++ .../infrastructure/src/python/redis/mod.rs | 2 ++ nautilus_core/model/src/ffi/mod.rs | 2 ++ nautilus_core/model/src/lib.rs | 15 +++++++++++++++ nautilus_core/model/src/python/data/mod.rs | 2 ++ nautilus_core/model/src/python/enums.rs | 2 ++ nautilus_core/model/src/python/events/mod.rs | 2 ++ nautilus_core/model/src/python/identifiers/mod.rs | 2 ++ nautilus_core/model/src/python/instruments/mod.rs | 2 ++ nautilus_core/model/src/python/macros.rs | 2 ++ nautilus_core/model/src/python/mod.rs | 2 ++ nautilus_core/model/src/python/orderbook/mod.rs | 2 ++ nautilus_core/model/src/python/orders/mod.rs | 2 ++ nautilus_core/model/src/python/types/mod.rs | 2 ++ nautilus_core/network/src/lib.rs | 13 +++++++++++++ nautilus_core/network/src/python/mod.rs | 2 ++ nautilus_core/persistence/src/lib.rs | 14 ++++++++++++++ nautilus_core/persistence/src/python/mod.rs | 2 ++ nautilus_core/pyo3/src/lib.rs | 13 +++++++++++++ 42 files changed, 227 insertions(+), 1 deletion(-) diff --git a/nautilus_core/accounting/src/lib.rs b/nautilus_core/accounting/src/lib.rs index 554d29267eb3..bcfb48942d0d 100644 --- a/nautilus_core/accounting/src/lib.rs +++ b/nautilus_core/accounting/src/lib.rs @@ -13,6 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `python`: Enables Python bindings from `pyo3` + pub mod account; #[cfg(test)] pub mod stubs; diff --git a/nautilus_core/accounting/src/python/mod.rs b/nautilus_core/accounting/src/python/mod.rs index ba3ea5fabf41..6ca8aac3763b 100644 --- a/nautilus_core/accounting/src/python/mod.rs +++ b/nautilus_core/accounting/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade use pyo3::{prelude::*, pymodule}; diff --git a/nautilus_core/adapters/src/databento/python/mod.rs b/nautilus_core/adapters/src/databento/python/mod.rs index 7d87fc51db5d..ba75fdead4ac 100644 --- a/nautilus_core/adapters/src/databento/python/mod.rs +++ b/nautilus_core/adapters/src/databento/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade pub mod enums; diff --git a/nautilus_core/adapters/src/lib.rs b/nautilus_core/adapters/src/lib.rs index 43a90522ae1a..18df0a001a63 100644 --- a/nautilus_core/adapters/src/lib.rs +++ b/nautilus_core/adapters/src/lib.rs @@ -13,5 +13,20 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `databento`: Includes the Databento integration adapter +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` +//! - `python`: Enables Python bindings from `pyo3` + #[cfg(feature = "databento")] pub mod databento; diff --git a/nautilus_core/backtest/src/lib.rs b/nautilus_core/backtest/src/lib.rs index 186dfbc301a7..d0053e70778b 100644 --- a/nautilus_core/backtest/src/lib.rs +++ b/nautilus_core/backtest/src/lib.rs @@ -13,5 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` +//! - `python`: Enables Python bindings from `pyo3` + pub mod engine; pub mod matching_engine; diff --git a/nautilus_core/common/src/ffi/mod.rs b/nautilus_core/common/src/ffi/mod.rs index 23aabbf0bf27..6ef20a9758be 100644 --- a/nautilus_core/common/src/ffi/mod.rs +++ b/nautilus_core/common/src/ffi/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a C foreign function interface (FFI) from `cbindgen`. + pub mod clock; pub mod enums; pub mod logging; diff --git a/nautilus_core/common/src/lib.rs b/nautilus_core/common/src/lib.rs index 7cef1eadcd2a..0f72c9f927bd 100644 --- a/nautilus_core/common/src/lib.rs +++ b/nautilus_core/common/src/lib.rs @@ -13,6 +13,21 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` +//! - `python`: Enables Python bindings from `pyo3` +//! - `stubs`: Enables type stubs for use in testing scenarios + pub mod cache; pub mod clock; pub mod enums; diff --git a/nautilus_core/common/src/python/mod.rs b/nautilus_core/common/src/python/mod.rs index 82994c3941bc..26d2a21b6315 100644 --- a/nautilus_core/common/src/python/mod.rs +++ b/nautilus_core/common/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade pub mod clock; diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index af9994f76ae0..e298f813d43f 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines static condition checks similar to the *design by contract* philosophy +//! Provides static condition checks similar to the *design by contract* philosophy //! to help ensure logical correctness. //! //! This module provides validation checking of function or method conditions. diff --git a/nautilus_core/core/src/datetime.rs b/nautilus_core/core/src/datetime.rs index d43aaad0040f..74e7071c1202 100644 --- a/nautilus_core/core/src/datetime.rs +++ b/nautilus_core/core/src/datetime.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides common data and time functions. + use std::time::{Duration, UNIX_EPOCH}; use chrono::{ diff --git a/nautilus_core/core/src/ffi/mod.rs b/nautilus_core/core/src/ffi/mod.rs index aeb11cf7be64..dcd5dcba6e5b 100644 --- a/nautilus_core/core/src/ffi/mod.rs +++ b/nautilus_core/core/src/ffi/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a C foreign function interface (FFI) from `cbindgen`. + pub mod cvec; pub mod datetime; pub mod parsing; diff --git a/nautilus_core/core/src/lib.rs b/nautilus_core/core/src/lib.rs index fc0d612011ae..bc8c144873d0 100644 --- a/nautilus_core/core/src/lib.rs +++ b/nautilus_core/core/src/lib.rs @@ -13,6 +13,20 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` +//! - `python`: Enables Python bindings from `pyo3` + pub mod correctness; pub mod datetime; pub mod message; diff --git a/nautilus_core/core/src/message.rs b/nautilus_core/core/src/message.rs index b029c851afe9..97f5a89d7489 100644 --- a/nautilus_core/core/src/message.rs +++ b/nautilus_core/core/src/message.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines common message types. + use crate::{nanos::UnixNanos, uuid::UUID4}; #[derive(Debug, Clone)] diff --git a/nautilus_core/core/src/nanos.rs b/nautilus_core/core/src/nanos.rs index 73738013c1ca..0b7cbafc787a 100644 --- a/nautilus_core/core/src/nanos.rs +++ b/nautilus_core/core/src/nanos.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines `UnixNanos` type for working with UNIX epoch (nanoseconds). + use std::{ cmp::Ordering, fmt::Display, diff --git a/nautilus_core/core/src/parsing.rs b/nautilus_core/core/src/parsing.rs index 1910d7d91a51..8d739087eafd 100644 --- a/nautilus_core/core/src/parsing.rs +++ b/nautilus_core/core/src/parsing.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides core parsing functions. + /// Returns the decimal precision inferred from the given string. #[must_use] pub fn precision_from_str(s: &str) -> u8 { diff --git a/nautilus_core/core/src/python/mod.rs b/nautilus_core/core/src/python/mod.rs index 7169ce63f3e6..0178025f5d1f 100644 --- a/nautilus_core/core/src/python/mod.rs +++ b/nautilus_core/core/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade use std::fmt; diff --git a/nautilus_core/core/src/serialization.rs b/nautilus_core/core/src/serialization.rs index 580a35d5ad0b..fc1af358a15f 100644 --- a/nautilus_core/core/src/serialization.rs +++ b/nautilus_core/core/src/serialization.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines common serialization traits. + use serde::{Deserialize, Serialize}; /// Represents types which are serializable for JSON and `MsgPack` specifications. diff --git a/nautilus_core/core/src/time.rs b/nautilus_core/core/src/time.rs index 608b8e327e17..680ad87a8926 100644 --- a/nautilus_core/core/src/time.rs +++ b/nautilus_core/core/src/time.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides the core `AtomicTime` real-time and static clocks. + use std::{ ops::Deref, sync::{ diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 324e49918f02..c4c0cc53d0b7 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -13,6 +13,9 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines a core `UUID4` universally unique identifier (UUID) version 4 based on a 128-bit +//! label as specified in RFC 4122. + use std::{ ffi::{CStr, CString}, fmt::{Debug, Display, Formatter}, diff --git a/nautilus_core/execution/src/lib.rs b/nautilus_core/execution/src/lib.rs index 8ab43302bca9..26b0b9de9813 100644 --- a/nautilus_core/execution/src/lib.rs +++ b/nautilus_core/execution/src/lib.rs @@ -13,6 +13,20 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` +//! - `python`: Enables Python bindings from `pyo3` + pub mod client; pub mod engine; pub mod matching_core; diff --git a/nautilus_core/indicators/src/lib.rs b/nautilus_core/indicators/src/lib.rs index b087f2f9d7a7..80b5902aa5ad 100644 --- a/nautilus_core/indicators/src/lib.rs +++ b/nautilus_core/indicators/src/lib.rs @@ -13,6 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `python`: Enables Python bindings from `pyo3` + pub mod average; pub mod book; pub mod indicator; diff --git a/nautilus_core/indicators/src/python/mod.rs b/nautilus_core/indicators/src/python/mod.rs index bf60684cb54f..d0a91df07f62 100644 --- a/nautilus_core/indicators/src/python/mod.rs +++ b/nautilus_core/indicators/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade use pyo3::{prelude::*, pymodule}; diff --git a/nautilus_core/infrastructure/src/lib.rs b/nautilus_core/infrastructure/src/lib.rs index ae05731698d4..98ae3ca7c054 100644 --- a/nautilus_core/infrastructure/src/lib.rs +++ b/nautilus_core/infrastructure/src/lib.rs @@ -13,6 +13,20 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `python`: Enables Python bindings from `pyo3` +//! - `redis`: Enables the Redis cache database and message bus backing implementations + #[cfg(feature = "python")] pub mod python; diff --git a/nautilus_core/infrastructure/src/python/mod.rs b/nautilus_core/infrastructure/src/python/mod.rs index 9d64d248117c..123d5ce1512a 100644 --- a/nautilus_core/infrastructure/src/python/mod.rs +++ b/nautilus_core/infrastructure/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade use pyo3::{prelude::*, pymodule}; diff --git a/nautilus_core/infrastructure/src/python/redis/mod.rs b/nautilus_core/infrastructure/src/python/redis/mod.rs index 99962cd55278..daad03e02891 100644 --- a/nautilus_core/infrastructure/src/python/redis/mod.rs +++ b/nautilus_core/infrastructure/src/python/redis/mod.rs @@ -13,5 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a Redis cache database and message bus backing. + pub mod cache; pub mod msgbus; diff --git a/nautilus_core/model/src/ffi/mod.rs b/nautilus_core/model/src/ffi/mod.rs index 349b67048cb8..c0a94de1cfcb 100644 --- a/nautilus_core/model/src/ffi/mod.rs +++ b/nautilus_core/model/src/ffi/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a C foreign function interface (FFI) from `cbindgen`. + pub mod data; pub mod enums; pub mod events; diff --git a/nautilus_core/model/src/lib.rs b/nautilus_core/model/src/lib.rs index 1678d5646fa5..52ae6ef1d9d4 100644 --- a/nautilus_core/model/src/lib.rs +++ b/nautilus_core/model/src/lib.rs @@ -13,6 +13,21 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` +//! - `python`: Enables Python bindings from `pyo3` +//! - `stubs`: Enables type stubs for use in testing scenarios + pub mod currencies; pub mod data; pub mod enums; diff --git a/nautilus_core/model/src/python/data/mod.rs b/nautilus_core/model/src/python/data/mod.rs index 902e3faf5ad9..a9293ad58f9e 100644 --- a/nautilus_core/model/src/python/data/mod.rs +++ b/nautilus_core/model/src/python/data/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines data types for the trading domain model. + pub mod bar; pub mod delta; pub mod deltas; diff --git a/nautilus_core/model/src/python/enums.rs b/nautilus_core/model/src/python/enums.rs index a2273cb27306..77d2398304da 100644 --- a/nautilus_core/model/src/python/enums.rs +++ b/nautilus_core/model/src/python/enums.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines enumerations for the trading domain model. + use std::str::FromStr; use nautilus_core::python::to_pyvalue_err; diff --git a/nautilus_core/model/src/python/events/mod.rs b/nautilus_core/model/src/python/events/mod.rs index bf5fb9126c59..816b152aa56f 100644 --- a/nautilus_core/model/src/python/events/mod.rs +++ b/nautilus_core/model/src/python/events/mod.rs @@ -13,5 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines events for the trading domain model. + pub mod account; pub mod order; diff --git a/nautilus_core/model/src/python/identifiers/mod.rs b/nautilus_core/model/src/python/identifiers/mod.rs index 2027ca542773..d652eceaa48e 100644 --- a/nautilus_core/model/src/python/identifiers/mod.rs +++ b/nautilus_core/model/src/python/identifiers/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines identifiers the trading domain model. + use std::str::FromStr; use nautilus_core::python::to_pyvalue_err; diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index 4e4e4439af0b..b23296c47fc2 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines instrument definitions the trading domain model. + use nautilus_core::python::to_pyvalue_err; use pyo3::{IntoPy, PyObject, PyResult, Python}; diff --git a/nautilus_core/model/src/python/macros.rs b/nautilus_core/model/src/python/macros.rs index 104079322bd6..a341a1995018 100644 --- a/nautilus_core/model/src/python/macros.rs +++ b/nautilus_core/model/src/python/macros.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides macros. + #[macro_export] macro_rules! identifier_for_python { ($ty:ty) => { diff --git a/nautilus_core/model/src/python/mod.rs b/nautilus_core/model/src/python/mod.rs index 12d6477588ba..9371a2c5f8c4 100644 --- a/nautilus_core/model/src/python/mod.rs +++ b/nautilus_core/model/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade use pyo3::prelude::*; diff --git a/nautilus_core/model/src/python/orderbook/mod.rs b/nautilus_core/model/src/python/orderbook/mod.rs index 6f48823c5966..7f1ffef768b9 100644 --- a/nautilus_core/model/src/python/orderbook/mod.rs +++ b/nautilus_core/model/src/python/orderbook/mod.rs @@ -13,5 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a generic L1/L2/L3 order book the trading domain model. + pub mod book; pub mod level; diff --git a/nautilus_core/model/src/python/orders/mod.rs b/nautilus_core/model/src/python/orders/mod.rs index a18825784cb6..ea54ed343ce4 100644 --- a/nautilus_core/model/src/python/orders/mod.rs +++ b/nautilus_core/model/src/python/orders/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines order types for the trading domain model. + pub mod limit; pub mod limit_if_touched; pub mod market; diff --git a/nautilus_core/model/src/python/types/mod.rs b/nautilus_core/model/src/python/types/mod.rs index 96d67927d03a..1fa3ba707554 100644 --- a/nautilus_core/model/src/python/types/mod.rs +++ b/nautilus_core/model/src/python/types/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines value types such as `Price`, `Quantity` and `Money` for the trading domain model. + pub mod balance; pub mod currency; pub mod money; diff --git a/nautilus_core/network/src/lib.rs b/nautilus_core/network/src/lib.rs index e19f668791b1..6d7b7b831b18 100644 --- a/nautilus_core/network/src/lib.rs +++ b/nautilus_core/network/src/lib.rs @@ -13,6 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `python`: Enables Python bindings from `pyo3` + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade pub mod http; diff --git a/nautilus_core/network/src/python/mod.rs b/nautilus_core/network/src/python/mod.rs index 71dfea7bafd6..6dc97869eae9 100644 --- a/nautilus_core/network/src/python/mod.rs +++ b/nautilus_core/network/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + use pyo3::prelude::*; use crate::{http, ratelimiter, socket, websocket}; diff --git a/nautilus_core/persistence/src/lib.rs b/nautilus_core/persistence/src/lib.rs index f5c9588804b2..f046008a57c7 100644 --- a/nautilus_core/persistence/src/lib.rs +++ b/nautilus_core/persistence/src/lib.rs @@ -13,6 +13,20 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` +//! - `python`: Enables Python bindings from `pyo3` + pub mod arrow; pub mod backend; pub mod db; diff --git a/nautilus_core/persistence/src/python/mod.rs b/nautilus_core/persistence/src/python/mod.rs index f354adff3450..59117f8e9e63 100644 --- a/nautilus_core/persistence/src/python/mod.rs +++ b/nautilus_core/persistence/src/python/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides Python bindings from `pyo3`. + #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade use pyo3::prelude::*; diff --git a/nautilus_core/pyo3/src/lib.rs b/nautilus_core/pyo3/src/lib.rs index d4a85e0bb0e6..799dc4f8d480 100644 --- a/nautilus_core/pyo3/src/lib.rs +++ b/nautilus_core/pyo3/src/lib.rs @@ -13,6 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the main `nautilus_trader` Python package, or as part of a Rust only build. +//! +//! - `ffi`: Enables the C foreign function interface (FFI) from `cbindgen` + use pyo3::{ prelude::*, types::{PyDict, PyString}, From 6070ce8b0a1a7a50703f2816b02155760fda9f0c Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 20 Apr 2024 23:03:29 +0200 Subject: [PATCH 004/193] Fix CurrencyPair.from_pyo3_c lot_size conversion (#1597) --- nautilus_trader/model/instruments/currency_pair.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/model/instruments/currency_pair.pyx b/nautilus_trader/model/instruments/currency_pair.pyx index a41745950e80..ca4199d26269 100644 --- a/nautilus_trader/model/instruments/currency_pair.pyx +++ b/nautilus_trader/model/instruments/currency_pair.pyx @@ -297,7 +297,7 @@ cdef class CurrencyPair(Instrument): size_precision=pyo3_instrument.size_precision, price_increment=Price.from_raw_c(pyo3_instrument.price_increment.raw, pyo3_instrument.price_precision), size_increment=Quantity.from_raw_c(pyo3_instrument.size_increment.raw, pyo3_instrument.size_precision), - lot_size=Quantity.from_str_c(pyo3_instrument.lot_size) if pyo3_instrument.lot_size is not None else None, + lot_size=Quantity.from_raw_c(pyo3_instrument.lot_size.raw,pyo3_instrument.lot_size.precision) if pyo3_instrument.lot_size is not None else None, max_quantity=Quantity.from_raw_c(pyo3_instrument.max_quantity.raw, pyo3_instrument.max_quantity.precision) if pyo3_instrument.max_quantity is not None else None, min_quantity=Quantity.from_raw_c(pyo3_instrument.min_quantity.raw, pyo3_instrument.min_quantity.precision) if pyo3_instrument.min_quantity is not None else None, max_notional=Money.from_str_c(str(pyo3_instrument.max_notional)) if pyo3_instrument.max_notional is not None else None, From 8769dae920ca43454612b328613b59464e186a31 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 20 Apr 2024 23:06:50 +0200 Subject: [PATCH 005/193] Refactor order create method for Cython and Rust (#1598) --- .../model/src/python/orders/limit.rs | 7 +++++++ .../src/python/orders/limit_if_touched.rs | 7 +++++++ .../model/src/python/orders/market.rs | 7 +++++++ .../src/python/orders/market_if_touched.rs | 7 +++++++ .../src/python/orders/market_to_limit.rs | 7 +++++++ .../model/src/python/orders/stop_limit.rs | 7 +++++++ .../model/src/python/orders/stop_market.rs | 7 +++++++ .../src/python/orders/trailing_stop_limit.rs | 7 +++++++ .../src/python/orders/trailing_stop_market.rs | 7 +++++++ nautilus_trader/core/nautilus_pyo3.pyi | 19 +++++++++++++++++++ nautilus_trader/model/orders/limit.pxd | 2 +- nautilus_trader/model/orders/limit.pyx | 6 +++++- .../model/orders/limit_if_touched.pxd | 2 +- .../model/orders/limit_if_touched.pyx | 6 +++++- nautilus_trader/model/orders/market.pxd | 2 +- nautilus_trader/model/orders/market.pyx | 6 +++++- .../model/orders/market_if_touched.pxd | 2 +- .../model/orders/market_if_touched.pyx | 6 +++++- .../model/orders/market_to_limit.pxd | 2 +- .../model/orders/market_to_limit.pyx | 6 +++++- nautilus_trader/model/orders/stop_limit.pxd | 2 +- nautilus_trader/model/orders/stop_limit.pyx | 6 +++++- nautilus_trader/model/orders/stop_market.pxd | 2 +- nautilus_trader/model/orders/stop_market.pyx | 6 +++++- .../model/orders/trailing_stop_limit.pxd | 2 +- .../model/orders/trailing_stop_limit.pyx | 6 +++++- .../model/orders/trailing_stop_market.pxd | 2 +- .../model/orders/trailing_stop_market.pyx | 6 +++++- nautilus_trader/model/orders/unpacker.pyx | 18 +++++++++--------- 29 files changed, 145 insertions(+), 27 deletions(-) diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index ca892e1f9a06..0c22d1627333 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -28,6 +28,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce, TriggerType, }, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -118,6 +119,12 @@ impl LimitOrder { self.to_string() } + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(LimitOrder::from(init)) + } + #[getter] #[pyo3(name = "trader_id")] fn py_trader_id(&self) -> TraderId { diff --git a/nautilus_core/model/src/python/orders/limit_if_touched.rs b/nautilus_core/model/src/python/orders/limit_if_touched.rs index 8f966d82f455..2dd5dc2ee300 100644 --- a/nautilus_core/model/src/python/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/python/orders/limit_if_touched.rs @@ -21,6 +21,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, TimeInForce, TriggerType}, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -95,4 +96,10 @@ impl LimitIfTouchedOrder { ) .unwrap()) } + + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(LimitIfTouchedOrder::from(init)) + } } diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index f74a8ad53d68..9f12849dfbcd 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -27,6 +27,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, OrderType, PositionSide, TimeInForce}, + events::order::initialized::OrderInitialized, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -105,6 +106,12 @@ impl MarketOrder { self.to_string() } + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(MarketOrder::from(init)) + } + #[pyo3(name = "signed_decimal_qty")] fn py_signed_decimal_qty(&self) -> Decimal { self.signed_decimal_qty() diff --git a/nautilus_core/model/src/python/orders/market_if_touched.rs b/nautilus_core/model/src/python/orders/market_if_touched.rs index 97db84845029..5a1c0ddd6a96 100644 --- a/nautilus_core/model/src/python/orders/market_if_touched.rs +++ b/nautilus_core/model/src/python/orders/market_if_touched.rs @@ -21,6 +21,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, TimeInForce, TriggerType}, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -91,4 +92,10 @@ impl MarketIfTouchedOrder { ) .unwrap()) } + + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(MarketIfTouchedOrder::from(init)) + } } diff --git a/nautilus_core/model/src/python/orders/market_to_limit.rs b/nautilus_core/model/src/python/orders/market_to_limit.rs index 211d7915bd71..ac82db96672e 100644 --- a/nautilus_core/model/src/python/orders/market_to_limit.rs +++ b/nautilus_core/model/src/python/orders/market_to_limit.rs @@ -21,6 +21,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, TimeInForce}, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -85,4 +86,10 @@ impl MarketToLimitOrder { ) .unwrap()) } + + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(MarketToLimitOrder::from(init)) + } } diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index b69bec23ccf5..7ca513cda729 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -21,6 +21,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType}, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -115,6 +116,12 @@ impl StopLimitOrder { self.to_string() } + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(StopLimitOrder::from(init)) + } + #[getter] #[pyo3(name = "trader_id")] fn py_trader_id(&self) -> TraderId { diff --git a/nautilus_core/model/src/python/orders/stop_market.rs b/nautilus_core/model/src/python/orders/stop_market.rs index b438f35cf987..964463797590 100644 --- a/nautilus_core/model/src/python/orders/stop_market.rs +++ b/nautilus_core/model/src/python/orders/stop_market.rs @@ -21,6 +21,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, TimeInForce, TriggerType}, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -91,4 +92,10 @@ impl StopMarketOrder { ) .unwrap()) } + + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(StopMarketOrder::from(init)) + } } diff --git a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs index ff1254c1a3e3..a117a0787dfc 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs @@ -21,6 +21,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -101,4 +102,10 @@ impl TrailingStopLimitOrder { ) .unwrap()) } + + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(TrailingStopLimitOrder::from(init)) + } } diff --git a/nautilus_core/model/src/python/orders/trailing_stop_market.rs b/nautilus_core/model/src/python/orders/trailing_stop_market.rs index f08d29f7af27..93e41bef8f77 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_market.rs @@ -21,6 +21,7 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -95,4 +96,10 @@ impl TrailingStopMarketOrder { ) .unwrap()) } + + #[staticmethod] + #[pyo3(name = "create")] + fn py_create(init: OrderInitialized) -> PyResult { + Ok(TrailingStopMarketOrder::from(init)) + } } diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 79dab082e247..bdbe7bc6b46d 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -909,6 +909,8 @@ class LimitOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ): ... + @classmethod + def create(cls, init: OrderInitialized) -> LimitOrder: ... def to_dict(self) -> dict[str, str]: ... @property def trader_id(self) -> TraderId: ... @@ -993,6 +995,8 @@ class LimitIfTouchedOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ) -> None: ... + @classmethod + def create(cls, init: OrderInitialized) -> LimitIfTouchedOrder: ... class MarketOrder: def __init__( @@ -1017,6 +1021,8 @@ class MarketOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ) -> None: ... + @classmethod + def create(cls, init: OrderInitialized) -> MarketOrder: ... def to_dict(self) -> dict[str, str]: ... @classmethod def from_dict(cls, values: dict[str, str]) -> MarketOrder: ... @@ -1079,6 +1085,8 @@ class MarketToLimitOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ): ... + @classmethod + def create(cls, init: OrderInitialized) -> MarketToLimitOrder: ... class MarketIfTouchedOrder: def __init__( @@ -1109,6 +1117,9 @@ class MarketIfTouchedOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ): ... + @classmethod + def create(cls, init: OrderInitialized) -> MarketIfTouchedOrder: ... + class StopLimitOrder: def __init__( self, @@ -1141,6 +1152,8 @@ class StopLimitOrder: tags: str | None = None, ): ... @classmethod + def create(cls, init: OrderInitialized) -> StopLimitOrder: ... + @classmethod def from_dict(cls, values: dict[str, str]) -> StopLimitOrder: ... def to_dict(self) -> dict[str, str]: ... @property @@ -1217,6 +1230,8 @@ class StopMarketOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ): ... + @classmethod + def create(cls, init: OrderInitialized) -> StopMarketOrder: ... class TrailingStopLimitOrder: def __init__( self, @@ -1251,6 +1266,8 @@ class TrailingStopLimitOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ): ... + @classmethod + def create(cls, init: OrderInitialized) -> TrailingStopLimitOrder: ... class TrailingStopMarketOrder: def __init__( self, @@ -1282,6 +1299,8 @@ class TrailingStopMarketOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ): ... + @classmethod + def create(cls, init: OrderInitialized) -> TrailingStopMarketOrder: ... ### Objects diff --git a/nautilus_trader/model/orders/limit.pxd b/nautilus_trader/model/orders/limit.pxd index 7b4c03c620da..b87bd5281a3b 100644 --- a/nautilus_trader/model/orders/limit.pxd +++ b/nautilus_trader/model/orders/limit.pxd @@ -30,7 +30,7 @@ cdef class LimitOrder(Order): """The quantity of the order to display on the public book (iceberg).\n\n:returns: `Quantity` or ``None``""" @staticmethod - cdef LimitOrder create(OrderInitialized init) + cdef LimitOrder create_c(OrderInitialized init) @staticmethod cdef LimitOrder transform(Order order, uint64_t ts_init, Price price=*) diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 92a2d6c3cb0f..5b309f32852d 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -346,7 +346,7 @@ cdef class LimitOrder(Order): } @staticmethod - cdef LimitOrder create(OrderInitialized init): + cdef LimitOrder create_c(OrderInitialized init): """ Return a `Limit` order from the given initialized event. @@ -398,6 +398,10 @@ cdef class LimitOrder(Order): tags=init.tags, ) + @staticmethod + def create(OrderInitialized init): + return LimitOrder.create_c(init) + @staticmethod cdef LimitOrder transform(Order order, uint64_t ts_init, Price price = None): """ diff --git a/nautilus_trader/model/orders/limit_if_touched.pxd b/nautilus_trader/model/orders/limit_if_touched.pxd index 79664cddae37..e1481e29db87 100644 --- a/nautilus_trader/model/orders/limit_if_touched.pxd +++ b/nautilus_trader/model/orders/limit_if_touched.pxd @@ -39,4 +39,4 @@ cdef class LimitIfTouchedOrder(Order): """The UNIX timestamp (nanoseconds) when the order was triggered (0 if not triggered).\n\n:returns: `uint64_t`""" @staticmethod - cdef LimitIfTouchedOrder create(OrderInitialized init) + cdef LimitIfTouchedOrder create_c(OrderInitialized init) diff --git a/nautilus_trader/model/orders/limit_if_touched.pyx b/nautilus_trader/model/orders/limit_if_touched.pyx index b342b8dcd3de..dddbf7c6a6ac 100644 --- a/nautilus_trader/model/orders/limit_if_touched.pyx +++ b/nautilus_trader/model/orders/limit_if_touched.pyx @@ -339,7 +339,7 @@ cdef class LimitIfTouchedOrder(Order): } @staticmethod - cdef LimitIfTouchedOrder create(OrderInitialized init): + cdef LimitIfTouchedOrder create_c(OrderInitialized init): """ Return a `Limit-If-Touched` order from the given initialized event. @@ -392,3 +392,7 @@ cdef class LimitIfTouchedOrder(Order): exec_spawn_id=init.exec_spawn_id, tags=init.tags, ) + + @staticmethod + def create(init): + return LimitIfTouchedOrder.create_c(init) diff --git a/nautilus_trader/model/orders/market.pxd b/nautilus_trader/model/orders/market.pxd index 96e89d71d120..ef48f030f9a9 100644 --- a/nautilus_trader/model/orders/market.pxd +++ b/nautilus_trader/model/orders/market.pxd @@ -21,7 +21,7 @@ from nautilus_trader.model.orders.base cimport Order cdef class MarketOrder(Order): @staticmethod - cdef MarketOrder create(OrderInitialized init) + cdef MarketOrder create_c(OrderInitialized init) @staticmethod cdef MarketOrder transform(Order order, uint64_t ts_init) diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index fc5532193064..48f03e1b43ea 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -266,7 +266,7 @@ cdef class MarketOrder(Order): } @staticmethod - cdef MarketOrder create(OrderInitialized init): + cdef MarketOrder create_c(OrderInitialized init): """ Return a `market` order from the given initialized event. @@ -310,6 +310,10 @@ cdef class MarketOrder(Order): tags=init.tags, ) + @staticmethod + def create(init): + return MarketOrder.create_c(init) + @staticmethod cdef MarketOrder transform(Order order, uint64_t ts_init): """ diff --git a/nautilus_trader/model/orders/market_if_touched.pxd b/nautilus_trader/model/orders/market_if_touched.pxd index 517c64ae4d5a..5233bb2de2db 100644 --- a/nautilus_trader/model/orders/market_if_touched.pxd +++ b/nautilus_trader/model/orders/market_if_touched.pxd @@ -30,4 +30,4 @@ cdef class MarketIfTouchedOrder(Order): """The order expiration (UNIX epoch nanoseconds), zero for no expiration.\n\n:returns: `uint64_t`""" @staticmethod - cdef MarketIfTouchedOrder create(OrderInitialized init) + cdef MarketIfTouchedOrder create_c(OrderInitialized init) diff --git a/nautilus_trader/model/orders/market_if_touched.pyx b/nautilus_trader/model/orders/market_if_touched.pyx index 8a790438b244..72569dfe5ac6 100644 --- a/nautilus_trader/model/orders/market_if_touched.pyx +++ b/nautilus_trader/model/orders/market_if_touched.pyx @@ -305,7 +305,7 @@ cdef class MarketIfTouchedOrder(Order): } @staticmethod - cdef MarketIfTouchedOrder create(OrderInitialized init): + cdef MarketIfTouchedOrder create_c(OrderInitialized init): """ Return a `Market-If-Touched` order from the given initialized event. @@ -353,3 +353,7 @@ cdef class MarketIfTouchedOrder(Order): exec_spawn_id=init.exec_spawn_id, tags=init.tags, ) + + @staticmethod + def create(init): + return MarketIfTouchedOrder.create_c(init) diff --git a/nautilus_trader/model/orders/market_to_limit.pxd b/nautilus_trader/model/orders/market_to_limit.pxd index 029011dbeae2..2b2acf4cb217 100644 --- a/nautilus_trader/model/orders/market_to_limit.pxd +++ b/nautilus_trader/model/orders/market_to_limit.pxd @@ -30,4 +30,4 @@ cdef class MarketToLimitOrder(Order): """The quantity of the limit order to display on the public book (iceberg).\n\n:returns: `Quantity` or ``None``""" @staticmethod - cdef MarketToLimitOrder create(OrderInitialized init) + cdef MarketToLimitOrder create_c(OrderInitialized init) diff --git a/nautilus_trader/model/orders/market_to_limit.pyx b/nautilus_trader/model/orders/market_to_limit.pyx index 4d370f4ef874..ff290b6d80b5 100644 --- a/nautilus_trader/model/orders/market_to_limit.pyx +++ b/nautilus_trader/model/orders/market_to_limit.pyx @@ -282,7 +282,7 @@ cdef class MarketToLimitOrder(Order): } @staticmethod - cdef MarketToLimitOrder create(OrderInitialized init): + cdef MarketToLimitOrder create_c(OrderInitialized init): """ Return a `Market-To-Limit` order from the given initialized event. @@ -329,3 +329,7 @@ cdef class MarketToLimitOrder(Order): exec_spawn_id=init.exec_spawn_id, tags=init.tags, ) + + @staticmethod + def create(init): + return MarketToLimitOrder.create_c(init) diff --git a/nautilus_trader/model/orders/stop_limit.pxd b/nautilus_trader/model/orders/stop_limit.pxd index ec00bde6b4be..898be78eab59 100644 --- a/nautilus_trader/model/orders/stop_limit.pxd +++ b/nautilus_trader/model/orders/stop_limit.pxd @@ -39,7 +39,7 @@ cdef class StopLimitOrder(Order): """The UNIX timestamp (nanoseconds) when the order was triggered (0 if not triggered).\n\n:returns: `uint64_t`""" @staticmethod - cdef StopLimitOrder create(OrderInitialized init) + cdef StopLimitOrder create_c(OrderInitialized init) @staticmethod cdef StopLimitOrder from_pyo3_c(pyo3_order) diff --git a/nautilus_trader/model/orders/stop_limit.pyx b/nautilus_trader/model/orders/stop_limit.pyx index 24d596d3e9a8..07c56a46d393 100644 --- a/nautilus_trader/model/orders/stop_limit.pyx +++ b/nautilus_trader/model/orders/stop_limit.pyx @@ -381,7 +381,7 @@ cdef class StopLimitOrder(Order): } @staticmethod - cdef StopLimitOrder create(OrderInitialized init): + cdef StopLimitOrder create_c(OrderInitialized init): """ Return a `Stop-Limit` order from the given initialized event. @@ -434,3 +434,7 @@ cdef class StopLimitOrder(Order): exec_spawn_id=init.exec_spawn_id, tags=init.tags, ) + + @staticmethod + def create(init): + return StopLimitOrder.create_c(init) diff --git a/nautilus_trader/model/orders/stop_market.pxd b/nautilus_trader/model/orders/stop_market.pxd index 83b2ded2f7ca..600a1a44ff98 100644 --- a/nautilus_trader/model/orders/stop_market.pxd +++ b/nautilus_trader/model/orders/stop_market.pxd @@ -30,4 +30,4 @@ cdef class StopMarketOrder(Order): """The order expiration (UNIX epoch nanoseconds), zero for no expiration.\n\n:returns: `uint64_t`""" @staticmethod - cdef StopMarketOrder create(OrderInitialized init) + cdef StopMarketOrder create_c(OrderInitialized init) diff --git a/nautilus_trader/model/orders/stop_market.pyx b/nautilus_trader/model/orders/stop_market.pyx index 1dc1c5771a3c..90ed019161e9 100644 --- a/nautilus_trader/model/orders/stop_market.pyx +++ b/nautilus_trader/model/orders/stop_market.pyx @@ -310,7 +310,7 @@ cdef class StopMarketOrder(Order): } @staticmethod - cdef StopMarketOrder create(OrderInitialized init): + cdef StopMarketOrder create_c(OrderInitialized init): """ Return a `Stop-Market` order from the given initialized event. @@ -358,3 +358,7 @@ cdef class StopMarketOrder(Order): exec_spawn_id=init.exec_spawn_id, tags=init.tags, ) + + @staticmethod + def create(init): + return StopMarketOrder.create_c(init) diff --git a/nautilus_trader/model/orders/trailing_stop_limit.pxd b/nautilus_trader/model/orders/trailing_stop_limit.pxd index c62dd0adade6..4d2549be072d 100644 --- a/nautilus_trader/model/orders/trailing_stop_limit.pxd +++ b/nautilus_trader/model/orders/trailing_stop_limit.pxd @@ -46,4 +46,4 @@ cdef class TrailingStopLimitOrder(Order): """The UNIX timestamp (nanoseconds) when the order was triggered (0 if not triggered).\n\n:returns: `uint64_t`""" @staticmethod - cdef TrailingStopLimitOrder create(OrderInitialized init) + cdef TrailingStopLimitOrder create_c(OrderInitialized init) diff --git a/nautilus_trader/model/orders/trailing_stop_limit.pyx b/nautilus_trader/model/orders/trailing_stop_limit.pyx index dcd027e2bc5e..6e5b9cbe0c23 100644 --- a/nautilus_trader/model/orders/trailing_stop_limit.pyx +++ b/nautilus_trader/model/orders/trailing_stop_limit.pyx @@ -356,7 +356,7 @@ cdef class TrailingStopLimitOrder(Order): } @staticmethod - cdef TrailingStopLimitOrder create(OrderInitialized init): + cdef TrailingStopLimitOrder create_c(OrderInitialized init): """ Return a `Trailing-Stop-Limit` order from the given initialized event. @@ -414,3 +414,7 @@ cdef class TrailingStopLimitOrder(Order): exec_spawn_id=init.exec_spawn_id, tags=init.tags, ) + + @staticmethod + def create(init): + return TrailingStopLimitOrder.create_c(init) diff --git a/nautilus_trader/model/orders/trailing_stop_market.pxd b/nautilus_trader/model/orders/trailing_stop_market.pxd index 9e20d1f34e66..4d6d91c7d669 100644 --- a/nautilus_trader/model/orders/trailing_stop_market.pxd +++ b/nautilus_trader/model/orders/trailing_stop_market.pxd @@ -35,4 +35,4 @@ cdef class TrailingStopMarketOrder(Order): """The order expiration (UNIX epoch nanoseconds), zero for no expiration.\n\n:returns: `uint64_t`""" @staticmethod - cdef TrailingStopMarketOrder create(OrderInitialized init) + cdef TrailingStopMarketOrder create_c(OrderInitialized init) diff --git a/nautilus_trader/model/orders/trailing_stop_market.pyx b/nautilus_trader/model/orders/trailing_stop_market.pyx index af4c5821022e..330b471f3cea 100644 --- a/nautilus_trader/model/orders/trailing_stop_market.pyx +++ b/nautilus_trader/model/orders/trailing_stop_market.pyx @@ -319,7 +319,7 @@ cdef class TrailingStopMarketOrder(Order): } @staticmethod - cdef TrailingStopMarketOrder create(OrderInitialized init): + cdef TrailingStopMarketOrder create_c(OrderInitialized init): """ Return a `Trailing-Stop-Market` order from the given initialized event. @@ -371,3 +371,7 @@ cdef class TrailingStopMarketOrder(Order): exec_spawn_id=init.exec_spawn_id, tags=init.tags, ) + + @staticmethod + def create(init): + return TrailingStopMarketOrder.create_c(init) diff --git a/nautilus_trader/model/orders/unpacker.pyx b/nautilus_trader/model/orders/unpacker.pyx index f0c3144837cc..554fc7b13f9c 100644 --- a/nautilus_trader/model/orders/unpacker.pyx +++ b/nautilus_trader/model/orders/unpacker.pyx @@ -42,23 +42,23 @@ cdef class OrderUnpacker: @staticmethod cdef Order from_init_c(OrderInitialized init): if init.order_type == OrderType.MARKET: - return MarketOrder.create(init=init) + return MarketOrder.create_c(init=init) elif init.order_type == OrderType.LIMIT: - return LimitOrder.create(init=init) + return LimitOrder.create_c(init=init) elif init.order_type == OrderType.STOP_MARKET: - return StopMarketOrder.create(init=init) + return StopMarketOrder.create_c(init=init) elif init.order_type == OrderType.STOP_LIMIT: - return StopLimitOrder.create(init=init) + return StopLimitOrder.create_c(init=init) elif init.order_type == OrderType.MARKET_TO_LIMIT: - return MarketToLimitOrder.create(init=init) + return MarketToLimitOrder.create_c(init=init) elif init.order_type == OrderType.MARKET_IF_TOUCHED: - return MarketIfTouchedOrder.create(init=init) + return MarketIfTouchedOrder.create_c(init=init) elif init.order_type == OrderType.LIMIT_IF_TOUCHED: - return LimitIfTouchedOrder.create(init=init) + return LimitIfTouchedOrder.create_c(init=init) elif init.order_type == OrderType.TRAILING_STOP_MARKET: - return TrailingStopMarketOrder.create(init=init) + return TrailingStopMarketOrder.create_c(init=init) elif init.order_type == OrderType.TRAILING_STOP_LIMIT: - return TrailingStopLimitOrder.create(init=init) + return TrailingStopLimitOrder.create_c(init=init) else: raise RuntimeError("invalid `OrderType`") # pragma: no cover (design-time error) From 30aff27d1ee6d2e93e20a61b062ce6af6d41836e Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 20 Apr 2024 23:09:48 +0200 Subject: [PATCH 006/193] Add from_str pyo3 method in identifier_for_python macro (#1599) --- nautilus_core/model/src/python/macros.rs | 6 ++++++ nautilus_trader/core/nautilus_pyo3.pyi | 26 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/nautilus_core/model/src/python/macros.rs b/nautilus_core/model/src/python/macros.rs index a341a1995018..dc02099a01f5 100644 --- a/nautilus_core/model/src/python/macros.rs +++ b/nautilus_core/model/src/python/macros.rs @@ -82,6 +82,12 @@ macro_rules! identifier_for_python { fn py_value(&self) -> String { self.to_string() } + + #[staticmethod] + #[pyo3(name = "from_str")] + fn py_from_str(value: &str) -> PyResult { + Self::from_str(value).map_err(to_pyvalue_err) + } } }; } diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index bdbe7bc6b46d..7f86aa4c525a 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -816,23 +816,33 @@ class LogColor(Enum): class AccountId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> AccountId: ... def value(self) -> str: ... class ClientId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> ClientId: ... def value(self) -> str: ... class ClientOrderId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> ClientOrderId: ... @property def value(self) -> str: ... class ComponentId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> ComponentId: ... def value(self) -> str: ... class ExecAlgorithmId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> ExecAlgorithmId: ... def value(self) -> str: ... class InstrumentId: @@ -847,35 +857,51 @@ class InstrumentId: class OrderListId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> OrderListId: ... def value(self) -> str: ... class PositionId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> PositionId: ... def value(self) -> str: ... class StrategyId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> StrategyId: ... def value(self) -> str: ... class Symbol: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> Symbol: ... @property def value(self) -> str: ... class TradeId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> TradeId: ... def value(self) -> str: ... class TraderId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> TraderId: ... def value(self) -> str: ... class Venue: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> Venue: ... def value(self) -> str: ... class VenueOrderId: def __init__(self, value: str) -> None: ... + @classmethod + def from_str(cls, value: str) -> VenueOrderId: ... def value(self) -> str: ... ### Orders From fbf67eb7905f49bf86e3c0b2c7e06181225a05c4 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 20 Apr 2024 23:13:24 +0200 Subject: [PATCH 007/193] Finish Instrument trait and implement sqlx trait FromRow for instruments (#1600) --- nautilus_core/Cargo.lock | 1 + nautilus_core/Cargo.toml | 1 + nautilus_core/model/Cargo.toml | 1 + .../model/src/instruments/crypto_future.rs | 153 +++++++++++++++- .../model/src/instruments/crypto_perpetual.rs | 167 ++++++++++++++++-- .../model/src/instruments/currency_pair.rs | 138 ++++++++++++++- nautilus_core/model/src/instruments/equity.rs | 129 +++++++++++++- .../model/src/instruments/futures_contract.rs | 141 ++++++++++++++- .../model/src/instruments/futures_spread.rs | 52 +++++- nautilus_core/model/src/instruments/mod.rs | 52 +++++- .../model/src/instruments/options_contract.rs | 148 +++++++++++++++- .../model/src/instruments/options_spread.rs | 52 +++++- nautilus_core/persistence/Cargo.toml | 2 +- 13 files changed, 989 insertions(+), 48 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index c8def8558528..2fb5745ce952 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2662,6 +2662,7 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "sqlx", "strum", "tabled", "thiserror", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 28d128c81c93..0db5919bb3eb 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -57,6 +57,7 @@ tracing = "0.1.40" tokio = { version = "1.37.0", features = ["full"] } ustr = { version = "1.0.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["v4"] } +sqlx = { version = "0.7.4", features = ["sqlite", "postgres", "any", "runtime-tokio"] } # dev-dependencies criterion = "0.5.1" diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index b466a3b15f55..abdf45967e9b 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -29,6 +29,7 @@ ustr = { workspace = true } chrono = { workspace = true } evalexpr = "11.3.0" tabled = "0.15.0" +sqlx = { workspace = true} [dev-dependencies] criterion = { workspace = true } diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index 06babbeea0c9..be9cdf598ced 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::hash::{Hash, Hasher}; +use std::{ + hash::{Hash, Hasher}, + str::FromStr, +}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, @@ -21,10 +24,12 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow, Row}; +use ustr::Ustr; use super::{Instrument, InstrumentAny}; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; @@ -173,6 +178,10 @@ impl Instrument for CryptoFuture { InstrumentClass::Future } + fn underlying(&self) -> Option { + Some(self.underlying.code) + } + fn quote_currency(&self) -> Currency { self.quote_currency } @@ -185,6 +194,18 @@ impl Instrument for CryptoFuture { self.settlement_currency } + fn isin(&self) -> Option { + None + } + + fn exchange(&self) -> Option { + None + } + + fn option_kind(&self) -> Option { + None + } + fn is_inverse(&self) -> bool { self.is_inverse } @@ -237,6 +258,134 @@ impl Instrument for CryptoFuture { fn ts_init(&self) -> UnixNanos { self.ts_init } + + fn strike_price(&self) -> Option { + None + } + + fn activation_ns(&self) -> Option { + Some(self.activation_ns) + } + + fn expiration_ns(&self) -> Option { + Some(self.expiration_ns) + } + + fn max_notional(&self) -> Option { + self.max_notional + } + + fn min_notional(&self) -> Option { + self.min_notional + } +} + +impl<'r> FromRow<'r, PgRow> for CryptoFuture { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let underlying = row + .try_get::("underlying") + .map(|res| Currency::from(res.as_str()))?; + let quote_currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let settlement_currency = row + .try_get::("settlement_currency") + .map(|res| Currency::from(res.as_str()))?; + let is_inverse = row.try_get::("is_inverse")?; + let activation_ns = row + .try_get::("activation_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let expiration_ns = row + .try_get::("expiration_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let size_precision = row.try_get::("size_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let size_increment = row + .try_get::("size_increment") + .map(|res| Quantity::from_str(res.as_str()).unwrap())?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|value| Quantity::from(value.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|value| Quantity::from(value.as_str()))); + let max_notional = row + .try_get::, _>("max_notional") + .ok() + .and_then(|res| res.map(|value| Money::from(value.as_str()))); + let min_notional = row + .try_get::, _>("min_notional") + .ok() + .and_then(|res| res.map(|value| Money::from(value.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|value| Price::from(value.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|value| Price::from(value.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + underlying, + quote_currency, + settlement_currency, + is_inverse, + activation_ns, + expiration_ns, + price_precision as u8, + size_precision as u8, + price_increment, + size_increment, + maker_fee, + taker_fee, + margin_init, + margin_maint, + Some(lot_size), + max_quantity, + min_quantity, + max_notional, + min_notional, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index e4a36f326448..b4aa27672a9c 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::hash::{Hash, Hasher}; +use std::{ + hash::{Hash, Hasher}, + str::FromStr, +}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, @@ -21,10 +24,12 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow, Row}; +use ustr::Ustr; use super::InstrumentAny; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, instruments::Instrument, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, @@ -167,19 +172,43 @@ impl Instrument for CryptoPerpetual { fn instrument_class(&self) -> InstrumentClass { InstrumentClass::Swap } - - fn quote_currency(&self) -> Currency { - self.quote_currency + fn underlying(&self) -> Option { + None } fn base_currency(&self) -> Option { Some(self.base_currency) } + fn quote_currency(&self) -> Currency { + self.quote_currency + } + fn settlement_currency(&self) -> Currency { self.settlement_currency } + fn isin(&self) -> Option { + None + } + fn option_kind(&self) -> Option { + None + } + fn exchange(&self) -> Option { + None + } + fn strike_price(&self) -> Option { + None + } + + fn activation_ns(&self) -> Option { + None + } + + fn expiration_ns(&self) -> Option { + None + } + fn is_inverse(&self) -> bool { self.is_inverse } @@ -216,6 +245,14 @@ impl Instrument for CryptoPerpetual { self.min_quantity } + fn max_notional(&self) -> Option { + self.max_notional + } + + fn min_notional(&self) -> Option { + self.min_notional + } + fn max_price(&self) -> Option { self.max_price } @@ -224,28 +261,128 @@ impl Instrument for CryptoPerpetual { self.min_price } - fn ts_event(&self) -> UnixNanos { - self.ts_event + fn margin_init(&self) -> Decimal { + self.margin_init } - fn ts_init(&self) -> UnixNanos { - self.ts_init + fn margin_maint(&self) -> Decimal { + self.margin_maint + } + + fn maker_fee(&self) -> Decimal { + self.maker_fee } fn taker_fee(&self) -> Decimal { self.taker_fee } - fn maker_fee(&self) -> Decimal { - self.maker_fee + fn ts_event(&self) -> UnixNanos { + self.ts_event } - fn margin_init(&self) -> Decimal { - self.margin_init + fn ts_init(&self) -> UnixNanos { + self.ts_init } +} - fn margin_maint(&self) -> Decimal { - self.margin_maint +impl<'r> FromRow<'r, PgRow> for CryptoPerpetual { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let base_currency = row + .try_get::("base_currency") + .map(|res| Currency::from(res.as_str()))?; + let quote_currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let settlement_currency = row + .try_get::("settlement_currency") + .map(|res| Currency::from(res.as_str()))?; + let is_inverse = row.try_get::("is_inverse")?; + let price_precision = row.try_get::("price_precision")?; + let size_precision = row.try_get::("size_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let size_increment = row + .try_get::("size_increment") + .map(|res| Quantity::from_str(res.as_str()).unwrap())?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let max_notional = row + .try_get::, _>("max_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let min_notional = row + .try_get::, _>("min_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + base_currency, + quote_currency, + settlement_currency, + is_inverse, + price_precision as u8, + size_precision as u8, + price_increment, + size_increment, + maker_fee, + taker_fee, + margin_init, + margin_maint, + Some(lot_size), + max_quantity, + min_quantity, + max_notional, + min_notional, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) } } diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 91225c42fe29..44e78051dd4d 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::hash::{Hash, Hasher}; +use std::{ + hash::{Hash, Hasher}, + str::FromStr, +}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, @@ -21,10 +24,12 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow, Row}; +use ustr::Ustr; use super::{Instrument, InstrumentAny}; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; @@ -160,6 +165,9 @@ impl Instrument for CurrencyPair { fn instrument_class(&self) -> InstrumentClass { InstrumentClass::Spot } + fn underlying(&self) -> Option { + None + } fn quote_currency(&self) -> Currency { self.quote_currency @@ -172,6 +180,9 @@ impl Instrument for CurrencyPair { fn settlement_currency(&self) -> Currency { self.quote_currency } + fn isin(&self) -> Option { + None + } fn is_inverse(&self) -> bool { false @@ -241,6 +252,129 @@ impl Instrument for CurrencyPair { fn maker_fee(&self) -> Decimal { self.maker_fee } + + fn option_kind(&self) -> Option { + None + } + + fn exchange(&self) -> Option { + None + } + + fn strike_price(&self) -> Option { + None + } + + fn activation_ns(&self) -> Option { + None + } + + fn expiration_ns(&self) -> Option { + None + } + + fn max_notional(&self) -> Option { + self.max_notional + } + + fn min_notional(&self) -> Option { + self.min_notional + } +} + +impl<'r> FromRow<'r, PgRow> for CurrencyPair { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let base_currency = row + .try_get::("base_currency") + .map(|res| Currency::from(res.as_str()))?; + let quote_currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let size_precision = row.try_get::("size_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from(res.as_str()))?; + let size_increment = row + .try_get::("size_increment") + .map(|res| Quantity::from(res.as_str()))?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::, _>("lot_size") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let max_notional = row + .try_get::, _>("max_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let min_notional = row + .try_get::, _>("min_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + base_currency, + quote_currency, + price_precision as u8, + size_precision as u8, + price_increment, + size_increment, + taker_fee, + maker_fee, + margin_init, + margin_maint, + lot_size, + max_quantity, + min_quantity, + max_notional, + min_notional, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index 4e64936ec716..bfdb1bf1d693 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::hash::{Hash, Hasher}; +use std::{ + hash::{Hash, Hasher}, + str::FromStr, +}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_valid_string_optional}, @@ -21,13 +24,14 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, - types::{currency::Currency, price::Price, quantity::Quantity}, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; #[repr(C)] @@ -144,19 +148,46 @@ impl Instrument for Equity { fn instrument_class(&self) -> InstrumentClass { InstrumentClass::Spot } - - fn quote_currency(&self) -> Currency { - self.currency + fn underlying(&self) -> Option { + None } fn base_currency(&self) -> Option { None } + fn quote_currency(&self) -> Currency { + self.currency + } + fn settlement_currency(&self) -> Currency { self.currency } + fn isin(&self) -> Option { + self.isin + } + + fn option_kind(&self) -> Option { + None + } + + fn exchange(&self) -> Option { + None + } + + fn strike_price(&self) -> Option { + None + } + + fn activation_ns(&self) -> Option { + None + } + + fn expiration_ns(&self) -> Option { + None + } + fn is_inverse(&self) -> bool { false } @@ -193,6 +224,14 @@ impl Instrument for Equity { self.min_quantity } + fn max_notional(&self) -> Option { + None + } + + fn min_notional(&self) -> Option { + None + } + fn max_price(&self) -> Option { self.max_price } @@ -210,6 +249,84 @@ impl Instrument for Equity { } } +impl<'r> FromRow<'r, PgRow> for Equity { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let isin = row + .try_get::, _>("isin") + .map(|res| res.map(|s| Ustr::from(s.as_str())))?; + let currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::, _>("lot_size") + .map(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap())); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap())); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + isin, + currency, + price_precision as u8, + price_increment, + Some(maker_fee), + Some(taker_fee), + Some(margin_init), + Some(margin_maint), + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 5ad9f10490a4..65d753dd9d08 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::hash::{Hash, Hasher}; +use std::{ + hash::{Hash, Hasher}, + str::FromStr, +}; use nautilus_core::{ correctness::{ @@ -23,13 +26,14 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, - types::{currency::Currency, price::Price, quantity::Quantity}, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; #[repr(C)] @@ -160,19 +164,46 @@ impl Instrument for FuturesContract { fn instrument_class(&self) -> InstrumentClass { InstrumentClass::Future } - - fn quote_currency(&self) -> Currency { - self.currency + fn underlying(&self) -> Option { + Some(self.underlying) } fn base_currency(&self) -> Option { None } + fn quote_currency(&self) -> Currency { + self.currency + } + fn settlement_currency(&self) -> Currency { self.currency } + fn isin(&self) -> Option { + None + } + + fn option_kind(&self) -> Option { + None + } + + fn exchange(&self) -> Option { + self.exchange + } + + fn strike_price(&self) -> Option { + None + } + + fn activation_ns(&self) -> Option { + Some(self.activation_ns) + } + + fn expiration_ns(&self) -> Option { + Some(self.expiration_ns) + } + fn is_inverse(&self) -> bool { false } @@ -209,6 +240,14 @@ impl Instrument for FuturesContract { self.min_quantity } + fn max_notional(&self) -> Option { + None + } + + fn min_notional(&self) -> Option { + None + } + fn max_price(&self) -> Option { self.max_price } @@ -226,6 +265,96 @@ impl Instrument for FuturesContract { } } +impl<'r> FromRow<'r, PgRow> for FuturesContract { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::new(res.as_str()).unwrap())?; + let asset_class = row + .try_get::("asset_class") + .map(|res| AssetClass::from_str(res.as_str()).unwrap())?; + let exchange = row + .try_get::, _>("exchange") + .map(|res| res.map(|s| Ustr::from(s.as_str())))?; + let underlying = row + .try_get::("underlying") + .map(|res| Ustr::from(res.as_str()))?; + let currency = row + .try_get::("quote_currency") + .map(|res| Currency::from_str(res.as_str()).unwrap())?; + let activation_ns = row + .try_get::("activation_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let expiration_ns = row + .try_get::("expiration_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from(res.as_str()))?; + let multiplier = row + .try_get::("multiplier") + .map(|res| Quantity::from(res.as_str()))?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + asset_class, + exchange, + underlying, + activation_ns, + expiration_ns, + currency, + price_precision as u8, + price_increment, + multiplier, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + Some(margin_init), + Some(margin_maint), + ts_event, + ts_init, + ) + .unwrap()) + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index ab9963d1ecd7..f7f0a09f23eb 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -23,13 +23,14 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, - types::{currency::Currency, price::Price, quantity::Quantity}, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; #[repr(C)] @@ -164,19 +165,46 @@ impl Instrument for FuturesSpread { fn instrument_class(&self) -> InstrumentClass { InstrumentClass::FutureSpread } - - fn quote_currency(&self) -> Currency { - self.currency + fn underlying(&self) -> Option { + Some(self.underlying) } fn base_currency(&self) -> Option { None } + fn quote_currency(&self) -> Currency { + self.currency + } + fn settlement_currency(&self) -> Currency { self.currency } + fn isin(&self) -> Option { + None + } + + fn option_kind(&self) -> Option { + None + } + + fn exchange(&self) -> Option { + self.exchange + } + + fn strike_price(&self) -> Option { + None + } + + fn activation_ns(&self) -> Option { + Some(self.activation_ns) + } + + fn expiration_ns(&self) -> Option { + Some(self.expiration_ns) + } + fn is_inverse(&self) -> bool { false } @@ -213,6 +241,14 @@ impl Instrument for FuturesSpread { self.min_quantity } + fn max_notional(&self) -> Option { + None + } + + fn min_notional(&self) -> Option { + None + } + fn max_price(&self) -> Option { self.max_price } @@ -230,6 +266,12 @@ impl Instrument for FuturesSpread { } } +impl<'r> FromRow<'r, PgRow> for FuturesSpread { + fn from_row(_row: &'r PgRow) -> Result { + todo!("Implement FromRow for FuturesSpread") + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index c7ea814f0743..c46ba79341e2 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -29,6 +29,8 @@ pub mod stubs; use nautilus_core::nanos::UnixNanos; use rust_decimal::Decimal; use rust_decimal_macros::dec; +use sqlx::{postgres::PgRow, Error, FromRow, Row}; +use ustr::Ustr; use self::{ crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair, @@ -36,7 +38,7 @@ use self::{ options_contract::OptionsContract, options_spread::OptionsSpread, }; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; @@ -272,6 +274,45 @@ impl InstrumentAny { } } +impl<'r> FromRow<'r, PgRow> for InstrumentAny { + fn from_row(row: &'r PgRow) -> Result { + let kind = row.get::("kind"); + if kind == "CRYPTO_FUTURE" { + Ok(InstrumentAny::CryptoFuture( + CryptoFuture::from_row(row).unwrap(), + )) + } else if kind == "CRYPTO_PERPETUAL" { + Ok(InstrumentAny::CryptoPerpetual( + CryptoPerpetual::from_row(row).unwrap(), + )) + } else if kind == "CURRENCY_PAIR" { + Ok(InstrumentAny::CurrencyPair( + CurrencyPair::from_row(row).unwrap(), + )) + } else if kind == "EQUITY" { + Ok(InstrumentAny::Equity(Equity::from_row(row).unwrap())) + } else if kind == "FUTURES_CONTRACT" { + Ok(InstrumentAny::FuturesContract( + FuturesContract::from_row(row).unwrap(), + )) + } else if kind == "FUTURES_SPREAD" { + Ok(InstrumentAny::FuturesSpread( + FuturesSpread::from_row(row).unwrap(), + )) + } else if kind == "OPTIONS_CONTRACT" { + Ok(InstrumentAny::OptionsContract( + OptionsContract::from_row(row).unwrap(), + )) + } else if kind == "OPTIONS_SPREAD" { + Ok(InstrumentAny::OptionsSpread( + OptionsSpread::from_row(row).unwrap(), + )) + } else { + panic!("Unknown instrument type") + } + } +} + pub trait Instrument: 'static + Send { fn into_any(self) -> InstrumentAny; fn id(&self) -> InstrumentId; @@ -284,9 +325,16 @@ pub trait Instrument: 'static + Send { fn raw_symbol(&self) -> Symbol; fn asset_class(&self) -> AssetClass; fn instrument_class(&self) -> InstrumentClass; + fn underlying(&self) -> Option; fn base_currency(&self) -> Option; fn quote_currency(&self) -> Currency; fn settlement_currency(&self) -> Currency; + fn isin(&self) -> Option; + fn option_kind(&self) -> Option; + fn exchange(&self) -> Option; + fn strike_price(&self) -> Option; + fn activation_ns(&self) -> Option; + fn expiration_ns(&self) -> Option; fn is_inverse(&self) -> bool; fn price_precision(&self) -> u8; fn size_precision(&self) -> u8; @@ -296,6 +344,8 @@ pub trait Instrument: 'static + Send { fn lot_size(&self) -> Option; fn max_quantity(&self) -> Option; fn min_quantity(&self) -> Option; + fn max_notional(&self) -> Option; + fn min_notional(&self) -> Option; fn max_price(&self) -> Option; fn min_price(&self) -> Option; fn margin_init(&self) -> Decimal { diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index 9be872f118e9..0e5741807c35 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::hash::{Hash, Hasher}; +use std::{ + hash::{Hash, Hasher}, + str::FromStr, +}; use nautilus_core::{ correctness::{ @@ -23,13 +26,14 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, - types::{currency::Currency, price::Price, quantity::Quantity}, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; #[repr(C)] @@ -166,19 +170,46 @@ impl Instrument for OptionsContract { fn instrument_class(&self) -> InstrumentClass { InstrumentClass::Option } - - fn quote_currency(&self) -> Currency { - self.currency + fn underlying(&self) -> Option { + Some(self.underlying) } fn base_currency(&self) -> Option { None } + fn quote_currency(&self) -> Currency { + self.currency + } + fn settlement_currency(&self) -> Currency { self.currency } + fn isin(&self) -> Option { + None + } + + fn option_kind(&self) -> Option { + Some(self.option_kind) + } + + fn exchange(&self) -> Option { + self.exchange + } + + fn strike_price(&self) -> Option { + Some(self.strike_price) + } + + fn activation_ns(&self) -> Option { + Some(self.activation_ns) + } + + fn expiration_ns(&self) -> Option { + Some(self.expiration_ns) + } + fn is_inverse(&self) -> bool { false } @@ -215,6 +246,14 @@ impl Instrument for OptionsContract { self.min_quantity } + fn max_notional(&self) -> Option { + None + } + + fn min_notional(&self) -> Option { + None + } + fn max_price(&self) -> Option { self.max_price } @@ -232,6 +271,105 @@ impl Instrument for OptionsContract { } } +impl<'r> FromRow<'r, PgRow> for OptionsContract { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::new(res.as_str()).unwrap())?; + let asset_class = row + .try_get::("asset_class") + .map(|res| AssetClass::from_str(res.as_str()).unwrap())?; + let exchange = row + .try_get::, _>("exchange") + .map(|res| res.map(|s| Ustr::from(s.as_str())))?; + let underlying = row + .try_get::("underlying") + .map(|res| Ustr::from(res.as_str()))?; + let option_kind = row + .try_get::("option_kind") + .map(|res| OptionKind::from_str(res.as_str()).unwrap())?; + let activation_ns = row + .try_get::("activation_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let expiration_ns = row + .try_get::("expiration_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let strike_price = row + .try_get::("strike_price") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let currency = row + .try_get::("quote_currency") + .map(|res| Currency::from_str(res.as_str()).unwrap())?; + let price_precision = row.try_get::("price_precision").unwrap(); + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let multiplier = row + .try_get::("multiplier") + .map(|res| Quantity::from(res.as_str()))?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str())) + .unwrap(); + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + asset_class, + exchange, + underlying, + option_kind, + activation_ns, + expiration_ns, + strike_price, + currency, + price_precision as u8, + price_increment, + multiplier, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + Some(margin_init), + Some(margin_maint), + ts_event, + ts_init, + ) + .unwrap()) + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 94fe426a3cdc..87752315be1d 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -23,13 +23,14 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use sqlx::{postgres::PgRow, FromRow}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; use crate::{ - enums::{AssetClass, InstrumentClass}, + enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, - types::{currency::Currency, price::Price, quantity::Quantity}, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; #[repr(C)] @@ -164,19 +165,46 @@ impl Instrument for OptionsSpread { fn instrument_class(&self) -> InstrumentClass { InstrumentClass::OptionSpread } - - fn quote_currency(&self) -> Currency { - self.currency + fn underlying(&self) -> Option { + Some(self.underlying) } fn base_currency(&self) -> Option { None } + fn quote_currency(&self) -> Currency { + self.currency + } + fn settlement_currency(&self) -> Currency { self.currency } + fn isin(&self) -> Option { + None + } + + fn option_kind(&self) -> Option { + None + } + + fn exchange(&self) -> Option { + self.exchange + } + + fn strike_price(&self) -> Option { + None + } + + fn activation_ns(&self) -> Option { + Some(self.activation_ns) + } + + fn expiration_ns(&self) -> Option { + Some(self.expiration_ns) + } + fn is_inverse(&self) -> bool { false } @@ -213,6 +241,14 @@ impl Instrument for OptionsSpread { self.min_quantity } + fn max_notional(&self) -> Option { + None + } + + fn min_notional(&self) -> Option { + None + } + fn max_price(&self) -> Option { self.max_price } @@ -230,6 +266,12 @@ impl Instrument for OptionsSpread { } } +impl<'r> FromRow<'r, PgRow> for OptionsSpread { + fn from_row(_row: &'r PgRow) -> Result { + todo!("Implement FromRow for OptionsSpread") + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index d8d4940274c5..a14120ea8162 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -23,7 +23,7 @@ binary-heap-plus = "0.5.0" compare = "0.1.0" datafusion = { version = "37.0.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } dotenv = "0.15.0" -sqlx = { version = "0.7.4", features = ["sqlite", "postgres", "any", "runtime-tokio"] } +sqlx = { workspace = true } [dev-dependencies] criterion = { workspace = true } From 7ebeed49c69bf48f3a16d3a647d8e8471e3a744b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 07:42:11 +1000 Subject: [PATCH 008/193] Group sql models and code under infrastructure --- nautilus_core/Cargo.lock | 3 +- nautilus_core/Cargo.toml | 1 - nautilus_core/infrastructure/Cargo.toml | 1 + .../src/sql/cache.rs} | 0 .../db => infrastructure/src/sql}/database.rs | 0 .../src/db => infrastructure/src/sql}/mod.rs | 3 +- .../src/sql/models/instruments.rs | 637 ++++++++++++++++++ .../db => infrastructure/src/sql}/schema.rs | 0 nautilus_core/model/Cargo.toml | 1 - .../model/src/instruments/crypto_future.rs | 114 +--- .../model/src/instruments/crypto_perpetual.rs | 106 +-- .../model/src/instruments/currency_pair.rs | 101 +-- nautilus_core/model/src/instruments/equity.rs | 84 +-- .../model/src/instruments/futures_contract.rs | 96 +-- .../model/src/instruments/futures_spread.rs | 7 - nautilus_core/model/src/instruments/mod.rs | 40 -- .../model/src/instruments/options_contract.rs | 105 +-- .../model/src/instruments/options_spread.rs | 7 - nautilus_core/persistence/Cargo.toml | 1 - nautilus_core/persistence/src/lib.rs | 1 - 20 files changed, 647 insertions(+), 661 deletions(-) rename nautilus_core/{persistence/src/db/sql.rs => infrastructure/src/sql/cache.rs} (100%) rename nautilus_core/{persistence/src/db => infrastructure/src/sql}/database.rs (100%) rename nautilus_core/{persistence/src/db => infrastructure/src/sql}/mod.rs (96%) create mode 100644 nautilus_core/infrastructure/src/sql/models/instruments.rs rename nautilus_core/{persistence/src/db => infrastructure/src/sql}/schema.rs (100%) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 2fb5745ce952..1d27d704c97d 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2638,6 +2638,7 @@ dependencies = [ "rmp-serde", "rstest", "serde_json", + "sqlx", "tracing", ] @@ -2662,7 +2663,6 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", - "sqlx", "strum", "tabled", "thiserror", @@ -2714,7 +2714,6 @@ dependencies = [ "quickcheck_macros", "rand", "rstest", - "sqlx", "thiserror", "tokio", ] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 0db5919bb3eb..28d128c81c93 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -57,7 +57,6 @@ tracing = "0.1.40" tokio = { version = "1.37.0", features = ["full"] } ustr = { version = "1.0.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["v4"] } -sqlx = { version = "0.7.4", features = ["sqlite", "postgres", "any", "runtime-tokio"] } # dev-dependencies criterion = "0.5.1" diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 2f508a105be8..1e1369ac7014 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -20,6 +20,7 @@ redis = { workspace = true, optional = true } rmp-serde = { workspace = true } serde_json = { workspace = true } tracing = {workspace = true } +sqlx = { version = "0.7.4", features = ["sqlite", "postgres", "any", "runtime-tokio"] } [dev-dependencies] rstest = { workspace = true } diff --git a/nautilus_core/persistence/src/db/sql.rs b/nautilus_core/infrastructure/src/sql/cache.rs similarity index 100% rename from nautilus_core/persistence/src/db/sql.rs rename to nautilus_core/infrastructure/src/sql/cache.rs diff --git a/nautilus_core/persistence/src/db/database.rs b/nautilus_core/infrastructure/src/sql/database.rs similarity index 100% rename from nautilus_core/persistence/src/db/database.rs rename to nautilus_core/infrastructure/src/sql/database.rs diff --git a/nautilus_core/persistence/src/db/mod.rs b/nautilus_core/infrastructure/src/sql/mod.rs similarity index 96% rename from nautilus_core/persistence/src/db/mod.rs rename to nautilus_core/infrastructure/src/sql/mod.rs index 87c3d546e888..cf32d3bfc0e9 100644 --- a/nautilus_core/persistence/src/db/mod.rs +++ b/nautilus_core/infrastructure/src/sql/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +pub mod cache; pub mod database; +pub mod models; pub mod schema; -pub mod sql; diff --git a/nautilus_core/infrastructure/src/sql/models/instruments.rs b/nautilus_core/infrastructure/src/sql/models/instruments.rs new file mode 100644 index 000000000000..c700ecd7fe16 --- /dev/null +++ b/nautilus_core/infrastructure/src/sql/models/instruments.rs @@ -0,0 +1,637 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 sqlx::{postgres::PgRow, FromRow, Row}; + +impl<'r> FromRow<'r, PgRow> for InstrumentAny { + fn from_row(row: &'r PgRow) -> Result { + let kind = row.get::("kind"); + if kind == "CRYPTO_FUTURE" { + Ok(InstrumentAny::CryptoFuture( + CryptoFuture::from_row(row).unwrap(), + )) + } else if kind == "CRYPTO_PERPETUAL" { + Ok(InstrumentAny::CryptoPerpetual( + CryptoPerpetual::from_row(row).unwrap(), + )) + } else if kind == "CURRENCY_PAIR" { + Ok(InstrumentAny::CurrencyPair( + CurrencyPair::from_row(row).unwrap(), + )) + } else if kind == "EQUITY" { + Ok(InstrumentAny::Equity(Equity::from_row(row).unwrap())) + } else if kind == "FUTURES_CONTRACT" { + Ok(InstrumentAny::FuturesContract( + FuturesContract::from_row(row).unwrap(), + )) + } else if kind == "FUTURES_SPREAD" { + Ok(InstrumentAny::FuturesSpread( + FuturesSpread::from_row(row).unwrap(), + )) + } else if kind == "OPTIONS_CONTRACT" { + Ok(InstrumentAny::OptionsContract( + OptionsContract::from_row(row).unwrap(), + )) + } else if kind == "OPTIONS_SPREAD" { + Ok(InstrumentAny::OptionsSpread( + OptionsSpread::from_row(row).unwrap(), + )) + } else { + panic!("Unknown instrument type") + } + } +} + +impl<'r> FromRow<'r, PgRow> for CryptoFuture { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let underlying = row + .try_get::("underlying") + .map(|res| Currency::from(res.as_str()))?; + let quote_currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let settlement_currency = row + .try_get::("settlement_currency") + .map(|res| Currency::from(res.as_str()))?; + let is_inverse = row.try_get::("is_inverse")?; + let activation_ns = row + .try_get::("activation_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let expiration_ns = row + .try_get::("expiration_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let size_precision = row.try_get::("size_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let size_increment = row + .try_get::("size_increment") + .map(|res| Quantity::from_str(res.as_str()).unwrap())?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|value| Quantity::from(value.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|value| Quantity::from(value.as_str()))); + let max_notional = row + .try_get::, _>("max_notional") + .ok() + .and_then(|res| res.map(|value| Money::from(value.as_str()))); + let min_notional = row + .try_get::, _>("min_notional") + .ok() + .and_then(|res| res.map(|value| Money::from(value.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|value| Price::from(value.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|value| Price::from(value.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + underlying, + quote_currency, + settlement_currency, + is_inverse, + activation_ns, + expiration_ns, + price_precision as u8, + size_precision as u8, + price_increment, + size_increment, + maker_fee, + taker_fee, + margin_init, + margin_maint, + Some(lot_size), + max_quantity, + min_quantity, + max_notional, + min_notional, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) + } +} + +impl<'r> FromRow<'r, PgRow> for CryptoPerpetual { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let base_currency = row + .try_get::("base_currency") + .map(|res| Currency::from(res.as_str()))?; + let quote_currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let settlement_currency = row + .try_get::("settlement_currency") + .map(|res| Currency::from(res.as_str()))?; + let is_inverse = row.try_get::("is_inverse")?; + let price_precision = row.try_get::("price_precision")?; + let size_precision = row.try_get::("size_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let size_increment = row + .try_get::("size_increment") + .map(|res| Quantity::from_str(res.as_str()).unwrap())?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let max_notional = row + .try_get::, _>("max_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let min_notional = row + .try_get::, _>("min_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + base_currency, + quote_currency, + settlement_currency, + is_inverse, + price_precision as u8, + size_precision as u8, + price_increment, + size_increment, + maker_fee, + taker_fee, + margin_init, + margin_maint, + Some(lot_size), + max_quantity, + min_quantity, + max_notional, + min_notional, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) + } +} + +impl<'r> FromRow<'r, PgRow> for CurrencyPair { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let base_currency = row + .try_get::("base_currency") + .map(|res| Currency::from(res.as_str()))?; + let quote_currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let size_precision = row.try_get::("size_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from(res.as_str()))?; + let size_increment = row + .try_get::("size_increment") + .map(|res| Quantity::from(res.as_str()))?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::, _>("lot_size") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); + let max_notional = row + .try_get::, _>("max_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let min_notional = row + .try_get::, _>("min_notional") + .ok() + .and_then(|res| res.map(|res| Money::from(res.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|res| Price::from(res.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + base_currency, + quote_currency, + price_precision as u8, + size_precision as u8, + price_increment, + size_increment, + taker_fee, + maker_fee, + margin_init, + margin_maint, + lot_size, + max_quantity, + min_quantity, + max_notional, + min_notional, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) + } +} + +impl<'r> FromRow<'r, PgRow> for Equity { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::from(res.as_str()))?; + let isin = row + .try_get::, _>("isin") + .map(|res| res.map(|s| Ustr::from(s.as_str())))?; + let currency = row + .try_get::("quote_currency") + .map(|res| Currency::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let maker_fee = row + .try_get::("maker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let taker_fee = row + .try_get::("taker_fee") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let lot_size = row + .try_get::, _>("lot_size") + .map(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap())); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap())); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + isin, + currency, + price_precision as u8, + price_increment, + Some(maker_fee), + Some(taker_fee), + Some(margin_init), + Some(margin_maint), + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + ts_event, + ts_init, + ) + .unwrap()) + } +} + +impl<'r> FromRow<'r, PgRow> for FuturesContract { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::new(res.as_str()).unwrap())?; + let asset_class = row + .try_get::("asset_class") + .map(|res| AssetClass::from_str(res.as_str()).unwrap())?; + let exchange = row + .try_get::, _>("exchange") + .map(|res| res.map(|s| Ustr::from(s.as_str())))?; + let underlying = row + .try_get::("underlying") + .map(|res| Ustr::from(res.as_str()))?; + let currency = row + .try_get::("quote_currency") + .map(|res| Currency::from_str(res.as_str()).unwrap())?; + let activation_ns = row + .try_get::("activation_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let expiration_ns = row + .try_get::("expiration_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let price_precision = row.try_get::("price_precision")?; + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from(res.as_str()))?; + let multiplier = row + .try_get::("multiplier") + .map(|res| Quantity::from(res.as_str()))?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str()))?; + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + asset_class, + exchange, + underlying, + activation_ns, + expiration_ns, + currency, + price_precision as u8, + price_increment, + multiplier, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + Some(margin_init), + Some(margin_maint), + ts_event, + ts_init, + ) + .unwrap()) + } +} + +impl<'r> FromRow<'r, PgRow> for FuturesSpread { + fn from_row(_row: &'r PgRow) -> Result { + todo!("Implement FromRow for FuturesSpread") + } +} + +impl<'r> FromRow<'r, PgRow> for OptionsContract { + fn from_row(row: &'r PgRow) -> Result { + let id = row + .try_get::("id") + .map(|res| InstrumentId::from(res.as_str()))?; + let raw_symbol = row + .try_get::("raw_symbol") + .map(|res| Symbol::new(res.as_str()).unwrap())?; + let asset_class = row + .try_get::("asset_class") + .map(|res| AssetClass::from_str(res.as_str()).unwrap())?; + let exchange = row + .try_get::, _>("exchange") + .map(|res| res.map(|s| Ustr::from(s.as_str())))?; + let underlying = row + .try_get::("underlying") + .map(|res| Ustr::from(res.as_str()))?; + let option_kind = row + .try_get::("option_kind") + .map(|res| OptionKind::from_str(res.as_str()).unwrap())?; + let activation_ns = row + .try_get::("activation_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let expiration_ns = row + .try_get::("expiration_ns") + .map(|res| UnixNanos::from(res.as_str()))?; + let strike_price = row + .try_get::("strike_price") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let currency = row + .try_get::("quote_currency") + .map(|res| Currency::from_str(res.as_str()).unwrap())?; + let price_precision = row.try_get::("price_precision").unwrap(); + let price_increment = row + .try_get::("price_increment") + .map(|res| Price::from_str(res.as_str()).unwrap())?; + let multiplier = row + .try_get::("multiplier") + .map(|res| Quantity::from(res.as_str()))?; + let lot_size = row + .try_get::("lot_size") + .map(|res| Quantity::from(res.as_str())) + .unwrap(); + let max_quantity = row + .try_get::, _>("max_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let min_quantity = row + .try_get::, _>("min_quantity") + .ok() + .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); + let max_price = row + .try_get::, _>("max_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let min_price = row + .try_get::, _>("min_price") + .ok() + .and_then(|res| res.map(|s| Price::from(s.as_str()))); + let margin_init = row + .try_get::("margin_init") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let margin_maint = row + .try_get::("margin_maint") + .map(|res| Decimal::from_str(res.as_str()).unwrap())?; + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + Ok(Self::new( + id, + raw_symbol, + asset_class, + exchange, + underlying, + option_kind, + activation_ns, + expiration_ns, + strike_price, + currency, + price_precision as u8, + price_increment, + multiplier, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + Some(margin_init), + Some(margin_maint), + ts_event, + ts_init, + ) + .unwrap()) + } +} + +impl<'r> FromRow<'r, PgRow> for OptionsSpread { + fn from_row(_row: &'r PgRow) -> Result { + todo!("Implement FromRow for OptionsSpread") + } +} diff --git a/nautilus_core/persistence/src/db/schema.rs b/nautilus_core/infrastructure/src/sql/schema.rs similarity index 100% rename from nautilus_core/persistence/src/db/schema.rs rename to nautilus_core/infrastructure/src/sql/schema.rs diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index abdf45967e9b..b466a3b15f55 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -29,7 +29,6 @@ ustr = { workspace = true } chrono = { workspace = true } evalexpr = "11.3.0" tabled = "0.15.0" -sqlx = { workspace = true} [dev-dependencies] criterion = { workspace = true } diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index be9cdf598ced..95f14b17386b 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -13,10 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{ - hash::{Hash, Hasher}, - str::FromStr, -}; +use std::hash::{Hash, Hasher}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, @@ -24,7 +21,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; @@ -280,114 +276,6 @@ impl Instrument for CryptoFuture { } } -impl<'r> FromRow<'r, PgRow> for CryptoFuture { - fn from_row(row: &'r PgRow) -> Result { - let id = row - .try_get::("id") - .map(|res| InstrumentId::from(res.as_str()))?; - let raw_symbol = row - .try_get::("raw_symbol") - .map(|res| Symbol::from(res.as_str()))?; - let underlying = row - .try_get::("underlying") - .map(|res| Currency::from(res.as_str()))?; - let quote_currency = row - .try_get::("quote_currency") - .map(|res| Currency::from(res.as_str()))?; - let settlement_currency = row - .try_get::("settlement_currency") - .map(|res| Currency::from(res.as_str()))?; - let is_inverse = row.try_get::("is_inverse")?; - let activation_ns = row - .try_get::("activation_ns") - .map(|res| UnixNanos::from(res.as_str()))?; - let expiration_ns = row - .try_get::("expiration_ns") - .map(|res| UnixNanos::from(res.as_str()))?; - let price_precision = row.try_get::("price_precision")?; - let size_precision = row.try_get::("size_precision")?; - let price_increment = row - .try_get::("price_increment") - .map(|res| Price::from_str(res.as_str()).unwrap())?; - let size_increment = row - .try_get::("size_increment") - .map(|res| Quantity::from_str(res.as_str()).unwrap())?; - let maker_fee = row - .try_get::("maker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let taker_fee = row - .try_get::("taker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_init = row - .try_get::("margin_init") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_maint = row - .try_get::("margin_maint") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let lot_size = row - .try_get::("lot_size") - .map(|res| Quantity::from(res.as_str()))?; - let max_quantity = row - .try_get::, _>("max_quantity") - .ok() - .and_then(|res| res.map(|value| Quantity::from(value.as_str()))); - let min_quantity = row - .try_get::, _>("min_quantity") - .ok() - .and_then(|res| res.map(|value| Quantity::from(value.as_str()))); - let max_notional = row - .try_get::, _>("max_notional") - .ok() - .and_then(|res| res.map(|value| Money::from(value.as_str()))); - let min_notional = row - .try_get::, _>("min_notional") - .ok() - .and_then(|res| res.map(|value| Money::from(value.as_str()))); - let max_price = row - .try_get::, _>("max_price") - .ok() - .and_then(|res| res.map(|value| Price::from(value.as_str()))); - let min_price = row - .try_get::, _>("min_price") - .ok() - .and_then(|res| res.map(|value| Price::from(value.as_str()))); - let ts_event = row - .try_get::("ts_event") - .map(|res| UnixNanos::from(res.as_str()))?; - let ts_init = row - .try_get::("ts_init") - .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( - id, - raw_symbol, - underlying, - quote_currency, - settlement_currency, - is_inverse, - activation_ns, - expiration_ns, - price_precision as u8, - size_precision as u8, - price_increment, - size_increment, - maker_fee, - taker_fee, - margin_init, - margin_maint, - Some(lot_size), - max_quantity, - min_quantity, - max_notional, - min_notional, - max_price, - min_price, - ts_event, - ts_init, - ) - .unwrap()) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index b4aa27672a9c..b4c0925aeb65 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -13,10 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{ - hash::{Hash, Hasher}, - str::FromStr, -}; +use std::hash::{Hash, Hasher}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, @@ -24,7 +21,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::InstrumentAny; @@ -286,106 +282,6 @@ impl Instrument for CryptoPerpetual { } } -impl<'r> FromRow<'r, PgRow> for CryptoPerpetual { - fn from_row(row: &'r PgRow) -> Result { - let id = row - .try_get::("id") - .map(|res| InstrumentId::from(res.as_str()))?; - let raw_symbol = row - .try_get::("raw_symbol") - .map(|res| Symbol::from(res.as_str()))?; - let base_currency = row - .try_get::("base_currency") - .map(|res| Currency::from(res.as_str()))?; - let quote_currency = row - .try_get::("quote_currency") - .map(|res| Currency::from(res.as_str()))?; - let settlement_currency = row - .try_get::("settlement_currency") - .map(|res| Currency::from(res.as_str()))?; - let is_inverse = row.try_get::("is_inverse")?; - let price_precision = row.try_get::("price_precision")?; - let size_precision = row.try_get::("size_precision")?; - let price_increment = row - .try_get::("price_increment") - .map(|res| Price::from_str(res.as_str()).unwrap())?; - let size_increment = row - .try_get::("size_increment") - .map(|res| Quantity::from_str(res.as_str()).unwrap())?; - let maker_fee = row - .try_get::("maker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let taker_fee = row - .try_get::("taker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_init = row - .try_get::("margin_init") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_maint = row - .try_get::("margin_maint") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let lot_size = row - .try_get::("lot_size") - .map(|res| Quantity::from(res.as_str()))?; - let max_quantity = row - .try_get::, _>("max_quantity") - .ok() - .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); - let min_quantity = row - .try_get::, _>("min_quantity") - .ok() - .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); - let max_notional = row - .try_get::, _>("max_notional") - .ok() - .and_then(|res| res.map(|res| Money::from(res.as_str()))); - let min_notional = row - .try_get::, _>("min_notional") - .ok() - .and_then(|res| res.map(|res| Money::from(res.as_str()))); - let max_price = row - .try_get::, _>("max_price") - .ok() - .and_then(|res| res.map(|res| Price::from(res.as_str()))); - let min_price = row - .try_get::, _>("min_price") - .ok() - .and_then(|res| res.map(|res| Price::from(res.as_str()))); - let ts_event = row - .try_get::("ts_event") - .map(|res| UnixNanos::from(res.as_str()))?; - let ts_init = row - .try_get::("ts_init") - .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( - id, - raw_symbol, - base_currency, - quote_currency, - settlement_currency, - is_inverse, - price_precision as u8, - size_precision as u8, - price_increment, - size_increment, - maker_fee, - taker_fee, - margin_init, - margin_maint, - Some(lot_size), - max_quantity, - min_quantity, - max_notional, - min_notional, - max_price, - min_price, - ts_event, - ts_init, - ) - .unwrap()) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 44e78051dd4d..9d285ed64478 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -13,10 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{ - hash::{Hash, Hasher}, - str::FromStr, -}; +use std::hash::{Hash, Hasher}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, @@ -24,7 +21,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; @@ -282,101 +278,6 @@ impl Instrument for CurrencyPair { } } -impl<'r> FromRow<'r, PgRow> for CurrencyPair { - fn from_row(row: &'r PgRow) -> Result { - let id = row - .try_get::("id") - .map(|res| InstrumentId::from(res.as_str()))?; - let raw_symbol = row - .try_get::("raw_symbol") - .map(|res| Symbol::from(res.as_str()))?; - let base_currency = row - .try_get::("base_currency") - .map(|res| Currency::from(res.as_str()))?; - let quote_currency = row - .try_get::("quote_currency") - .map(|res| Currency::from(res.as_str()))?; - let price_precision = row.try_get::("price_precision")?; - let size_precision = row.try_get::("size_precision")?; - let price_increment = row - .try_get::("price_increment") - .map(|res| Price::from(res.as_str()))?; - let size_increment = row - .try_get::("size_increment") - .map(|res| Quantity::from(res.as_str()))?; - let maker_fee = row - .try_get::("maker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let taker_fee = row - .try_get::("taker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_init = row - .try_get::("margin_init") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_maint = row - .try_get::("margin_maint") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let lot_size = row - .try_get::, _>("lot_size") - .ok() - .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); - let max_quantity = row - .try_get::, _>("max_quantity") - .ok() - .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); - let min_quantity = row - .try_get::, _>("min_quantity") - .ok() - .and_then(|res| res.map(|res| Quantity::from(res.as_str()))); - let max_notional = row - .try_get::, _>("max_notional") - .ok() - .and_then(|res| res.map(|res| Money::from(res.as_str()))); - let min_notional = row - .try_get::, _>("min_notional") - .ok() - .and_then(|res| res.map(|res| Money::from(res.as_str()))); - let max_price = row - .try_get::, _>("max_price") - .ok() - .and_then(|res| res.map(|res| Price::from(res.as_str()))); - let min_price = row - .try_get::, _>("min_price") - .ok() - .and_then(|res| res.map(|res| Price::from(res.as_str()))); - let ts_event = row - .try_get::("ts_event") - .map(|res| UnixNanos::from(res.as_str()))?; - let ts_init = row - .try_get::("ts_init") - .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( - id, - raw_symbol, - base_currency, - quote_currency, - price_precision as u8, - size_precision as u8, - price_increment, - size_increment, - taker_fee, - maker_fee, - margin_init, - margin_maint, - lot_size, - max_quantity, - min_quantity, - max_notional, - min_notional, - max_price, - min_price, - ts_event, - ts_init, - ) - .unwrap()) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests /////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index bfdb1bf1d693..911829b0b70e 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -13,10 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{ - hash::{Hash, Hasher}, - str::FromStr, -}; +use std::hash::{Hash, Hasher}; use nautilus_core::{ correctness::{check_equal_u8, check_positive_i64, check_valid_string_optional}, @@ -24,7 +21,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; @@ -249,84 +245,6 @@ impl Instrument for Equity { } } -impl<'r> FromRow<'r, PgRow> for Equity { - fn from_row(row: &'r PgRow) -> Result { - let id = row - .try_get::("id") - .map(|res| InstrumentId::from(res.as_str()))?; - let raw_symbol = row - .try_get::("raw_symbol") - .map(|res| Symbol::from(res.as_str()))?; - let isin = row - .try_get::, _>("isin") - .map(|res| res.map(|s| Ustr::from(s.as_str())))?; - let currency = row - .try_get::("quote_currency") - .map(|res| Currency::from(res.as_str()))?; - let price_precision = row.try_get::("price_precision")?; - let price_increment = row - .try_get::("price_increment") - .map(|res| Price::from_str(res.as_str()).unwrap())?; - let maker_fee = row - .try_get::("maker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let taker_fee = row - .try_get::("taker_fee") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_init = row - .try_get::("margin_init") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_maint = row - .try_get::("margin_maint") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let lot_size = row - .try_get::, _>("lot_size") - .map(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap()))?; - let max_quantity = row - .try_get::, _>("max_quantity") - .ok() - .and_then(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap())); - let min_quantity = row - .try_get::, _>("min_quantity") - .ok() - .and_then(|res| res.map(|s| Quantity::from_str(s.as_str()).unwrap())); - let max_price = row - .try_get::, _>("max_price") - .ok() - .and_then(|res| res.map(|s| Price::from(s.as_str()))); - let min_price = row - .try_get::, _>("min_price") - .ok() - .and_then(|res| res.map(|s| Price::from(s.as_str()))); - let ts_event = row - .try_get::("ts_event") - .map(|res| UnixNanos::from(res.as_str()))?; - let ts_init = row - .try_get::("ts_init") - .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( - id, - raw_symbol, - isin, - currency, - price_precision as u8, - price_increment, - Some(maker_fee), - Some(taker_fee), - Some(margin_init), - Some(margin_maint), - lot_size, - max_quantity, - min_quantity, - max_price, - min_price, - ts_event, - ts_init, - ) - .unwrap()) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 65d753dd9d08..82a2b01feea4 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -13,10 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{ - hash::{Hash, Hasher}, - str::FromStr, -}; +use std::hash::{Hash, Hasher}; use nautilus_core::{ correctness::{ @@ -26,7 +23,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; @@ -265,96 +261,6 @@ impl Instrument for FuturesContract { } } -impl<'r> FromRow<'r, PgRow> for FuturesContract { - fn from_row(row: &'r PgRow) -> Result { - let id = row - .try_get::("id") - .map(|res| InstrumentId::from(res.as_str()))?; - let raw_symbol = row - .try_get::("raw_symbol") - .map(|res| Symbol::new(res.as_str()).unwrap())?; - let asset_class = row - .try_get::("asset_class") - .map(|res| AssetClass::from_str(res.as_str()).unwrap())?; - let exchange = row - .try_get::, _>("exchange") - .map(|res| res.map(|s| Ustr::from(s.as_str())))?; - let underlying = row - .try_get::("underlying") - .map(|res| Ustr::from(res.as_str()))?; - let currency = row - .try_get::("quote_currency") - .map(|res| Currency::from_str(res.as_str()).unwrap())?; - let activation_ns = row - .try_get::("activation_ns") - .map(|res| UnixNanos::from(res.as_str()))?; - let expiration_ns = row - .try_get::("expiration_ns") - .map(|res| UnixNanos::from(res.as_str()))?; - let price_precision = row.try_get::("price_precision")?; - let price_increment = row - .try_get::("price_increment") - .map(|res| Price::from(res.as_str()))?; - let multiplier = row - .try_get::("multiplier") - .map(|res| Quantity::from(res.as_str()))?; - let lot_size = row - .try_get::("lot_size") - .map(|res| Quantity::from(res.as_str()))?; - let max_quantity = row - .try_get::, _>("max_quantity") - .ok() - .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); - let min_quantity = row - .try_get::, _>("min_quantity") - .ok() - .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); - let max_price = row - .try_get::, _>("max_price") - .ok() - .and_then(|res| res.map(|s| Price::from(s.as_str()))); - let min_price = row - .try_get::, _>("min_price") - .ok() - .and_then(|res| res.map(|s| Price::from(s.as_str()))); - let margin_init = row - .try_get::("margin_init") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_maint = row - .try_get::("margin_maint") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let ts_event = row - .try_get::("ts_event") - .map(|res| UnixNanos::from(res.as_str()))?; - let ts_init = row - .try_get::("ts_init") - .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( - id, - raw_symbol, - asset_class, - exchange, - underlying, - activation_ns, - expiration_ns, - currency, - price_precision as u8, - price_increment, - multiplier, - lot_size, - max_quantity, - min_quantity, - max_price, - min_price, - Some(margin_init), - Some(margin_maint), - ts_event, - ts_init, - ) - .unwrap()) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index f7f0a09f23eb..72ed78588da2 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -23,7 +23,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; @@ -266,12 +265,6 @@ impl Instrument for FuturesSpread { } } -impl<'r> FromRow<'r, PgRow> for FuturesSpread { - fn from_row(_row: &'r PgRow) -> Result { - todo!("Implement FromRow for FuturesSpread") - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index c46ba79341e2..677157372c89 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -29,7 +29,6 @@ pub mod stubs; use nautilus_core::nanos::UnixNanos; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use sqlx::{postgres::PgRow, Error, FromRow, Row}; use ustr::Ustr; use self::{ @@ -274,45 +273,6 @@ impl InstrumentAny { } } -impl<'r> FromRow<'r, PgRow> for InstrumentAny { - fn from_row(row: &'r PgRow) -> Result { - let kind = row.get::("kind"); - if kind == "CRYPTO_FUTURE" { - Ok(InstrumentAny::CryptoFuture( - CryptoFuture::from_row(row).unwrap(), - )) - } else if kind == "CRYPTO_PERPETUAL" { - Ok(InstrumentAny::CryptoPerpetual( - CryptoPerpetual::from_row(row).unwrap(), - )) - } else if kind == "CURRENCY_PAIR" { - Ok(InstrumentAny::CurrencyPair( - CurrencyPair::from_row(row).unwrap(), - )) - } else if kind == "EQUITY" { - Ok(InstrumentAny::Equity(Equity::from_row(row).unwrap())) - } else if kind == "FUTURES_CONTRACT" { - Ok(InstrumentAny::FuturesContract( - FuturesContract::from_row(row).unwrap(), - )) - } else if kind == "FUTURES_SPREAD" { - Ok(InstrumentAny::FuturesSpread( - FuturesSpread::from_row(row).unwrap(), - )) - } else if kind == "OPTIONS_CONTRACT" { - Ok(InstrumentAny::OptionsContract( - OptionsContract::from_row(row).unwrap(), - )) - } else if kind == "OPTIONS_SPREAD" { - Ok(InstrumentAny::OptionsSpread( - OptionsSpread::from_row(row).unwrap(), - )) - } else { - panic!("Unknown instrument type") - } - } -} - pub trait Instrument: 'static + Send { fn into_any(self) -> InstrumentAny; fn id(&self) -> InstrumentId; diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index 0e5741807c35..524a36c19aa0 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -13,10 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{ - hash::{Hash, Hasher}, - str::FromStr, -}; +use std::hash::{Hash, Hasher}; use nautilus_core::{ correctness::{ @@ -26,7 +23,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; @@ -271,105 +267,6 @@ impl Instrument for OptionsContract { } } -impl<'r> FromRow<'r, PgRow> for OptionsContract { - fn from_row(row: &'r PgRow) -> Result { - let id = row - .try_get::("id") - .map(|res| InstrumentId::from(res.as_str()))?; - let raw_symbol = row - .try_get::("raw_symbol") - .map(|res| Symbol::new(res.as_str()).unwrap())?; - let asset_class = row - .try_get::("asset_class") - .map(|res| AssetClass::from_str(res.as_str()).unwrap())?; - let exchange = row - .try_get::, _>("exchange") - .map(|res| res.map(|s| Ustr::from(s.as_str())))?; - let underlying = row - .try_get::("underlying") - .map(|res| Ustr::from(res.as_str()))?; - let option_kind = row - .try_get::("option_kind") - .map(|res| OptionKind::from_str(res.as_str()).unwrap())?; - let activation_ns = row - .try_get::("activation_ns") - .map(|res| UnixNanos::from(res.as_str()))?; - let expiration_ns = row - .try_get::("expiration_ns") - .map(|res| UnixNanos::from(res.as_str()))?; - let strike_price = row - .try_get::("strike_price") - .map(|res| Price::from_str(res.as_str()).unwrap())?; - let currency = row - .try_get::("quote_currency") - .map(|res| Currency::from_str(res.as_str()).unwrap())?; - let price_precision = row.try_get::("price_precision").unwrap(); - let price_increment = row - .try_get::("price_increment") - .map(|res| Price::from_str(res.as_str()).unwrap())?; - let multiplier = row - .try_get::("multiplier") - .map(|res| Quantity::from(res.as_str()))?; - let lot_size = row - .try_get::("lot_size") - .map(|res| Quantity::from(res.as_str())) - .unwrap(); - let max_quantity = row - .try_get::, _>("max_quantity") - .ok() - .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); - let min_quantity = row - .try_get::, _>("min_quantity") - .ok() - .and_then(|res| res.map(|s| Quantity::from(s.as_str()))); - let max_price = row - .try_get::, _>("max_price") - .ok() - .and_then(|res| res.map(|s| Price::from(s.as_str()))); - let min_price = row - .try_get::, _>("min_price") - .ok() - .and_then(|res| res.map(|s| Price::from(s.as_str()))); - let margin_init = row - .try_get::("margin_init") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let margin_maint = row - .try_get::("margin_maint") - .map(|res| Decimal::from_str(res.as_str()).unwrap())?; - let ts_event = row - .try_get::("ts_event") - .map(|res| UnixNanos::from(res.as_str()))?; - let ts_init = row - .try_get::("ts_init") - .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( - id, - raw_symbol, - asset_class, - exchange, - underlying, - option_kind, - activation_ns, - expiration_ns, - strike_price, - currency, - price_precision as u8, - price_increment, - multiplier, - lot_size, - max_quantity, - min_quantity, - max_price, - min_price, - Some(margin_init), - Some(margin_maint), - ts_event, - ts_init, - ) - .unwrap()) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 87752315be1d..89a8a1a42063 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -23,7 +23,6 @@ use nautilus_core::{ }; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgRow, FromRow}; use ustr::Ustr; use super::{Instrument, InstrumentAny}; @@ -266,12 +265,6 @@ impl Instrument for OptionsSpread { } } -impl<'r> FromRow<'r, PgRow> for OptionsSpread { - fn from_row(_row: &'r PgRow) -> Result { - todo!("Implement FromRow for OptionsSpread") - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index a14120ea8162..e3f41ea1f6c1 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -23,7 +23,6 @@ binary-heap-plus = "0.5.0" compare = "0.1.0" datafusion = { version = "37.0.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } dotenv = "0.15.0" -sqlx = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/nautilus_core/persistence/src/lib.rs b/nautilus_core/persistence/src/lib.rs index f046008a57c7..3ade813989db 100644 --- a/nautilus_core/persistence/src/lib.rs +++ b/nautilus_core/persistence/src/lib.rs @@ -29,7 +29,6 @@ pub mod arrow; pub mod backend; -pub mod db; #[cfg(feature = "python")] pub mod python; From 341481b1cd6c91d7772dbeaa2d97393473e641c3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 08:31:31 +1000 Subject: [PATCH 009/193] Formatting --- nautilus_trader/core/nautilus_pyo3.pyi | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 7f86aa4c525a..d54d2050e984 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -404,10 +404,15 @@ class CashAccount: ) -> list[Money]: ... ### Accounting transformers -def cash_account_from_account_events(events: list[dict],calculate_account_state) -> CashAccount: ... - -def margin_account_from_account_events(events: list[dict],calculate_account_state) -> MarginAccount: ... +def cash_account_from_account_events( + events: list[dict], + calculate_account_state: bool, +) -> CashAccount: ... +def margin_account_from_account_events( + events: list[dict], + calculate_account_state: bool, +) -> MarginAccount: ... ### Data types @@ -989,7 +994,6 @@ class LimitOrder: @classmethod def from_dict(cls, values: dict[str, str]) -> LimitOrder: ... - class LimitIfTouchedOrder: def __init__( self, @@ -1258,6 +1262,7 @@ class StopMarketOrder: ): ... @classmethod def create(cls, init: OrderInitialized) -> StopMarketOrder: ... + class TrailingStopLimitOrder: def __init__( self, @@ -1294,6 +1299,7 @@ class TrailingStopLimitOrder: ): ... @classmethod def create(cls, init: OrderInitialized) -> TrailingStopLimitOrder: ... + class TrailingStopMarketOrder: def __init__( self, From a4e97f97c215b245127c4ae9639bab39a746e0a4 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 08:42:55 +1000 Subject: [PATCH 010/193] Reorganize infrastructure crate --- nautilus_core/Cargo.lock | 3 + nautilus_core/Cargo.toml | 8 - nautilus_core/infrastructure/Cargo.toml | 22 ++- nautilus_core/infrastructure/src/lib.rs | 3 + .../infrastructure/src/python/mod.rs | 8 +- .../infrastructure/src/python/redis/mod.rs | 2 + nautilus_core/infrastructure/src/sql/cache.rs | 8 +- .../infrastructure/src/sql/database.rs | 5 +- .../src/sql/models/instruments.rs | 159 +++++++++++------- .../src/{postgres => sql/models}/mod.rs | 2 + 10 files changed, 141 insertions(+), 79 deletions(-) rename nautilus_core/infrastructure/src/{postgres => sql/models}/mod.rs (97%) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 1d27d704c97d..769081d0bb06 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2637,9 +2637,12 @@ dependencies = [ "redis", "rmp-serde", "rstest", + "rust_decimal", "serde_json", "sqlx", + "tokio", "tracing", + "ustr", ] [[package]] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 28d128c81c93..aa17549c28d3 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -37,14 +37,6 @@ log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_ pyo3 = { version = "0.20.3", features = ["rust_decimal"] } pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" -redis = { version = "0.25.3", features = [ - "connection-manager", - "keep-alive", - "tls-rustls", - "tls-rustls-webpki-roots", - "tokio-comp", - "tokio-rustls-comp", -] } rmp-serde = "1.2.0" rust_decimal = "1.35.0" rust_decimal_macros = "1.34.2" diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 1e1369ac7014..95a2ad9eff91 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -16,17 +16,32 @@ nautilus-core = { path = "../core" , features = ["python"] } nautilus-model = { path = "../model" , features = ["python"] } anyhow = { workspace = true } pyo3 = { workspace = true, optional = true } -redis = { workspace = true, optional = true } rmp-serde = { workspace = true } +rust_decimal = { workspace = true } serde_json = { workspace = true } +tokio = { workspace = true } tracing = {workspace = true } -sqlx = { version = "0.7.4", features = ["sqlite", "postgres", "any", "runtime-tokio"] } +ustr = { workspace = true } +redis = { version = "0.25.3", features = [ + "connection-manager", + "keep-alive", + "tls-rustls", + "tls-rustls-webpki-roots", + "tokio-comp", + "tokio-rustls-comp", +], optional = true } +sqlx = { version = "0.7.4", features = [ + "sqlite", + "postgres", + "any", + "runtime-tokio", +], optional = true } [dev-dependencies] rstest = { workspace = true } [features] -default = ["redis"] +default = ["redis"] # redis needed by `nautilus_trader` by default for now extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", @@ -35,3 +50,4 @@ extension-module = [ ] python = ["pyo3"] redis = ["dep:redis"] +sql = ["dep:sqlx"] diff --git a/nautilus_core/infrastructure/src/lib.rs b/nautilus_core/infrastructure/src/lib.rs index 98ae3ca7c054..1724ca9ee60c 100644 --- a/nautilus_core/infrastructure/src/lib.rs +++ b/nautilus_core/infrastructure/src/lib.rs @@ -32,3 +32,6 @@ pub mod python; #[cfg(feature = "redis")] pub mod redis; + +#[cfg(feature = "sql")] +pub mod sql; diff --git a/nautilus_core/infrastructure/src/python/mod.rs b/nautilus_core/infrastructure/src/python/mod.rs index 123d5ce1512a..e4d266e0f9b7 100644 --- a/nautilus_core/infrastructure/src/python/mod.rs +++ b/nautilus_core/infrastructure/src/python/mod.rs @@ -15,16 +15,16 @@ //! Provides Python bindings from `pyo3`. -#![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade - -use pyo3::{prelude::*, pymodule}; - #[cfg(feature = "redis")] pub mod redis; +use pyo3::{prelude::*, pymodule}; + #[pymodule] pub fn infrastructure(_: Python<'_>, m: &PyModule) -> PyResult<()> { + #[cfg(feature = "redis")] m.add_class::()?; + #[cfg(feature = "redis")] m.add_class::()?; Ok(()) } diff --git a/nautilus_core/infrastructure/src/python/redis/mod.rs b/nautilus_core/infrastructure/src/python/redis/mod.rs index daad03e02891..873d79d766a9 100644 --- a/nautilus_core/infrastructure/src/python/redis/mod.rs +++ b/nautilus_core/infrastructure/src/python/redis/mod.rs @@ -15,5 +15,7 @@ //! Provides a Redis cache database and message bus backing. +#![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade + pub mod cache; pub mod msgbus; diff --git a/nautilus_core/infrastructure/src/sql/cache.rs b/nautilus_core/infrastructure/src/sql/cache.rs index ab33377afd99..fecf1072d482 100644 --- a/nautilus_core/infrastructure/src/sql/cache.rs +++ b/nautilus_core/infrastructure/src/sql/cache.rs @@ -16,7 +16,7 @@ use nautilus_model::identifiers::trader_id::TraderId; use sqlx::Error; -use crate::db::{database::Database, schema::GeneralItem}; +use crate::sql::{database::Database, schema::GeneralItem}; pub struct SqlCacheDatabase { trader_id: TraderId, @@ -64,10 +64,8 @@ impl SqlCacheDatabase { mod tests { use nautilus_model::identifiers::stubs::trader_id; - use crate::db::{ - database::{init_db_schema, setup_test_database}, - sql::SqlCacheDatabase, - }; + use super::SqlCacheDatabase; + use crate::sql::database::{init_db_schema, setup_test_database}; async fn setup_sql_cache_database() -> SqlCacheDatabase { let db = setup_test_database().await; diff --git a/nautilus_core/infrastructure/src/sql/database.rs b/nautilus_core/infrastructure/src/sql/database.rs index 43630b1bd370..f757025b99ae 100644 --- a/nautilus_core/infrastructure/src/sql/database.rs +++ b/nautilus_core/infrastructure/src/sql/database.rs @@ -147,12 +147,15 @@ pub async fn setup_test_database() -> Database { Database::new(Some(DatabaseEngine::SQLITE), Some("sqlite:test_db.sqlite")).await } +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { use sqlx::{FromRow, Row}; - use crate::db::database::{setup_test_database, Database}; + use crate::sql::database::{setup_test_database, Database}; async fn init_item_table(database: &Database) { database diff --git a/nautilus_core/infrastructure/src/sql/models/instruments.rs b/nautilus_core/infrastructure/src/sql/models/instruments.rs index c700ecd7fe16..46959076f3c2 100644 --- a/nautilus_core/infrastructure/src/sql/models/instruments.rs +++ b/nautilus_core/infrastructure/src/sql/models/instruments.rs @@ -13,48 +13,79 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +// Under development +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::str::FromStr; + +use nautilus_core::nanos::UnixNanos; +use nautilus_model::{ + enums::{AssetClass, OptionKind}, + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::{ + crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, + currency_pair::CurrencyPair, equity::Equity, futures_contract::FuturesContract, + futures_spread::FuturesSpread, options_contract::OptionsContract, + options_spread::OptionsSpread, InstrumentAny, + }, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, +}; +use rust_decimal::Decimal; use sqlx::{postgres::PgRow, FromRow, Row}; +use ustr::Ustr; -impl<'r> FromRow<'r, PgRow> for InstrumentAny { - fn from_row(row: &'r PgRow) -> Result { - let kind = row.get::("kind"); - if kind == "CRYPTO_FUTURE" { - Ok(InstrumentAny::CryptoFuture( - CryptoFuture::from_row(row).unwrap(), - )) - } else if kind == "CRYPTO_PERPETUAL" { - Ok(InstrumentAny::CryptoPerpetual( - CryptoPerpetual::from_row(row).unwrap(), - )) - } else if kind == "CURRENCY_PAIR" { - Ok(InstrumentAny::CurrencyPair( - CurrencyPair::from_row(row).unwrap(), - )) - } else if kind == "EQUITY" { - Ok(InstrumentAny::Equity(Equity::from_row(row).unwrap())) - } else if kind == "FUTURES_CONTRACT" { - Ok(InstrumentAny::FuturesContract( - FuturesContract::from_row(row).unwrap(), - )) - } else if kind == "FUTURES_SPREAD" { - Ok(InstrumentAny::FuturesSpread( - FuturesSpread::from_row(row).unwrap(), - )) - } else if kind == "OPTIONS_CONTRACT" { - Ok(InstrumentAny::OptionsContract( - OptionsContract::from_row(row).unwrap(), - )) - } else if kind == "OPTIONS_SPREAD" { - Ok(InstrumentAny::OptionsSpread( - OptionsSpread::from_row(row).unwrap(), - )) - } else { - panic!("Unknown instrument type") - } - } -} +struct InstrumentAnyModel(InstrumentAny); +struct CryptoFutureModel(CryptoFuture); +struct CryptoPerpetualModel(CryptoPerpetual); +struct CurrencyPairModel(CurrencyPair); +struct EquityModel(Equity); +struct FuturesContractModel(FuturesContract); +struct FuturesSpreadModel(FuturesSpread); +struct OptionsContractModel(OptionsContract); +struct OptionsSpreadModel(OptionsSpread); + +// TBD +// impl<'r> FromRow<'r, PgRow> for InstrumentAnyModel { +// fn from_row(row: &'r PgRow) -> Result { +// let kind = row.get::("kind"); +// if kind == "CRYPTO_FUTURE" { +// Ok(InstrumentAny::CryptoFuture( +// CryptoFutureModel::from_row(row).unwrap().0, +// )) +// } else if kind == "CRYPTO_PERPETUAL" { +// Ok(InstrumentAny::CryptoPerpetual( +// CryptoPerpetual::from_row(row).unwrap(), +// )) +// } else if kind == "CURRENCY_PAIR" { +// Ok(InstrumentAny::CurrencyPair( +// CurrencyPair::from_row(row).unwrap(), +// )) +// } else if kind == "EQUITY" { +// Ok(InstrumentAny::Equity(Equity::from_row(row).unwrap())) +// } else if kind == "FUTURES_CONTRACT" { +// Ok(InstrumentAny::FuturesContract( +// FuturesContract::from_row(row).unwrap(), +// )) +// } else if kind == "FUTURES_SPREAD" { +// Ok(InstrumentAny::FuturesSpread( +// FuturesSpread::from_row(row).unwrap(), +// )) +// } else if kind == "OPTIONS_CONTRACT" { +// Ok(InstrumentAny::OptionsContract( +// OptionsContract::from_row(row).unwrap(), +// )) +// } else if kind == "OPTIONS_SPREAD" { +// Ok(InstrumentAny::OptionsSpread( +// OptionsSpread::from_row(row).unwrap(), +// )) +// } else { +// panic!("Unknown instrument type") +// } +// } +// } -impl<'r> FromRow<'r, PgRow> for CryptoFuture { +impl<'r> FromRow<'r, PgRow> for CryptoFutureModel { fn from_row(row: &'r PgRow) -> Result { let id = row .try_get::("id") @@ -131,7 +162,8 @@ impl<'r> FromRow<'r, PgRow> for CryptoFuture { let ts_init = row .try_get::("ts_init") .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( + + let inst = CryptoFuture::new( id, raw_symbol, underlying, @@ -158,11 +190,12 @@ impl<'r> FromRow<'r, PgRow> for CryptoFuture { ts_event, ts_init, ) - .unwrap()) + .unwrap(); + Ok(CryptoFutureModel(inst)) } } -impl<'r> FromRow<'r, PgRow> for CryptoPerpetual { +impl<'r> FromRow<'r, PgRow> for CryptoPerpetualModel { fn from_row(row: &'r PgRow) -> Result { let id = row .try_get::("id") @@ -233,7 +266,8 @@ impl<'r> FromRow<'r, PgRow> for CryptoPerpetual { let ts_init = row .try_get::("ts_init") .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( + + let inst = CryptoPerpetual::new( id, raw_symbol, base_currency, @@ -258,11 +292,12 @@ impl<'r> FromRow<'r, PgRow> for CryptoPerpetual { ts_event, ts_init, ) - .unwrap()) + .unwrap(); + Ok(CryptoPerpetualModel(inst)) } } -impl<'r> FromRow<'r, PgRow> for CurrencyPair { +impl<'r> FromRow<'r, PgRow> for CurrencyPairModel { fn from_row(row: &'r PgRow) -> Result { let id = row .try_get::("id") @@ -330,7 +365,8 @@ impl<'r> FromRow<'r, PgRow> for CurrencyPair { let ts_init = row .try_get::("ts_init") .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( + + let inst = CurrencyPair::new( id, raw_symbol, base_currency, @@ -353,11 +389,12 @@ impl<'r> FromRow<'r, PgRow> for CurrencyPair { ts_event, ts_init, ) - .unwrap()) + .unwrap(); + Ok(CurrencyPairModel(inst)) } } -impl<'r> FromRow<'r, PgRow> for Equity { +impl<'r> FromRow<'r, PgRow> for EquityModel { fn from_row(row: &'r PgRow) -> Result { let id = row .try_get::("id") @@ -412,7 +449,8 @@ impl<'r> FromRow<'r, PgRow> for Equity { let ts_init = row .try_get::("ts_init") .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( + + let inst = Equity::new( id, raw_symbol, isin, @@ -431,11 +469,12 @@ impl<'r> FromRow<'r, PgRow> for Equity { ts_event, ts_init, ) - .unwrap()) + .unwrap(); + Ok(EquityModel(inst)) } } -impl<'r> FromRow<'r, PgRow> for FuturesContract { +impl<'r> FromRow<'r, PgRow> for FuturesContractModel { fn from_row(row: &'r PgRow) -> Result { let id = row .try_get::("id") @@ -499,7 +538,8 @@ impl<'r> FromRow<'r, PgRow> for FuturesContract { let ts_init = row .try_get::("ts_init") .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( + + let inst = FuturesContract::new( id, raw_symbol, asset_class, @@ -521,17 +561,18 @@ impl<'r> FromRow<'r, PgRow> for FuturesContract { ts_event, ts_init, ) - .unwrap()) + .unwrap(); + Ok(FuturesContractModel(inst)) } } -impl<'r> FromRow<'r, PgRow> for FuturesSpread { +impl<'r> FromRow<'r, PgRow> for FuturesSpreadModel { fn from_row(_row: &'r PgRow) -> Result { todo!("Implement FromRow for FuturesSpread") } } -impl<'r> FromRow<'r, PgRow> for OptionsContract { +impl<'r> FromRow<'r, PgRow> for OptionsContractModel { fn from_row(row: &'r PgRow) -> Result { let id = row .try_get::("id") @@ -602,7 +643,8 @@ impl<'r> FromRow<'r, PgRow> for OptionsContract { let ts_init = row .try_get::("ts_init") .map(|res| UnixNanos::from(res.as_str()))?; - Ok(Self::new( + + let inst = OptionsContract::new( id, raw_symbol, asset_class, @@ -626,11 +668,12 @@ impl<'r> FromRow<'r, PgRow> for OptionsContract { ts_event, ts_init, ) - .unwrap()) + .unwrap(); + Ok(OptionsContractModel(inst)) } } -impl<'r> FromRow<'r, PgRow> for OptionsSpread { +impl<'r> FromRow<'r, PgRow> for OptionsSpreadModel { fn from_row(_row: &'r PgRow) -> Result { todo!("Implement FromRow for OptionsSpread") } diff --git a/nautilus_core/infrastructure/src/postgres/mod.rs b/nautilus_core/infrastructure/src/sql/models/mod.rs similarity index 97% rename from nautilus_core/infrastructure/src/postgres/mod.rs rename to nautilus_core/infrastructure/src/sql/models/mod.rs index 97d459d8d1e8..02d78ede908a 100644 --- a/nautilus_core/infrastructure/src/postgres/mod.rs +++ b/nautilus_core/infrastructure/src/sql/models/mod.rs @@ -12,3 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------------------------------- + +pub mod instruments; From 9c13852ffef4f7dacd3435ad3bfbf078336ac794 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 08:50:50 +1000 Subject: [PATCH 011/193] Update dependencies --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 13 ++-- nautilus_core/Cargo.toml | 2 +- poetry.lock | 153 +++++++++++++++++++++------------------ pyproject.toml | 2 +- 5 files changed, 91 insertions(+), 81 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0774c1f46f43..1486b0a7a918 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,7 +82,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.7 + rev: v0.4.1 hooks: - id: ruff args: ["--fix"] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 769081d0bb06..a364c14075c4 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -694,12 +694,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -4655,18 +4656,18 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index aa17549c28d3..9af3b16dcdeb 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -43,7 +43,7 @@ rust_decimal_macros = "1.34.2" serde = { version = "1.0.198", features = ["derive"] } serde_json = "1.0.116" strum = { version = "0.26.2", features = ["derive"] } -thiserror = "1.0.58" +thiserror = "1.0.59" thousands = "0.2.0" tracing = "0.1.40" tokio = { version = "1.37.0", features = ["full"] } diff --git a/poetry.lock b/poetry.lock index ed0f5d79f5d6..62b556447012 100644 --- a/poetry.lock +++ b/poetry.lock @@ -464,7 +464,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -776,13 +776,13 @@ tqdm = ["tqdm"] [[package]] name = "identify" -version = "2.5.35" +version = "2.5.36" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, - {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, ] [package.extras] @@ -932,7 +932,6 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1511,7 +1510,6 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -1604,13 +1602,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest- [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -1676,51 +1674,51 @@ files = [ [[package]] name = "pyarrow" -version = "15.0.2" +version = "16.0.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8"}, - {file = "pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c6753ed4f6adb8461e7c383e418391b8d8453c5d67e17f416c3a5d5709afbd"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f639c059035011db8c0497e541a8a45d98a58dbe34dc8fadd0ef128f2cee46e5"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:290e36a59a0993e9a5224ed2fb3e53375770f07379a0ea03ee2fce2e6d30b423"}, - {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06c2bb2a98bc792f040bef31ad3e9be6a63d0cb39189227c08a7d955db96816e"}, - {file = "pyarrow-15.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:f7a197f3670606a960ddc12adbe8075cea5f707ad7bf0dffa09637fdbb89f76c"}, - {file = "pyarrow-15.0.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5f8bc839ea36b1f99984c78e06e7a06054693dc2af8920f6fb416b5bca9944e4"}, - {file = "pyarrow-15.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5e81dfb4e519baa6b4c80410421528c214427e77ca0ea9461eb4097c328fa33"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4f240852b302a7af4646c8bfe9950c4691a419847001178662a98915fd7ee7"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e7d9cfb5a1e648e172428c7a42b744610956f3b70f524aa3a6c02a448ba853e"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2d4f905209de70c0eb5b2de6763104d5a9a37430f137678edfb9a675bac9cd98"}, - {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90adb99e8ce5f36fbecbbc422e7dcbcbed07d985eed6062e459e23f9e71fd197"}, - {file = "pyarrow-15.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b116e7fd7889294cbd24eb90cd9bdd3850be3738d61297855a71ac3b8124ee38"}, - {file = "pyarrow-15.0.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:25335e6f1f07fdaa026a61c758ee7d19ce824a866b27bba744348fa73bb5a440"}, - {file = "pyarrow-15.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90f19e976d9c3d8e73c80be84ddbe2f830b6304e4c576349d9360e335cd627fc"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a22366249bf5fd40ddacc4f03cd3160f2d7c247692945afb1899bab8a140ddfb"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2a335198f886b07e4b5ea16d08ee06557e07db54a8400cc0d03c7f6a22f785f"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e6d459c0c22f0b9c810a3917a1de3ee704b021a5fb8b3bacf968eece6df098f"}, - {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:033b7cad32198754d93465dcfb71d0ba7cb7cd5c9afd7052cab7214676eec38b"}, - {file = "pyarrow-15.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:29850d050379d6e8b5a693098f4de7fd6a2bea4365bfd073d7c57c57b95041ee"}, - {file = "pyarrow-15.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7167107d7fb6dcadb375b4b691b7e316f4368f39f6f45405a05535d7ad5e5058"}, - {file = "pyarrow-15.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e85241b44cc3d365ef950432a1b3bd44ac54626f37b2e3a0cc89c20e45dfd8bf"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:248723e4ed3255fcd73edcecc209744d58a9ca852e4cf3d2577811b6d4b59818"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ff3bdfe6f1b81ca5b73b70a8d482d37a766433823e0c21e22d1d7dde76ca33f"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:f3d77463dee7e9f284ef42d341689b459a63ff2e75cee2b9302058d0d98fe142"}, - {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:8c1faf2482fb89766e79745670cbca04e7018497d85be9242d5350cba21357e1"}, - {file = "pyarrow-15.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:28f3016958a8e45a1069303a4a4f6a7d4910643fc08adb1e2e4a7ff056272ad3"}, - {file = "pyarrow-15.0.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:89722cb64286ab3d4daf168386f6968c126057b8c7ec3ef96302e81d8cdb8ae4"}, - {file = "pyarrow-15.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0ba387705044b3ac77b1b317165c0498299b08261d8122c96051024f953cd5"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2459bf1f22b6a5cdcc27ebfd99307d5526b62d217b984b9f5c974651398832"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58922e4bfece8b02abf7159f1f53a8f4d9f8e08f2d988109126c17c3bb261f22"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:adccc81d3dc0478ea0b498807b39a8d41628fa9210729b2f718b78cb997c7c91"}, - {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8bd2baa5fe531571847983f36a30ddbf65261ef23e496862ece83bdceb70420d"}, - {file = "pyarrow-15.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6669799a1d4ca9da9c7e06ef48368320f5856f36f9a4dd31a11839dda3f6cc8c"}, - {file = "pyarrow-15.0.2.tar.gz", hash = "sha256:9c9bc803cb3b7bfacc1e96ffbfd923601065d9d3f911179d81e72d99fd74a3d9"}, + {file = "pyarrow-16.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:22a1fdb1254e5095d629e29cd1ea98ed04b4bbfd8e42cc670a6b639ccc208b60"}, + {file = "pyarrow-16.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:574a00260a4ed9d118a14770edbd440b848fcae5a3024128be9d0274dbcaf858"}, + {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0815d0ddb733b8c1b53a05827a91f1b8bde6240f3b20bf9ba5d650eb9b89cdf"}, + {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df0080339387b5d30de31e0a149c0c11a827a10c82f0c67d9afae3981d1aabb7"}, + {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:edf38cce0bf0dcf726e074159c60516447e4474904c0033f018c1f33d7dac6c5"}, + {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91d28f9a40f1264eab2af7905a4d95320ac2f287891e9c8b0035f264fe3c3a4b"}, + {file = "pyarrow-16.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:99af421ee451a78884d7faea23816c429e263bd3618b22d38e7992c9ce2a7ad9"}, + {file = "pyarrow-16.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d22d0941e6c7bafddf5f4c0662e46f2075850f1c044bf1a03150dd9e189427ce"}, + {file = "pyarrow-16.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:266ddb7e823f03733c15adc8b5078db2df6980f9aa93d6bb57ece615df4e0ba7"}, + {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cc23090224b6594f5a92d26ad47465af47c1d9c079dd4a0061ae39551889efe"}, + {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56850a0afe9ef37249d5387355449c0f94d12ff7994af88f16803a26d38f2016"}, + {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:705db70d3e2293c2f6f8e84874b5b775f690465798f66e94bb2c07bab0a6bb55"}, + {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5448564754c154997bc09e95a44b81b9e31ae918a86c0fcb35c4aa4922756f55"}, + {file = "pyarrow-16.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:729f7b262aa620c9df8b9967db96c1575e4cfc8c25d078a06968e527b8d6ec05"}, + {file = "pyarrow-16.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fb8065dbc0d051bf2ae2453af0484d99a43135cadabacf0af588a3be81fbbb9b"}, + {file = "pyarrow-16.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:20ce707d9aa390593ea93218b19d0eadab56390311cb87aad32c9a869b0e958c"}, + {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5823275c8addbbb50cd4e6a6839952682a33255b447277e37a6f518d6972f4e1"}, + {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ab8b9050752b16a8b53fcd9853bf07d8daf19093533e990085168f40c64d978"}, + {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:42e56557bc7c5c10d3e42c3b32f6cff649a29d637e8f4e8b311d334cc4326730"}, + {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2a7abdee4a4a7cfa239e2e8d721224c4b34ffe69a0ca7981354fe03c1328789b"}, + {file = "pyarrow-16.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ef2f309b68396bcc5a354106741d333494d6a0d3e1951271849787109f0229a6"}, + {file = "pyarrow-16.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ed66e5217b4526fa3585b5e39b0b82f501b88a10d36bd0d2a4d8aa7b5a48e2df"}, + {file = "pyarrow-16.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc8814310486f2a73c661ba8354540f17eef51e1b6dd090b93e3419d3a097b3a"}, + {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c2f5e239db7ed43e0ad2baf46a6465f89c824cc703f38ef0fde927d8e0955f7"}, + {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f293e92d1db251447cb028ae12f7bc47526e4649c3a9924c8376cab4ad6b98bd"}, + {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:dd9334a07b6dc21afe0857aa31842365a62eca664e415a3f9536e3a8bb832c07"}, + {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:d91073d1e2fef2c121154680e2ba7e35ecf8d4969cc0af1fa6f14a8675858159"}, + {file = "pyarrow-16.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:71d52561cd7aefd22cf52538f262850b0cc9e4ec50af2aaa601da3a16ef48877"}, + {file = "pyarrow-16.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b93c9a50b965ee0bf4fef65e53b758a7e8dcc0c2d86cebcc037aaaf1b306ecc0"}, + {file = "pyarrow-16.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d831690844706e374c455fba2fb8cfcb7b797bfe53ceda4b54334316e1ac4fa4"}, + {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35692ce8ad0b8c666aa60f83950957096d92f2a9d8d7deda93fb835e6053307e"}, + {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dd3151d098e56f16a8389c1247137f9e4c22720b01c6f3aa6dec29a99b74d80"}, + {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:bd40467bdb3cbaf2044ed7a6f7f251c8f941c8b31275aaaf88e746c4f3ca4a7a"}, + {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:00a1dcb22ad4ceb8af87f7bd30cc3354788776c417f493089e0a0af981bc8d80"}, + {file = "pyarrow-16.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:fda9a7cebd1b1d46c97b511f60f73a5b766a6de4c5236f144f41a5d5afec1f35"}, + {file = "pyarrow-16.0.0.tar.gz", hash = "sha256:59bb1f1edbbf4114c72415f039f1359f1a57d166a331c3229788ccbfbb31689a"}, ] [package.dependencies] -numpy = ">=1.16.6,<2" +numpy = ">=1.16.6" [[package]] name = "pygments" @@ -1853,19 +1851,19 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-xdist" -version = "3.6.0" +version = "3.5.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "pytest_xdist-3.6.0-py3-none-any.whl", hash = "sha256:958e08f38472e1b3a83450d8d3e682e90fdbffee39a97dd0f27185a3bd9074d1"}, - {file = "pytest_xdist-3.6.0.tar.gz", hash = "sha256:2bf346fb1f1481c8d255750f80bc1dfb9fb18b9ad5286ead0b741b6fd56d15b7"}, + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, ] [package.dependencies] -execnet = ">=2.1" +execnet = ">=1.1" psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} -pytest = ">=7.0.0" +pytest = ">=6.2.0" [package.extras] psutil = ["psutil (>=3.0)"] @@ -1950,6 +1948,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1957,8 +1956,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1975,6 +1982,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1982,6 +1990,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2010,28 +2019,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.3.7" +version = "0.4.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, - {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, - {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, - {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, - {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, - {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, - {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, - {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, + {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2d9ef6231e3fbdc0b8c72404a1a0c46fd0dcea84efca83beb4681c318ea6a953"}, + {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9485f54a7189e6f7433e0058cf8581bee45c31a25cd69009d2a040d1bd4bfaef"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2921ac03ce1383e360e8a95442ffb0d757a6a7ddd9a5be68561a671e0e5807e"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eec8d185fe193ad053eda3a6be23069e0c8ba8c5d20bc5ace6e3b9e37d246d3f"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa27d9d72a94574d250f42b7640b3bd2edc4c58ac8ac2778a8c82374bb27984"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f1ee41580bff1a651339eb3337c20c12f4037f6110a36ae4a2d864c52e5ef954"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0926cefb57fc5fced629603fbd1a23d458b25418681d96823992ba975f050c2b"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6e37f2e3cd74496a74af9a4fa67b547ab3ca137688c484749189bf3a686ceb"}, + {file = "ruff-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd703a5975ac1998c2cc5e9494e13b28f31e66c616b0a76e206de2562e0843c"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b92f03b4aa9fa23e1799b40f15f8b95cdc418782a567d6c43def65e1bbb7f1cf"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c859f294f8633889e7d77de228b203eb0e9a03071b72b5989d89a0cf98ee262"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b34510141e393519a47f2d7b8216fec747ea1f2c81e85f076e9f2910588d4b64"}, + {file = "ruff-0.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6e68d248ed688b9d69fd4d18737edcbb79c98b251bba5a2b031ce2470224bdf9"}, + {file = "ruff-0.4.1-py3-none-win32.whl", hash = "sha256:b90506f3d6d1f41f43f9b7b5ff845aeefabed6d2494307bc7b178360a8805252"}, + {file = "ruff-0.4.1-py3-none-win_amd64.whl", hash = "sha256:c7d391e5936af5c9e252743d767c564670dc3889aff460d35c518ee76e4b26d7"}, + {file = "ruff-0.4.1-py3-none-win_arm64.whl", hash = "sha256:a1eaf03d87e6a7cd5e661d36d8c6e874693cb9bc3049d110bc9a97b350680c43"}, + {file = "ruff-0.4.1.tar.gz", hash = "sha256:d592116cdbb65f8b1b7e2a2b48297eb865f6bdc20641879aa9d7b9c11d86db79"}, ] [[package]] @@ -2676,4 +2685,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "62991f4994c321310719023cd9018b2d18c8efe5e6dce1e7150f909fe9980ee3" +content-hash = "5e0eec1cefd3fe5f6bfe2466e93a37556069006b750b73168e8951a9612f3d44" diff --git a/pyproject.toml b/pyproject.toml index dc5bc2939d76..d7caf0768779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ docformatter = "^1.7.5" mypy = "^1.9.0" pandas-stubs = "^2.2.1" pre-commit = "^3.7.0" -ruff = "^0.3.7" +ruff = "^0.4.1" types-pytz = "^2023.3" types-requests = "^2.31" types-toml = "^0.10.2" From 7a87e21feb954ec242711fd3c1902dca7c4b80dd Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 08:57:36 +1000 Subject: [PATCH 012/193] Fix clippy lints --- nautilus_core/common/src/cache/database.rs | 2 + nautilus_core/common/src/cache/mod.rs | 89 +++++++++++++------ nautilus_core/common/src/clock.rs | 2 +- nautilus_core/common/src/enums.rs | 1 + nautilus_core/common/src/ffi/logging.rs | 10 +-- nautilus_core/common/src/logging/headers.rs | 6 +- nautilus_core/common/src/logging/logger.rs | 40 +++++---- nautilus_core/common/src/logging/mod.rs | 11 ++- nautilus_core/common/src/logging/writer.rs | 4 + nautilus_core/common/src/stubs.rs | 2 +- nautilus_core/common/src/timer.rs | 6 +- nautilus_core/common/src/xrate.rs | 9 +- nautilus_core/core/src/datetime.rs | 1 + nautilus_core/core/src/nanos.rs | 2 +- .../model/src/identifiers/client_order_id.rs | 2 + 15 files changed, 120 insertions(+), 67 deletions(-) diff --git a/nautilus_core/common/src/cache/database.rs b/nautilus_core/common/src/cache/database.rs index 426b400cc190..e1efdf67f09d 100644 --- a/nautilus_core/common/src/cache/database.rs +++ b/nautilus_core/common/src/cache/database.rs @@ -56,6 +56,7 @@ pub struct DatabaseCommand { } impl DatabaseCommand { + #[must_use] pub fn new(op_type: DatabaseOperation, key: String, payload: Option>>) -> Self { Self { op_type, @@ -65,6 +66,7 @@ impl DatabaseCommand { } /// Initialize a `Close` database command, this is meant to close the database cache channel. + #[must_use] pub fn close() -> Self { Self { op_type: DatabaseOperation::Close, diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 2276ed595e09..4a85ce5829dc 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -41,7 +41,7 @@ use nautilus_model::{ orders::base::OrderAny, polymorphism::{ GetClientOrderId, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetOrderSide, - GetStrategyId, GetVenueOrderId, + GetStrategyId, }, position::Position, types::currency::Currency, @@ -64,6 +64,7 @@ pub struct CacheConfig { impl CacheConfig { #[allow(clippy::too_many_arguments)] + #[must_use] pub fn new( encoding: SerializationEncoding, timestamps_as_iso8601: bool, @@ -191,6 +192,7 @@ impl Default for Cache { } impl Cache { + #[must_use] pub fn new(config: CacheConfig, database: Option) -> Self { let index = CacheIndex { venue_account: HashMap::new(), @@ -334,6 +336,7 @@ impl Cache { todo!() // Needs order query methods } + #[must_use] pub fn check_integrity(&self) -> bool { true // TODO } @@ -374,7 +377,7 @@ impl Cache { pub fn dispose(&self) -> anyhow::Result<()> { if let Some(database) = &self.database { // TODO: Log operations in database adapter - database.close()? + database.close()?; } Ok(()) } @@ -382,7 +385,7 @@ impl Cache { pub fn flush_db(&self) -> anyhow::Result<()> { if let Some(database) = &self.database { // TODO: Log operations in database adapter - database.flush()? + database.flush()?; } Ok(()) } @@ -426,7 +429,7 @@ impl Cache { .entry(instrument_id) .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - for quote in quotes.iter() { + for quote in quotes { quotes_deque.push_front(*quote); } Ok(()) @@ -452,7 +455,7 @@ impl Cache { .entry(instrument_id) .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - for trade in trades.iter() { + for trade in trades { trades_deque.push_front(*trade); } Ok(()) @@ -478,7 +481,7 @@ impl Cache { .entry(bar_type) .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - for bar in bars.iter() { + for bar in bars { bars_deque.push_front(*bar); } Ok(()) @@ -513,7 +516,7 @@ impl Cache { database.add_synthetic(&synthetic)?; } - self.synthetics.insert(synthetic.id, synthetic.clone()); + self.synthetics.insert(synthetic.id, synthetic); Ok(()) } @@ -698,7 +701,7 @@ impl Cache { self.index .venue_orders .get(&venue) - .map_or(HashSet::new(), |o| o.iter().cloned().collect()), + .map_or(HashSet::new(), |o| o.iter().copied().collect()), ); }; @@ -707,12 +710,12 @@ impl Cache { .index .instrument_orders .get(&instrument_id) - .map_or(HashSet::new(), |o| o.iter().cloned().collect()); + .map_or(HashSet::new(), |o| o.iter().copied().collect()); if let Some(existing_query) = &mut query { *existing_query = existing_query .intersection(&instrument_orders) - .cloned() + .copied() .collect(); } else { query = Some(instrument_orders); @@ -724,12 +727,12 @@ impl Cache { .index .strategy_orders .get(&strategy_id) - .map_or(HashSet::new(), |o| o.iter().cloned().collect()); + .map_or(HashSet::new(), |o| o.iter().copied().collect()); if let Some(existing_query) = &mut query { *existing_query = existing_query .intersection(&strategy_orders) - .cloned() + .copied() .collect(); } else { query = Some(strategy_orders); @@ -752,7 +755,7 @@ impl Cache { self.index .venue_positions .get(&venue) - .map_or(HashSet::new(), |p| p.iter().cloned().collect()), + .map_or(HashSet::new(), |p| p.iter().copied().collect()), ); }; @@ -761,13 +764,13 @@ impl Cache { .index .instrument_positions .get(&instrument_id) - .map_or(HashSet::new(), |p| p.iter().cloned().collect()); + .map_or(HashSet::new(), |p| p.iter().copied().collect()); if let Some(existing_query) = query { query = Some( existing_query .intersection(&instrument_positions) - .cloned() + .copied() .collect(), ); } else { @@ -780,13 +783,13 @@ impl Cache { .index .strategy_positions .get(&strategy_id) - .map_or(HashSet::new(), |p| p.iter().cloned().collect()); + .map_or(HashSet::new(), |p| p.iter().copied().collect()); if let Some(existing_query) = query { query = Some( existing_query .intersection(&strategy_positions) - .cloned() + .copied() .collect(), ); } else { @@ -839,6 +842,7 @@ impl Cache { positions } + #[must_use] pub fn client_order_ids( &self, venue: Option, @@ -847,11 +851,12 @@ impl Cache { ) -> HashSet { let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); match query { - Some(query) => self.index.orders.intersection(&query).cloned().collect(), + Some(query) => self.index.orders.intersection(&query).copied().collect(), None => self.index.orders.clone(), } } + #[must_use] pub fn client_order_ids_open( &self, venue: Option, @@ -864,12 +869,13 @@ impl Cache { .index .orders_open .intersection(&query) - .cloned() + .copied() .collect(), None => self.index.orders_open.clone(), } } + #[must_use] pub fn client_order_ids_closed( &self, venue: Option, @@ -882,12 +888,13 @@ impl Cache { .index .orders_closed .intersection(&query) - .cloned() + .copied() .collect(), None => self.index.orders_closed.clone(), } } + #[must_use] pub fn client_order_ids_emulated( &self, venue: Option, @@ -900,12 +907,13 @@ impl Cache { .index .orders_emulated .intersection(&query) - .cloned() + .copied() .collect(), None => self.index.orders_emulated.clone(), } } + #[must_use] pub fn client_order_ids_inflight( &self, venue: Option, @@ -918,12 +926,13 @@ impl Cache { .index .orders_inflight .intersection(&query) - .cloned() + .copied() .collect(), None => self.index.orders_inflight.clone(), } } + #[must_use] pub fn position_ids( &self, venue: Option, @@ -932,11 +941,12 @@ impl Cache { ) -> HashSet { let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); match query { - Some(query) => self.index.positions.intersection(&query).cloned().collect(), + Some(query) => self.index.positions.intersection(&query).copied().collect(), None => self.index.positions.clone(), } } + #[must_use] pub fn position_open_ids( &self, venue: Option, @@ -949,12 +959,13 @@ impl Cache { .index .positions_open .intersection(&query) - .cloned() + .copied() .collect(), None => self.index.positions_open.clone(), } } + #[must_use] pub fn position_closed_ids( &self, venue: Option, @@ -967,44 +978,52 @@ impl Cache { .index .positions_closed .intersection(&query) - .cloned() + .copied() .collect(), None => self.index.positions_closed.clone(), } } + #[must_use] pub fn actor_ids(&self) -> HashSet { self.index.actors.clone() } + #[must_use] pub fn strategy_ids(&self) -> HashSet { self.index.strategies.clone() } + #[must_use] pub fn exec_algorithm_ids(&self) -> HashSet { self.index.exec_algorithms.clone() } // -- ORDER QUERIES ------------------------------------------------------- + #[must_use] pub fn order(&self, client_order_id: ClientOrderId) -> Option<&OrderAny> { self.orders.get(&client_order_id) } + #[must_use] pub fn client_order_id(&self, venue_order_id: VenueOrderId) -> Option<&ClientOrderId> { self.index.order_ids.get(&venue_order_id) } + #[must_use] pub fn venue_order_id(&self, client_order_id: ClientOrderId) -> Option { self.orders .get(&client_order_id) - .and_then(|o| o.venue_order_id()) + .and_then(nautilus_model::polymorphism::GetVenueOrderId::venue_order_id) } + #[must_use] pub fn client_id(&self, client_order_id: ClientOrderId) -> Option<&ClientId> { self.index.order_client.get(&client_order_id) } + #[must_use] pub fn orders( &self, venue: Option, @@ -1016,6 +1035,7 @@ impl Cache { self.get_orders_for_ids(client_order_ids, side) } + #[must_use] pub fn orders_open( &self, venue: Option, @@ -1027,6 +1047,7 @@ impl Cache { self.get_orders_for_ids(client_order_ids, side) } + #[must_use] pub fn orders_closed( &self, venue: Option, @@ -1038,6 +1059,7 @@ impl Cache { self.get_orders_for_ids(client_order_ids, side) } + #[must_use] pub fn orders_emulated( &self, venue: Option, @@ -1049,6 +1071,7 @@ impl Cache { self.get_orders_for_ids(client_order_ids, side) } + #[must_use] pub fn orders_inflight( &self, venue: Option, @@ -1060,40 +1083,48 @@ impl Cache { self.get_orders_for_ids(client_order_ids, side) } + #[must_use] pub fn orders_for_position(&self, position_id: PositionId) -> Vec<&OrderAny> { let client_order_ids = self.index.position_orders.get(&position_id); match client_order_ids { Some(client_order_ids) => { - self.get_orders_for_ids(client_order_ids.iter().cloned().collect(), None) + self.get_orders_for_ids(client_order_ids.iter().copied().collect(), None) } None => Vec::new(), } } + #[must_use] pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool { self.index.orders.contains(&client_order_id) } + #[must_use] pub fn is_order_open(&self, client_order_id: ClientOrderId) -> bool { self.index.orders_open.contains(&client_order_id) } + #[must_use] pub fn is_order_closed(&self, client_order_id: ClientOrderId) -> bool { self.index.orders_closed.contains(&client_order_id) } + #[must_use] pub fn is_order_emulated(&self, client_order_id: ClientOrderId) -> bool { self.index.orders_emulated.contains(&client_order_id) } + #[must_use] pub fn is_order_inflight(&self, client_order_id: ClientOrderId) -> bool { self.index.orders_inflight.contains(&client_order_id) } + #[must_use] pub fn is_order_pending_cancel_local(&self, client_order_id: ClientOrderId) -> bool { self.index.orders_pending_cancel.contains(&client_order_id) } + #[must_use] pub fn orders_open_count( &self, venue: Option, @@ -1105,6 +1136,7 @@ impl Cache { .len() } + #[must_use] pub fn orders_closed_count( &self, venue: Option, @@ -1116,6 +1148,7 @@ impl Cache { .len() } + #[must_use] pub fn orders_emulated_count( &self, venue: Option, @@ -1127,6 +1160,7 @@ impl Cache { .len() } + #[must_use] pub fn orders_inflight_count( &self, venue: Option, @@ -1138,6 +1172,7 @@ impl Cache { .len() } + #[must_use] pub fn orders_total_count( &self, venue: Option, diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index dd8c2ea4c952..8f7829441a8f 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -142,7 +142,7 @@ fn create_time_event_handler(event: TimeEvent, handler: &EventHandler) -> TimeEv TimeEventHandler { event, - callback_ptr: handler.callback.as_ptr() as *mut c_char, + callback_ptr: handler.callback.as_ptr().cast::(), } } diff --git a/nautilus_core/common/src/enums.rs b/nautilus_core/common/src/enums.rs index 8c082a680b2d..bb38656e456a 100644 --- a/nautilus_core/common/src/enums.rs +++ b/nautilus_core/common/src/enums.rs @@ -226,6 +226,7 @@ pub enum LogColor { } impl LogColor { + #[must_use] pub fn as_ansi(&self) -> &str { match *self { Self::Normal => "", diff --git a/nautilus_core/common/src/ffi/logging.rs b/nautilus_core/common/src/ffi/logging.rs index 8600e0e19514..ea0153155656 100644 --- a/nautilus_core/common/src/ffi/logging.rs +++ b/nautilus_core/common/src/ffi/logging.rs @@ -107,9 +107,9 @@ pub unsafe extern "C" fn logging_init( u8_as_bool(print_config), ); - let directory = optional_cstr_to_str(directory_ptr).map(|s| s.to_string()); - let file_name = optional_cstr_to_str(file_name_ptr).map(|s| s.to_string()); - let file_format = optional_cstr_to_str(file_format_ptr).map(|s| s.to_string()); + let directory = optional_cstr_to_str(directory_ptr).map(std::string::ToString::to_string); + let file_name = optional_cstr_to_str(file_name_ptr).map(std::string::ToString::to_string); + let file_format = optional_cstr_to_str(file_format_ptr).map(std::string::ToString::to_string); let file_config = FileWriterConfig::new(directory, file_name, file_format); if u8_as_bool(is_bypassed) { @@ -170,11 +170,11 @@ pub unsafe extern "C" fn logging_log_header( #[no_mangle] pub unsafe extern "C" fn logging_log_sysinfo(component_ptr: *const c_char) { let component = cstr_to_ustr(component_ptr); - headers::log_sysinfo(component) + headers::log_sysinfo(component); } /// Flushes global logger buffers of any records. #[no_mangle] pub extern "C" fn logger_drop(log_guard: LogGuard_API) { - drop(log_guard) + drop(log_guard); } diff --git a/nautilus_core/common/src/logging/headers.rs b/nautilus_core/common/src/logging/headers.rs index 074802032737..7785239e2370 100644 --- a/nautilus_core/common/src/logging/headers.rs +++ b/nautilus_core/common/src/logging/headers.rs @@ -30,8 +30,8 @@ pub fn log_header(trader_id: TraderId, machine_id: &str, instance_id: UUID4, com let c = component; - let kernel_version = System::kernel_version().map_or("".to_string(), |v| format!("kernel-{v} ")); - let os_version = System::long_os_version().unwrap_or("".to_string()); + let kernel_version = System::kernel_version().map_or(String::new(), |v| format!("kernel-{v} ")); + let os_version = System::long_os_version().unwrap_or_default(); let pid = std::process::id(); header_sepr(c, "================================================================="); @@ -58,7 +58,7 @@ pub fn log_header(trader_id: TraderId, machine_id: &str, instance_id: UUID4, com header_sepr(c, "================================================================="); header_line(c, &format!("CPU architecture: {}", sys.cpus()[0].brand())); header_line(c, &format!("CPU(s): {} @ {} Mhz", sys.cpus().len(), sys.cpus()[0].frequency())); - header_line(c, &format!("OS: {}{}", kernel_version, os_version)); + header_line(c, &format!("OS: {kernel_version}{os_version}")); log_sysinfo(component); diff --git a/nautilus_core/common/src/logging/logger.rs b/nautilus_core/common/src/logging/logger.rs index 093050502f80..5e1b84ae9c09 100644 --- a/nautilus_core/common/src/logging/logger.rs +++ b/nautilus_core/common/src/logging/logger.rs @@ -76,6 +76,7 @@ impl Default for LoggerConfig { } impl LoggerConfig { + #[must_use] pub fn new( stdout_level: LevelFilter, fileout_level: LevelFilter, @@ -92,6 +93,7 @@ impl LoggerConfig { } } + #[must_use] pub fn from_spec(spec: &str) -> Self { let Self { mut stdout_level, @@ -129,9 +131,10 @@ impl LoggerConfig { } } + #[must_use] pub fn from_env() -> Self { match env::var("NAUTILUS_LOG") { - Ok(spec) => LoggerConfig::from_spec(&spec), + Ok(spec) => Self::from_spec(&spec), Err(e) => panic!("Error parsing `LoggerConfig` spec: {e}"), } } @@ -191,8 +194,9 @@ pub struct LogLineWrapper { } impl LogLineWrapper { + #[must_use] pub fn new(line: LogLine, trader_id: Ustr, timestamp: UnixNanos) -> Self { - LogLineWrapper { + Self { line, cache: None, colored: None, @@ -228,6 +232,7 @@ impl LogLineWrapper { }) } + #[must_use] pub fn get_json(&self) -> String { let json_string = serde_json::to_string(&self).expect("Error serializing log event to string"); @@ -267,16 +272,16 @@ impl Log for Logger { .get("color".into()) .and_then(|v| v.to_u64().map(|v| (v as u8).into())) .unwrap_or(LogColor::Normal); - let component = key_values - .get("component".into()) - .map(|v| Ustr::from(&v.to_string())) - .unwrap_or_else(|| Ustr::from(record.metadata().target())); + let component = key_values.get("component".into()).map_or_else( + || Ustr::from(record.metadata().target()), + |v| Ustr::from(&v.to_string()), + ); let line = LogLine { level: record.level(), color, component, - message: format!("{}", record.args()).to_string(), + message: format!("{}", record.args()), }; if let Err(SendError(LogEvent::Log(line))) = self.tx.send(LogEvent::Log(line)) { eprintln!("Error sending log event: {line}"); @@ -298,7 +303,7 @@ impl Logger { file_config: FileWriterConfig, ) -> LogGuard { let config = LoggerConfig::from_env(); - Logger::init_with_config(trader_id, instance_id, config, file_config) + Self::init_with_config(trader_id, instance_id, config, file_config) } #[must_use] @@ -318,12 +323,12 @@ impl Logger { let print_config = config.print_config; if print_config { println!("STATIC_MAX_LEVEL={STATIC_MAX_LEVEL}"); - println!("Logger initialized with {:?} {:?}", config, file_config); + println!("Logger initialized with {config:?} {file_config:?}"); } let mut handle: Option> = None; match set_boxed_logger(Box::new(logger)) { - Ok(_) => { + Ok(()) => { handle = Some( thread::Builder::new() .name("logging".to_string()) @@ -346,7 +351,7 @@ impl Logger { } } Err(e) => { - eprintln!("Cannot set logger because of error: {e}") + eprintln!("Cannot set logger because of error: {e}"); } } @@ -361,7 +366,7 @@ impl Logger { rx: Receiver, ) { if config.print_config { - println!("Logger thread `handle_messages` initialized") + println!("Logger thread `handle_messages` initialized"); } let LoggerConfig { @@ -380,7 +385,7 @@ impl Logger { // Conditionally create file writer based on fileout_level let mut file_writer_opt = if fileout_level != LevelFilter::Off { - FileWriter::new(trader_id.clone(), instance_id, file_config, fileout_level) + FileWriter::new(trader_id, instance_id, file_config, fileout_level) } else { None }; @@ -474,8 +479,9 @@ pub struct LogGuard { } impl LogGuard { + #[must_use] pub fn new(handle: Option>) -> Self { - LogGuard { handle } + Self { handle } } } @@ -489,7 +495,7 @@ impl Drop for LogGuard { fn drop(&mut self) { log::logger().flush(); if let Some(handle) = self.handle.take() { - handle.join().expect("Error joining logging handle") + handle.join().expect("Error joining logging handle"); } } } @@ -549,7 +555,7 @@ mod tests { is_colored: true, print_config: false, } - ) + ); } #[rstest] @@ -564,7 +570,7 @@ mod tests { is_colored: false, print_config: true, } - ) + ); } #[rstest] diff --git a/nautilus_core/common/src/logging/mod.rs b/nautilus_core/common/src/logging/mod.rs index d7973ffa7e9e..1943acc1f892 100644 --- a/nautilus_core/common/src/logging/mod.rs +++ b/nautilus_core/common/src/logging/mod.rs @@ -45,13 +45,13 @@ static LOGGING_COLORED: AtomicBool = AtomicBool::new(true); /// Returns whether the core logger is enabled. #[no_mangle] pub extern "C" fn logging_is_initialized() -> u8 { - LOGGING_INITIALIZED.load(Ordering::Relaxed) as u8 + u8::from(LOGGING_INITIALIZED.load(Ordering::Relaxed)) } /// Sets the logging system to bypass mode. #[no_mangle] pub extern "C" fn logging_set_bypass() { - LOGGING_BYPASSED.store(true, Ordering::Relaxed) + LOGGING_BYPASSED.store(true, Ordering::Relaxed); } /// Shuts down the logging system. @@ -63,7 +63,7 @@ pub extern "C" fn logging_shutdown() { /// Returns whether the core logger is using ANSI colors. #[no_mangle] pub extern "C" fn logging_is_colored() -> u8 { - LOGGING_COLORED.load(Ordering::Relaxed) as u8 + u8::from(LOGGING_COLORED.load(Ordering::Relaxed)) } /// Sets the global logging clock to real-time mode. @@ -123,6 +123,7 @@ pub fn init_logging( Logger::init_with_config(trader_id, instance_id, config, file_config) } +#[must_use] pub fn map_log_level_to_filter(log_level: LogLevel) -> LevelFilter { match log_level { LogLevel::Off => LevelFilter::Off, @@ -133,15 +134,17 @@ pub fn map_log_level_to_filter(log_level: LogLevel) -> LevelFilter { } } +#[must_use] pub fn parse_level_filter_str(s: &str) -> LevelFilter { let mut log_level_str = s.to_string().to_uppercase(); if log_level_str == "WARNING" { - log_level_str = "WARN".to_string() + log_level_str = "WARN".to_string(); } LevelFilter::from_str(&log_level_str) .unwrap_or_else(|_| panic!("Invalid `LevelFilter` string, was {log_level_str}")) } +#[must_use] pub fn parse_component_levels( original_map: Option>, ) -> HashMap { diff --git a/nautilus_core/common/src/logging/writer.rs b/nautilus_core/common/src/logging/writer.rs index da1635c55860..27bb6732d0bf 100644 --- a/nautilus_core/common/src/logging/writer.rs +++ b/nautilus_core/common/src/logging/writer.rs @@ -42,6 +42,7 @@ pub struct StdoutWriter { } impl StdoutWriter { + #[must_use] pub fn new(level: LevelFilter, is_colored: bool) -> Self { Self { buf: BufWriter::new(io::stdout()), @@ -79,6 +80,7 @@ pub struct StderrWriter { } impl StderrWriter { + #[must_use] pub fn new(is_colored: bool) -> Self { Self { buf: BufWriter::new(io::stderr()), @@ -119,6 +121,7 @@ pub struct FileWriterConfig { } impl FileWriterConfig { + #[must_use] pub fn new( directory: Option, file_name: Option, @@ -213,6 +216,7 @@ impl FileWriter { file_path } + #[must_use] pub fn should_rotate_file(&self) -> bool { let current_date_utc = Utc::now().date_naive(); let metadata = self diff --git a/nautilus_core/common/src/stubs.rs b/nautilus_core/common/src/stubs.rs index ddfa1dfa25d7..063511b9bfa3 100644 --- a/nautilus_core/common/src/stubs.rs +++ b/nautilus_core/common/src/stubs.rs @@ -14,7 +14,7 @@ // ------------------------------------------------------------------------------------------------- use nautilus_core::time::get_atomic_clock_static; -use nautilus_model::identifiers::stubs::*; +use nautilus_model::identifiers::stubs::{strategy_id_ema_cross, trader_id}; use rstest::fixture; use crate::factories::OrderFactory; diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 934c68febe92..94bd18a88597 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -61,6 +61,7 @@ pub struct TimeEvent { /// Assumes `name` is a valid string. impl TimeEvent { + #[must_use] pub fn new(name: Ustr, event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos) -> Self { Self { name, @@ -255,6 +256,7 @@ impl LiveTimer { }) } + #[must_use] pub fn is_expired(&self) -> bool { self.is_expired.load(atomic::Ordering::SeqCst) } @@ -299,7 +301,7 @@ impl LiveTimer { assert!( start_time_ns + interval_ns <= stop_time_ns, "start_time + interval was > stop_time" - ) + ); }; let mut timer = tokio::time::interval_at(start, Duration::from_nanos(interval_ns)); @@ -364,7 +366,7 @@ fn call_python_with_time_event( Ok(_) => {} Err(e) => error!("Error on callback: {:?}", e), }; - }) + }); } #[cfg(not(feature = "python"))] diff --git a/nautilus_core/common/src/xrate.rs b/nautilus_core/common/src/xrate.rs index e1d45886eb7f..3dade8021426 100644 --- a/nautilus_core/common/src/xrate.rs +++ b/nautilus_core/common/src/xrate.rs @@ -67,16 +67,13 @@ pub fn get_exchange_rate( } calculation_quotes } - _ => panic!( - "Cannot calculate exchange rate for PriceType {:?}", - price_type - ), + _ => panic!("Cannot calculate exchange rate for PriceType {price_type:?}"), }; let mut exchange_rates: HashMap> = HashMap::new(); // Build quote table - for (symbol, quote) in calculation_quotes.iter() { + for (symbol, quote) in &calculation_quotes { let pieces: Vec<&str> = symbol.as_str().split('/').collect(); let code_lhs = Ustr::from(pieces[0]); let code_rhs = Ustr::from(pieces[1]); @@ -149,5 +146,5 @@ pub fn get_exchange_rate( let empty: HashMap = HashMap::new(); let quotes = exchange_rates.get(&from_currency.code).unwrap_or(&empty); - Ok(quotes.get(&to_currency.code).cloned().unwrap_or(dec!(0.0))) + Ok(quotes.get(&to_currency.code).copied().unwrap_or(dec!(0.0))) } diff --git a/nautilus_core/core/src/datetime.rs b/nautilus_core/core/src/datetime.rs index 74e7071c1202..c45a30400c5b 100644 --- a/nautilus_core/core/src/datetime.rs +++ b/nautilus_core/core/src/datetime.rs @@ -95,6 +95,7 @@ pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String { } /// Floor the given UNIX nanoseconds to the nearest microsecond. +#[must_use] pub fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 { (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND } diff --git a/nautilus_core/core/src/nanos.rs b/nautilus_core/core/src/nanos.rs index 0b7cbafc787a..622b8b6a7b07 100644 --- a/nautilus_core/core/src/nanos.rs +++ b/nautilus_core/core/src/nanos.rs @@ -234,7 +234,7 @@ mod tests { #[rstest] fn test_edge_case_max_value() { let nanos = UnixNanos::from(u64::MAX); - assert_eq!(format!("{}", nanos), format!("{}", u64::MAX)); + assert_eq!(format!("{nanos}"), format!("{}", u64::MAX)); } #[rstest] diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index 4a2d2a3765cd..ffc94a89cec7 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -48,11 +48,13 @@ impl ClientOrderId { } /// Returns the inner identifier value. + #[must_use] pub fn inner(&self) -> Ustr { self.0 } /// Returns the inner identifier value as a string slice. + #[must_use] pub fn as_str(&self) -> &str { self.0.as_str() } From 9acab20e131d32fd7f9dfbfdcd38a1a14b49663f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 09:03:23 +1000 Subject: [PATCH 013/193] Add type annotation --- nautilus_trader/persistence/catalog/parquet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index bd5567ce9182..d9793df276eb 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -337,7 +337,7 @@ def key(obj: Any) -> tuple[str, str | None]: return name, obj.instrument_id.value return name, None - def obj_to_type(obj) -> type: + def obj_to_type(obj: Data) -> type: return type(obj) if not isinstance(obj, CustomData) else obj.data.__class__ name_to_cls = {cls.__name__: cls for cls in {obj_to_type(d) for d in data}} From c1fe03868e67f32be2cadb515eeb37574258de52 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 10:00:28 +1000 Subject: [PATCH 014/193] Add additional Rust docs --- Makefile | 2 +- nautilus_core/accounting/src/account/mod.rs | 2 ++ nautilus_core/accounting/src/python/mod.rs | 2 +- nautilus_core/adapters/src/databento/mod.rs | 2 ++ nautilus_core/adapters/src/databento/python/mod.rs | 2 +- nautilus_core/backtest/src/engine.rs | 2 ++ nautilus_core/backtest/src/matching_engine.rs | 2 ++ nautilus_core/common/src/cache/mod.rs | 2 ++ nautilus_core/common/src/clock.rs | 2 ++ nautilus_core/common/src/enums.rs | 2 ++ nautilus_core/common/src/factories.rs | 2 ++ nautilus_core/common/src/generators/mod.rs | 2 ++ nautilus_core/common/src/handlers.rs | 2 ++ nautilus_core/common/src/interface/mod.rs | 2 ++ nautilus_core/common/src/logging/mod.rs | 2 ++ nautilus_core/common/src/msgbus/mod.rs | 2 ++ nautilus_core/common/src/python/mod.rs | 2 +- nautilus_core/common/src/runtime.rs | 2 ++ nautilus_core/common/src/stubs.rs | 2 ++ nautilus_core/common/src/testing.rs | 2 ++ nautilus_core/common/src/timer.rs | 2 ++ nautilus_core/core/src/python/mod.rs | 2 +- nautilus_core/core/src/uuid.rs | 4 ++-- nautilus_core/execution/src/client.rs | 2 ++ nautilus_core/execution/src/engine.rs | 2 ++ nautilus_core/execution/src/matching_core.rs | 2 ++ nautilus_core/execution/src/messages/mod.rs | 2 ++ nautilus_core/indicators/src/average/mod.rs | 2 ++ nautilus_core/indicators/src/book/mod.rs | 2 ++ nautilus_core/indicators/src/indicator.rs | 2 ++ nautilus_core/indicators/src/momentum/mod.rs | 2 ++ nautilus_core/indicators/src/python/mod.rs | 2 +- nautilus_core/indicators/src/ratio/mod.rs | 2 ++ nautilus_core/indicators/src/stubs.rs | 2 ++ nautilus_core/indicators/src/testing.rs | 2 ++ nautilus_core/indicators/src/volatility/mod.rs | 2 ++ nautilus_core/infrastructure/src/lib.rs | 1 + nautilus_core/infrastructure/src/python/mod.rs | 2 +- nautilus_core/infrastructure/src/redis/mod.rs | 2 ++ nautilus_core/infrastructure/src/sql/mod.rs | 2 ++ nautilus_core/model/src/currencies.rs | 2 ++ nautilus_core/model/src/data/mod.rs | 2 ++ nautilus_core/model/src/events/mod.rs | 2 ++ nautilus_core/model/src/identifiers/mod.rs | 2 ++ nautilus_core/model/src/instruments/mod.rs | 2 ++ nautilus_core/model/src/macros.rs | 2 ++ nautilus_core/model/src/orderbook/mod.rs | 2 ++ nautilus_core/model/src/orders/mod.rs | 2 ++ nautilus_core/model/src/polymorphism.rs | 2 ++ nautilus_core/model/src/position.rs | 2 ++ nautilus_core/model/src/python/mod.rs | 2 +- nautilus_core/model/src/stubs.rs | 2 ++ nautilus_core/model/src/types/mod.rs | 2 ++ nautilus_core/model/src/venues.rs | 2 ++ nautilus_core/network/src/http.rs | 2 ++ nautilus_core/network/src/python/mod.rs | 2 +- nautilus_core/network/src/socket.rs | 2 ++ nautilus_core/network/src/websocket.rs | 2 ++ nautilus_core/persistence/src/arrow/mod.rs | 2 ++ nautilus_core/persistence/src/backend/mod.rs | 2 ++ nautilus_core/persistence/src/python/mod.rs | 2 +- 61 files changed, 111 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 5d7ee82aec98..608156e68cb8 100644 --- a/Makefile +++ b/Makefile @@ -70,7 +70,7 @@ docs-python: install-just-deps-all .PHONY: docs-rust docs-rust: - (cd nautilus_core && RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --no-deps) + (cd nautilus_core && RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --all-features --no-deps --workspace --exclude tokio-tungstenite) .PHONY: clippy clippy: diff --git a/nautilus_core/accounting/src/account/mod.rs b/nautilus_core/accounting/src/account/mod.rs index 1d53aadca2f6..3aee414fe5f6 100644 --- a/nautilus_core/accounting/src/account/mod.rs +++ b/nautilus_core/accounting/src/account/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides account types and accounting functionality. + pub mod base; pub mod cash; pub mod margin; diff --git a/nautilus_core/accounting/src/python/mod.rs b/nautilus_core/accounting/src/python/mod.rs index 6ca8aac3763b..63d245a4b45a 100644 --- a/nautilus_core/accounting/src/python/mod.rs +++ b/nautilus_core/accounting/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade diff --git a/nautilus_core/adapters/src/databento/mod.rs b/nautilus_core/adapters/src/databento/mod.rs index 8b45d33b78f2..70880f3c5581 100644 --- a/nautilus_core/adapters/src/databento/mod.rs +++ b/nautilus_core/adapters/src/databento/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides the [Databento](https://databento.com) integration adapter. + pub mod common; pub mod decode; pub mod enums; diff --git a/nautilus_core/adapters/src/databento/python/mod.rs b/nautilus_core/adapters/src/databento/python/mod.rs index ba75fdead4ac..bab30299cd7e 100644 --- a/nautilus_core/adapters/src/databento/python/mod.rs +++ b/nautilus_core/adapters/src/databento/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade diff --git a/nautilus_core/backtest/src/engine.rs b/nautilus_core/backtest/src/engine.rs index 3ad0dabc8934..924de33c7b86 100644 --- a/nautilus_core/backtest/src/engine.rs +++ b/nautilus_core/backtest/src/engine.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! The core `BacktestEngine` for backtesting on historical data. + use std::ops::{Deref, DerefMut}; use nautilus_common::{clock::TestClock, ffi::clock::TestClock_API, timer::TimeEventHandler}; diff --git a/nautilus_core/backtest/src/matching_engine.rs b/nautilus_core/backtest/src/matching_engine.rs index 63d03639962b..46a8c7caa688 100644 --- a/nautilus_core/backtest/src/matching_engine.rs +++ b/nautilus_core/backtest/src/matching_engine.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides an `OrderMatchingEngine` for use in research, backtesting and sandbox environments. + // Under development #![allow(dead_code)] #![allow(unused_variables)] diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 4a85ce5829dc..3864f3a9fc84 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! A common in-memory `Cache` for market and execution related data. + // Under development #![allow(dead_code)] #![allow(unused_variables)] diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index 8f7829441a8f..de5f37ac4b37 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides real-time and static test `Clock` implementations. + use std::{collections::HashMap, ops::Deref}; use nautilus_core::{ diff --git a/nautilus_core/common/src/enums.rs b/nautilus_core/common/src/enums.rs index bb38656e456a..4a302e786bcc 100644 --- a/nautilus_core/common/src/enums.rs +++ b/nautilus_core/common/src/enums.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines common enumerations. + use std::fmt::Debug; use serde::{Deserialize, Serialize}; diff --git a/nautilus_core/common/src/factories.rs b/nautilus_core/common/src/factories.rs index eff962784ae9..68725fff29a6 100644 --- a/nautilus_core/common/src/factories.rs +++ b/nautilus_core/common/src/factories.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides factories for constructing domain objects such as orders. + use std::collections::HashMap; use nautilus_core::{time::AtomicTime, uuid::UUID4}; diff --git a/nautilus_core/common/src/generators/mod.rs b/nautilus_core/common/src/generators/mod.rs index 9df51c6a20cc..77a829abd4ef 100644 --- a/nautilus_core/common/src/generators/mod.rs +++ b/nautilus_core/common/src/generators/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides generation of identifiers such as `ClientOrderId` and `PositionId`. + pub mod client_order_id; pub mod order_list_id; pub mod position_id; diff --git a/nautilus_core/common/src/handlers.rs b/nautilus_core/common/src/handlers.rs index 85694cb2ede0..1ebbcd39f38d 100644 --- a/nautilus_core/common/src/handlers.rs +++ b/nautilus_core/common/src/handlers.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides common message handlers. + #[cfg(not(feature = "python"))] use std::ffi::c_char; use std::{fmt, sync::Arc}; diff --git a/nautilus_core/common/src/interface/mod.rs b/nautilus_core/common/src/interface/mod.rs index ba41ba2b9861..718b12cd4e7a 100644 --- a/nautilus_core/common/src/interface/mod.rs +++ b/nautilus_core/common/src/interface/mod.rs @@ -13,4 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Interface traits to faciliate a ports and adapters style architecture. + pub mod account; diff --git a/nautilus_core/common/src/logging/mod.rs b/nautilus_core/common/src/logging/mod.rs index 1943acc1f892..01b1a29da819 100644 --- a/nautilus_core/common/src/logging/mod.rs +++ b/nautilus_core/common/src/logging/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! The logging framework for Nautilus systems. + use std::{ collections::HashMap, env, diff --git a/nautilus_core/common/src/msgbus/mod.rs b/nautilus_core/common/src/msgbus/mod.rs index 50a9fb0ed67a..63ec48f78fb3 100644 --- a/nautilus_core/common/src/msgbus/mod.rs +++ b/nautilus_core/common/src/msgbus/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! A common in-memory `MessageBus` for loosely coupled message passing patterns. + pub mod database; use std::{ diff --git a/nautilus_core/common/src/python/mod.rs b/nautilus_core/common/src/python/mod.rs index 26d2a21b6315..19636d0daac3 100644 --- a/nautilus_core/common/src/python/mod.rs +++ b/nautilus_core/common/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade diff --git a/nautilus_core/common/src/runtime.rs b/nautilus_core/common/src/runtime.rs index 98dd9fc4a54c..e370bf5eeff5 100644 --- a/nautilus_core/common/src/runtime.rs +++ b/nautilus_core/common/src/runtime.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! The centralized Tokio runtime for a running Nautilus system. + use std::sync::OnceLock; use tokio::runtime::Runtime; diff --git a/nautilus_core/common/src/stubs.rs b/nautilus_core/common/src/stubs.rs index 063511b9bfa3..abb5dd10e958 100644 --- a/nautilus_core/common/src/stubs.rs +++ b/nautilus_core/common/src/stubs.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Type stubs to facilitate testing. + use nautilus_core::time::get_atomic_clock_static; use nautilus_model::identifiers::stubs::{strategy_id_ema_cross, trader_id}; use rstest::fixture; diff --git a/nautilus_core/common/src/testing.rs b/nautilus_core/common/src/testing.rs index dc108d1f0528..cf60ac7f4ee4 100644 --- a/nautilus_core/common/src/testing.rs +++ b/nautilus_core/common/src/testing.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Common test related helper functions. + use std::{ thread, time::{Duration, Instant}, diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 94bd18a88597..94d899840691 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides real-time and test timers for use with `Clock` implementations. + use std::{ cmp::Ordering, ffi::c_char, diff --git a/nautilus_core/core/src/python/mod.rs b/nautilus_core/core/src/python/mod.rs index 0178025f5d1f..bb534ee78e38 100644 --- a/nautilus_core/core/src/python/mod.rs +++ b/nautilus_core/core/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index c4c0cc53d0b7..5e90fdbf0ddf 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -13,8 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines a core `UUID4` universally unique identifier (UUID) version 4 based on a 128-bit -//! label as specified in RFC 4122. +//! A core `UUID4` universally unique identifier (UUID) version 4 based on a 128-bit +//! label (RFC 4122). use std::{ ffi::{CStr, CString}, diff --git a/nautilus_core/execution/src/client.rs b/nautilus_core/execution/src/client.rs index 7987561c79dd..8abdc660c82a 100644 --- a/nautilus_core/execution/src/client.rs +++ b/nautilus_core/execution/src/client.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides execution client base functionality. + // Under development #![allow(dead_code)] #![allow(unused_variables)] diff --git a/nautilus_core/execution/src/engine.rs b/nautilus_core/execution/src/engine.rs index 0dedaa62594f..aa3a87d2d756 100644 --- a/nautilus_core/execution/src/engine.rs +++ b/nautilus_core/execution/src/engine.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a generic `ExecutionEngine` for backtesting and live environments. + // Under development #![allow(dead_code)] #![allow(unused_variables)] diff --git a/nautilus_core/execution/src/matching_core.rs b/nautilus_core/execution/src/matching_core.rs index ecd17ddb017e..fb96f99087c3 100644 --- a/nautilus_core/execution/src/matching_core.rs +++ b/nautilus_core/execution/src/matching_core.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! A common `OrderMatchingCore` for the `OrderMatchingEngine` and other components. + // Under development #![allow(dead_code)] #![allow(unused_variables)] diff --git a/nautilus_core/execution/src/messages/mod.rs b/nautilus_core/execution/src/messages/mod.rs index 7fae514754b5..c4f34ebd76d4 100644 --- a/nautilus_core/execution/src/messages/mod.rs +++ b/nautilus_core/execution/src/messages/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines execution specific messages such as order commands. + use nautilus_model::identifiers::{client_id::ClientId, instrument_id::InstrumentId}; use strum::Display; diff --git a/nautilus_core/indicators/src/average/mod.rs b/nautilus_core/indicators/src/average/mod.rs index 80e3259d5e45..95db06550fba 100644 --- a/nautilus_core/indicators/src/average/mod.rs +++ b/nautilus_core/indicators/src/average/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Moving average type indicators. + use nautilus_model::enums::PriceType; use strum::{AsRefStr, Display, EnumIter, EnumString, FromRepr}; diff --git a/nautilus_core/indicators/src/book/mod.rs b/nautilus_core/indicators/src/book/mod.rs index 030ac78385ce..49266242da96 100644 --- a/nautilus_core/indicators/src/book/mod.rs +++ b/nautilus_core/indicators/src/book/mod.rs @@ -13,4 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Order book specific indicators. + pub mod imbalance; diff --git a/nautilus_core/indicators/src/indicator.rs b/nautilus_core/indicators/src/indicator.rs index 98fccd870c0d..99b6d73020b8 100644 --- a/nautilus_core/indicators/src/indicator.rs +++ b/nautilus_core/indicators/src/indicator.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines a common `Indicator` trait. + use std::{fmt, fmt::Debug}; use nautilus_model::{ diff --git a/nautilus_core/indicators/src/momentum/mod.rs b/nautilus_core/indicators/src/momentum/mod.rs index 7ace632bf2db..6a11fd173c08 100644 --- a/nautilus_core/indicators/src/momentum/mod.rs +++ b/nautilus_core/indicators/src/momentum/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Momentum type indicators. + pub mod aroon; pub mod bias; pub mod cmo; diff --git a/nautilus_core/indicators/src/python/mod.rs b/nautilus_core/indicators/src/python/mod.rs index d0a91df07f62..ed63431a324e 100644 --- a/nautilus_core/indicators/src/python/mod.rs +++ b/nautilus_core/indicators/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade diff --git a/nautilus_core/indicators/src/ratio/mod.rs b/nautilus_core/indicators/src/ratio/mod.rs index 5ca24611bf69..e13423493fad 100644 --- a/nautilus_core/indicators/src/ratio/mod.rs +++ b/nautilus_core/indicators/src/ratio/mod.rs @@ -13,4 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Ratio type indicators. + pub mod efficiency_ratio; diff --git a/nautilus_core/indicators/src/stubs.rs b/nautilus_core/indicators/src/stubs.rs index 04c6d2fc8d0e..610b90805abe 100644 --- a/nautilus_core/indicators/src/stubs.rs +++ b/nautilus_core/indicators/src/stubs.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Type stubs to facilitate testing. + use nautilus_model::{ data::{ bar::{Bar, BarSpecification, BarType}, diff --git a/nautilus_core/indicators/src/testing.rs b/nautilus_core/indicators/src/testing.rs index aa5614689b84..b6427b0b22e0 100644 --- a/nautilus_core/indicators/src/testing.rs +++ b/nautilus_core/indicators/src/testing.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Common test related helper functions. + /// Checks if two floating-point numbers are approximately equal within the /// margin of floating-point precision. /// diff --git a/nautilus_core/indicators/src/volatility/mod.rs b/nautilus_core/indicators/src/volatility/mod.rs index 799b0bb38a10..aac24710eae5 100644 --- a/nautilus_core/indicators/src/volatility/mod.rs +++ b/nautilus_core/indicators/src/volatility/mod.rs @@ -13,4 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Volatility type indicators. + pub mod atr; diff --git a/nautilus_core/infrastructure/src/lib.rs b/nautilus_core/infrastructure/src/lib.rs index 1724ca9ee60c..6f87ec9dc619 100644 --- a/nautilus_core/infrastructure/src/lib.rs +++ b/nautilus_core/infrastructure/src/lib.rs @@ -26,6 +26,7 @@ //! //! - `python`: Enables Python bindings from `pyo3` //! - `redis`: Enables the Redis cache database and message bus backing implementations +//! - `sql`: Enables the SQL models and cache database #[cfg(feature = "python")] pub mod python; diff --git a/nautilus_core/infrastructure/src/python/mod.rs b/nautilus_core/infrastructure/src/python/mod.rs index e4d266e0f9b7..6c07e5f0dbfc 100644 --- a/nautilus_core/infrastructure/src/python/mod.rs +++ b/nautilus_core/infrastructure/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #[cfg(feature = "redis")] pub mod redis; diff --git a/nautilus_core/infrastructure/src/redis/mod.rs b/nautilus_core/infrastructure/src/redis/mod.rs index fc9e3aa59792..24b945aed94b 100644 --- a/nautilus_core/infrastructure/src/redis/mod.rs +++ b/nautilus_core/infrastructure/src/redis/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a Redis backed `CacheDatabase` and `MessageBusDatabase` implementation. + pub mod cache; pub mod msgbus; diff --git a/nautilus_core/infrastructure/src/sql/mod.rs b/nautilus_core/infrastructure/src/sql/mod.rs index cf32d3bfc0e9..2df57cccc3be 100644 --- a/nautilus_core/infrastructure/src/sql/mod.rs +++ b/nautilus_core/infrastructure/src/sql/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a SQL data model and `CacheDatabase` implementation. + pub mod cache; pub mod database; pub mod models; diff --git a/nautilus_core/model/src/currencies.rs b/nautilus_core/model/src/currencies.rs index 02b1b8a5ff46..2a75caab5fbe 100644 --- a/nautilus_core/model/src/currencies.rs +++ b/nautilus_core/model/src/currencies.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Common `Currency` constants. + use std::{ collections::HashMap, sync::{Mutex, OnceLock}, diff --git a/nautilus_core/model/src/data/mod.rs b/nautilus_core/model/src/data/mod.rs index 19a0564debc4..948ce4cf00f8 100644 --- a/nautilus_core/model/src/data/mod.rs +++ b/nautilus_core/model/src/data/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines `Data` types for the trading domain model. + pub mod bar; pub mod delta; pub mod deltas; diff --git a/nautilus_core/model/src/events/mod.rs b/nautilus_core/model/src/events/mod.rs index 718817de89d0..bc27a5283ee9 100644 --- a/nautilus_core/model/src/events/mod.rs +++ b/nautilus_core/model/src/events/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines order, position and account events for the trading domain model. + pub mod account; pub mod order; pub mod position; diff --git a/nautilus_core/model/src/identifiers/mod.rs b/nautilus_core/model/src/identifiers/mod.rs index d952191a2f75..2439bb019933 100644 --- a/nautilus_core/model/src/identifiers/mod.rs +++ b/nautilus_core/model/src/identifiers/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines identifiers for the trading domain models. + use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index 677157372c89..9e5f43adb069 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines instrument definitions for the trading domain models. + pub mod crypto_future; pub mod crypto_perpetual; pub mod currency_pair; diff --git a/nautilus_core/model/src/macros.rs b/nautilus_core/model/src/macros.rs index b59ae112a045..9f7e0847d503 100644 --- a/nautilus_core/model/src/macros.rs +++ b/nautilus_core/model/src/macros.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Model specific macros. + #[macro_export] macro_rules! enum_strum_serde { ($type:ty) => { diff --git a/nautilus_core/model/src/orderbook/mod.rs b/nautilus_core/model/src/orderbook/mod.rs index ebadfa649427..d42aff96dbea 100644 --- a/nautilus_core/model/src/orderbook/mod.rs +++ b/nautilus_core/model/src/orderbook/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a generic order book which can handle L1/L2/L3 data. + pub mod aggregation; pub mod analysis; pub mod book; diff --git a/nautilus_core/model/src/orders/mod.rs b/nautilus_core/model/src/orders/mod.rs index 8a58c59e8aa8..59027c636043 100644 --- a/nautilus_core/model/src/orders/mod.rs +++ b/nautilus_core/model/src/orders/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines order types for the trading domain model. + #![allow(dead_code)] pub mod base; diff --git a/nautilus_core/model/src/polymorphism.rs b/nautilus_core/model/src/polymorphism.rs index 9d80c3685202..a1e703c69f64 100644 --- a/nautilus_core/model/src/polymorphism.rs +++ b/nautilus_core/model/src/polymorphism.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines traits to faciliate polymorphism. + use nautilus_core::nanos::UnixNanos; use crate::{ diff --git a/nautilus_core/model/src/position.rs b/nautilus_core/model/src/position.rs index 9921bbaa2acc..71be2472e420 100644 --- a/nautilus_core/model/src/position.rs +++ b/nautilus_core/model/src/position.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines a `Position` for the trading domain model. + use std::{ collections::{HashMap, HashSet}, fmt::Display, diff --git a/nautilus_core/model/src/python/mod.rs b/nautilus_core/model/src/python/mod.rs index 9371a2c5f8c4..3a96310f7ca7 100644 --- a/nautilus_core/model/src/python/mod.rs +++ b/nautilus_core/model/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade diff --git a/nautilus_core/model/src/stubs.rs b/nautilus_core/model/src/stubs.rs index 1fa1bf8855c7..abe380c5889f 100644 --- a/nautilus_core/model/src/stubs.rs +++ b/nautilus_core/model/src/stubs.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Type stubs to facilitate testing. + use rstest::fixture; use rust_decimal::prelude::ToPrimitive; diff --git a/nautilus_core/model/src/types/mod.rs b/nautilus_core/model/src/types/mod.rs index f5e1c1e4ca32..3d34dffa31c5 100644 --- a/nautilus_core/model/src/types/mod.rs +++ b/nautilus_core/model/src/types/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines value types for the trading domain model such as `Price`, `Quantity` and `Money`. + pub mod balance; pub mod currency; pub mod fixed; diff --git a/nautilus_core/model/src/venues.rs b/nautilus_core/model/src/venues.rs index b479f2207bce..536fc4c48171 100644 --- a/nautilus_core/model/src/venues.rs +++ b/nautilus_core/model/src/venues.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Common `Venue` constants. + use std::{ collections::HashMap, sync::{Mutex, OnceLock}, diff --git a/nautilus_core/network/src/http.rs b/nautilus_core/network/src/http.rs index b6957a27cd0a..2116c7d60de7 100644 --- a/nautilus_core/network/src/http.rs +++ b/nautilus_core/network/src/http.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a high-performance HTTP client implementation. + use std::{ collections::{hash_map::DefaultHasher, HashMap}, hash::{Hash, Hasher}, diff --git a/nautilus_core/network/src/python/mod.rs b/nautilus_core/network/src/python/mod.rs index 6dc97869eae9..a0fa1ffcb031 100644 --- a/nautilus_core/network/src/python/mod.rs +++ b/nautilus_core/network/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. use pyo3::prelude::*; diff --git a/nautilus_core/network/src/socket.rs b/nautilus_core/network/src/socket.rs index fc88bf6cb895..9db1bfbde901 100644 --- a/nautilus_core/network/src/socket.rs +++ b/nautilus_core/network/src/socket.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a high-performance raw TCP client implementation with TLS capability. + use std::{sync::Arc, time::Duration}; use nautilus_core::python::to_pyruntime_err; diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index 0bc4e0f6ec30..a0dda5dca218 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a high-performance WebSocket client implementation. + use std::{str::FromStr, sync::Arc, time::Duration}; use futures_util::{ diff --git a/nautilus_core/persistence/src/arrow/mod.rs b/nautilus_core/persistence/src/arrow/mod.rs index 45a8adcd26b0..42b76c3033d1 100644 --- a/nautilus_core/persistence/src/arrow/mod.rs +++ b/nautilus_core/persistence/src/arrow/mod.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Defines the Apache Arrow schema for Nautilus types. + pub mod bar; pub mod delta; pub mod depth; diff --git a/nautilus_core/persistence/src/backend/mod.rs b/nautilus_core/persistence/src/backend/mod.rs index b98400745205..10062490b8d6 100644 --- a/nautilus_core/persistence/src/backend/mod.rs +++ b/nautilus_core/persistence/src/backend/mod.rs @@ -13,5 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides an Apache Parquet backend powered by [DataFusion](https://arrow.apache.org/datafusion). + pub mod kmerge_batch; pub mod session; diff --git a/nautilus_core/persistence/src/python/mod.rs b/nautilus_core/persistence/src/python/mod.rs index 59117f8e9e63..a02aefc0630b 100644 --- a/nautilus_core/persistence/src/python/mod.rs +++ b/nautilus_core/persistence/src/python/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides Python bindings from `pyo3`. +//! Python bindings from `pyo3`. #![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade From 42dc7e2513f7ae0d430657689793784be6d26ac5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 10:04:35 +1000 Subject: [PATCH 015/193] Add release notes --- RELEASES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index f157b7ec54f2..57b19d21e4d0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,18 @@ +# NautilusTrader 1.192.0 Beta + +Released on TBD (UTC). + +### Enhancements +None + +### Breaking Changes +None + +### Fixes +None + +--- + # NautilusTrader 1.191.0 Beta Released on 20th April 2024 (UTC). From 04bda9eb571c58a18a389bcd3dc8e6caef28ec9b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 10:25:41 +1000 Subject: [PATCH 016/193] Fix ParquetDataCatalog bar queries by instrument_id --- RELEASES.md | 2 +- .../persistence/catalog/parquet.py | 17 ++++++++++++-- tests/unit_tests/persistence/test_catalog.py | 22 ++++++++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 57b19d21e4d0..4f25cd44276d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,7 +9,7 @@ None None ### Fixes -None +- Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) --- diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index d9793df276eb..3068a592254c 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -421,10 +421,23 @@ def backend_session( # Parse the parent directory which *should* be the instrument ID, # this prevents us matching all instrument ID substrings. dir = path.split("/")[-2] - if instrument_ids and not any(dir == urisafe_instrument_id(x) for x in instrument_ids): - continue + + # Filter by instrument ID + if data_cls == Bar: + if instrument_ids and not any( + dir.startswith(urisafe_instrument_id(x) + "-") for x in instrument_ids + ): + continue + else: + if instrument_ids and not any( + dir == urisafe_instrument_id(x) for x in instrument_ids + ): + continue + + # Filter by bar type if bar_types and not any(dir == urisafe_instrument_id(x) for x in bar_types): continue + table = f"{file_prefix}_{idx}" query = self._build_query( table, diff --git a/tests/unit_tests/persistence/test_catalog.py b/tests/unit_tests/persistence/test_catalog.py index 4af347ab62fa..3bb577dd3d38 100644 --- a/tests/unit_tests/persistence/test_catalog.py +++ b/tests/unit_tests/persistence/test_catalog.py @@ -241,7 +241,7 @@ def test_catalog_custom_data(catalog: ParquetDataCatalog) -> None: assert isinstance(data[0], CustomData) -def test_catalog_bars(catalog: ParquetDataCatalog) -> None: +def test_catalog_bars_querying_by_bar_type(catalog: ParquetDataCatalog) -> None: # Arrange bar_type = TestDataStubs.bartype_adabtc_binance_1min_last() instrument = TestInstrumentProvider.adabtc_binance() @@ -261,6 +261,24 @@ def test_catalog_bars(catalog: ParquetDataCatalog) -> None: assert len(bars) == len(stub_bars) == 10 +def test_catalog_bars_querying_by_instrument_id(catalog: ParquetDataCatalog) -> None: + # Arrange + bar_type = TestDataStubs.bartype_adabtc_binance_1min_last() + instrument = TestInstrumentProvider.adabtc_binance() + stub_bars = TestDataStubs.binance_bars_from_csv( + "ADABTC-1m-2021-11-27.csv", + bar_type, + instrument, + ) + + # Act + catalog.write_data(stub_bars) + + # Assert + bars = catalog.bars(instrument_ids=[instrument.id.value]) + assert len(bars) == len(stub_bars) == 10 + + def test_catalog_write_pyo3_order_book_depth10(catalog: ParquetDataCatalog) -> None: # Arrange instrument = TestInstrumentProvider.ethusdt_binance() @@ -339,9 +357,11 @@ def test_catalog_multiple_bar_types(catalog: ParquetDataCatalog) -> None: # Assert bars1 = catalog.bars(bar_types=[str(bar_type1)]) bars2 = catalog.bars(bar_types=[str(bar_type2)]) + bars3 = catalog.bars(instrument_ids=[instrument1.id.value]) all_bars = catalog.bars() assert len(bars1) == 10 assert len(bars2) == 10 + assert len(bars3) == 10 assert len(all_bars) == 20 From 02669aeb87a6821127c0ae7c639557844fcd554d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 13:48:53 +1000 Subject: [PATCH 017/193] Repair TestTimer tests --- nautilus_core/common/src/timer.rs | 122 +++++++++++++++++------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 94d899840691..d47939e295a7 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -386,59 +386,77 @@ fn call_python_with_time_event( //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { - // use nautilus_core::nanos::UnixNanos; - // use rstest::*; - // - // use super::{TestTimer, TimeEvent}; - // - // #[rstest] - // fn test_test_timer_pop_event() { - // let mut timer = TestTimer::new("test_timer", 0, UnixNanos::from(1), None).unwrap(); - // - // assert!(timer.next().is_some()); - // assert!(timer.next().is_some()); - // timer.is_expired = true; - // assert!(timer.next().is_none()); - // } - // - // #[rstest] - // fn test_test_timer_advance_within_next_time_ns() { - // let mut timer = TestTimer::new("test_timer", 5, UnixNanos::from(0), None).unwrap(); - // let _: Vec = timer.advance(UnixNanos::from(1)).collect(); - // let _: Vec = timer.advance(UnixNanos::from(2)).collect(); - // let _: Vec = timer.advance(UnixNanos::from(3)).collect(); - // assert_eq!(timer.advance(UnixNanos::from(4)).count(), 0); - // assert_eq!(timer.next_time_ns, 5); - // assert!(!timer.is_expired); - // } + use nautilus_core::nanos::UnixNanos; + use rstest::*; - // #[rstest] - // fn test_test_timer_advance_up_to_next_time_ns() { - // let mut timer = TestTimer::new("test_timer", 1, 0, None); - // assert_eq!(timer.advance(1).count(), 1); - // assert!(!timer.is_expired); - // } - // - // #[rstest] - // fn test_test_timer_advance_up_to_next_time_ns_with_stop_time() { - // let mut timer = TestTimer::new("test_timer", 1, 0, Some(2)); - // assert_eq!(timer.advance(2).count(), 2); - // assert!(timer.is_expired); - // } - // - // #[rstest] - // fn test_test_timer_advance_beyond_next_time_ns() { - // let mut timer = TestTimer::new("test_timer", 1, 0, Some(5)); - // assert_eq!(timer.advance(5).count(), 5); - // assert!(timer.is_expired); - // } - // - // #[rstest] - // fn test_test_timer_advance_beyond_stop_time() { - // let mut timer = TestTimer::new("test_timer", 1, 0, Some(5)); - // assert_eq!(timer.advance(10).count(), 5); - // assert!(timer.is_expired); - // } + use super::{TestTimer, TimeEvent}; + + #[rstest] + fn test_test_timer_pop_event() { + let mut timer = TestTimer::new("test_timer", 1, UnixNanos::from(1), None).unwrap(); + + assert!(timer.next().is_some()); + assert!(timer.next().is_some()); + timer.is_expired = true; + assert!(timer.next().is_none()); + } + + #[rstest] + fn test_test_timer_advance_within_next_time_ns() { + let mut timer = TestTimer::new("test_timer", 5, UnixNanos::default(), None).unwrap(); + let _: Vec = timer.advance(UnixNanos::from(1)).collect(); + let _: Vec = timer.advance(UnixNanos::from(2)).collect(); + let _: Vec = timer.advance(UnixNanos::from(3)).collect(); + assert_eq!(timer.advance(UnixNanos::from(4)).count(), 0); + assert_eq!(timer.next_time_ns, 5); + assert!(!timer.is_expired); + } + + #[rstest] + fn test_test_timer_advance_up_to_next_time_ns() { + let mut timer = TestTimer::new("test_timer", 1, UnixNanos::default(), None).unwrap(); + assert_eq!(timer.advance(UnixNanos::from(1)).count(), 1); + assert!(!timer.is_expired); + } + + #[rstest] + fn test_test_timer_advance_up_to_next_time_ns_with_stop_time() { + let mut timer = TestTimer::new( + "test_timer", + 1, + UnixNanos::default(), + Some(UnixNanos::from(2)), + ) + .unwrap(); + assert_eq!(timer.advance(UnixNanos::from(2)).count(), 2); + assert!(timer.is_expired); + } + + #[rstest] + fn test_test_timer_advance_beyond_next_time_ns() { + let mut timer = TestTimer::new( + "test_timer", + 1, + UnixNanos::default(), + Some(UnixNanos::from(5)), + ) + .unwrap(); + assert_eq!(timer.advance(UnixNanos::from(5)).count(), 5); + assert!(timer.is_expired); + } + + #[rstest] + fn test_test_timer_advance_beyond_stop_time() { + let mut timer = TestTimer::new( + "test_timer", + 1, + UnixNanos::default(), + Some(UnixNanos::from(5)), + ) + .unwrap(); + assert_eq!(timer.advance(UnixNanos::from(10)).count(), 5); + assert!(timer.is_expired); + } // #[tokio::test] // async fn test_live_timer_starts_and_stops() { From 5f4e484c1b746c016236f1269ae1c027dbbeaa77 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 15:54:15 +1000 Subject: [PATCH 018/193] Add LiveTimer tests --- nautilus_core/common/src/timer.rs | 122 ++++++++++++++++-------------- 1 file changed, 66 insertions(+), 56 deletions(-) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index d47939e295a7..0c8530004243 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -386,10 +386,21 @@ fn call_python_with_time_event( //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { - use nautilus_core::nanos::UnixNanos; + use nautilus_core::{ + datetime::NANOSECONDS_IN_MILLISECOND, nanos::UnixNanos, time::get_atomic_clock_realtime, + }; + use pyo3::prelude::*; use rstest::*; + use tokio::time::Duration; - use super::{TestTimer, TimeEvent}; + use super::{LiveTimer, TestTimer, TimeEvent}; + use crate::{handlers::EventHandler, testing::wait_until}; + + #[pyfunction] + fn receive_event(_py: Python, _event: TimeEvent) -> PyResult<()> { + // TODO: Assert the length of a handler vec + Ok(()) + } #[rstest] fn test_test_timer_pop_event() { @@ -458,58 +469,57 @@ mod tests { assert!(timer.is_expired); } - // #[tokio::test] - // async fn test_live_timer_starts_and_stops() { - // // Create a callback that increments a counter - // let event_list = Python::with_gil(|py| PyList::empty(py)); - // - // // Create a new LiveTimer with a short interval and start immediately - // let clock = get_atomic_clock_realtime(); - // let start_time = UnixNanos::from(clock.get_time_ns()); - // let interval_ns = 100_000_000; // 100 ms - // let mut timer = - // LiveTimer::new("TEST_TIMER", interval_ns, start_time, None, handler).unwrap(); - // timer.start(); - // - // // Wait for a short time to allow the timer to run - // tokio::time::sleep(Duration::from_millis(250)).await; - // - // // Stop the timer and assert that the counter has been incremented - // timer.cancel().unwrap(); - // // let counter = counter.lock().unwrap(); - // // assert!(*counter > 0); - // assert!(timer.is_expired()) - // } - - // #[tokio::test] - // async fn test_live_timer_with_stop_time() { - // // Create a callback that increments a counter - // let counter = Arc::new(Mutex::new(0)); - // let counter_clone = Arc::clone(&counter); - // let callback = move || { - // let mut counter = counter_clone.lock().unwrap(); - // *counter += 1; - // }; - // - // // Create a new LiveTimer with a short interval and stop time - // let start_time = UnixNanos::now(); - // let interval_ns = 100_000_000; // 100 ms - // let stop_time = start_time + 500_000_000; // 500 ms - // let mut live_timer = LiveTimer::new( - // "TEST_TIMER", - // interval_ns, - // start_time, - // Some(stop_time), - // callback, - // ) - // .unwrap(); - // live_timer.start(); - // - // // Wait for a longer time than the stop time - // tokio::time::sleep(Duration::from_millis(750)).await; - // - // // Check that the counter has not been incremented beyond the stop time - // let counter = counter.lock().unwrap(); - // assert!(*counter <= 5); // 500 ms / 100 ms = 5 increments - // } + #[tokio::test] + async fn test_live_timer_starts_and_stops() { + pyo3::prepare_freethreaded_python(); + + let handler = Python::with_gil(|py| { + let callable = wrap_pyfunction!(receive_event, py).unwrap(); + EventHandler::new(callable.into_py(py)) + }); + + // Create a new LiveTimer with no stop time + let clock = get_atomic_clock_realtime(); + let start_time = UnixNanos::from(clock.get_time_ns()); + let interval_ns = 100 * NANOSECONDS_IN_MILLISECOND; + let mut timer = + LiveTimer::new("TEST_TIMER", interval_ns, start_time, None, handler).unwrap(); + timer.start(); + + // Wait for timer to run + tokio::time::sleep(Duration::from_millis(300)).await; + + timer.cancel().unwrap(); + wait_until(|| timer.is_expired(), Duration::from_secs(2)); + } + + #[tokio::test] + async fn test_live_timer_with_stop_time() { + pyo3::prepare_freethreaded_python(); + + let handler = Python::with_gil(|py| { + let callable = wrap_pyfunction!(receive_event, py).unwrap(); + EventHandler::new(callable.into_py(py)) + }); + + // Create a new LiveTimer with a stop time + let clock = get_atomic_clock_realtime(); + let start_time = UnixNanos::from(clock.get_time_ns()); + let interval_ns = 100 * NANOSECONDS_IN_MILLISECOND; + let stop_time = start_time + 500 * NANOSECONDS_IN_MILLISECOND; + let mut timer = LiveTimer::new( + "TEST_TIMER", + interval_ns, + start_time, + Some(stop_time), + handler, + ) + .unwrap(); + timer.start(); + + // Wait for a longer time than the stop time + tokio::time::sleep(Duration::from_secs(1)).await; + + wait_until(|| timer.is_expired(), Duration::from_secs(2)); + } } From 30e964b324bd7ff5da2c815d06c887b459eb8fcd Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 21 Apr 2024 17:39:26 +1000 Subject: [PATCH 019/193] Change tags type to list[str] --- RELEASES.md | 2 +- docs/concepts/orders.md | 10 +-- nautilus_core/common/src/factories.rs | 2 +- .../model/src/events/order/initialized.rs | 12 +-- nautilus_core/model/src/orders/base.rs | 4 +- nautilus_core/model/src/orders/limit.rs | 6 +- .../model/src/orders/limit_if_touched.rs | 6 +- nautilus_core/model/src/orders/market.rs | 6 +- .../model/src/orders/market_if_touched.rs | 6 +- .../model/src/orders/market_to_limit.rs | 6 +- nautilus_core/model/src/orders/stop_limit.rs | 12 +-- nautilus_core/model/src/orders/stop_market.rs | 6 +- .../model/src/orders/trailing_stop_limit.rs | 6 +- .../model/src/orders/trailing_stop_market.rs | 6 +- .../src/python/events/order/initialized.rs | 17 ++-- .../model/src/python/orders/limit.rs | 23 +++-- .../src/python/orders/limit_if_touched.rs | 4 +- .../model/src/python/orders/market.rs | 23 +++-- .../src/python/orders/market_if_touched.rs | 4 +- .../src/python/orders/market_to_limit.rs | 4 +- .../model/src/python/orders/stop_limit.rs | 18 ++-- .../model/src/python/orders/stop_market.rs | 4 +- .../src/python/orders/trailing_stop_limit.rs | 4 +- .../src/python/orders/trailing_stop_market.rs | 4 +- nautilus_trader/common/factories.pxd | 24 ++--- nautilus_trader/common/factories.pyx | 88 +++++++++---------- nautilus_trader/core/nautilus_pyo3.pyi | 20 ++--- nautilus_trader/execution/algorithm.pxd | 6 +- nautilus_trader/execution/algorithm.pyx | 21 ++--- nautilus_trader/live/execution_engine.py | 2 +- nautilus_trader/model/events/order.pxd | 4 +- nautilus_trader/model/events/order.pyx | 16 ++-- nautilus_trader/model/orders/base.pxd | 4 +- nautilus_trader/model/orders/limit.pyx | 7 +- .../model/orders/limit_if_touched.pyx | 7 +- nautilus_trader/model/orders/market.pyx | 7 +- .../model/orders/market_if_touched.pyx | 7 +- .../model/orders/market_to_limit.pyx | 7 +- nautilus_trader/model/orders/stop_limit.pyx | 7 +- nautilus_trader/model/orders/stop_market.pyx | 7 +- .../model/orders/trailing_stop_limit.pyx | 7 +- .../model/orders/trailing_stop_market.pyx | 7 +- nautilus_trader/test_kit/rust/events_pyo3.py | 2 +- nautilus_trader/test_kit/rust/orders_pyo3.py | 2 +- nautilus_trader/trading/strategy.pxd | 4 +- nautilus_trader/trading/strategy.pyx | 8 +- tests/unit_tests/execution/test_algorithm.py | 16 ++-- tests/unit_tests/execution/test_messages.py | 20 ++--- .../orders/test_stop_limit_order_pyo3.py | 2 +- tests/unit_tests/model/test_events.py | 6 +- tests/unit_tests/model/test_orders.py | 36 ++++---- .../unit_tests/serialization/test_msgpack.py | 64 +++++++------- tests/unit_tests/trading/test_strategy.py | 8 +- 53 files changed, 308 insertions(+), 303 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 4f25cd44276d..4ae2a3c5a3ec 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,7 +6,7 @@ Released on TBD (UTC). None ### Breaking Changes -None +- Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) ### Fixes - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) diff --git a/docs/concepts/orders.md b/docs/concepts/orders.md index b5c6d2bbb27e..197c2add0675 100644 --- a/docs/concepts/orders.md +++ b/docs/concepts/orders.md @@ -170,7 +170,7 @@ order: MarketOrder = self.order_factory.market( quantity=Quantity.from_int(100_000), time_in_force=TimeInForce.IOC, # <-- optional (default GTC) reduce_only=False, # <-- optional (default False) - tags="ENTRY", # <-- optional (default None) + tags=["ENTRY"], # <-- optional (default None) ) ``` [API Reference](https://docs.nautilustrader.io/api_reference/model/orders.html#module-nautilus_trader.model.orders.market) @@ -324,7 +324,7 @@ order: MarketIfTouchedOrder = self.order_factory.market_if_touched( time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=False, # <-- optional (default False) - tags="ENTRY", # <-- optional (default None) + tags=["ENTRY"], # <-- optional (default None) ) ``` @@ -359,7 +359,7 @@ order: StopLimitOrder = self.order_factory.limit_if_touched( expire_time=pd.Timestamp("2022-06-06T12:00"), post_only=True, # <-- optional (default False) reduce_only=False, # <-- optional (default False) - tags="TAKE_PROFIT", # <-- optional (default None) + tags=["TAKE_PROFIT"], # <-- optional (default None) ) ``` @@ -396,7 +396,7 @@ order: TrailingStopMarketOrder = self.order_factory.trailing_stop_market( time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=True, # <-- optional (default False) - tags="TRAILING_STOP-1", # <-- optional (default None) + tags=["TRAILING_STOP-1"], # <-- optional (default None) ) ``` @@ -436,7 +436,7 @@ order: TrailingStopLimitOrder = self.order_factory.trailing_stop_limit( time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=True, # <-- optional (default False) - tags="TRAILING_STOP", # <-- optional (default None) + tags=["TRAILING_STOP"], # <-- optional (default None) ) ``` diff --git a/nautilus_core/common/src/factories.rs b/nautilus_core/common/src/factories.rs index 68725fff29a6..25f0d7a8662c 100644 --- a/nautilus_core/common/src/factories.rs +++ b/nautilus_core/common/src/factories.rs @@ -104,7 +104,7 @@ impl OrderFactory { quote_quantity: Option, exec_algorithm_id: Option, exec_algorithm_params: Option>, - tags: Option, + tags: Option>, ) -> MarketOrder { let client_order_id = self.generate_client_order_id(); let exec_spawn_id: Option = if exec_algorithm_id.is_none() { diff --git a/nautilus_core/model/src/events/order/initialized.rs b/nautilus_core/model/src/events/order/initialized.rs index dc2333162d92..a27f05b445d2 100644 --- a/nautilus_core/model/src/events/order/initialized.rs +++ b/nautilus_core/model/src/events/order/initialized.rs @@ -74,7 +74,7 @@ pub struct OrderInitialized { pub exec_algorithm_id: Option, pub exec_algorithm_params: Option>, pub exec_spawn_id: Option, - pub tags: Option, + pub tags: Option>, } impl Default for OrderInitialized { @@ -152,7 +152,7 @@ impl OrderInitialized { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> anyhow::Result { Ok(Self { trader_id, @@ -266,9 +266,11 @@ impl Display for OrderInitialized { .map_or("None".to_string(), |exec_spawn_id| format!( "{exec_spawn_id}" )), - self.tags - .as_ref() - .map_or("None".to_string(), |tags| format!("{tags}")), + self.tags.as_ref().map_or("None".to_string(), |tags| tags + .iter() + .map(|s| s.to_string()) + .collect::>() + .join(", ")), ) } } diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 62ade0204884..11e290a8ef8e 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -629,7 +629,7 @@ pub trait Order { fn exec_algorithm_id(&self) -> Option; fn exec_algorithm_params(&self) -> Option>; fn exec_spawn_id(&self) -> Option; - fn tags(&self) -> Option; + fn tags(&self) -> Option>; fn filled_qty(&self) -> Quantity; fn leaves_qty(&self) -> Quantity; fn avg_px(&self) -> Option; @@ -832,7 +832,7 @@ pub struct OrderCore { pub exec_algorithm_id: Option, pub exec_algorithm_params: Option>, pub exec_spawn_id: Option, - pub tags: Option, + pub tags: Option>, pub filled_qty: Quantity, pub leaves_qty: Quantity, pub avg_px: Option, diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index a0d730247794..6e13eea38528 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -82,7 +82,7 @@ impl LimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -314,8 +314,8 @@ impl Order for LimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index c3e5e84c44f3..cd14d112ee6c 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -83,7 +83,7 @@ impl LimitIfTouchedOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -301,8 +301,8 @@ impl Order for LimitIfTouchedOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 80b08dbe7e55..d5756c918a56 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -73,7 +73,7 @@ impl MarketOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> anyhow::Result { check_quantity_positive(quantity)?; if time_in_force == TimeInForce::Gtd { @@ -291,8 +291,8 @@ impl Order for MarketOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index 36537180a935..1078f9dc0c1f 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -79,7 +79,7 @@ impl MarketIfTouchedOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -295,8 +295,8 @@ impl Order for MarketIfTouchedOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index 06c3d79b66bd..77a4af948bae 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -74,7 +74,7 @@ impl MarketToLimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -287,8 +287,8 @@ impl Order for MarketToLimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index 579a3280d9c5..2ddc7dd28a88 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -84,7 +84,7 @@ impl StopLimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -308,8 +308,8 @@ impl Order for StopLimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { @@ -439,9 +439,9 @@ impl Display for StopLimitOrder { self.time_in_force, self.status, self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}") ), - self.position_id.map_or_else(|| "None".to_string(), |position_id| format!("{position_id}")), - self.tags.map_or_else(|| "None".to_string(), |tags| format!("{tags}")) + self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.position_id.map_or("None".to_string(), |position_id| format!("{position_id}")), + self.tags.clone().map_or("None".to_string(), |tags| tags.iter().map(|s| s.to_string()).collect::>().join(", ")), ) } } diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index 32d43cc69b3a..53ed886dd99f 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -80,7 +80,7 @@ impl StopMarketOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -296,8 +296,8 @@ impl Order for StopMarketOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index 451be0fee930..ec80b3ca8363 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -89,7 +89,7 @@ impl TrailingStopLimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -310,8 +310,8 @@ impl Order for TrailingStopLimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index 8862a2242c09..62bd75207cbd 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -84,7 +84,7 @@ impl TrailingStopMarketOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { @@ -302,8 +302,8 @@ impl Order for TrailingStopMarketOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags + fn tags(&self) -> Option> { + self.tags.clone() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/python/events/order/initialized.rs b/nautilus_core/model/src/python/events/order/initialized.rs index 323fdaa83d50..9f6285b40c05 100644 --- a/nautilus_core/model/src/python/events/order/initialized.rs +++ b/nautilus_core/model/src/python/events/order/initialized.rs @@ -76,7 +76,7 @@ impl OrderInitialized { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { Self::new( trader_id, @@ -111,7 +111,7 @@ impl OrderInitialized { exec_algorithm_id, exec_algorithm_params.map(str_hashmap_to_ustr), exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.iter().map(|s| Ustr::from(&s)).collect()), ) .map_err(to_pyvalue_err) } @@ -201,9 +201,11 @@ impl OrderInitialized { .map_or("None".to_string(), |exec_spawn_id| format!( "{exec_spawn_id}" )), - self.tags - .as_ref() - .map_or("None".to_string(), |tags| format!("{tags}")), + self.tags.as_ref().map_or("None".to_string(), |tags| tags + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ")), self.event_id, self.ts_init ) @@ -338,7 +340,10 @@ impl OrderInitialized { None => dict.set_item("exec_spawn_id", py.None())?, } match &self.tags { - Some(tags) => dict.set_item("tags", tags.to_string())?, + Some(tags) => dict.set_item( + "tags", + tags.iter().map(|x| x.to_string()).collect::>(), + )?, None => dict.set_item("tags", py.None())?, } Ok(dict.into()) diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index 0c22d1627333..60d51278a819 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -70,7 +70,7 @@ impl LimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Ok(Self::new( @@ -96,7 +96,7 @@ impl LimitOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) @@ -329,8 +329,10 @@ impl LimitOrder { #[getter] #[pyo3(name = "tags")] - fn py_tags(&self) -> Option { - self.tags.map(|x| x.to_string()) + fn py_tags(&self) -> Option> { + self.tags + .clone() + .map(|vec| vec.iter().map(|s| s.to_string()).collect()) } #[getter] @@ -506,9 +508,9 @@ impl LimitOrder { })?; let tags = dict.get_item("tags").map(|x| { x.and_then(|inner| { - let extracted_str = inner.extract::<&str>(); + let extracted_str = inner.extract::>(); match extracted_str { - Ok(item) => Some(Ustr::from(item)), + Ok(item) => Some(item.iter().map(|s| Ustr::from(&s)).collect()), Err(_) => None, } }) @@ -636,9 +638,14 @@ impl LimitOrder { || dict.set_item("exec_spawn_id", py.None()), |x| dict.set_item("exec_spawn_id", x.to_string()), )?; - self.tags.map_or_else( + self.tags.clone().map_or_else( || dict.set_item("tags", py.None()), - |x| dict.set_item("tags", x.to_string()), + |x| { + dict.set_item( + "tags", + x.iter().map(|x| x.to_string()).collect::>(), + ) + }, )?; self.account_id.map_or_else( || dict.set_item("account_id", py.None()), diff --git a/nautilus_core/model/src/python/orders/limit_if_touched.rs b/nautilus_core/model/src/python/orders/limit_if_touched.rs index 2dd5dc2ee300..8817efd30fa7 100644 --- a/nautilus_core/model/src/python/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/python/orders/limit_if_touched.rs @@ -62,7 +62,7 @@ impl LimitIfTouchedOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Ok(Self::new( @@ -90,7 +90,7 @@ impl LimitIfTouchedOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index 9f12849dfbcd..bc007759efa0 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -63,7 +63,7 @@ impl MarketOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Self::new( @@ -85,7 +85,7 @@ impl MarketOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), ) .map_err(to_pyvalue_err) } @@ -264,8 +264,10 @@ impl MarketOrder { #[getter] #[pyo3(name = "tags")] - fn py_tags(&self) -> Option { - self.tags.map(|x| x.to_string()) + fn py_tags(&self) -> Option> { + self.tags + .clone() + .map(|vec| vec.iter().map(|s| s.to_string()).collect()) } #[staticmethod] @@ -353,9 +355,14 @@ impl MarketOrder { || dict.set_item("exec_spawn_id", py.None()), |x| dict.set_item("exec_spawn_id", x.to_string()), )?; - self.tags.map_or_else( + self.tags.clone().map_or_else( || dict.set_item("tags", py.None()), - |x| dict.set_item("tags", x.to_string()), + |x| { + dict.set_item( + "tags", + x.iter().map(|x| x.to_string()).collect::>(), + ) + }, )?; self.account_id.map_or_else( || dict.set_item("account_id", py.None()), @@ -493,9 +500,9 @@ impl MarketOrder { })?; let tags = dict.get_item("tags").map(|x| { x.and_then(|inner| { - let extracted_str = inner.extract::<&str>(); + let extracted_str = inner.extract::>(); match extracted_str { - Ok(item) => Some(Ustr::from(item)), + Ok(item) => Some(item.iter().map(|s| Ustr::from(&s)).collect()), Err(_) => None, } }) diff --git a/nautilus_core/model/src/python/orders/market_if_touched.rs b/nautilus_core/model/src/python/orders/market_if_touched.rs index 5a1c0ddd6a96..29860e441720 100644 --- a/nautilus_core/model/src/python/orders/market_if_touched.rs +++ b/nautilus_core/model/src/python/orders/market_if_touched.rs @@ -60,7 +60,7 @@ impl MarketIfTouchedOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Ok(Self::new( @@ -86,7 +86,7 @@ impl MarketIfTouchedOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) diff --git a/nautilus_core/model/src/python/orders/market_to_limit.rs b/nautilus_core/model/src/python/orders/market_to_limit.rs index ac82db96672e..31a67dc70a53 100644 --- a/nautilus_core/model/src/python/orders/market_to_limit.rs +++ b/nautilus_core/model/src/python/orders/market_to_limit.rs @@ -57,7 +57,7 @@ impl MarketToLimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Ok(Self::new( @@ -80,7 +80,7 @@ impl MarketToLimitOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index 7ca513cda729..fb0e7eef76a7 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -66,7 +66,7 @@ impl StopLimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Self::new( @@ -94,7 +94,7 @@ impl StopLimitOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) @@ -341,8 +341,10 @@ impl StopLimitOrder { #[getter] #[pyo3(name = "tags")] - fn py_tags(&self) -> Option { - self.tags.map(|x| x.to_string()) + fn py_tags(&self) -> Option> { + self.tags + .clone() + .map(|vec| vec.iter().map(|s| s.to_string()).collect()) } #[pyo3(name = "to_dict")] @@ -452,7 +454,9 @@ impl StopLimitOrder { )?; dict.set_item( "tags", - self.tags.as_ref().map(std::string::ToString::to_string), + self.tags + .as_ref() + .map(|vec| vec.iter().map(|s| s.to_string()).collect::>()), )?; Ok(dict.into()) } @@ -610,9 +614,9 @@ impl StopLimitOrder { .unwrap(); let tags = dict.get_item("tags").map(|x| { x.and_then(|inner| { - let extracted_str = inner.extract::<&str>(); + let extracted_str = inner.extract::>(); match extracted_str { - Ok(item) => Some(Ustr::from(item)), + Ok(item) => Some(item.iter().map(|s| Ustr::from(&s)).collect()), Err(_) => None, } }) diff --git a/nautilus_core/model/src/python/orders/stop_market.rs b/nautilus_core/model/src/python/orders/stop_market.rs index 964463797590..fe62e22193c4 100644 --- a/nautilus_core/model/src/python/orders/stop_market.rs +++ b/nautilus_core/model/src/python/orders/stop_market.rs @@ -60,7 +60,7 @@ impl StopMarketOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Ok(Self::new( @@ -86,7 +86,7 @@ impl StopMarketOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) diff --git a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs index a117a0787dfc..ddcde7cded63 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs @@ -65,7 +65,7 @@ impl TrailingStopLimitOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Ok(Self::new( @@ -96,7 +96,7 @@ impl TrailingStopLimitOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) diff --git a/nautilus_core/model/src/python/orders/trailing_stop_market.rs b/nautilus_core/model/src/python/orders/trailing_stop_market.rs index 93e41bef8f77..5a0112b193a2 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_market.rs @@ -62,7 +62,7 @@ impl TrailingStopMarketOrder { exec_algorithm_id: Option, exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option>, ) -> PyResult { let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); Ok(Self::new( @@ -90,7 +90,7 @@ impl TrailingStopMarketOrder { exec_algorithm_id, exec_algorithm_params, exec_spawn_id, - tags.map(|s| Ustr::from(&s)), + tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()), init_id, ts_init.into(), ) diff --git a/nautilus_trader/common/factories.pxd b/nautilus_trader/common/factories.pxd index 17e005499670..cc1406404fc9 100644 --- a/nautilus_trader/common/factories.pxd +++ b/nautilus_trader/common/factories.pxd @@ -76,7 +76,7 @@ cdef class OrderFactory: bint quote_quantity=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef LimitOrder limit( @@ -95,7 +95,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef StopMarketOrder stop_market( @@ -113,7 +113,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef StopLimitOrder stop_limit( @@ -134,7 +134,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef MarketToLimitOrder market_to_limit( @@ -149,7 +149,7 @@ cdef class OrderFactory: Quantity display_qty=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef MarketIfTouchedOrder market_if_touched( @@ -167,7 +167,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef LimitIfTouchedOrder limit_if_touched( @@ -188,7 +188,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef TrailingStopMarketOrder trailing_stop_market( @@ -208,7 +208,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef TrailingStopLimitOrder trailing_stop_limit( @@ -232,7 +232,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id=*, ExecAlgorithmId exec_algorithm_id=*, dict exec_algorithm_params=*, - str tags=*, + list[str] tags=*, ) cpdef OrderList bracket( @@ -261,7 +261,7 @@ cdef class OrderFactory: dict entry_exec_algorithm_params=*, dict tp_exec_algorithm_params=*, dict sl_exec_algorithm_params=*, - str entry_tags=*, - str tp_tags=*, - str sl_tags=*, + list[str] entry_tags=*, + list[str] tp_tags=*, + list[str] sl_tags=*, ) diff --git a/nautilus_trader/common/factories.pyx b/nautilus_trader/common/factories.pyx index 469a126fdec8..347a589769a7 100644 --- a/nautilus_trader/common/factories.pyx +++ b/nautilus_trader/common/factories.pyx @@ -220,7 +220,7 @@ cdef class OrderFactory: bint quote_quantity = False, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``MARKET`` order. @@ -243,9 +243,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -298,7 +297,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``LIMIT`` order. @@ -333,9 +332,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -395,7 +393,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``STOP_MARKET`` conditional order. @@ -428,9 +426,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -494,7 +491,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``STOP_LIMIT`` conditional order. @@ -533,9 +530,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -598,7 +594,7 @@ cdef class OrderFactory: Quantity display_qty = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``MARKET`` order. @@ -625,9 +621,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -680,7 +675,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``MARKET_IF_TOUCHED`` (MIT) conditional order. @@ -713,9 +708,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -779,7 +773,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``LIMIT_IF_TOUCHED`` (LIT) conditional order. @@ -818,9 +812,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -888,7 +881,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``TRAILING_STOP_MARKET`` conditional order. @@ -924,9 +917,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -997,7 +989,7 @@ cdef class OrderFactory: InstrumentId trigger_instrument_id = None, ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, - str tags = None, + list[str] tags = None, ): """ Create a new ``TRAILING_STOP_LIMIT`` conditional order. @@ -1044,9 +1036,8 @@ cdef class OrderFactory: The execution algorithm ID for the order. exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Returns ------- @@ -1128,9 +1119,9 @@ cdef class OrderFactory: dict entry_exec_algorithm_params = None, dict tp_exec_algorithm_params = None, dict sl_exec_algorithm_params = None, - str entry_tags = "ENTRY", - str tp_tags = "TAKE_PROFIT", - str sl_tags = "STOP_LOSS", + list[str] entry_tags = None, + list[str] tp_tags = None, + list[str] sl_tags = None, ): """ Create a bracket order with optional entry of take-profit order types. @@ -1189,21 +1180,22 @@ cdef class OrderFactory: The execution algorithm parameters for the order. sl_exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. - entry_tags : str, default "ENTRY" - The custom user tags for the entry order. These are optional and can - contain any arbitrary delimiter if required. - tp_tags : str, default "TAKE_PROFIT" - The custom user tags for the take-profit order. These are optional and can - contain any arbitrary delimiter if required. - sl_tags : str, default "STOP_LOSS" - The custom user tags for the stop-loss order. These are optional and can - contain any arbitrary delimiter if required. + entry_tags : list[str], default ["ENTRY"] + The custom user tags for the entry order. + tp_tags : list[str], default ["TAKE_PROFIT"] + The custom user tags for the take-profit order. + sl_tags : list[str], default ["STOP_LOSS"] + The custom user tags for the stop-loss order. Returns ------- OrderList """ + entry_tags = entry_tags if entry_tags is not None else ["ENTRY"] + sl_tags = sl_tags if sl_tags is not None else ["STOP_LOSS"] + tp_tags = tp_tags if tp_tags is not None else ["TAKE_PROFIT"] + cdef OrderListId order_list_id = self._order_list_id_generator.generate() cdef ClientOrderId entry_client_order_id = self._order_id_generator.generate() cdef ClientOrderId sl_client_order_id = self._order_id_generator.generate() diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index d54d2050e984..ea41259eb7cf 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -938,7 +938,7 @@ class LimitOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ): ... @classmethod def create(cls, init: OrderInitialized) -> LimitOrder: ... @@ -1023,7 +1023,7 @@ class LimitIfTouchedOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ) -> None: ... @classmethod def create(cls, init: OrderInitialized) -> LimitIfTouchedOrder: ... @@ -1049,7 +1049,7 @@ class MarketOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ) -> None: ... @classmethod def create(cls, init: OrderInitialized) -> MarketOrder: ... @@ -1113,7 +1113,7 @@ class MarketToLimitOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ): ... @classmethod def create(cls, init: OrderInitialized) -> MarketToLimitOrder: ... @@ -1145,7 +1145,7 @@ class MarketIfTouchedOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ): ... @classmethod def create(cls, init: OrderInitialized) -> MarketIfTouchedOrder: ... @@ -1179,7 +1179,7 @@ class StopLimitOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ): ... @classmethod def create(cls, init: OrderInitialized) -> StopLimitOrder: ... @@ -1258,7 +1258,7 @@ class StopMarketOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ): ... @classmethod def create(cls, init: OrderInitialized) -> StopMarketOrder: ... @@ -1295,7 +1295,7 @@ class TrailingStopLimitOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ): ... @classmethod def create(cls, init: OrderInitialized) -> TrailingStopLimitOrder: ... @@ -1329,7 +1329,7 @@ class TrailingStopMarketOrder: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ): ... @classmethod def create(cls, init: OrderInitialized) -> TrailingStopMarketOrder: ... @@ -1966,7 +1966,7 @@ class OrderInitialized: exec_algorithm_id: ExecAlgorithmId | None = None, exec_algorithm_params: dict[str, str] | None = None, exec_spawn_id: ClientOrderId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ) -> None: ... @classmethod def from_dict(cls, values: dict[str, str]) -> OrderInitialized: ... diff --git a/nautilus_trader/execution/algorithm.pxd b/nautilus_trader/execution/algorithm.pxd index 61ccccfc21c9..d42361e811b8 100644 --- a/nautilus_trader/execution/algorithm.pxd +++ b/nautilus_trader/execution/algorithm.pxd @@ -128,7 +128,7 @@ cdef class ExecAlgorithm(Actor): Quantity quantity, TimeInForce time_in_force=*, bint reduce_only=*, - str tags=*, + list[str] tags=*, bint reduce_primary=*, ) @@ -143,7 +143,7 @@ cdef class ExecAlgorithm(Actor): bint reduce_only=*, Quantity display_qty=*, TriggerType emulation_trigger=*, - str tags=*, + list[str] tags=*, bint reduce_primary=*, ) @@ -156,7 +156,7 @@ cdef class ExecAlgorithm(Actor): bint reduce_only=*, Quantity display_qty=*, TriggerType emulation_trigger=*, - str tags=*, + list[str] tags=*, bint reduce_primary=*, ) diff --git a/nautilus_trader/execution/algorithm.pyx b/nautilus_trader/execution/algorithm.pyx index 0021d12b1bec..0f654ef4711c 100644 --- a/nautilus_trader/execution/algorithm.pyx +++ b/nautilus_trader/execution/algorithm.pyx @@ -785,7 +785,7 @@ cdef class ExecAlgorithm(Actor): Quantity quantity, TimeInForce time_in_force = TimeInForce.GTC, bint reduce_only = False, - str tags = None, + list[str] tags = None, bint reduce_primary = True, ): """ @@ -801,9 +801,8 @@ cdef class ExecAlgorithm(Actor): The spawned orders time in force. Often not applicable for market orders. reduce_only : bool, default False If the spawned order carries the 'reduce-only' execution instruction. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. reduce_primary : bool, default True If the primary order quantity should be reduced by the given `quantity`. @@ -859,7 +858,7 @@ cdef class ExecAlgorithm(Actor): bint reduce_only = False, Quantity display_qty = None, TriggerType emulation_trigger = TriggerType.NO_TRIGGER, - str tags = None, + list[str] tags = None, bint reduce_primary = True, ): """ @@ -885,9 +884,8 @@ cdef class ExecAlgorithm(Actor): The quantity of the spawned order to display on the public book (iceberg). emulation_trigger : TriggerType, default ``NO_TRIGGER`` The spawned orders emulation trigger. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. reduce_primary : bool, default True If the primary order quantity should be reduced by the given `quantity`. @@ -948,7 +946,7 @@ cdef class ExecAlgorithm(Actor): bint reduce_only = False, Quantity display_qty = None, TriggerType emulation_trigger = TriggerType.NO_TRIGGER, - str tags = None, + list[str] tags = None, bint reduce_primary = True, ): """ @@ -970,9 +968,8 @@ cdef class ExecAlgorithm(Actor): The quantity of the spawned order to display on the public book (iceberg). emulation_trigger : TriggerType, default ``NO_TRIGGER`` The spawned orders emulation trigger. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. reduce_primary : bool, default True If the primary order quantity should be reduced by the given `quantity`. diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index 183aa2db7c6e..0dac5bbcc654 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -870,7 +870,7 @@ def _generate_external_order(self, report: OrderStatusReport) -> Order | None: strategy_id = self.get_external_order_claim(report.instrument_id) if strategy_id is None: strategy_id = StrategyId("EXTERNAL") - tags = "EXTERNAL" + tags = ["EXTERNAL"] else: tags = None diff --git a/nautilus_trader/model/events/order.pxd b/nautilus_trader/model/events/order.pxd index 333ced2c20f7..b4b6e814ca06 100644 --- a/nautilus_trader/model/events/order.pxd +++ b/nautilus_trader/model/events/order.pxd @@ -93,8 +93,8 @@ cdef class OrderInitialized(OrderEvent): """The execution algorithm parameters for the order.\n\n:returns: `dict[str, Any]` or ``None``""" cdef readonly ClientOrderId exec_spawn_id """The execution algorithm spawning client order ID.\n\n:returns: `ClientOrderId` or ``None``""" - cdef readonly str tags - """The order custom user tags.\n\n:returns: `str` or ``None``""" + cdef readonly list[str] tags + """The order custom user tags.\n\n:returns: `list[str]` or ``None``""" @staticmethod cdef OrderInitialized from_dict_c(dict values) diff --git a/nautilus_trader/model/events/order.pyx b/nautilus_trader/model/events/order.pyx index 2db2542bcc1f..94511c633f20 100644 --- a/nautilus_trader/model/events/order.pyx +++ b/nautilus_trader/model/events/order.pyx @@ -248,9 +248,8 @@ cdef class OrderInitialized(OrderEvent): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional with no default so ``None`` must be passed explicitly The execution algorithm spawning primary client order ID. - tags : str, optional with no default so ``None`` must be passed explicitly - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional with no default so ``None`` must be passed explicitly + The custom user tags for the order. event_id : UUID4 The event ID. ts_init : uint64_t @@ -279,17 +278,17 @@ cdef class OrderInitialized(OrderEvent): bint post_only, bint reduce_only, bint quote_quantity, - dict options not None, + dict[str, object] options not None, TriggerType emulation_trigger, InstrumentId trigger_instrument_id: InstrumentId | None, ContingencyType contingency_type, OrderListId order_list_id: OrderListId | None, - list linked_order_ids: list[ClientOrderId] | None, + list[ClientOrderId] linked_order_ids: list[ClientOrderId] | None, ClientOrderId parent_order_id: ClientOrderId | None, ExecAlgorithmId exec_algorithm_id: ExecAlgorithmId | None, - dict exec_algorithm_params: dict[str, Any] | None, + dict[str, object] exec_algorithm_params: dict[str, object] | None, ClientOrderId exec_spawn_id: ClientOrderId | None, - str tags: str | None, + list[str] tags: list[str] | None, UUID4 event_id not None, uint64_t ts_init, bint reconciliation=False, @@ -526,6 +525,7 @@ cdef class OrderInitialized(OrderEvent): cdef str parent_order_id_str = values["parent_order_id"] cdef str exec_algorithm_id_str = values["exec_algorithm_id"] cdef str exec_spawn_id_str = values["exec_spawn_id"] + tags = values["tags"] return OrderInitialized( trader_id=TraderId(values["trader_id"]), strategy_id=StrategyId(values["strategy_id"]), @@ -548,7 +548,7 @@ cdef class OrderInitialized(OrderEvent): exec_algorithm_id=ExecAlgorithmId(exec_algorithm_id_str) if exec_algorithm_id_str is not None else None, exec_algorithm_params=values["exec_algorithm_params"], exec_spawn_id=ClientOrderId(exec_spawn_id_str) if exec_spawn_id_str is not None else None, - tags=values["tags"], + tags=tags.split(",") if isinstance(tags, str) else tags, event_id=UUID4(values["event_id"]), ts_init=values["ts_init"], reconciliation=values.get("reconciliation", False), diff --git a/nautilus_trader/model/orders/base.pxd b/nautilus_trader/model/orders/base.pxd index a550fbfd5d18..b10f3a6018ab 100644 --- a/nautilus_trader/model/orders/base.pxd +++ b/nautilus_trader/model/orders/base.pxd @@ -122,8 +122,8 @@ cdef class Order: """The execution algorithm parameters for the order.\n\n:returns: `dict[str, Any]` or ``None``""" cdef readonly ClientOrderId exec_spawn_id """The execution algorithm spawning client order ID.\n\n:returns: `ClientOrderId` or ``None``""" - cdef readonly str tags - """The order custom user tags.\n\n:returns: `str` or ``None``""" + cdef readonly list[str] tags + """The order custom user tags.\n\n:returns: `list[str]` or ``None``""" cdef readonly UUID4 init_id """The event ID of the `OrderInitialized` event.\n\n:returns: `UUID4`""" cdef readonly uint64_t ts_init diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 5b309f32852d..38fff7506ff8 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -108,9 +108,8 @@ cdef class LimitOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -154,7 +153,7 @@ cdef class LimitOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") if time_in_force == TimeInForce.GTD: diff --git a/nautilus_trader/model/orders/limit_if_touched.pyx b/nautilus_trader/model/orders/limit_if_touched.pyx index dddbf7c6a6ac..7a301b2e2281 100644 --- a/nautilus_trader/model/orders/limit_if_touched.pyx +++ b/nautilus_trader/model/orders/limit_if_touched.pyx @@ -112,9 +112,8 @@ cdef class LimitIfTouchedOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -164,7 +163,7 @@ cdef class LimitIfTouchedOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(trigger_type, TriggerType.NO_TRIGGER, "trigger_type", "NO_TRIGGER") diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index 48f03e1b43ea..5cbed929bf04 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -94,9 +94,8 @@ cdef class MarketOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -132,7 +131,7 @@ cdef class MarketOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(time_in_force, TimeInForce.GTD, "time_in_force", "GTD") diff --git a/nautilus_trader/model/orders/market_if_touched.pyx b/nautilus_trader/model/orders/market_if_touched.pyx index 72569dfe5ac6..3641563975cf 100644 --- a/nautilus_trader/model/orders/market_if_touched.pyx +++ b/nautilus_trader/model/orders/market_if_touched.pyx @@ -102,9 +102,8 @@ cdef class MarketIfTouchedOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -149,7 +148,7 @@ cdef class MarketIfTouchedOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(trigger_type, TriggerType.NO_TRIGGER, "trigger_type", "NO_TRIGGER") diff --git a/nautilus_trader/model/orders/market_to_limit.pyx b/nautilus_trader/model/orders/market_to_limit.pyx index ff290b6d80b5..7a5c51e5447c 100644 --- a/nautilus_trader/model/orders/market_to_limit.pyx +++ b/nautilus_trader/model/orders/market_to_limit.pyx @@ -93,9 +93,8 @@ cdef class MarketToLimitOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -133,7 +132,7 @@ cdef class MarketToLimitOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(time_in_force, TimeInForce.AT_THE_OPEN, "time_in_force", "AT_THE_OPEN`") diff --git a/nautilus_trader/model/orders/stop_limit.pyx b/nautilus_trader/model/orders/stop_limit.pyx index 07c56a46d393..d518b635faa8 100644 --- a/nautilus_trader/model/orders/stop_limit.pyx +++ b/nautilus_trader/model/orders/stop_limit.pyx @@ -117,9 +117,8 @@ cdef class StopLimitOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -169,7 +168,7 @@ cdef class StopLimitOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(trigger_type, TriggerType.NO_TRIGGER, "trigger_type", "NO_TRIGGER") diff --git a/nautilus_trader/model/orders/stop_market.pyx b/nautilus_trader/model/orders/stop_market.pyx index 90ed019161e9..899df7a64349 100644 --- a/nautilus_trader/model/orders/stop_market.pyx +++ b/nautilus_trader/model/orders/stop_market.pyx @@ -107,9 +107,8 @@ cdef class StopMarketOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -154,7 +153,7 @@ cdef class StopMarketOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(trigger_type, TriggerType.NO_TRIGGER, "trigger_type", "NO_TRIGGER") diff --git a/nautilus_trader/model/orders/trailing_stop_limit.pyx b/nautilus_trader/model/orders/trailing_stop_limit.pyx index 6e5b9cbe0c23..3a86c210e916 100644 --- a/nautilus_trader/model/orders/trailing_stop_limit.pyx +++ b/nautilus_trader/model/orders/trailing_stop_limit.pyx @@ -115,9 +115,8 @@ cdef class TrailingStopLimitOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -168,7 +167,7 @@ cdef class TrailingStopLimitOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(trigger_type, TriggerType.NO_TRIGGER, "trigger_type", "NO_TRIGGER") diff --git a/nautilus_trader/model/orders/trailing_stop_market.pyx b/nautilus_trader/model/orders/trailing_stop_market.pyx index 330b471f3cea..f56793af84ca 100644 --- a/nautilus_trader/model/orders/trailing_stop_market.pyx +++ b/nautilus_trader/model/orders/trailing_stop_market.pyx @@ -105,9 +105,8 @@ cdef class TrailingStopMarketOrder(Order): The execution algorithm parameters for the order. exec_spawn_id : ClientOrderId, optional The execution algorithm spawning primary client order ID. - tags : str, optional - The custom user tags for the order. These are optional and can - contain any arbitrary delimiter if required. + tags : list[str], optional + The custom user tags for the order. Raises ------ @@ -152,7 +151,7 @@ cdef class TrailingStopMarketOrder(Order): ExecAlgorithmId exec_algorithm_id = None, dict exec_algorithm_params = None, ClientOrderId exec_spawn_id = None, - str tags = None, + list[str] tags = None, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NO_ORDER_SIDE") Condition.not_equal(trigger_type, TriggerType.NO_TRIGGER, "trigger_type", "NO_TRIGGER") diff --git a/nautilus_trader/test_kit/rust/events_pyo3.py b/nautilus_trader/test_kit/rust/events_pyo3.py index 3faec3fdfc16..627f57774091 100644 --- a/nautilus_trader/test_kit/rust/events_pyo3.py +++ b/nautilus_trader/test_kit/rust/events_pyo3.py @@ -246,7 +246,7 @@ def order_initialized() -> OrderInitialized: exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, - tags="ENTRY", + tags=["ENTRY"], ts_init=0, ts_event=0, ) diff --git a/nautilus_trader/test_kit/rust/orders_pyo3.py b/nautilus_trader/test_kit/rust/orders_pyo3.py index d6866a8ec1c6..e056bf24a132 100644 --- a/nautilus_trader/test_kit/rust/orders_pyo3.py +++ b/nautilus_trader/test_kit/rust/orders_pyo3.py @@ -97,7 +97,7 @@ def stop_limit_order( client_order_id: ClientOrderId | None = None, time_in_force: TimeInForce | None = None, exec_algorithm_id: ExecAlgorithmId | None = None, - tags: str | None = None, + tags: list[str] | None = None, ) -> StopLimitOrder: return StopLimitOrder( trader_id=trader_id or TestIdProviderPyo3.trader_id(), diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index 74526e3cba13..61c7c49a807e 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -144,8 +144,8 @@ cdef class Strategy(Actor): cpdef void cancel_order(self, Order order, ClientId client_id=*) cpdef void cancel_orders(self, list orders, ClientId client_id=*) cpdef void cancel_all_orders(self, InstrumentId instrument_id, OrderSide order_side=*, ClientId client_id=*) - cpdef void close_position(self, Position position, ClientId client_id=*, str tags=*) - cpdef void close_all_positions(self, InstrumentId instrument_id, PositionSide position_side=*, ClientId client_id=*, str tags=*) + cpdef void close_position(self, Position position, ClientId client_id=*, list[str] tags=*) + cpdef void close_all_positions(self, InstrumentId instrument_id, PositionSide position_side=*, ClientId client_id=*, list[str] tags=*) cpdef void query_order(self, Order order, ClientId client_id=*) cdef ModifyOrder _create_modify_order( self, diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index 099c4a6534af..bacaf75423b5 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -1189,7 +1189,7 @@ cdef class Strategy(Actor): self, Position position, ClientId client_id = None, - str tags = None, + list[str] tags = None, ): """ Close the given position. @@ -1204,7 +1204,7 @@ cdef class Strategy(Actor): client_id : ClientId, optional The specific client ID for the command. If ``None`` then will be inferred from the venue in the instrument ID. - tags : str, optional + tags : list[str], optional The tags for the market order closing the position. """ @@ -1240,7 +1240,7 @@ cdef class Strategy(Actor): InstrumentId instrument_id, PositionSide position_side = PositionSide.NO_POSITION_SIDE, ClientId client_id = None, - str tags = None, + list[str] tags = None, ): """ Close all positions for the given instrument ID for this strategy. @@ -1254,7 +1254,7 @@ cdef class Strategy(Actor): client_id : ClientId, optional The specific client ID for the command. If ``None`` then will be inferred from the venue in the instrument ID. - tags : str, optional + tags : list[str], optional The tags for the market orders closing the positions. """ diff --git a/tests/unit_tests/execution/test_algorithm.py b/tests/unit_tests/execution/test_algorithm.py index 5f2468fe1a7c..1101c440d240 100644 --- a/tests/unit_tests/execution/test_algorithm.py +++ b/tests/unit_tests/execution/test_algorithm.py @@ -234,7 +234,7 @@ def test_exec_algorithm_spawn_market_order_with_quantity_too_high(self) -> None: quantity=ETHUSDT_PERP_BINANCE.make_qty(Decimal("2")), # <-- Greater than primary time_in_force=TimeInForce.FOK, reduce_only=True, - tags="EXIT", + tags=["EXIT"], ) def test_exec_algorithm_spawn_market_order(self) -> None: @@ -268,7 +268,7 @@ def test_exec_algorithm_spawn_market_order(self) -> None: quantity=ETHUSDT_PERP_BINANCE.make_qty(spawned_qty), time_in_force=TimeInForce.FOK, reduce_only=True, - tags="EXIT", + tags=["EXIT"], ) # Assert @@ -280,7 +280,7 @@ def test_exec_algorithm_spawn_market_order(self) -> None: assert spawned_order.quantity == spawned_qty assert spawned_order.time_in_force == TimeInForce.FOK assert spawned_order.is_reduce_only - assert spawned_order.tags == "EXIT" + assert spawned_order.tags == ["EXIT"] def test_exec_algorithm_spawn_limit_order(self) -> None: """ @@ -315,7 +315,7 @@ def test_exec_algorithm_spawn_limit_order(self) -> None: price=ETHUSDT_PERP_BINANCE.make_price(Decimal("5000.25")), time_in_force=TimeInForce.DAY, reduce_only=False, - tags="ENTRY", + tags=["ENTRY"], ) # Assert @@ -327,7 +327,7 @@ def test_exec_algorithm_spawn_limit_order(self) -> None: assert spawned_order.quantity == spawned_qty assert spawned_order.time_in_force == TimeInForce.DAY assert not spawned_order.is_reduce_only - assert spawned_order.tags == "ENTRY" + assert spawned_order.tags == ["ENTRY"] assert primary_order.is_primary assert not primary_order.is_spawned assert not spawned_order.is_primary @@ -366,7 +366,7 @@ def test_exec_algorithm_spawn_market_to_limit_order(self) -> None: time_in_force=TimeInForce.GTD, expire_time=UNIX_EPOCH + timedelta(minutes=60), reduce_only=False, - tags="ENTRY", + tags=["ENTRY"], ) # Assert @@ -379,7 +379,7 @@ def test_exec_algorithm_spawn_market_to_limit_order(self) -> None: assert spawned_order.time_in_force == TimeInForce.GTD assert spawned_order.expire_time_ns == 3_600_000_000_000 assert not spawned_order.is_reduce_only - assert spawned_order.tags == "ENTRY" + assert spawned_order.tags == ["ENTRY"] def test_exec_algorithm_modify_order_in_place(self) -> None: """ @@ -413,7 +413,7 @@ def test_exec_algorithm_modify_order_in_place(self) -> None: price=ETHUSDT_PERP_BINANCE.make_price(Decimal("5000.25")), time_in_force=TimeInForce.DAY, reduce_only=False, - tags="ENTRY", + tags=["ENTRY"], ) new_price = ETHUSDT_PERP_BINANCE.make_price(Decimal("5001.0")) diff --git a/tests/unit_tests/execution/test_messages.py b/tests/unit_tests/execution/test_messages.py index f842d28e3a45..9fe5dc0344eb 100644 --- a/tests/unit_tests/execution/test_messages.py +++ b/tests/unit_tests/execution/test_messages.py @@ -126,9 +126,9 @@ def test_submit_bracket_order_command_to_from_dict_and_str_repr(self): quantity=Quantity.from_int(100_000), sl_trigger_price=Price.from_str("1.00000"), tp_price=Price.from_str("1.00100"), - entry_tags="ENTRY", - tp_tags="TAKE_PROFIT", - sl_tags="STOP_LOSS", + entry_tags=["ENTRY"], + tp_tags=["TAKE_PROFIT"], + sl_tags=["STOP_LOSS"], ) command = SubmitOrderList( @@ -145,11 +145,11 @@ def test_submit_bracket_order_command_to_from_dict_and_str_repr(self): assert SubmitOrderList.from_dict(SubmitOrderList.to_dict(command)) == command assert ( str(command) - == "SubmitOrderList(order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=ENTRY), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=STOP_LOSS), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=TAKE_PROFIT)]), position_id=P-001)" # noqa + == "SubmitOrderList(order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=['ENTRY']), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=['STOP_LOSS']), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=['TAKE_PROFIT'])]), position_id=P-001)" # noqa ) assert ( repr(command) - == f"SubmitOrderList(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=ENTRY), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=STOP_LOSS), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=TAKE_PROFIT)]), position_id=P-001, command_id={uuid}, ts_init=0)" # noqa + == f"SubmitOrderList(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=['ENTRY']), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=['STOP_LOSS']), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=['TAKE_PROFIT'])]), position_id=P-001, command_id={uuid}, ts_init=0)" # noqa ) def test_submit_bracket_order_command_with_exec_algorithm_to_from_dict_and_str_repr(self): @@ -162,9 +162,9 @@ def test_submit_bracket_order_command_with_exec_algorithm_to_from_dict_and_str_r quantity=Quantity.from_int(100_000), sl_trigger_price=Price.from_str("1.00000"), tp_price=Price.from_str("1.00100"), - entry_tags="ENTRY", - tp_tags="TAKE_PROFIT", - sl_tags="STOP_LOSS", + entry_tags=["ENTRY"], + tp_tags=["TAKE_PROFIT"], + sl_tags=["STOP_LOSS"], ) command = SubmitOrderList( @@ -180,11 +180,11 @@ def test_submit_bracket_order_command_with_exec_algorithm_to_from_dict_and_str_r assert SubmitOrderList.from_dict(SubmitOrderList.to_dict(command)) == command assert ( str(command) - == "SubmitOrderList(order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=ENTRY), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=STOP_LOSS), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=TAKE_PROFIT)]), position_id=P-001)" # noqa + == "SubmitOrderList(order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=['ENTRY']), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=['STOP_LOSS']), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=['TAKE_PROFIT'])]), position_id=P-001)" # noqa ) assert ( repr(command) - == f"SubmitOrderList(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=ENTRY), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=STOP_LOSS), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=TAKE_PROFIT)]), position_id=P-001, command_id={uuid}, ts_init=0)" # noqa + == f"SubmitOrderList(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, order_list=OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=['ENTRY']), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 1.00000[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=['STOP_LOSS']), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00100 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=['TAKE_PROFIT'])]), position_id=P-001, command_id={uuid}, ts_init=0)" # noqa ) def test_modify_order_command_to_from_dict_and_str_repr(self): diff --git a/tests/unit_tests/model/orders/test_stop_limit_order_pyo3.py b/tests/unit_tests/model/orders/test_stop_limit_order_pyo3.py index b3788a44832b..8c174f04886c 100644 --- a/tests/unit_tests/model/orders/test_stop_limit_order_pyo3.py +++ b/tests/unit_tests/model/orders/test_stop_limit_order_pyo3.py @@ -33,7 +33,7 @@ quantity=Quantity.from_int(100_000), price=Price.from_str("1.00000"), trigger_price=Price.from_str("1.10010"), - tags="ENTRY", + tags=["ENTRY"], ) diff --git a/tests/unit_tests/model/test_events.py b/tests/unit_tests/model/test_events.py index 918149d570dc..9621b02da487 100644 --- a/tests/unit_tests/model/test_events.py +++ b/tests/unit_tests/model/test_events.py @@ -165,7 +165,7 @@ def test_order_initialized_event_to_from_dict_and_str_repr(self): exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, - tags="ENTRY", + tags=["ENTRY"], event_id=uuid, ts_init=0, ) @@ -174,11 +174,11 @@ def test_order_initialized_event_to_from_dict_and_str_repr(self): assert OrderInitialized.from_dict(OrderInitialized.to_dict(event)) == event assert ( str(event) - == f"OrderInitialized(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, side=BUY, type=LIMIT, quantity=0.561000, time_in_force=DAY, post_only=True, reduce_only=True, quote_quantity=False, options={{'price': '15200.10'}}, emulation_trigger=BID_ASK, trigger_instrument_id=USD/JPY.SIM, contingency_type=OTO, order_list_id=1, linked_order_ids=['O-2020872378424'], parent_order_id=None, exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, tags=ENTRY)" # noqa + == f"OrderInitialized(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, side=BUY, type=LIMIT, quantity=0.561000, time_in_force=DAY, post_only=True, reduce_only=True, quote_quantity=False, options={{'price': '15200.10'}}, emulation_trigger=BID_ASK, trigger_instrument_id=USD/JPY.SIM, contingency_type=OTO, order_list_id=1, linked_order_ids=['O-2020872378424'], parent_order_id=None, exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, tags=['ENTRY'])" # noqa ) assert ( repr(event) - == f"OrderInitialized(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, side=BUY, type=LIMIT, quantity=0.561000, time_in_force=DAY, post_only=True, reduce_only=True, quote_quantity=False, options={{'price': '15200.10'}}, emulation_trigger=BID_ASK, trigger_instrument_id=USD/JPY.SIM, contingency_type=OTO, order_list_id=1, linked_order_ids=['O-2020872378424'], parent_order_id=None, exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, tags=ENTRY, event_id={uuid}, ts_init=0)" # noqa + == f"OrderInitialized(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, side=BUY, type=LIMIT, quantity=0.561000, time_in_force=DAY, post_only=True, reduce_only=True, quote_quantity=False, options={{'price': '15200.10'}}, emulation_trigger=BID_ASK, trigger_instrument_id=USD/JPY.SIM, contingency_type=OTO, order_list_id=1, linked_order_ids=['O-2020872378424'], parent_order_id=None, exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, tags=['ENTRY'], event_id={uuid}, ts_init=0)" # noqa ) def test_order_denied_event_to_from_dict_and_str_repr(self): diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index 6598a1df945b..c3b3a0f36568 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -360,18 +360,18 @@ def test_order_hash_str_and_repr(self): AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), - tags="ENTRY", + tags=["ENTRY"], ) # Act, Assert assert isinstance(hash(order), int) assert ( str(order) - == "MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=ENTRY)" # noqa + == "MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) assert ( repr(order) - == "MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=ENTRY)" # noqa + == "MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) def test_market_order_to_dict(self): @@ -635,7 +635,7 @@ def test_initialize_stop_limit_order(self): Quantity.from_int(100_000), Price.from_str("1.00000"), Price.from_str("1.10010"), - tags="ENTRY", + tags=["ENTRY"], ) # Assert @@ -651,11 +651,11 @@ def test_initialize_stop_limit_order(self): assert isinstance(order.init_event, OrderInitialized) assert ( str(order) - == "StopLimitOrder(BUY 100_000 AUD/USD.SIM STOP_LIMIT @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=ENTRY)" # noqa + == "StopLimitOrder(BUY 100_000 AUD/USD.SIM STOP_LIMIT @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) assert ( repr(order) - == "StopLimitOrder(BUY 100_000 AUD/USD.SIM STOP_LIMIT @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=ENTRY)" # noqa + == "StopLimitOrder(BUY 100_000 AUD/USD.SIM STOP_LIMIT @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) def test_stop_limit_order_to_dict(self): @@ -667,7 +667,7 @@ def test_stop_limit_order_to_dict(self): Price.from_str("1.00000"), Price.from_str("1.10010"), trigger_type=TriggerType.MARK_PRICE, - tags="STOP_LOSS", + tags=["STOP_LOSS"], ) # Act @@ -712,7 +712,7 @@ def test_stop_limit_order_to_dict(self): "order_list_id": None, "linked_order_ids": None, "parent_order_id": None, - "tags": "STOP_LOSS", + "tags": ["STOP_LOSS"], "ts_init": 0, "ts_last": 0, } @@ -890,7 +890,7 @@ def test_initialize_limit_if_touched_order(self): Price.from_str("1.00000"), Price.from_str("1.10010"), emulation_trigger=TriggerType.LAST_TRADE, - tags="ENTRY", + tags=["ENTRY"], ) # Assert @@ -908,11 +908,11 @@ def test_initialize_limit_if_touched_order(self): assert isinstance(order.init_event, OrderInitialized) assert ( str(order) - == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_TRADE], status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=ENTRY)" # noqa + == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_TRADE], status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) assert ( repr(order) - == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_TRADE], status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=ENTRY)" # noqa + == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_TRADE], status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) def test_limit_if_touched_order_to_dict(self): @@ -926,7 +926,7 @@ def test_limit_if_touched_order_to_dict(self): trigger_type=TriggerType.MARK_PRICE, emulation_trigger=TriggerType.LAST_TRADE, trigger_instrument_id=TestIdStubs.usdjpy_id(), - tags="STOP_LOSS", + tags=["STOP_LOSS"], ) # Act @@ -969,7 +969,7 @@ def test_limit_if_touched_order_to_dict(self): "order_list_id": None, "linked_order_ids": None, "parent_order_id": None, - "tags": "STOP_LOSS", + "tags": ["STOP_LOSS"], "ts_init": 0, "ts_last": 0, } @@ -1516,17 +1516,17 @@ def test_order_list_str_and_repr(self): Quantity.from_int(100_000), sl_trigger_price=Price.from_str("0.99990"), tp_price=Price.from_str("1.00010"), - entry_tags="ENTRY", - tp_tags="TAKE_PROFIT", - sl_tags="STOP_LOSS", + entry_tags=["ENTRY"], + tp_tags=["TAKE_PROFIT"], + sl_tags=["STOP_LOSS"], ) # Assert assert str(bracket) == ( - "OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=ENTRY), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 0.99990[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=STOP_LOSS), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00010 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=TAKE_PROFIT)])" # noqa + "OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=['ENTRY']), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 0.99990[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=['STOP_LOSS']), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00010 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=['TAKE_PROFIT'])])" # noqa ) assert repr(bracket) == ( - "OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=ENTRY), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 0.99990[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=STOP_LOSS), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00010 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=TAKE_PROFIT)])" # noqa + "OrderList(id=OL-19700101-0000-000-001-1, instrument_id=AUD/USD.SIM, strategy_id=S-001, orders=[MarketOrder(BUY 100_000 AUD/USD.SIM MARKET GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, contingency_type=OTO, linked_order_ids=[O-19700101-0000-000-001-2, O-19700101-0000-000-001-3], tags=['ENTRY']), StopMarketOrder(SELL 100_000 AUD/USD.SIM STOP_MARKET @ 0.99990[DEFAULT] GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-2, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-3], parent_order_id=O-19700101-0000-000-001-1, tags=['STOP_LOSS']), LimitOrder(SELL 100_000 AUD/USD.SIM LIMIT @ 1.00010 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-3, venue_order_id=None, position_id=None, contingency_type=OUO, linked_order_ids=[O-19700101-0000-000-001-2], parent_order_id=O-19700101-0000-000-001-1, tags=['TAKE_PROFIT'])])" # noqa ) def test_apply_order_denied_event(self): diff --git a/tests/unit_tests/serialization/test_msgpack.py b/tests/unit_tests/serialization/test_msgpack.py index 6b36f53308b4..f69538d8ed46 100644 --- a/tests/unit_tests/serialization/test_msgpack.py +++ b/tests/unit_tests/serialization/test_msgpack.py @@ -146,7 +146,7 @@ def test_pack_and_unpack_market_orders(self): order = self.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), ) # Act @@ -161,10 +161,10 @@ def test_pack_and_unpack_limit_orders(self): order = self.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), Price(1.00000, precision=5), TimeInForce.DAY, - display_qty=Quantity(50000, precision=0), + display_qty=Quantity(50_000, precision=0), ) # Act @@ -182,7 +182,7 @@ def test_pack_and_unpack_limit_orders_with_expiration(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), price=Price(1.00000, precision=5), time_in_force=TimeInForce.GTD, expire_time_ns=1_000_000_000 * 60, @@ -205,7 +205,7 @@ def test_pack_and_unpack_stop_market_orders(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), trigger_price=Price(1.00000, precision=5), trigger_type=TriggerType.DEFAULT, time_in_force=TimeInForce.GTC, @@ -229,7 +229,7 @@ def test_pack_and_unpack_stop_market_orders_with_expiration(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), trigger_price=Price(1.00000, precision=5), trigger_type=TriggerType.DEFAULT, time_in_force=TimeInForce.GTD, @@ -253,7 +253,7 @@ def test_pack_and_unpack_stop_limit_orders(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), price=Price(1.00000, precision=5), trigger_price=Price(1.00010, precision=5), trigger_type=TriggerType.BID_ASK, @@ -278,7 +278,7 @@ def test_pack_and_unpack_market_to_limit__orders(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), time_in_force=TimeInForce.GTD, # <-- invalid expire_time_ns=1_000_000_000 * 60, init_id=UUID4(), @@ -300,7 +300,7 @@ def test_pack_and_unpack_market_if_touched_orders(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), trigger_price=Price(1.00000, precision=5), trigger_type=TriggerType.DEFAULT, time_in_force=TimeInForce.GTD, @@ -324,7 +324,7 @@ def test_pack_and_unpack_limit_if_touched_orders(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), price=Price(1.00000, precision=5), trigger_price=Price(1.00010, precision=5), trigger_type=TriggerType.BID_ASK, @@ -349,7 +349,7 @@ def test_pack_and_unpack_stop_limit_orders_with_expiration(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), price=Price(1.00000, precision=5), trigger_price=Price(1.00010, precision=5), trigger_type=TriggerType.LAST_TRADE, @@ -374,7 +374,7 @@ def test_pack_and_unpack_trailing_stop_market_orders_with_expiration(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), trigger_price=Price(1.00000, precision=5), trigger_type=TriggerType.DEFAULT, trailing_offset=Decimal("0.00010"), @@ -400,7 +400,7 @@ def test_pack_and_unpack_trailing_stop_market_orders_no_initial_prices(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), trigger_price=None, trigger_type=TriggerType.DEFAULT, trailing_offset=Decimal("0.00010"), @@ -426,7 +426,7 @@ def test_pack_and_unpack_trailing_stop_limit_orders_with_expiration(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), price=Price(1.00000, precision=5), trigger_price=Price(1.00010, precision=5), trigger_type=TriggerType.MARK_PRICE, @@ -454,7 +454,7 @@ def test_pack_and_unpack_trailing_stop_limit_orders_with_no_initial_prices(self) AUDUSD_SIM.id, ClientOrderId("O-123456"), OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), price=None, trigger_price=None, trigger_type=TriggerType.MARK_PRICE, @@ -479,7 +479,7 @@ def test_serialize_and_deserialize_submit_order_commands(self): order = self.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), exec_algorithm_id=ExecAlgorithmId("VWAP"), ) @@ -510,7 +510,7 @@ def test_serialize_and_deserialize_submit_order_list_commands(self): bracket = self.order_factory.bracket( AUDUSD_SIM.id, OrderSide.BUY, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), sl_trigger_price=Price(0.99900, precision=5), tp_price=Price(1.00010, precision=5), ) @@ -543,7 +543,7 @@ def test_serialize_and_deserialize_modify_order_commands(self): AUDUSD_SIM.id, ClientOrderId("O-123456"), VenueOrderId("001"), - Quantity(100000, precision=0), + Quantity(100_000, precision=0), Price(1.00001, precision=5), None, UUID4(), @@ -610,15 +610,15 @@ def test_serialize_and_deserialize_account_state_with_base_currency_events(self) reported=True, balances=[ AccountBalance( - Money(1525000, USD), - Money(25000, USD), - Money(1500000, USD), + Money(1_525_000, USD), + Money(25_000, USD), + Money(1_500_000, USD), ), ], margins=[ MarginBalance( Money(5000, USD), - Money(20000, USD), + Money(20_000, USD), AUDUSD_SIM.id, ), ], @@ -672,7 +672,7 @@ def test_serialize_and_deserialize_market_order_initialized_events(self): ClientOrderId("O-123456"), OrderSide.SELL, OrderType.MARKET, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), TimeInForce.FOK, post_only=False, reduce_only=True, @@ -687,7 +687,7 @@ def test_serialize_and_deserialize_market_order_initialized_events(self): exec_algorithm_id=ExecAlgorithmId("VWAP"), exec_algorithm_params={"period": 60}, exec_spawn_id=ClientOrderId("O-1"), - tags="ENTRY", + tags=["ENTRY"], event_id=UUID4(), ts_init=0, ) @@ -713,7 +713,7 @@ def test_serialize_and_deserialize_limit_order_initialized_events(self): ClientOrderId("O-123456"), OrderSide.SELL, OrderType.LIMIT, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), TimeInForce.DAY, post_only=True, reduce_only=False, @@ -754,7 +754,7 @@ def test_serialize_and_deserialize_stop_market_order_initialized_events(self): ClientOrderId("O-123456"), OrderSide.SELL, OrderType.STOP_MARKET, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), TimeInForce.DAY, post_only=False, reduce_only=True, @@ -797,7 +797,7 @@ def test_serialize_and_deserialize_stop_limit_order_initialized_events(self): ClientOrderId("O-123456"), OrderSide.SELL, OrderType.STOP_LIMIT, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), TimeInForce.DAY, post_only=True, reduce_only=True, @@ -812,7 +812,7 @@ def test_serialize_and_deserialize_stop_limit_order_initialized_events(self): exec_algorithm_id=ExecAlgorithmId("VWAP"), exec_algorithm_params={"period": 60}, exec_spawn_id=ClientOrderId("O-1"), - tags="entry,bulk", + tags=["entry", "bulk"], event_id=UUID4(), ts_init=0, ) @@ -824,7 +824,7 @@ def test_serialize_and_deserialize_stop_limit_order_initialized_events(self): # Assert assert deserialized == event assert deserialized.options == options - assert deserialized.tags == "entry,bulk" + assert deserialized.tags == ["entry", "bulk"] def test_serialize_and_deserialize_order_denied_events(self): # Arrange @@ -1060,7 +1060,7 @@ def test_serialize_and_deserialize_order_modify_events(self): ClientOrderId("O-123456"), VenueOrderId("1"), self.account_id, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), Price(0.80010, precision=5), Price(0.80050, precision=5), UUID4(), @@ -1130,7 +1130,7 @@ def test_serialize_and_deserialize_order_partially_filled_events(self): PositionId("T123456"), OrderSide.SELL, OrderType.MARKET, - Quantity(50000, precision=0), + Quantity(50_000, precision=0), Price(1.00000, precision=5), AUDUSD_SIM.quote_currency, Money(0, USD), @@ -1160,7 +1160,7 @@ def test_serialize_and_deserialize_order_filled_events(self): PositionId("T123456"), OrderSide.SELL, OrderType.MARKET, - Quantity(100000, precision=0), + Quantity(100_000, precision=0), Price(1.00000, precision=5), AUDUSD_SIM.quote_currency, Money(0, USD), diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index 252e7b7648ca..bfe903a1a4fd 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -1601,7 +1601,7 @@ def test_close_position(self) -> None: position = self.cache.positions_open()[0] # Act - strategy.close_position(position, tags="EXIT") + strategy.close_position(position, tags=["EXIT"]) self.exchange.process(0) # Assert @@ -1610,7 +1610,7 @@ def test_close_position(self) -> None: orders = self.cache.orders(instrument_id=_USDJPY_SIM.id) for order in orders: if order.side == OrderSide.SELL: - assert order.tags == "EXIT" + assert order.tags == ["EXIT"] def test_close_all_positions(self) -> None: # Arrange @@ -1642,7 +1642,7 @@ def test_close_all_positions(self) -> None: self.exchange.process(0) # Act - strategy.close_all_positions(_USDJPY_SIM.id, tags="EXIT") + strategy.close_all_positions(_USDJPY_SIM.id, tags=["EXIT"]) self.exchange.process(0) # Assert @@ -1652,7 +1652,7 @@ def test_close_all_positions(self) -> None: orders = self.cache.orders(instrument_id=_USDJPY_SIM.id) for order in orders: if order.side == OrderSide.SELL: - assert order.tags == "EXIT" + assert order.tags == ["EXIT"] @pytest.mark.parametrize( ("contingency_type"), From 13a42b0b7580437e5590382e0f6eb4b4959315b9 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Mon, 22 Apr 2024 00:09:14 +0200 Subject: [PATCH 020/193] Add Nautilus CLI (#1602) --- .docker/docker-compose.yml | 48 ++++ Makefile | 9 +- docs/developer_guide/environment_setup.md | 90 ++++++++ nautilus_core/Cargo.lock | 130 ++++++++++- nautilus_core/Cargo.toml | 2 + nautilus_core/cli/Cargo.toml | 25 +++ nautilus_core/cli/src/bin/cli.rs | 30 +++ nautilus_core/cli/src/database/mod.rs | 16 ++ nautilus_core/cli/src/database/postgres.rs | 237 ++++++++++++++++++++ nautilus_core/cli/src/lib.rs | 29 +++ nautilus_core/cli/src/opt.rs | 63 ++++++ nautilus_core/common/build.rs | 1 + nautilus_core/core/build.rs | 1 + nautilus_core/infrastructure/src/sql/mod.rs | 7 +- nautilus_core/infrastructure/src/sql/pg.rs | 114 ++++++++++ nautilus_core/model/build.rs | 1 + schema/tables.sql | 130 ++++++++++- 17 files changed, 920 insertions(+), 13 deletions(-) create mode 100644 .docker/docker-compose.yml create mode 100644 nautilus_core/cli/Cargo.toml create mode 100644 nautilus_core/cli/src/bin/cli.rs create mode 100644 nautilus_core/cli/src/database/mod.rs create mode 100644 nautilus_core/cli/src/database/postgres.rs create mode 100644 nautilus_core/cli/src/lib.rs create mode 100644 nautilus_core/cli/src/opt.rs create mode 100644 nautilus_core/infrastructure/src/sql/pg.rs diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml new file mode 100644 index 000000000000..b8f93300508e --- /dev/null +++ b/.docker/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.5' + +services: + postgres: + container_name: nautilus-database + image: postgres + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-pass} + POSTGRES_DATABASE: ${POSTGRES_DATABASE:-postgres} + PGDATA: /data/postgres + volumes: + - nautilus-database:/data/postgres + ports: + - "5432:5432" + networks: + - nautilus-network + restart: unless-stopped + + pgadmin: + container_name: nautilus-pgadmin + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@mail.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + volumes: + - pgadmin:/root/.pgadmin + ports: + - "${PGADMIN_PORT:-5051}:80" + networks: + - nautilus-network + restart: unless-stopped + + redis: + container_name: nautilus-redis + image: redis + ports: + - 6379:6379 + restart: unless-stopped + networks: + - nautilus-network + +networks: + nautilus-network: + +volumes: + nautilus-database: + pgadmin: diff --git a/Makefile b/Makefile index 608156e68cb8..87c27bf3cedd 100644 --- a/Makefile +++ b/Makefile @@ -153,11 +153,8 @@ test-examples: install-talib: bash scripts/install-talib.sh -.PHONY: init-db -init-db: - (cd nautilus_core && cargo run --bin init-db) +.PHONY: install-cli +install-cli: + (cd nautilus_core && cargo install --path cli --bin nautilus) -.PHONY: drop-db -drop-db: - (cd nautilus_core && cargo run --bin drop-db) diff --git a/docs/developer_guide/environment_setup.md b/docs/developer_guide/environment_setup.md index 9238db8c1778..7a97b7a22b2a 100644 --- a/docs/developer_guide/environment_setup.md +++ b/docs/developer_guide/environment_setup.md @@ -32,3 +32,93 @@ Following any changes to `.pyx` or `.pxd` files, you can re-compile by running: or make build + +## Services +You can use `docker-compose.yml` file located in `.docker` directory +to bootstrap the Nautilus working environment. This will start the following services: + +```bash +docker-compose up -d +``` + +If you only want specific services running (like `postgres` for example), you can start them with command: + +```bash +docker-compose up -d postgres +``` + +Used services are: + +- `postgres` - Postgres database with root user `POSTRES_USER` which defaults to `postgres`, `POSTGRES_PASSWORD` which defaults to `pass` and `POSTGRES_DB` which defaults to `postgres` +- `redis` - Redis server +- `pgadmin` - PgAdmin4 for database management and administration + +> **Note:** Please use this as development environment only. For production, use a proper and more secure setup. + +After the services has been started, you must log in with `psql` cli to create `nautilus` Postgres database. +To do that you can run, and type `POSTGRES_PASSWORD` from docker service setup + +```bash +psql -h localhost -p 5432 -U postgres +``` + +After you have logged in as `postgres` administrator, run `CREATE DATABASE` command with target db name (we use `nautilus`): + +``` +psql (16.2, server 15.2 (Debian 15.2-1.pgdg110+1)) +Type "help" for help. + +postgres=# CREATE DATABASE nautilus; +CREATE DATABASE + +``` + +## Nautilus CLI Developer Guide + +## Introduction + +The Nautilus CLI is a command-line interface tool designed to interact +with the Nautilus Trader ecosystem. It provides commands for managing the Postgres database and other trading operations. + +> **Note:** The Nautilus CLI command is only supported on UNIX-like systems. + + +## Install + +You can install nautilus cli command with from Make file target, which will use `cargo install` under the hood. +And this command will install `nautilus` bin executable in your path if Rust `cargo` is properly configured. + +```bash +make install-cli +``` + +## Commands +You can run `nautilus --help` to inspect structure of CLI and groups of commands: + +### Database +These are commands related to the bootstrapping the Postgres database. +For that you work, you need to supply right connection configuration. You can do that through +command line arguments or `.env` file in the root directory or where the commands is being run. + +- `--host` arg or `POSTGRES_HOST` for database host +- `--port` arg or `POSTGRES_PORT` for database port +- `--user` arg or `POSTGRES_USER` for root administrator user to run command with (namely `postgres` root user here) +- `--password` arg or `POSTGRES_PASSWORD` for root administrator password +- `--database` arg or `POSTGRES_DATABASE` for both database **name and new user** that will have privileges of this database + ( if you provided `nautilus` as value, then new user will be created with name `nautilus` that will inherit the password from `POSTGRES_PASSWORD` + and `nautilus` database with be bootstrapped with this user as owner) + +Example of `.env` file + +``` +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USERNAME=postgres +POSTGRES_DATABASE=nautilus +POSTGRES_PASSWORD=pass +``` + +List of commands are: + +1. `nautilus database init` - it will bootstrap schema, roles and all sql files located in `schema` root directory (like `tables.sql`) +2. `nautilus database drop` - it will drop all tables, role and data in target Postgres database diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index a364c14075c4..fdb116dab972 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -93,12 +93,54 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.82" @@ -788,7 +830,7 @@ dependencies = [ "bitflags 1.3.2", "clap_lex 0.2.4", "indexmap 1.9.3", - "strsim", + "strsim 0.10.0", "termcolor", "textwrap", ] @@ -800,6 +842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -808,8 +851,22 @@ version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ + "anstream", "anstyle", "clap_lex 0.7.0", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.60", ] [[package]] @@ -827,6 +884,22 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -1066,7 +1139,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 2.0.60", ] @@ -2542,6 +2615,24 @@ dependencies = [ "ustr", ] +[[package]] +name = "nautilus-cli" +version = "0.22.0" +dependencies = [ + "anyhow", + "clap 4.5.4", + "clap_derive", + "dotenvy", + "log", + "nautilus-common", + "nautilus-core", + "nautilus-infrastructure", + "nautilus-model", + "simple_logger", + "sqlx", + "tokio", +] + [[package]] name = "nautilus-common" version = "0.22.0" @@ -2903,6 +2994,15 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -4142,6 +4242,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simple_logger" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.48.0", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -4481,6 +4593,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.26.2" @@ -4709,7 +4827,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -5160,6 +5280,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.8.0" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 9af3b16dcdeb..5c6c37ea814a 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -14,6 +14,7 @@ members = [ "network/tokio-tungstenite", "persistence", "pyo3", + "cli" ] [workspace.package] @@ -49,6 +50,7 @@ tracing = "0.1.40" tokio = { version = "1.37.0", features = ["full"] } ustr = { version = "1.0.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["v4"] } +sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio"] } # dev-dependencies criterion = "0.5.1" diff --git a/nautilus_core/cli/Cargo.toml b/nautilus_core/cli/Cargo.toml new file mode 100644 index 000000000000..941f013ea46e --- /dev/null +++ b/nautilus_core/cli/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nautilus-cli" +version.workspace = true +edition.workspace = true +authors.workspace = true +description.workspace = true +documentation.workspace = true + +[[bin]] +name = "nautilus" +path = "src/bin/cli.rs" + +[dependencies] +nautilus-common = { path = "../common"} +nautilus-model = { path = "../model" } +nautilus-core = { path = "../core" } +nautilus-infrastructure = { path = "../infrastructure" , features = ['sql']} +anyhow = { workspace = true } +tokio = {workspace = true} +log = { workspace = true } +clap = { version = "4.5.4", features = ["derive", "env"] } +clap_derive = { version = "4.5.4" } +dotenvy = { version = "0.15.7" } +sqlx = {workspace = true } +simple_logger = "4.3.3" diff --git a/nautilus_core/cli/src/bin/cli.rs b/nautilus_core/cli/src/bin/cli.rs new file mode 100644 index 000000000000..a7ceaf08953b --- /dev/null +++ b/nautilus_core/cli/src/bin/cli.rs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 clap::Parser; +use log::{error, LevelFilter}; +use nautilus_cli::opt::NautilusCli; + +#[tokio::main] +async fn main() { + dotenvy::dotenv().ok(); + simple_logger::SimpleLogger::new() + .with_module_level("sqlx", LevelFilter::Off) + .init() + .unwrap(); + if let Err(err) = nautilus_cli::run(NautilusCli::parse()).await { + error!("Error executing Nautilus CLI: {}", err); + } +} diff --git a/nautilus_core/cli/src/database/mod.rs b/nautilus_core/cli/src/database/mod.rs new file mode 100644 index 000000000000..337a8dbe66bd --- /dev/null +++ b/nautilus_core/cli/src/database/mod.rs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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. +// ------------------------------------------------------------------------------------------------- + +pub mod postgres; diff --git a/nautilus_core/cli/src/database/postgres.rs b/nautilus_core/cli/src/database/postgres.rs new file mode 100644 index 000000000000..89a7ff1a05f8 --- /dev/null +++ b/nautilus_core/cli/src/database/postgres.rs @@ -0,0 +1,237 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 log::{error, info}; +use nautilus_infrastructure::sql::pg::{connect_pg, get_postgres_connect_options}; +use sqlx::PgPool; + +use crate::opt::{DatabaseCommand, DatabaseOpt}; + +/// Scans current path with keyword nautilus_trader and build schema dir +fn get_schema_dir() -> anyhow::Result { + std::env::var("SCHEMA_DIR").or_else(|_| { + let nautilus_git_repo_name = "nautilus_trader"; + let binding = std::env::current_dir().unwrap(); + let current_dir = binding.to_str().unwrap(); + match current_dir.find(nautilus_git_repo_name){ + Some(index) => { + let schema_path = current_dir[0..index + nautilus_git_repo_name.len()].to_string() + "/schema"; + Ok(schema_path) + } + None => anyhow::bail!("Could not calculate schema dir from current directory path or SCHEMA_DIR env variable") + } + }) +} + +pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> anyhow::Result<()> { + info!("Initializing Postgres database with target permissions and schema"); + // create public schema + match sqlx::query("CREATE SCHEMA IF NOT EXISTS public;") + .execute(pg) + .await + { + Ok(_) => info!("Schema public created successfully"), + Err(err) => error!("Error creating schema public: {:?}", err), + } + // create role if not exists + match sqlx::query(format!("CREATE ROLE {} PASSWORD '{}' LOGIN;", database, password).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Role {} created successfully", database), + Err(err) => { + if err.to_string().contains("already exists") { + info!("Role {} already exists", database); + } else { + error!("Error creating role {}: {:?}", database, err); + } + } + } + // execute all the sql files in schema dir + let schema_dir = get_schema_dir()?; + let mut sql_files = + std::fs::read_dir(schema_dir)?.collect::, std::io::Error>>()?; + for file in &mut sql_files { + let file_name = file.file_name(); + info!("Executing schema file: {:?}", file_name); + let file_path = file.path(); + let sql_content = std::fs::read_to_string(file_path.clone())?; + for sql_statement in sql_content.split(';').filter(|s| !s.trim().is_empty()) { + sqlx::query(sql_statement).execute(pg).await?; + } + } + // grant connect + match sqlx::query(format!("GRANT CONNECT ON DATABASE {0} TO {0};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Connect privileges granted to role {}", database), + Err(err) => error!( + "Error granting connect privileges to role {}: {:?}", + database, err + ), + } + // grant all schema privileges to the role + match sqlx::query(format!("GRANT ALL PRIVILEGES ON SCHEMA public TO {};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("All schema privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + // grant all table privileges to the role + match sqlx::query( + format!( + "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {};", + database + ) + .as_str(), + ) + .execute(pg) + .await + { + Ok(_) => info!("All tables privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + // grant all sequence privileges to the role + match sqlx::query( + format!( + "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {};", + database + ) + .as_str(), + ) + .execute(pg) + .await + { + Ok(_) => info!("All sequences privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + // grant all function privileges to the role + match sqlx::query( + format!( + "GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO {};", + database + ) + .as_str(), + ) + .execute(pg) + .await + { + Ok(_) => info!("All functions privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + + Ok(()) +} + +pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> { + // execute drop owned + match sqlx::query(format!("DROP OWNED BY {}", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Dropped owned objects by role {}", database), + Err(err) => error!("Error dropping owned by role {}: {:?}", database, err), + } + // revoke connect + match sqlx::query(format!("REVOKE CONNECT ON DATABASE {0} FROM {0};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Revoked connect privileges from role {}", database), + Err(err) => error!( + "Error revoking connect privileges from role {}: {:?}", + database, err + ), + } + // revoke privileges + match sqlx::query(format!("REVOKE ALL PRIVILEGES ON DATABASE {0} FROM {0};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Revoked all privileges from role {}", database), + Err(err) => error!( + "Error revoking all privileges from role {}: {:?}", + database, err + ), + } + // execute drop schema + match sqlx::query("DROP SCHEMA IF EXISTS public CASCADE") + .execute(pg) + .await + { + Ok(_) => info!("Dropped schema public"), + Err(err) => error!("Error dropping schema public: {:?}", err), + } + // drop role + match sqlx::query(format!("DROP ROLE IF EXISTS {};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Dropped role {}", database), + Err(err) => error!("Error dropping role {}: {:?}", database, err), + } + Ok(()) +} + +pub async fn run_database_command(opt: DatabaseOpt) -> anyhow::Result<()> { + let command = opt.command.clone(); + + match command { + DatabaseCommand::Init(config) => { + let pg_connect_options = get_postgres_connect_options( + config.host, + config.port, + config.username, + config.password, + config.database, + ) + .unwrap(); + let pg = connect_pg(pg_connect_options.clone().into()).await?; + init_postgres( + &pg, + pg_connect_options.database, + pg_connect_options.password, + ) + .await? + } + DatabaseCommand::Drop(config) => { + let pg_connect_options = get_postgres_connect_options( + config.host, + config.port, + config.username, + config.password, + config.database, + ) + .unwrap(); + let pg = connect_pg(pg_connect_options.clone().into()).await?; + drop_postgres(&pg, pg_connect_options.database).await? + } + } + Ok(()) +} diff --git a/nautilus_core/cli/src/lib.rs b/nautilus_core/cli/src/lib.rs new file mode 100644 index 000000000000..1ede33247246 --- /dev/null +++ b/nautilus_core/cli/src/lib.rs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 crate::{ + database::postgres::run_database_command, + opt::{Commands, NautilusCli}, +}; + +mod database; +pub mod opt; + +pub async fn run(opt: NautilusCli) -> anyhow::Result<()> { + match opt.command { + Commands::Database(database_opt) => run_database_command(database_opt).await?, + } + Ok(()) +} diff --git a/nautilus_core/cli/src/opt.rs b/nautilus_core/cli/src/opt.rs new file mode 100644 index 000000000000..8ef5b9c2b1d2 --- /dev/null +++ b/nautilus_core/cli/src/opt.rs @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 clap::Parser; + +#[derive(Parser)] +#[clap(version, about, author)] +pub struct NautilusCli { + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Parser, Debug)] +pub enum Commands { + Database(DatabaseOpt), +} + +#[derive(Parser, Debug)] +#[command(about = "Postgres database operations", long_about = None)] +pub struct DatabaseOpt { + #[clap(subcommand)] + pub command: DatabaseCommand, +} + +#[derive(Parser, Debug, Clone)] +pub struct DatabaseConfig { + /// Hostname or IP address of the database server + #[arg(long)] + pub host: Option, + /// Port number of the database server + #[arg(long)] + pub port: Option, + /// Username for connecting to the database + #[arg(long)] + pub username: Option, + /// Name of the database + #[arg(long)] + pub database: Option, + /// Password for connecting to the database + #[arg(long)] + pub password: Option, +} + +#[derive(Parser, Debug, Clone)] +#[command(about = "Postgres database operations", long_about = None)] +pub enum DatabaseCommand { + /// Initializes a new Postgres database with the latest schema + Init(DatabaseConfig), + /// Drops roles, privileges and deletes all data from the database + Drop(DatabaseConfig), +} diff --git a/nautilus_core/common/build.rs b/nautilus_core/common/build.rs index 5a1e2ea4c76d..b1d175f50968 100644 --- a/nautilus_core/common/build.rs +++ b/nautilus_core/common/build.rs @@ -13,6 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +#[cfg(feature = "ffi")] use std::env; #[allow(clippy::expect_used)] // OK in build script diff --git a/nautilus_core/core/build.rs b/nautilus_core/core/build.rs index ef33be7effc2..c3d2875f6ce3 100644 --- a/nautilus_core/core/build.rs +++ b/nautilus_core/core/build.rs @@ -13,6 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +#[cfg(feature = "ffi")] use std::env; #[allow(clippy::expect_used)] // OK in build script diff --git a/nautilus_core/infrastructure/src/sql/mod.rs b/nautilus_core/infrastructure/src/sql/mod.rs index 2df57cccc3be..f098c3cf9d49 100644 --- a/nautilus_core/infrastructure/src/sql/mod.rs +++ b/nautilus_core/infrastructure/src/sql/mod.rs @@ -13,9 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a SQL data model and `CacheDatabase` implementation. +/// Be careful about ordering and foreign key constraints when deleting data. +/// We can use this list for manual truncation of tables. +pub const NAUTILUS_TABLES: [&str; 5] = + ["general", "instrument", "currency", "order", "order_event"]; -pub mod cache; pub mod database; pub mod models; +pub mod pg; pub mod schema; diff --git a/nautilus_core/infrastructure/src/sql/pg.rs b/nautilus_core/infrastructure/src/sql/pg.rs new file mode 100644 index 000000000000..c86a5245e0d3 --- /dev/null +++ b/nautilus_core/infrastructure/src/sql/pg.rs @@ -0,0 +1,114 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 sqlx::{postgres::PgConnectOptions, query, ConnectOptions, PgPool}; + +use crate::sql::NAUTILUS_TABLES; + +#[derive(Debug, Clone)] +pub struct PostgresConnectOptions { + pub host: String, + pub port: u16, + pub username: String, + pub password: String, + pub database: String, +} + +impl PostgresConnectOptions { + pub fn new( + host: String, + port: u16, + username: String, + password: String, + database: String, + ) -> Self { + Self { + host, + port, + username, + password, + database, + } + } +} + +impl From for PgConnectOptions { + fn from(opt: PostgresConnectOptions) -> Self { + PgConnectOptions::new() + .host(opt.host.as_str()) + .port(opt.port) + .username(opt.username.as_str()) + .password(opt.password.as_str()) + .database(opt.database.as_str()) + .disable_statement_logging() + } +} + +pub fn get_postgres_connect_options( + host: Option, + port: Option, + username: Option, + password: Option, + database: Option, +) -> anyhow::Result { + let host = match host.or_else(|| std::env::var("POSTGRES_HOST").ok()) { + Some(host) => host, + None => anyhow::bail!("No host provided from argument or POSTGRES_HOST env variable"), + }; + let port = match port.or_else(|| { + std::env::var("POSTGRES_PORT") + .map(|port| port.parse::().unwrap()) + .ok() + }) { + Some(port) => port, + None => anyhow::bail!("No port provided from argument or POSTGRES_PORT env variable"), + }; + let username = match username.or_else(|| std::env::var("POSTGRES_USERNAME").ok()) { + Some(username) => username, + None => { + anyhow::bail!("No username provided from argument or POSTGRES_USERNAME env variable") + } + }; + let database = match database.or_else(|| std::env::var("POSTGRES_DATABASE").ok()) { + Some(database) => database, + None => { + anyhow::bail!("No database provided from argument or POSTGRES_DATABASE env variable") + } + }; + let password = match password.or_else(|| std::env::var("POSTGRES_PASSWORD").ok()) { + Some(password) => password, + None => { + anyhow::bail!("No password provided from argument or POSTGRES_PASSWORD env variable") + } + }; + Ok(PostgresConnectOptions::new( + host, port, username, password, database, + )) +} + +pub async fn delete_nautilus_postgres_tables(db: &PgPool) -> anyhow::Result<()> { + // iterate over NAUTILUS_TABLES and delete all rows + for table in NAUTILUS_TABLES { + query(format!("DELETE FROM \"{}\" WHERE true", table).as_str()) + .execute(db) + .await + .unwrap_or_else(|_| panic!("Failed to delete table {}", table)); + } + Ok(()) +} + +pub async fn connect_pg(options: PgConnectOptions) -> anyhow::Result { + Ok(PgPool::connect_with(options).await.unwrap()) +} diff --git a/nautilus_core/model/build.rs b/nautilus_core/model/build.rs index e5e12ef3a7dc..b51220f5fd63 100644 --- a/nautilus_core/model/build.rs +++ b/nautilus_core/model/build.rs @@ -13,6 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +#[cfg(feature = "ffi")] use std::env; #[allow(clippy::expect_used)] // OK in build script diff --git a/schema/tables.sql b/schema/tables.sql index 95ea61d136d5..eea7c0415438 100644 --- a/schema/tables.sql +++ b/schema/tables.sql @@ -1,4 +1,128 @@ -CREATE TABLE IF NOT EXISTS general ( - key SERIAL PRIMARY KEY, - value TEXT NOT NULL +------------------- ENUMS ------------------- + +CREATE TYPE ACCOUNT_TYPE AS ENUM ('Cash', 'Margin', 'Betting'); +CREATE TYPE AGGREGATION_SOURCE AS ENUM ('External', 'Internal'); +CREATE TYPE AGGRESSOR_SIDE AS ENUM ('NoAggressor', 'Buyer', 'Seller'); +CREATE TYPE ASSET_CLASS AS ENUM ('Fx', 'Equity', 'Commodity', 'Debt', 'Index', 'Cryptocurrency', 'Alternative'); +CREATE TYPE INSTRUMENT_CLASS AS ENUM ('Spot', 'Swap', 'Future', 'FutureSpread', 'Forward', 'Cfg', 'Bond', 'Option', 'OptionSpread', 'Warrant', 'SportsBetting'); +CREATE TYPE BAR_AGGREGATION AS ENUM ('Tick', 'TickImbalance', 'TickRuns', 'Volume', 'VolumeImbalance', 'VolumeRuns', 'Value', 'ValueImbalance', 'ValueRuns', 'Millisecond', 'Second', 'Minute', 'Hour', 'Day', 'Week', 'Month'); +CREATE TYPE BOOK_ACTION AS ENUM ('Add', 'Update', 'Delete','Clear'); +CREATE TYPE ORDER_STATUS AS ENUM ('Initialized', 'Denied', 'Emulated', 'Released', 'Submitted', 'Accepted', 'Rejected', 'Canceled', 'Expired', 'Triggered', 'PendingUpdate', 'PendingCancel', 'PartiallyFilled', 'Filled'); + + +------------------- TABLES ------------------- + +CREATE TABLE IF NOT EXISTS "general" ( + key TEXT PRIMARY KEY NOT NULL, + value bytea not null +); + +CREATE TABLE IF NOT EXISTS "trader" ( + id TEXT PRIMARY KEY NOT NULL, + instance_id UUID NOT NULL +); + +CREATE TABLE IF NOT EXISTS "strategy" ( + id TEXT PRIMARY KEY NOT NULL, + order_id_tag TEXT, + oms_type TEXT, + manage_contingent_orders BOOLEAN, + manage_gtd_expiry BOOLEAN + +); + +CREATE TABLE IF NOT EXISTS "currency" ( + code TEXT PRIMARY KEY NOT NULL, + precision INTEGER, + iso4217 INTEGER, + name TEXT, + currency_type TEXT +); + +CREATE TABLE IF NOT EXISTS "instrument" ( + id TEXT PRIMARY KEY NOT NULL, + kind TEXT, + raw_symbol TEXT NOT NULL, + base_currency TEXT REFERENCES currency(code), + underlying TEXT, + quote_currency TEXT REFERENCES currency(code), + settlement_currency TEXT REFERENCES currency(code), + isin TEXT, + asset_class TEXT, + exchange TEXT, + multiplier TEXT, + option_kind TEXT, + is_inverse BOOLEAN DEFAULT FALSE, + strike_price TEXT, + activation_ns TEXT, + expiration_ns TEXT, + price_precision INTEGER NOT NULL , + size_precision INTEGER, + price_increment TEXT NOT NULL, + size_increment TEXT, + maker_fee TEXT NULL, + taker_fee TEXT NULL, + margin_init TEXT NOT NULL, + margin_maint TEXT NOT NULL, + lot_size TEXT, + max_quantity TEXT, + min_quantity TEXT, + max_notional TEXT, + min_notional TEXT, + max_price TEXT, + min_price TEXT, + ts_init TEXT NOT NULL, + ts_event TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS "order" ( + id TEXT PRIMARY KEY NOT NULL, + kind TEXT NOT NULL, + order_type TEXT, + status TEXT, +-- trader_id TEXT REFERENCES trader(id) ON DELETE CASCADE, +-- strategy_id TEXT REFERENCES strategy(id) ON DELETE CASCADE, +-- instrument_id TEXT REFERENCES instrument(id) ON DELETE CASCADE, + symbol TEXT, + venue TEXT, + venue_order_id TEXT, + position_id TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS "order_event" ( + id TEXT PRIMARY KEY NOT NULL, + kind TEXT NOT NULL, + order_id TEXT DEFAULT NULL, + order_type TEXT, + trader_id TEXT NOT NULL, + strategy_id TEXT NOT NULL, + instrument_id TEXT NOT NULL, + quantity TEXT, + post_only BOOLEAN DEFAULT FALSE, + reduce_only BOOLEAN DEFAULT FALSE, + quote_quantity BOOLEAN DEFAULT FALSE, + reconciliation BOOLEAN DEFAULT FALSE, + price TEXT, + trigger_price TEXT, + limit_offset TEXT, + trailing_offset TEXT, + expire_time TEXT, + display_qty TEXT, + trigger_instrument_id TEXT, + order_list_id TEXT, + linked_order_ids TEXT[], + parent_order_id TEXT, + exec_algorithm_id TEXT, + exec_spawn_id TEXT, + venue_order_id TEXT, + account_id TEXT, + tags TEXT, + ts_event TEXT NOT NULL, + ts_init TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); \ No newline at end of file From 524816e40d94e10391dc803d8f6009f79b041975 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 22 Apr 2024 08:14:16 +1000 Subject: [PATCH 021/193] Update release notes --- RELEASES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 4ae2a3c5a3ec..0419baf231a7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,7 +3,7 @@ Released on TBD (UTC). ### Enhancements -None +- Added Nautilus CLI (see [docs](https://docs.nautilustrader.io/nightly/developer_guide/index.html)), many thanks @filipmacek ### Breaking Changes - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) From 9d07ad4567c7b4b4fcb73df7d5ddcb4dfdb528c8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 22 Apr 2024 18:27:33 +1000 Subject: [PATCH 022/193] Update dependencies --- nautilus_core/Cargo.lock | 8 ++++---- poetry.lock | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index fdb116dab972..9f20478e1760 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -3976,9 +3976,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" dependencies = [ "bitflags 2.5.0", "errno", @@ -4219,9 +4219,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] diff --git a/poetry.lock b/poetry.lock index 62b556447012..b023bb044376 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2685,4 +2685,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "5e0eec1cefd3fe5f6bfe2466e93a37556069006b750b73168e8951a9612f3d44" +content-hash = "fbc78c9e9804eed52bab970597c88434a4cb25eb4c9b39635d8d751b69830dc7" diff --git a/pyproject.toml b/pyproject.toml index d7caf0768779..6e6ac0b15ab1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ click = "^8.1.7" fsspec = "==2023.6.0" # Pinned due breaking changes msgspec = "^0.18.6" pandas = "^2.2.2" -pyarrow = ">=15.0.2" +pyarrow = ">=16.0.0" pytz = ">=2023.4.0" tqdm = "^4.66.2" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} From 1a4604b1e17009a7d46d286eff173c909ae845e9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 22 Apr 2024 19:12:47 +1000 Subject: [PATCH 023/193] Optimize collection returns --- nautilus_core/model/src/orders/base.rs | 12 ++++++------ nautilus_core/model/src/orders/limit.rs | 12 ++++++------ nautilus_core/model/src/orders/limit_if_touched.rs | 12 ++++++------ nautilus_core/model/src/orders/market.rs | 12 ++++++------ nautilus_core/model/src/orders/market_if_touched.rs | 12 ++++++------ nautilus_core/model/src/orders/market_to_limit.rs | 12 ++++++------ nautilus_core/model/src/orders/stop_limit.rs | 12 ++++++------ nautilus_core/model/src/orders/stop_market.rs | 12 ++++++------ .../model/src/orders/trailing_stop_limit.rs | 12 ++++++------ .../model/src/orders/trailing_stop_market.rs | 12 ++++++------ nautilus_core/model/src/python/orders/limit.rs | 12 ++++++------ nautilus_core/model/src/python/orders/market.rs | 12 ++++++------ nautilus_core/model/src/python/orders/stop_limit.rs | 12 ++++++------ 13 files changed, 78 insertions(+), 78 deletions(-) diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 11e290a8ef8e..846aa2c462c9 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -624,12 +624,12 @@ pub trait Order { fn trigger_instrument_id(&self) -> Option; fn contingency_type(&self) -> Option; fn order_list_id(&self) -> Option; - fn linked_order_ids(&self) -> Option>; + fn linked_order_ids(&self) -> Option<&[ClientOrderId]>; fn parent_order_id(&self) -> Option; fn exec_algorithm_id(&self) -> Option; - fn exec_algorithm_params(&self) -> Option>; + fn exec_algorithm_params(&self) -> Option<&HashMap>; fn exec_spawn_id(&self) -> Option; - fn tags(&self) -> Option>; + fn tags(&self) -> Option<&[Ustr]>; fn filled_qty(&self) -> Quantity; fn leaves_qty(&self) -> Quantity; fn avg_px(&self) -> Option; @@ -787,12 +787,12 @@ where trigger_instrument_id: order.trigger_instrument_id(), contingency_type: order.contingency_type(), order_list_id: order.order_list_id(), - linked_order_ids: order.linked_order_ids(), + linked_order_ids: order.linked_order_ids().map(|x| x.to_vec()), parent_order_id: order.parent_order_id(), exec_algorithm_id: order.exec_algorithm_id(), - exec_algorithm_params: order.exec_algorithm_params(), + exec_algorithm_params: order.exec_algorithm_params().map(|x| x.to_owned()), exec_spawn_id: order.exec_spawn_id(), - tags: order.tags(), + tags: order.tags().map(|x| x.to_vec()), event_id: order.init_id(), ts_event: order.ts_init(), ts_init: order.ts_init(), diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index 6e13eea38528..c1b34780f8b8 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -294,8 +294,8 @@ impl Order for LimitOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -306,16 +306,16 @@ impl Order for LimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index cd14d112ee6c..735aff6ed204 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -281,8 +281,8 @@ impl Order for LimitIfTouchedOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -293,16 +293,16 @@ impl Order for LimitIfTouchedOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index d5756c918a56..a4640a10e670 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -271,8 +271,8 @@ impl Order for MarketOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -283,16 +283,16 @@ impl Order for MarketOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index 1078f9dc0c1f..8b0a013a6fce 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -275,8 +275,8 @@ impl Order for MarketIfTouchedOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -287,16 +287,16 @@ impl Order for MarketIfTouchedOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index 77a4af948bae..a4a78d8017cb 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -267,8 +267,8 @@ impl Order for MarketToLimitOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -279,16 +279,16 @@ impl Order for MarketToLimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index 2ddc7dd28a88..8095cf983bda 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -288,8 +288,8 @@ impl Order for StopLimitOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -300,16 +300,16 @@ impl Order for StopLimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index 53ed886dd99f..ed0b93c9e968 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -276,8 +276,8 @@ impl Order for StopMarketOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -288,16 +288,16 @@ impl Order for StopMarketOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index ec80b3ca8363..29b3a457eb33 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -290,8 +290,8 @@ impl Order for TrailingStopLimitOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -302,16 +302,16 @@ impl Order for TrailingStopLimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index 62bd75207cbd..4dc23855ba1e 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -282,8 +282,8 @@ impl Order for TrailingStopMarketOrder { self.order_list_id } - fn linked_order_ids(&self) -> Option> { - self.linked_order_ids.clone() + fn linked_order_ids(&self) -> Option<&[ClientOrderId]> { + self.linked_order_ids.as_deref() } fn parent_order_id(&self) -> Option { @@ -294,16 +294,16 @@ impl Order for TrailingStopMarketOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone() + fn exec_algorithm_params(&self) -> Option<&HashMap> { + self.exec_algorithm_params.as_ref() } fn exec_spawn_id(&self) -> Option { self.exec_spawn_id } - fn tags(&self) -> Option> { - self.tags.clone() + fn tags(&self) -> Option<&[Ustr]> { + self.tags.as_deref() } fn filled_qty(&self) -> Quantity { diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index 60d51278a819..78a8b4677fc1 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -319,20 +319,20 @@ impl LimitOrder { #[getter] #[pyo3(name = "exec_algorithm_params")] - fn py_exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone().map(|x| { + fn py_exec_algorithm_params(&self) -> Option> { + self.exec_algorithm_params.as_ref().map(|x| { x.into_iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.as_str(), v.as_str())) .collect() }) } #[getter] #[pyo3(name = "tags")] - fn py_tags(&self) -> Option> { + fn py_tags(&self) -> Option> { self.tags - .clone() - .map(|vec| vec.iter().map(|s| s.to_string()).collect()) + .as_ref() + .map(|vec| vec.iter().map(|s| s.as_str()).collect()) } #[getter] diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index bc007759efa0..388be7f2872f 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -200,10 +200,10 @@ impl MarketOrder { #[getter] #[pyo3(name = "exec_algorithm_params")] - fn py_exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone().map(|x| { + fn py_exec_algorithm_params(&self) -> Option> { + self.exec_algorithm_params.as_ref().map(|x| { x.into_iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.as_str(), v.as_str())) .collect() }) } @@ -264,10 +264,10 @@ impl MarketOrder { #[getter] #[pyo3(name = "tags")] - fn py_tags(&self) -> Option> { + fn py_tags(&self) -> Option> { self.tags - .clone() - .map(|vec| vec.iter().map(|s| s.to_string()).collect()) + .as_ref() + .map(|vec| vec.iter().map(|s| s.as_str()).collect()) } #[staticmethod] diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index fb0e7eef76a7..f454fd2a3e7e 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -325,10 +325,10 @@ impl StopLimitOrder { #[getter] #[pyo3(name = "exec_algorithm_params")] - fn py_exec_algorithm_params(&self) -> Option> { - self.exec_algorithm_params.clone().map(|x| { + fn py_exec_algorithm_params(&self) -> Option> { + self.exec_algorithm_params.as_ref().map(|x| { x.into_iter() - .map(|(k, v)| (k.to_string(), v.to_string())) + .map(|(k, v)| (k.as_str(), v.as_str())) .collect() }) } @@ -341,10 +341,10 @@ impl StopLimitOrder { #[getter] #[pyo3(name = "tags")] - fn py_tags(&self) -> Option> { + fn py_tags(&self) -> Option> { self.tags - .clone() - .map(|vec| vec.iter().map(|s| s.to_string()).collect()) + .as_ref() + .map(|vec| vec.iter().map(|s| s.as_str()).collect()) } #[pyo3(name = "to_dict")] From ca66fdf0eb09295d0503dc7c64e0c8af2b22614c Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:49:29 +0300 Subject: [PATCH 024/193] Fix IBOrderTags list support (#1603) --- .../adapters/interactive_brokers/common.py | 2 +- .../adapters/interactive_brokers/execution.py | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/common.py b/nautilus_trader/adapters/interactive_brokers/common.py index 18ac594f32a0..76f313b7e050 100644 --- a/nautilus_trader/adapters/interactive_brokers/common.py +++ b/nautilus_trader/adapters/interactive_brokers/common.py @@ -167,7 +167,7 @@ class IBOrderTags(NautilusConfig, frozen=True, repr_omit_defaults=True): @property def value(self): - return self.json().decode() + return f"IBOrderTags:{self.json().decode()}" def __str__(self): return self.value diff --git a/nautilus_trader/adapters/interactive_brokers/execution.py b/nautilus_trader/adapters/interactive_brokers/execution.py index 17cbf1e28a94..de127cc3ac67 100644 --- a/nautilus_trader/adapters/interactive_brokers/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/execution.py @@ -30,6 +30,7 @@ from nautilus_trader.adapters.interactive_brokers.client import InteractiveBrokersClient from nautilus_trader.adapters.interactive_brokers.client.common import IBPosition from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE +from nautilus_trader.adapters.interactive_brokers.common import IBOrderTags from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig from nautilus_trader.adapters.interactive_brokers.parsing.execution import MAP_ORDER_ACTION from nautilus_trader.adapters.interactive_brokers.parsing.execution import MAP_ORDER_FIELDS @@ -529,20 +530,20 @@ def _transform_order_to_ib_order(self, order: Order) -> IBOrder: return ib_order def _attach_order_tags(self, ib_order: IBOrder, order: Order) -> IBOrder: - try: - tags: dict = json.loads(order.tags) - for tag in tags: - if tag == "conditions": - for condition in tags[tag]: - pass # TODO: - else: - setattr(ib_order, tag, tags[tag]) - return ib_order - except (json.JSONDecodeError, TypeError): - self._log.warning( - f"{order.client_order_id} {order.tags=} ignored, must be valid IBOrderTags.value", - ) - return ib_order + tags: dict = {} + for ot in order.tags: + if ot.startswith("IBOrderTags:"): + tags = IBOrderTags.parse(ot.replace("IBOrderTags:", "")).dict() + break + + for tag in tags: + if tag == "conditions": + for condition in tags[tag]: + pass # TODO: + else: + setattr(ib_order, tag, tags[tag]) + + return ib_order async def _submit_order(self, command: SubmitOrder) -> None: PyCondition.type(command, SubmitOrder, "command") From 509392be10f3232eba08e4f6e30b79bdec38bee5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 23 Apr 2024 18:07:45 +1000 Subject: [PATCH 025/193] Upgrade datafusion --- nautilus_core/Cargo.lock | 52 ++++++++++++++-------------- nautilus_core/persistence/Cargo.toml | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 9f20478e1760..0c9fb6ca0d07 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1196,9 +1196,9 @@ dependencies = [ [[package]] name = "datafusion" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812a53e154009ee2bd6b2f8a9ab8f30cbf2c693cb860e60f0aa3315ba3486e39" +checksum = "85069782056753459dc47e386219aa1fdac5b731f26c28abb8c0ffd4b7c5ab11" dependencies = [ "ahash 0.8.11", "arrow", @@ -1246,9 +1246,9 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99d4d7ccdad4dffa8ff4569f45792d0678a0c7ee08e3fdf1b0a52ebb9cf201e" +checksum = "309d9040751f6dc9e33c85dce6abb55a46ef7ea3644577dd014611c379447ef3" dependencies = [ "ahash 0.8.11", "arrow", @@ -1268,18 +1268,18 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5cf713ae1f5423b5625aeb3ddfb0d5c29e880cf6a0d2059d0724219c873a76c" +checksum = "a3e4a44d8ef1b1e85d32234e6012364c411c3787859bb3bba893b0332cb03dfd" dependencies = [ "tokio", ] [[package]] name = "datafusion-execution" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f69d00325b77c3886b7080d96e3aa8e9a5ef16fe368a434c14b2f1b63b68803" +checksum = "06a3a29ae36bcde07d179cc33b45656a8e7e4d023623e320e48dcf1200eeee95" dependencies = [ "arrow", "chrono", @@ -1298,9 +1298,9 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbe71343a95c2079fa443aa840dfdbd2034532cfc00449a57204c8a6fdcf928" +checksum = "2a3542aa322029c2121a671ce08000d4b274171070df13f697b14169ccf4f628" dependencies = [ "ahash 0.8.11", "arrow", @@ -1315,9 +1315,9 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c046800d26d2267fab3bd5fc0b9bc0a7b1ae47e688b01c674ed39daa84cd3cc5" +checksum = "dd221792c666eac174ecc09e606312844772acc12cbec61a420c2fca1ee70959" dependencies = [ "arrow", "base64 0.22.0", @@ -1340,9 +1340,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d48972fffe5a4ee2af2b8b72a3db5cdbc800d5dd5af54f8df0ab508bb5545c" +checksum = "76bd7f5087817deb961764e8c973d243b54f8572db414a8f0a8f33a48f991e0a" dependencies = [ "arrow", "async-trait", @@ -1358,9 +1358,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e001baf1aaa95a418ee9fcb979f5fc18f16b81a8a5f6a260b05df9494344adb" +checksum = "5cabc0d9aaa0f5eb1b472112f16223c9ffd2fb04e58cbf65c0a331ee6e993f96" dependencies = [ "ahash 0.8.11", "arrow", @@ -1393,9 +1393,9 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5421ed2c5789bafc6d48231627d17c6836549a26c8162569354589202212ef" +checksum = "17c0523e9c8880f2492a88bbd857dde02bed1ed23f3e9211a89d3d7ec3b44af9" dependencies = [ "ahash 0.8.11", "arrow", @@ -1424,9 +1424,9 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "37.0.0" +version = "37.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f70d881337f733b7d0548e468073c0ae8b256557c33b299fd6afea0ea5d5162" +checksum = "49eb54b42227136f6287573f2434b1de249fe1b8e6cd6cc73a634e4a3ec29356" dependencies = [ "arrow", "arrow-array", @@ -2293,9 +2293,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -3976,9 +3976,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.33" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -4041,9 +4041,9 @@ checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index e3f41ea1f6c1..59e60ca1c617 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -21,7 +21,7 @@ tokio = { workspace = true } thiserror = { workspace = true } binary-heap-plus = "0.5.0" compare = "0.1.0" -datafusion = { version = "37.0.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } +datafusion = { version = "37.1.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } dotenv = "0.15.0" [dev-dependencies] From e0fc34bab3cf7c45c52cc6c978a9e513c62c68c2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 23 Apr 2024 18:21:30 +1000 Subject: [PATCH 026/193] Add Rust docs --- nautilus_core/core/src/datetime.rs | 2 ++ nautilus_core/core/src/parsing.rs | 2 +- nautilus_core/model/src/currencies.rs | 1 + nautilus_core/model/src/data/bar.rs | 2 ++ nautilus_core/model/src/data/delta.rs | 2 ++ nautilus_core/model/src/data/deltas.rs | 2 ++ nautilus_core/model/src/data/depth.rs | 6 ++++-- nautilus_core/model/src/data/order.rs | 2 ++ nautilus_core/model/src/data/quote.rs | 2 ++ nautilus_core/model/src/data/trade.rs | 2 ++ nautilus_trader/core/includes/model.h | 4 ++-- nautilus_trader/core/rust/model.pxd | 4 ++-- 12 files changed, 24 insertions(+), 7 deletions(-) diff --git a/nautilus_core/core/src/datetime.rs b/nautilus_core/core/src/datetime.rs index c45a30400c5b..9ceb2e75d890 100644 --- a/nautilus_core/core/src/datetime.rs +++ b/nautilus_core/core/src/datetime.rs @@ -100,6 +100,7 @@ pub fn floor_to_nearest_microsecond(unix_nanos: u64) -> u64 { (unix_nanos / NANOSECONDS_IN_MICROSECOND) * NANOSECONDS_IN_MICROSECOND } +/// Calculates the last weekday (Mon-Fri) from the given `year`, `month` and `day`. pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result { let date = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?; @@ -128,6 +129,7 @@ pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result anyhow::Result { let timestamp_ns = timestamp_ns.as_u64(); let seconds = timestamp_ns / NANOSECONDS_IN_SECOND; diff --git a/nautilus_core/core/src/parsing.rs b/nautilus_core/core/src/parsing.rs index 8d739087eafd..46fe3e8094ac 100644 --- a/nautilus_core/core/src/parsing.rs +++ b/nautilus_core/core/src/parsing.rs @@ -29,7 +29,7 @@ pub fn precision_from_str(s: &str) -> u8 { return lower_s.split('.').last().unwrap().len() as u8; } -/// Returns a usize from the given bytes. +/// Returns a `usize` from the given bytes. pub fn bytes_to_usize(bytes: &[u8]) -> anyhow::Result { // Check bytes width if bytes.len() >= std::mem::size_of::() { diff --git a/nautilus_core/model/src/currencies.rs b/nautilus_core/model/src/currencies.rs index 2a75caab5fbe..19ddc0bae4fe 100644 --- a/nautilus_core/model/src/currencies.rs +++ b/nautilus_core/model/src/currencies.rs @@ -988,6 +988,7 @@ impl Currency { } } +/// Provides a map of built-in `Currency` constants. pub static CURRENCY_MAP: Lazy>> = Lazy::new(|| { let mut map = HashMap::new(); /////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index e5be5fb8397a..42fea573f0c4 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Bar aggregate structures, data types and functionality. + use std::{ collections::HashMap, fmt::{Debug, Display, Formatter}, diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs index 74c49218ca67..a844949de8b7 100644 --- a/nautilus_core/model/src/data/delta.rs +++ b/nautilus_core/model/src/data/delta.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! An `OrderBookDelta` data type intended to carry book state information. + use std::{ collections::HashMap, fmt::{Display, Formatter}, diff --git a/nautilus_core/model/src/data/deltas.rs b/nautilus_core/model/src/data/deltas.rs index 8b13b64ed4a2..2d5778787885 100644 --- a/nautilus_core/model/src/data/deltas.rs +++ b/nautilus_core/model/src/data/deltas.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! An `OrderBookDeltas` container type to carry a bulk of `OrderBookDelta` records. + use std::{ fmt::{Display, Formatter}, hash::{Hash, Hasher}, diff --git a/nautilus_core/model/src/data/depth.rs b/nautilus_core/model/src/data/depth.rs index af16a94810a5..087fe8d0f2b6 100644 --- a/nautilus_core/model/src/data/depth.rs +++ b/nautilus_core/model/src/data/depth.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! An `OrderBookDepth10` aggregated top-of-book data type with a fixed depth of 10 levels per side. + use std::{ collections::HashMap, fmt::{Display, Formatter}, @@ -27,9 +29,9 @@ use crate::identifiers::instrument_id::InstrumentId; pub const DEPTH10_LEN: usize = 10; -/// Represents a self-contained order book update with a fixed depth of 10 levels per side. +/// Represents a aggregated order book update with a fixed depth of 10 levels per side. /// -/// This struct is specifically designed for scenarios where a snapshot of the top 10 bid and +/// This structure is specifically designed for scenarios where a snapshot of the top 10 bid and /// ask levels in an order book is needed. It differs from `OrderBookDelta` or `OrderBookDeltas` /// in its fixed-depth nature and is optimized for cases where a full depth representation is not /// required or practical. diff --git a/nautilus_core/model/src/data/order.rs b/nautilus_core/model/src/data/order.rs index c25fcd83f9df..37d426840319 100644 --- a/nautilus_core/model/src/data/order.rs +++ b/nautilus_core/model/src/data/order.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! A `BookOrder` for use with the `OrderBookDelta` data type. + use std::{ fmt::{Display, Formatter}, hash::{Hash, Hasher}, diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 0d82d2bbdbd8..909d42bc6a36 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! A `QuoteTick` data type representing a top-of-book quote state. + use std::{ cmp, collections::HashMap, diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index 4c7c13bc9160..98db02bc08c7 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! A `TradeTick` data type representing a single trade in a market. + use std::{ collections::HashMap, fmt::{Display, Formatter}, diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index bf73015a0fdf..85ee4062b12a 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -838,9 +838,9 @@ typedef struct OrderBookDeltas_API { } OrderBookDeltas_API; /** - * Represents a self-contained order book update with a fixed depth of 10 levels per side. + * Represents a aggregated order book update with a fixed depth of 10 levels per side. * - * This struct is specifically designed for scenarios where a snapshot of the top 10 bid and + * This structure is specifically designed for scenarios where a snapshot of the top 10 bid and * ask levels in an order book is needed. It differs from `OrderBookDelta` or `OrderBookDeltas` * in its fixed-depth nature and is optimized for cases where a full depth representation is not * required or practical. diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 4dd425b87c7a..0a3f2e6218f2 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -462,9 +462,9 @@ cdef extern from "../includes/model.h": cdef struct OrderBookDeltas_API: OrderBookDeltas_t *_0; - # Represents a self-contained order book update with a fixed depth of 10 levels per side. + # Represents a aggregated order book update with a fixed depth of 10 levels per side. # - # This struct is specifically designed for scenarios where a snapshot of the top 10 bid and + # This structure is specifically designed for scenarios where a snapshot of the top 10 bid and # ask levels in an order book is needed. It differs from `OrderBookDelta` or `OrderBookDeltas` # in its fixed-depth nature and is optimized for cases where a full depth representation is not # required or practical. From 2b170073289f1b201e0239074ea569825b96b2ae Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 23 Apr 2024 18:27:23 +1000 Subject: [PATCH 027/193] Update Binance Futures fee rates --- nautilus_trader/adapters/binance/futures/providers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/adapters/binance/futures/providers.py b/nautilus_trader/adapters/binance/futures/providers.py index 3162ea6f9884..6ac222131697 100644 --- a/nautilus_trader/adapters/binance/futures/providers.py +++ b/nautilus_trader/adapters/binance/futures/providers.py @@ -99,8 +99,9 @@ def __init__( # These fee rates assume USD-M Futures Trading without the 10% off for using BNB or BUSD. # The next step is to enable users to pass their own fee rates map via the config. # In the future, we aim to represent this fee model with greater accuracy for backtesting. + # https://www.binance.com/en/fee/futureFee self._fee_rates = { - 0: BinanceFuturesFeeRates(feeTier=0, maker="0.000200", taker="0.000400"), + 0: BinanceFuturesFeeRates(feeTier=0, maker="0.000200", taker="0.000500"), 1: BinanceFuturesFeeRates(feeTier=1, maker="0.000160", taker="0.000400"), 2: BinanceFuturesFeeRates(feeTier=2, maker="0.000140", taker="0.000350"), 3: BinanceFuturesFeeRates(feeTier=3, maker="0.000120", taker="0.000320"), From 649238e36971ff513bbc460d3aab38a1273512a1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 23 Apr 2024 18:36:19 +1000 Subject: [PATCH 028/193] Add Rust docs --- nautilus_core/common/src/cache/database.rs | 2 ++ nautilus_core/model/src/data/mod.rs | 8 ++++++-- nautilus_core/model/src/data/stubs.rs | 2 ++ nautilus_trader/core/includes/model.h | 6 ++++++ nautilus_trader/core/rust/model.pxd | 4 ++++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/nautilus_core/common/src/cache/database.rs b/nautilus_core/common/src/cache/database.rs index e1efdf67f09d..6eb5c019e66d 100644 --- a/nautilus_core/common/src/cache/database.rs +++ b/nautilus_core/common/src/cache/database.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Provides a `Cache` database backing. + // Under development #![allow(dead_code)] #![allow(unused_variables)] diff --git a/nautilus_core/model/src/data/mod.rs b/nautilus_core/model/src/data/mod.rs index 948ce4cf00f8..c6abae3d3250 100644 --- a/nautilus_core/model/src/data/mod.rs +++ b/nautilus_core/model/src/data/mod.rs @@ -37,13 +37,17 @@ use self::{ }; use crate::polymorphism::GetTsInit; +/// A built-in Nautilus data type. +/// +/// Not recommended for storing large amounts of data, as the largest variant is significantly +/// larger (10x) than the smallest. #[repr(C)] #[derive(Clone, Debug)] -#[allow(clippy::large_enum_variant)] // TODO: Optimize this (largest variant 1008 vs 136 bytes) +#[allow(clippy::large_enum_variant)] pub enum Data { Delta(OrderBookDelta), Deltas(OrderBookDeltas_API), - Depth10(OrderBookDepth10), + Depth10(OrderBookDepth10), // This variant is significantly larger Quote(QuoteTick), Trade(TradeTick), Bar(Bar), diff --git a/nautilus_core/model/src/data/stubs.rs b/nautilus_core/model/src/data/stubs.rs index 4f50155d1f5f..7dc2ebec20f1 100644 --- a/nautilus_core/model/src/data/stubs.rs +++ b/nautilus_core/model/src/data/stubs.rs @@ -13,6 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +//! Type stubs to facilitate testing. + use rstest::fixture; use super::OrderBookDelta; diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 85ee4062b12a..c185e6009de6 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -1048,6 +1048,12 @@ typedef struct Bar_t { uint64_t ts_init; } Bar_t; +/** + * A built-in Nautilus data type. + * + * Not recommended for storing large amounts of data, as the largest variant is significantly + * larger (10x) than the smallest. + */ typedef enum Data_t_Tag { DELTA, DELTAS, diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 0a3f2e6218f2..185108e6d0dc 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -576,6 +576,10 @@ cdef extern from "../includes/model.h": # The UNIX timestamp (nanoseconds) when the struct was initialized. uint64_t ts_init; + # A built-in Nautilus data type. + # + # Not recommended for storing large amounts of data, as the largest variant is significantly + # larger (10x) than the smallest. cpdef enum Data_t_Tag: DELTA, DELTAS, From ab55a2a529fd899269c450aa6aa96822410d5237 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 23 Apr 2024 19:45:25 +1000 Subject: [PATCH 029/193] Continue Cache in Rust --- nautilus_core/common/src/cache/mod.rs | 96 ++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 3864f3a9fc84..f123e7548404 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -31,28 +31,29 @@ use nautilus_model::{ quote::QuoteTick, trade::TradeTick, }, - enums::{OrderSide, PositionSide}, + enums::{OrderSide, PositionSide, PriceType}, identifiers::{ account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, - position_id::PositionId, strategy_id::StrategyId, venue::Venue, + order_list_id::OrderListId, position_id::PositionId, strategy_id::StrategyId, venue::Venue, venue_order_id::VenueOrderId, }, instruments::{synthetic::SyntheticInstrument, InstrumentAny}, orderbook::book::OrderBook, - orders::base::OrderAny, + orders::{base::OrderAny, list::OrderList}, polymorphism::{ GetClientOrderId, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetOrderSide, GetStrategyId, }, position::Position, - types::currency::Currency, + types::{currency::Currency, price::Price}, }; use ustr::Ustr; use self::database::CacheDatabaseAdapter; use crate::{enums::SerializationEncoding, interface::account::Account}; +/// The configuration for `Cache` instances. pub struct CacheConfig { pub encoding: SerializationEncoding, pub timestamps_as_iso8601: bool, @@ -105,6 +106,7 @@ impl Default for CacheConfig { } } +/// A key-value lookup index for a `Cache`. pub struct CacheIndex { venue_account: HashMap, venue_orders: HashMap>, @@ -168,6 +170,7 @@ impl CacheIndex { } } +/// A common in-memory `Cache` for market and execution related data. pub struct Cache { config: CacheConfig, index: CacheIndex, @@ -182,7 +185,7 @@ pub struct Cache { synthetics: HashMap, accounts: HashMap>, orders: HashMap, - // order_lists: HashMap>, TODO: Need `OrderList` + order_lists: HashMap>, positions: HashMap, position_snapshots: HashMap>, } @@ -240,7 +243,7 @@ impl Cache { synthetics: HashMap::new(), accounts: HashMap::new(), orders: HashMap::new(), - // order_lists: HashMap>, TODO: Need `OrderList` + order_lists: HashMap::new(), positions: HashMap::new(), position_snapshots: HashMap::new(), } @@ -317,13 +320,6 @@ impl Cache { Ok(()) } - // pub fn cache_order_lists(&mut self) -> anyhow::Result<()> { - // - // - // info!("Cached {} order lists from database", self.general.len()); - // Ok(()) - // } - pub fn cache_positions(&mut self) -> anyhow::Result<()> { self.positions = match &self.database { Some(db) => db.load_positions()?, @@ -1185,12 +1181,78 @@ impl Cache { self.orders(venue, instrument_id, strategy_id, side).len() } - // -- DATA QUERIES -------------------------------------------------------- + // -- GENERAL ------------------------------------------------------------- - pub fn get(&self, key: &str) -> anyhow::Result>> { + pub fn get(&self, key: &str) -> anyhow::Result> { check_valid_string(key, stringify!(key))?; - Ok(self.general.get(key)) + Ok(self.general.get(key).map(|x| x.as_slice())) + } + + // -- DATA QUERIES -------------------------------------------------------- + + pub fn price(&self, instrument_id: &InstrumentId, price_type: PriceType) -> Option { + match price_type { + PriceType::Bid => self + .quotes + .get(instrument_id) + .and_then(|quotes| quotes.front().map(|quote| quote.bid_price)), + PriceType::Ask => self + .quotes + .get(instrument_id) + .and_then(|quotes| quotes.front().map(|quote| quote.ask_price)), + PriceType::Mid => self.quotes.get(instrument_id).and_then(|quotes| { + quotes.front().map(|quote| { + Price::new( + (quote.ask_price.as_f64() + quote.bid_price.as_f64()) / 2.0, + quote.bid_price.precision + 1, + ) + .expect("Error calculating mid price") + }) + }), + PriceType::Last => self + .trades + .get(instrument_id) + .and_then(|trades| trades.front().map(|trade| trade.price)), + } + } + + pub fn quote_ticks(&self, instrument_id: &InstrumentId) -> Option> { + self.quotes + .get(instrument_id) + .map(|quotes| quotes.iter().cloned().collect()) + } + + pub fn trade_ticks(&self, instrument_id: &InstrumentId) -> Option> { + self.trades + .get(instrument_id) + .map(|trades| trades.iter().cloned().collect()) + } + + pub fn bars(&self, bar_type: &BarType) -> Option> { + self.bars + .get(bar_type) + .map(|bars| bars.iter().cloned().collect()) + } + + pub fn order_book(&self, instrument_id: &InstrumentId) -> Option<&OrderBook> { + self.books.get(instrument_id) + } + + pub fn quote_tick(&self, instrument_id: &InstrumentId) -> Option<&QuoteTick> { + self.quotes + .get(instrument_id) + .and_then(|quotes| quotes.front()) + } + + pub fn trade_tick(&self, instrument_id: &InstrumentId) -> Option<&TradeTick> { + self.trades + .get(instrument_id) + .and_then(|trades| trades.front()) + } + + pub fn bar(&self, bar_type: &BarType) -> Option<&Bar> { + self.bars.get(bar_type).and_then(|bars| bars.front()) } } @@ -1245,6 +1307,6 @@ mod tests { cache.add(key, value.clone()).unwrap(); let result = cache.get(key).unwrap(); - assert_eq!(result, Some(&value)); + assert_eq!(result, Some(&value.as_slice()).copied()); } } From 828ca94b2bbda0e46d72b84954d3cf2638c1d394 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 23 Apr 2024 21:56:19 +1000 Subject: [PATCH 030/193] Fix clippy lints --- nautilus_core/cli/src/database/postgres.rs | 44 +++++++++------------- nautilus_core/common/src/cache/mod.rs | 16 ++++++-- nautilus_core/common/src/timer.rs | 4 +- 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/nautilus_core/cli/src/database/postgres.rs b/nautilus_core/cli/src/database/postgres.rs index 89a7ff1a05f8..5536276b5499 100644 --- a/nautilus_core/cli/src/database/postgres.rs +++ b/nautilus_core/cli/src/database/postgres.rs @@ -19,7 +19,7 @@ use sqlx::PgPool; use crate::opt::{DatabaseCommand, DatabaseOpt}; -/// Scans current path with keyword nautilus_trader and build schema dir +/// Scans current path with keyword `nautilus_trader` and build schema dir fn get_schema_dir() -> anyhow::Result { std::env::var("SCHEMA_DIR").or_else(|_| { let nautilus_git_repo_name = "nautilus_trader"; @@ -46,7 +46,7 @@ pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> a Err(err) => error!("Error creating schema public: {:?}", err), } // create role if not exists - match sqlx::query(format!("CREATE ROLE {} PASSWORD '{}' LOGIN;", database, password).as_str()) + match sqlx::query(format!("CREATE ROLE {database} PASSWORD '{password}' LOGIN;").as_str()) .execute(pg) .await { @@ -73,7 +73,7 @@ pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> a } } // grant connect - match sqlx::query(format!("GRANT CONNECT ON DATABASE {0} TO {0};", database).as_str()) + match sqlx::query(format!("GRANT CONNECT ON DATABASE {database} TO {database};").as_str()) .execute(pg) .await { @@ -84,7 +84,7 @@ pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> a ), } // grant all schema privileges to the role - match sqlx::query(format!("GRANT ALL PRIVILEGES ON SCHEMA public TO {};", database).as_str()) + match sqlx::query(format!("GRANT ALL PRIVILEGES ON SCHEMA public TO {database};").as_str()) .execute(pg) .await { @@ -96,11 +96,7 @@ pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> a } // grant all table privileges to the role match sqlx::query( - format!( - "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {};", - database - ) - .as_str(), + format!("GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {database};").as_str(), ) .execute(pg) .await @@ -113,11 +109,7 @@ pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> a } // grant all sequence privileges to the role match sqlx::query( - format!( - "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {};", - database - ) - .as_str(), + format!("GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {database};").as_str(), ) .execute(pg) .await @@ -130,11 +122,7 @@ pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> a } // grant all function privileges to the role match sqlx::query( - format!( - "GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO {};", - database - ) - .as_str(), + format!("GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO {database};").as_str(), ) .execute(pg) .await @@ -151,7 +139,7 @@ pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> a pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> { // execute drop owned - match sqlx::query(format!("DROP OWNED BY {}", database).as_str()) + match sqlx::query(format!("DROP OWNED BY {database}").as_str()) .execute(pg) .await { @@ -159,7 +147,7 @@ pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> Err(err) => error!("Error dropping owned by role {}: {:?}", database, err), } // revoke connect - match sqlx::query(format!("REVOKE CONNECT ON DATABASE {0} FROM {0};", database).as_str()) + match sqlx::query(format!("REVOKE CONNECT ON DATABASE {database} FROM {database};").as_str()) .execute(pg) .await { @@ -170,9 +158,11 @@ pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> ), } // revoke privileges - match sqlx::query(format!("REVOKE ALL PRIVILEGES ON DATABASE {0} FROM {0};", database).as_str()) - .execute(pg) - .await + match sqlx::query( + format!("REVOKE ALL PRIVILEGES ON DATABASE {database} FROM {database};").as_str(), + ) + .execute(pg) + .await { Ok(_) => info!("Revoked all privileges from role {}", database), Err(err) => error!( @@ -189,7 +179,7 @@ pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> Err(err) => error!("Error dropping schema public: {:?}", err), } // drop role - match sqlx::query(format!("DROP ROLE IF EXISTS {};", database).as_str()) + match sqlx::query(format!("DROP ROLE IF EXISTS {database};").as_str()) .execute(pg) .await { @@ -218,7 +208,7 @@ pub async fn run_database_command(opt: DatabaseOpt) -> anyhow::Result<()> { pg_connect_options.database, pg_connect_options.password, ) - .await? + .await?; } DatabaseCommand::Drop(config) => { let pg_connect_options = get_postgres_connect_options( @@ -230,7 +220,7 @@ pub async fn run_database_command(opt: DatabaseOpt) -> anyhow::Result<()> { ) .unwrap(); let pg = connect_pg(pg_connect_options.clone().into()).await?; - drop_postgres(&pg, pg_connect_options.database).await? + drop_postgres(&pg, pg_connect_options.database).await?; } } Ok(()) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index f123e7548404..54cd41f8e50b 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -1186,11 +1186,12 @@ impl Cache { pub fn get(&self, key: &str) -> anyhow::Result> { check_valid_string(key, stringify!(key))?; - Ok(self.general.get(key).map(|x| x.as_slice())) + Ok(self.general.get(key).map(std::vec::Vec::as_slice)) } // -- DATA QUERIES -------------------------------------------------------- + #[must_use] pub fn price(&self, instrument_id: &InstrumentId, price_type: PriceType) -> Option { match price_type { PriceType::Bid => self @@ -1217,40 +1218,47 @@ impl Cache { } } + #[must_use] pub fn quote_ticks(&self, instrument_id: &InstrumentId) -> Option> { self.quotes .get(instrument_id) - .map(|quotes| quotes.iter().cloned().collect()) + .map(|quotes| quotes.iter().copied().collect()) } + #[must_use] pub fn trade_ticks(&self, instrument_id: &InstrumentId) -> Option> { self.trades .get(instrument_id) - .map(|trades| trades.iter().cloned().collect()) + .map(|trades| trades.iter().copied().collect()) } + #[must_use] pub fn bars(&self, bar_type: &BarType) -> Option> { self.bars .get(bar_type) - .map(|bars| bars.iter().cloned().collect()) + .map(|bars| bars.iter().copied().collect()) } + #[must_use] pub fn order_book(&self, instrument_id: &InstrumentId) -> Option<&OrderBook> { self.books.get(instrument_id) } + #[must_use] pub fn quote_tick(&self, instrument_id: &InstrumentId) -> Option<&QuoteTick> { self.quotes .get(instrument_id) .and_then(|quotes| quotes.front()) } + #[must_use] pub fn trade_tick(&self, instrument_id: &InstrumentId) -> Option<&TradeTick> { self.trades .get(instrument_id) .and_then(|trades| trades.front()) } + #[must_use] pub fn bar(&self, bar_type: &BarType) -> Option<&Bar> { self.bars.get(bar_type).and_then(|bars| bars.front()) } diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 0c8530004243..919b1541e7e3 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -480,7 +480,7 @@ mod tests { // Create a new LiveTimer with no stop time let clock = get_atomic_clock_realtime(); - let start_time = UnixNanos::from(clock.get_time_ns()); + let start_time = clock.get_time_ns(); let interval_ns = 100 * NANOSECONDS_IN_MILLISECOND; let mut timer = LiveTimer::new("TEST_TIMER", interval_ns, start_time, None, handler).unwrap(); @@ -504,7 +504,7 @@ mod tests { // Create a new LiveTimer with a stop time let clock = get_atomic_clock_realtime(); - let start_time = UnixNanos::from(clock.get_time_ns()); + let start_time = clock.get_time_ns(); let interval_ns = 100 * NANOSECONDS_IN_MILLISECOND; let stop_time = start_time + 500 * NANOSECONDS_IN_MILLISECOND; let mut timer = LiveTimer::new( From 1e342164fa53752c63e007957fdf9ec53add9e72 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 24 Apr 2024 07:40:47 +1000 Subject: [PATCH 031/193] Fix TimeBarAggregator.stop setting timer name to None --- nautilus_trader/data/aggregation.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/nautilus_trader/data/aggregation.pyx b/nautilus_trader/data/aggregation.pyx index 1fdf79ea5156..5cbbfde4bdaa 100644 --- a/nautilus_trader/data/aggregation.pyx +++ b/nautilus_trader/data/aggregation.pyx @@ -657,7 +657,6 @@ cdef class TimeBarAggregator(BarAggregator): Stop the bar aggregator. """ self._clock.cancel_timer(str(self.bar_type)) - self._timer_name = None cdef timedelta _get_interval(self): cdef BarAggregation aggregation = self.bar_type.spec.aggregation From b008fb32bdc644f858600eb4d6d23a10893fae09 Mon Sep 17 00:00:00 2001 From: Nisayo Date: Tue, 23 Apr 2024 23:48:06 +0200 Subject: [PATCH 032/193] Add CFD and Commodity support for Interactive Brokers (#1604) --- .../adapters/interactive_brokers/common.py | 13 +- .../parsing/instruments.py | 169 +++++++++- nautilus_trader/model/instruments/__init__.py | 4 + nautilus_trader/model/instruments/cfd.pxd | 30 ++ nautilus_trader/model/instruments/cfd.pyx | 306 ++++++++++++++++++ .../model/instruments/commodity.pxd | 30 ++ .../model/instruments/commodity.pyx | 301 +++++++++++++++++ .../arrow/implementations/instruments.py | 38 +++ 8 files changed, 886 insertions(+), 5 deletions(-) create mode 100644 nautilus_trader/model/instruments/cfd.pxd create mode 100644 nautilus_trader/model/instruments/cfd.pyx create mode 100644 nautilus_trader/model/instruments/commodity.pxd create mode 100644 nautilus_trader/model/instruments/commodity.pyx diff --git a/nautilus_trader/adapters/interactive_brokers/common.py b/nautilus_trader/adapters/interactive_brokers/common.py index 76f313b7e050..fb3eab2b10b4 100644 --- a/nautilus_trader/adapters/interactive_brokers/common.py +++ b/nautilus_trader/adapters/interactive_brokers/common.py @@ -101,7 +101,18 @@ class IBContract(NautilusConfig, frozen=True, repr_omit_defaults=True): """ - secType: Literal["CASH", "STK", "OPT", "FUT", "FOP", "CONTFUT", "CRYPTO", ""] = "" + secType: Literal[ + "CASH", + "STK", + "OPT", + "FUT", + "FOP", + "CONTFUT", + "CRYPTO", + "CFD", + "CMDTY", + "", + ] = "" conId: int = 0 exchange: str = "" primaryExchange: str = "" diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index 8c7f2d44d009..b982baaf8911 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -32,6 +32,8 @@ from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.instruments import Cfd +from nautilus_trader.model.instruments import Commodity from nautilus_trader.model.instruments import CryptoPerpetual from nautilus_trader.model.instruments import CurrencyPair from nautilus_trader.model.instruments import Equity @@ -74,8 +76,13 @@ "NYBOT", # US "SNFE", # AU ] +VENUES_CFD = [ + "IBCFD", # self named, in fact mapping to "SMART" when parsing +] +VENUES_CMDTY = ["IBCMDTY"] # self named, in fact mapping to "SMART" when parsing RE_CASH = re.compile(r"^(?P[A-Z]{3})\/(?P[A-Z]{3})$") +RE_CFD_CASH = re.compile(r"^(?P[A-Z]{3})\.(?P[A-Z]{3})$") RE_OPT = re.compile( r"^(?P^[A-Z]{1,6})(?P\d{6})(?P[CP])(?P\d{5})(?P\d{3})$", ) @@ -116,6 +123,7 @@ def sec_type_to_asset_class(sec_type: str) -> AssetClass: "IND": "INDEX", "CASH": "FX", "BOND": "DEBT", + "CMDTY": "COMMODITY", } return asset_class_from_str(mapping.get(sec_type, sec_type)) @@ -145,6 +153,10 @@ def parse_instrument( return parse_forex_contract(details=contract_details, instrument_id=instrument_id) elif security_type == "CRYPTO": return parse_crypto_contract(details=contract_details, instrument_id=instrument_id) + elif security_type == "CFD": + return parse_cfd_contract(details=contract_details, instrument_id=instrument_id) + elif security_type == "CMDTY": + return parse_commodity_contract(details=contract_details, instrument_id=instrument_id) else: raise ValueError(f"Unknown {security_type=}") @@ -319,6 +331,99 @@ def parse_crypto_contract( ) +def parse_cfd_contract( + details: IBContractDetails, + instrument_id: InstrumentId, +) -> Cfd: + price_precision: int = _tick_size_to_precision(details.minTick) + size_precision: int = _tick_size_to_precision(details.minSize) + timestamp = time.time_ns() + if RE_CFD_CASH.match(details.contract.localSymbol): + return Cfd( + instrument_id=instrument_id, + raw_symbol=Symbol(details.contract.localSymbol), + asset_class=sec_type_to_asset_class(details.underSecType), + base_currency=Currency.from_str(details.contract.symbol), + quote_currency=Currency.from_str(details.contract.currency), + price_precision=price_precision, + size_precision=size_precision, + price_increment=Price(details.minTick, price_precision), + size_increment=Quantity(details.sizeIncrement, size_precision), + lot_size=None, + max_quantity=None, + min_quantity=None, + max_notional=None, + min_notional=None, + max_price=None, + min_price=None, + margin_init=Decimal(0), + margin_maint=Decimal(0), + maker_fee=Decimal(0), + taker_fee=Decimal(0), + ts_event=timestamp, + ts_init=timestamp, + info=contract_details_to_dict(details), + ) + else: + return Cfd( + instrument_id=instrument_id, + raw_symbol=Symbol(details.contract.localSymbol), + asset_class=sec_type_to_asset_class(details.underSecType), + quote_currency=Currency.from_str(details.contract.currency), + price_precision=price_precision, + size_precision=size_precision, + price_increment=Price(details.minTick, price_precision), + size_increment=Quantity(details.sizeIncrement, size_precision), + lot_size=None, + max_quantity=None, + min_quantity=None, + max_notional=None, + min_notional=None, + max_price=None, + min_price=None, + margin_init=Decimal(0), + margin_maint=Decimal(0), + maker_fee=Decimal(0), + taker_fee=Decimal(0), + ts_event=timestamp, + ts_init=timestamp, + info=contract_details_to_dict(details), + ) + + +def parse_commodity_contract( + details: IBContractDetails, + instrument_id: InstrumentId, +) -> Commodity: + price_precision: int = _tick_size_to_precision(details.minTick) + size_precision: int = _tick_size_to_precision(details.minSize) + timestamp = time.time_ns() + return Commodity( + instrument_id=instrument_id, + raw_symbol=Symbol(details.contract.localSymbol), + asset_class=AssetClass.COMMODITY, + quote_currency=Currency.from_str(details.contract.currency), + price_precision=price_precision, + size_precision=size_precision, + price_increment=Price(details.minTick, price_precision), + size_increment=Quantity(details.sizeIncrement, size_precision), + lot_size=None, + max_quantity=None, + min_quantity=None, + max_notional=None, + min_notional=None, + max_price=None, + min_price=None, + margin_init=Decimal(0), + margin_maint=Decimal(0), + maker_fee=Decimal(0), + taker_fee=Decimal(0), + ts_event=timestamp, + ts_init=timestamp, + info=contract_details_to_dict(details), + ) + + def decade_digit(last_digit: str, contract: IBContract) -> int: if year := contract.lastTradeDateOrContractMonth[:4]: return int(year[2:3]) @@ -341,12 +446,21 @@ def ib_contract_to_instrument_id( def ib_contract_to_instrument_id_strict_symbology(contract: IBContract) -> InstrumentId: - symbol = f"{contract.localSymbol}={contract.secType}" - venue = (contract.primaryExchange or contract.exchange).replace(".", "/") + if contract.secType == "CFD": + symbol = f"{contract.localSymbol}={contract.secType}" + venue = "IBCFD" + elif contract.secType == "CMDTY": + symbol = f"{contract.localSymbol}={contract.secType}" + venue = "IBCMDTY" + else: + symbol = f"{contract.localSymbol}={contract.secType}" + venue = (contract.primaryExchange or contract.exchange).replace(".", "/") return InstrumentId.from_str(f"{symbol}.{venue}") -def ib_contract_to_instrument_id_simplified_symbology(contract: IBContract) -> InstrumentId: +def ib_contract_to_instrument_id_simplified_symbology( # noqa: C901 (too complex) + contract: IBContract, +) -> InstrumentId: security_type = contract.secType if security_type == "STK": symbol = (contract.localSymbol or contract.symbol).replace(" ", "-") @@ -372,6 +486,19 @@ def ib_contract_to_instrument_id_simplified_symbology(contract: IBContract) -> I f"{contract.localSymbol}".replace(".", "/") or f"{contract.symbol}/{contract.currency}" ) venue = contract.exchange + elif security_type == "CFD": + if m := RE_CFD_CASH.match(contract.localSymbol): + symbol = ( + f"{contract.localSymbol}".replace(".", "/") + or f"{contract.symbol}/{contract.currency}" + ) + venue = "IBCFD" + else: + symbol = (contract.symbol).replace(" ", "-") + venue = "IBCFD" + elif security_type == "CMDTY": + symbol = (contract.symbol).replace(" ", "-") + venue = "IBCMDTY" else: symbol = None venue = None @@ -402,6 +529,18 @@ def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) - primaryExchange=exchange, localSymbol=local_symbol, ) + elif security_type == "CFD": + return IBContract( + secType=security_type, + exchange="SMART", + localSymbol=local_symbol, # by IB is a cfd's local symbol of STK with a "n" as tail, e.g. "NVDAn". " + ) + elif security_type == "CMDTY": + return IBContract( + secType=security_type, + exchange="SMART", + localSymbol=local_symbol, + ) else: return IBContract( secType=security_type, @@ -410,7 +549,9 @@ def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) - ) -def instrument_id_to_ib_contract_simplified_symbology(instrument_id: InstrumentId) -> IBContract: +def instrument_id_to_ib_contract_simplified_symbology( # noqa: C901 (too complex) + instrument_id: InstrumentId, +) -> IBContract: if instrument_id.venue.value in VENUES_CASH and ( m := RE_CASH.match(instrument_id.symbol.value) ): @@ -464,6 +605,26 @@ def instrument_id_to_ib_contract_simplified_symbology(instrument_id: InstrumentI ) else: raise ValueError(f"Cannot parse {instrument_id}, use 2-digit year for FUT and FOP") + elif instrument_id.venue.value in VENUES_CFD: + if m := RE_CASH.match(instrument_id.symbol.value): + return IBContract( + secType="CFD", + exchange="SMART", + symbol=m["symbol"], + localSymbol=f"{m['symbol']}.{m['currency']}", + ) + else: + return IBContract( + secType="CFD", + exchange="SMART", + symbol=f"{instrument_id.symbol.value}".replace("-", " "), + ) + elif instrument_id.venue.value in VENUES_CMDTY: + return IBContract( + secType="CMDTY", + exchange="SMART", + symbol=f"{instrument_id.symbol.value}".replace("-", " "), + ) elif instrument_id.venue.value == "InteractiveBrokers": # keep until a better approach # This will allow to make Instrument request using IBContract from within Strategy # and depending on the Strategy requirement diff --git a/nautilus_trader/model/instruments/__init__.py b/nautilus_trader/model/instruments/__init__.py index 3a9130e84f47..d18f8259802a 100644 --- a/nautilus_trader/model/instruments/__init__.py +++ b/nautilus_trader/model/instruments/__init__.py @@ -20,6 +20,8 @@ from nautilus_trader.model.instruments.base import Instrument from nautilus_trader.model.instruments.base import instruments_from_pyo3 from nautilus_trader.model.instruments.betting import BettingInstrument +from nautilus_trader.model.instruments.cfd import Cfd +from nautilus_trader.model.instruments.commodity import Commodity from nautilus_trader.model.instruments.crypto_future import CryptoFuture from nautilus_trader.model.instruments.crypto_perpetual import CryptoPerpetual from nautilus_trader.model.instruments.currency_pair import CurrencyPair @@ -42,6 +44,8 @@ "FuturesSpread", "OptionsContract", "OptionsSpread", + "Cfd", + "Commodity", "SyntheticInstrument", "instruments_from_pyo3", ] diff --git a/nautilus_trader/model/instruments/cfd.pxd b/nautilus_trader/model/instruments/cfd.pxd new file mode 100644 index 000000000000..8c50fadcfb9c --- /dev/null +++ b/nautilus_trader/model/instruments/cfd.pxd @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.model.instruments.base cimport Instrument + + +cdef class Cfd(Instrument): + cdef readonly str isin + """The instruments International Securities Identification Number (ISIN).\n\n:returns: `str` or ``None``""" + + @staticmethod + cdef Cfd from_dict_c(dict values) + + @staticmethod + cdef dict to_dict_c(Cfd obj) + + @staticmethod + cdef Cfd from_pyo3_c(pyo3_instrument) diff --git a/nautilus_trader/model/instruments/cfd.pyx b/nautilus_trader/model/instruments/cfd.pyx new file mode 100644 index 000000000000..20c35197331d --- /dev/null +++ b/nautilus_trader/model/instruments/cfd.pyx @@ -0,0 +1,306 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +from decimal import Decimal + +from libc.stdint cimport uint64_t + +from nautilus_trader.core.correctness cimport Condition +from nautilus_trader.core.rust.model cimport AssetClass +from nautilus_trader.core.rust.model cimport CurrencyType +from nautilus_trader.core.rust.model cimport InstrumentClass +from nautilus_trader.model.functions cimport asset_class_from_str +from nautilus_trader.model.functions cimport asset_class_to_str +from nautilus_trader.model.identifiers cimport InstrumentId +from nautilus_trader.model.identifiers cimport Symbol +from nautilus_trader.model.instruments.base cimport Instrument +from nautilus_trader.model.objects cimport Currency +from nautilus_trader.model.objects cimport Money +from nautilus_trader.model.objects cimport Price +from nautilus_trader.model.objects cimport Quantity + + +cdef class Cfd(Instrument): + """ + Represents a generic currency pair instrument in a spot/cash market. + + Can represent both Fiat FX and Cryptocurrency pairs. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument ID for the instrument. + raw_symbol : Symbol + The raw/local/native symbol for the instrument, assigned by the venue. + asset_class : AssetClass + The cfd contract asset class. + base_currency : Currency, optional + The base currency. + quote_currency : Currency + The quote currency. + price_precision : int + The price decimal precision. + size_precision : int + The trading size decimal precision. + price_increment : Price + The minimum price increment (tick size). + size_increment : Quantity + The minimum size increment. + margin_init : Decimal + The initial (order) margin requirement in percentage of order value. + margin_maint : Decimal + The maintenance (position) margin in percentage of position value. + maker_fee : Decimal + The fee rate for liquidity makers as a percentage of order value. + taker_fee : Decimal + The fee rate for liquidity takers as a percentage of order value. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the data event occurred. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the data object was initialized. + lot_size : Quantity, optional + The rounded lot unit size. + max_quantity : Quantity, optional + The maximum allowable order quantity. + min_quantity : Quantity, optional + The minimum allowable order quantity. + max_notional : Money, optional + The maximum allowable order notional value. + min_notional : Money, optional + The minimum allowable order notional value. + max_price : Price, optional + The maximum allowable quoted price. + min_price : Price, optional + The minimum allowable quoted price. + tick_scheme_name : str, optional + The name of the tick scheme. + info : dict[str, object], optional + The additional instrument information. + + Raises + ------ + ValueError + If `tick_scheme_name` is not a valid string. + ValueError + If `price_precision` is negative (< 0). + ValueError + If `size_precision` is negative (< 0). + ValueError + If `price_increment` is not positive (> 0). + ValueError + If `size_increment` is not positive (> 0). + ValueError + If `price_precision` is not equal to price_increment.precision. + ValueError + If `size_increment` is not equal to size_increment.precision. + ValueError + If `lot_size` is not positive (> 0). + ValueError + If `max_quantity` is not positive (> 0). + ValueError + If `min_quantity` is negative (< 0). + ValueError + If `max_notional` is not positive (> 0). + ValueError + If `min_notional` is negative (< 0). + ValueError + If `max_price` is not positive (> 0). + ValueError + If `min_price` is negative (< 0). + """ + + def __init__( + self, + InstrumentId instrument_id not None, + Symbol raw_symbol not None, + AssetClass asset_class, + Currency quote_currency not None, + int price_precision, + int size_precision, + Price price_increment not None, + Quantity size_increment not None, + margin_init not None: Decimal, + margin_maint not None: Decimal, + maker_fee not None: Decimal, + taker_fee not None: Decimal, + uint64_t ts_event, + uint64_t ts_init, + Currency base_currency: Currency | None = None, + Quantity lot_size: Quantity | None = None, + Quantity max_quantity: Quantity | None = None, + Quantity min_quantity: Quantity | None = None, + Money max_notional: Money | None = None, + Money min_notional: Money | None = None, + Price max_price: Price | None = None, + Price min_price: Price | None = None, + str tick_scheme_name = None, + dict info = None, + ): + + super().__init__( + instrument_id=instrument_id, + raw_symbol=raw_symbol, + asset_class=asset_class, + instrument_class=InstrumentClass.CFD, + quote_currency=quote_currency, + is_inverse=False, + price_precision=price_precision, + size_precision=size_precision, + price_increment=price_increment, + size_increment=size_increment, + multiplier=Quantity.from_int_c(1), + lot_size=lot_size, + max_quantity=max_quantity, + min_quantity=min_quantity, + max_notional=max_notional, + min_notional=min_notional, + max_price=max_price, + min_price=min_price, + margin_init=margin_init, + margin_maint=margin_maint, + maker_fee=maker_fee, + taker_fee=taker_fee, + tick_scheme_name=tick_scheme_name, + ts_event=ts_event, + ts_init=ts_init, + info=info, + ) + + @staticmethod + cdef Cfd from_dict_c(dict values): + Condition.not_none(values, "values") + cdef str base_c = values["base_currency"] + cdef str lot_s = values["lot_size"] + cdef str max_q = values["max_quantity"] + cdef str min_q = values["min_quantity"] + cdef str max_n = values["max_notional"] + cdef str min_n = values["min_notional"] + cdef str max_p = values["max_price"] + cdef str min_p = values["min_price"] + return Cfd( + instrument_id=InstrumentId.from_str_c(values["id"]), + raw_symbol=Symbol(values["raw_symbol"]), + asset_class=asset_class_from_str(values["asset_class"]), + quote_currency=Currency.from_str_c(values["quote_currency"]), + price_precision=values["price_precision"], + size_precision=values["size_precision"], + price_increment=Price.from_str_c(values["price_increment"]), + size_increment=Quantity.from_str_c(values["size_increment"]), + base_currency=Currency.from_str_c(values["base_currency"]) if base_c is not None else None, + lot_size=Quantity.from_str_c(lot_s) if lot_s is not None else None, + max_quantity=Quantity.from_str_c(max_q) if max_q is not None else None, + min_quantity=Quantity.from_str_c(min_q) if min_q is not None else None, + max_notional=Money.from_str_c(max_n) if max_n is not None else None, + min_notional=Money.from_str_c(min_n) if min_n is not None else None, + max_price=Price.from_str_c(max_p) if max_p is not None else None, + min_price=Price.from_str_c(min_p) if min_p is not None else None, + margin_init=Decimal(values["margin_init"]), + margin_maint=Decimal(values["margin_maint"]), + maker_fee=Decimal(values["maker_fee"]), + taker_fee=Decimal(values["taker_fee"]), + ts_event=values["ts_event"], + ts_init=values["ts_init"], + info=values["info"], + ) + + @staticmethod + cdef dict to_dict_c(Cfd obj): + Condition.not_none(obj, "obj") + return { + "type": "Cfd", + "id": obj.id.to_str(), + "raw_symbol": obj.raw_symbol.to_str(), + "asset_class": asset_class_to_str(obj.asset_class), + "quote_currency": obj.quote_currency.code, + "price_precision": obj.price_precision, + "price_increment": str(obj.price_increment), + "size_precision": obj.size_precision, + "size_increment": str(obj.size_increment), + "lot_size": str(obj.lot_size) if obj.lot_size is not None else None, + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, + "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, + "margin_init": str(obj.margin_init), + "margin_maint": str(obj.margin_maint), + "maker_fee": str(obj.maker_fee), + "taker_fee": str(obj.taker_fee), + "ts_event": obj.ts_event, + "ts_init": obj.ts_init, + "info": obj.info, + } + + @staticmethod + def from_dict(dict values) -> Cfd: + """ + Return an instrument from the given initialization values. + + Parameters + ---------- + values : dict[str, object] + The values to initialize the instrument with. + + Returns + ------- + Cfd + + """ + return Cfd.from_dict_c(values) + + @staticmethod + def to_dict(Cfd obj) -> dict[str, object]: + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, object] + + """ + return Cfd.to_dict_c(obj) + + @staticmethod + cdef Cfd from_pyo3_c(pyo3_instrument): + return Cfd( + instrument_id=InstrumentId.from_str_c(pyo3_instrument.id.value), + raw_symbol=Symbol(pyo3_instrument.id.symbol.value), + asset_class=asset_class_from_str(str(pyo3_instrument.asset_class)), + quote_currency=Currency.from_str_c(pyo3_instrument.quote_currency.code), + price_precision=pyo3_instrument.price_precision, + size_precision=pyo3_instrument.size_precision, + price_increment=Price.from_raw_c(pyo3_instrument.price_increment.raw, pyo3_instrument.price_precision), + size_increment=Quantity.from_raw_c(pyo3_instrument.size_increment.raw, pyo3_instrument.size_precision), + base_currency=Currency.from_str_c(pyo3_instrument.base_currency.code) if pyo3_instrument.base_currency is not None else None, + lot_size=Quantity.from_str_c(pyo3_instrument.lot_size) if pyo3_instrument.lot_size is not None else None, + max_quantity=Quantity.from_raw_c(pyo3_instrument.max_quantity.raw, pyo3_instrument.max_quantity.precision) if pyo3_instrument.max_quantity is not None else None, + min_quantity=Quantity.from_raw_c(pyo3_instrument.min_quantity.raw, pyo3_instrument.min_quantity.precision) if pyo3_instrument.min_quantity is not None else None, + max_notional=Money.from_str_c(str(pyo3_instrument.max_notional)) if pyo3_instrument.max_notional is not None else None, + min_notional=Money.from_str_c(str(pyo3_instrument.min_notional)) if pyo3_instrument.min_notional is not None else None, + max_price=Price.from_raw_c(pyo3_instrument.max_price.raw,pyo3_instrument.max_price.precision) if pyo3_instrument.max_price is not None else None, + min_price=Price.from_raw_c(pyo3_instrument.min_price.raw,pyo3_instrument.min_price.precision) if pyo3_instrument.min_price is not None else None, + margin_init=Decimal(pyo3_instrument.margin_init), + margin_maint=Decimal(pyo3_instrument.margin_maint), + maker_fee=Decimal(pyo3_instrument.maker_fee), + taker_fee=Decimal(pyo3_instrument.taker_fee), + ts_event=pyo3_instrument.ts_event, + ts_init=pyo3_instrument.ts_init, + info=pyo3_instrument.info, + ) + + @staticmethod + def from_pyo3(pyo3_instrument): + return Cfd.from_pyo3_c(pyo3_instrument) diff --git a/nautilus_trader/model/instruments/commodity.pxd b/nautilus_trader/model/instruments/commodity.pxd new file mode 100644 index 000000000000..cbd057f151cf --- /dev/null +++ b/nautilus_trader/model/instruments/commodity.pxd @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.model.instruments.base cimport Instrument + + +cdef class Commodity(Instrument): + cdef readonly str isin + """The instruments International Securities Identification Number (ISIN).\n\n:returns: `str` or ``None``""" + + @staticmethod + cdef Commodity from_dict_c(dict values) + + @staticmethod + cdef dict to_dict_c(Commodity obj) + + @staticmethod + cdef Commodity from_pyo3_c(pyo3_instrument) diff --git a/nautilus_trader/model/instruments/commodity.pyx b/nautilus_trader/model/instruments/commodity.pyx new file mode 100644 index 000000000000..32a28094d8b0 --- /dev/null +++ b/nautilus_trader/model/instruments/commodity.pyx @@ -0,0 +1,301 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +from decimal import Decimal + +from libc.stdint cimport uint64_t + +from nautilus_trader.core.correctness cimport Condition +from nautilus_trader.core.rust.model cimport AssetClass +from nautilus_trader.core.rust.model cimport CurrencyType +from nautilus_trader.core.rust.model cimport InstrumentClass +from nautilus_trader.model.functions cimport asset_class_from_str +from nautilus_trader.model.functions cimport asset_class_to_str +from nautilus_trader.model.identifiers cimport InstrumentId +from nautilus_trader.model.identifiers cimport Symbol +from nautilus_trader.model.instruments.base cimport Instrument +from nautilus_trader.model.objects cimport Currency +from nautilus_trader.model.objects cimport Money +from nautilus_trader.model.objects cimport Price +from nautilus_trader.model.objects cimport Quantity + + +cdef class Commodity(Instrument): + """ + Represents a generic currency pair instrument in a spot/cash market. + + Can represent both Fiat FX and Cryptocurrency pairs. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument ID for the instrument. + raw_symbol : Symbol + The raw/local/native symbol for the instrument, assigned by the venue. + asset_class : AssetClass + The Commodity contract asset class. + quote_currency : Currency + The quote currency. + price_precision : int + The price decimal precision. + size_precision : int + The trading size decimal precision. + price_increment : Price + The minimum price increment (tick size). + size_increment : Quantity + The minimum size increment. + margin_init : Decimal + The initial (order) margin requirement in percentage of order value. + margin_maint : Decimal + The maintenance (position) margin in percentage of position value. + maker_fee : Decimal + The fee rate for liquidity makers as a percentage of order value. + taker_fee : Decimal + The fee rate for liquidity takers as a percentage of order value. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the data event occurred. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the data object was initialized. + lot_size : Quantity, optional + The rounded lot unit size. + max_quantity : Quantity, optional + The maximum allowable order quantity. + min_quantity : Quantity, optional + The minimum allowable order quantity. + max_notional : Money, optional + The maximum allowable order notional value. + min_notional : Money, optional + The minimum allowable order notional value. + max_price : Price, optional + The maximum allowable quoted price. + min_price : Price, optional + The minimum allowable quoted price. + tick_scheme_name : str, optional + The name of the tick scheme. + info : dict[str, object], optional + The additional instrument information. + + Raises + ------ + ValueError + If `tick_scheme_name` is not a valid string. + ValueError + If `price_precision` is negative (< 0). + ValueError + If `size_precision` is negative (< 0). + ValueError + If `price_increment` is not positive (> 0). + ValueError + If `size_increment` is not positive (> 0). + ValueError + If `price_precision` is not equal to price_increment.precision. + ValueError + If `size_increment` is not equal to size_increment.precision. + ValueError + If `lot_size` is not positive (> 0). + ValueError + If `max_quantity` is not positive (> 0). + ValueError + If `min_quantity` is negative (< 0). + ValueError + If `max_notional` is not positive (> 0). + ValueError + If `min_notional` is negative (< 0). + ValueError + If `max_price` is not positive (> 0). + ValueError + If `min_price` is negative (< 0). + """ + + def __init__( + self, + InstrumentId instrument_id not None, + Symbol raw_symbol not None, + AssetClass asset_class, + Currency quote_currency not None, + int price_precision, + int size_precision, + Price price_increment not None, + Quantity size_increment not None, + margin_init not None: Decimal, + margin_maint not None: Decimal, + maker_fee not None: Decimal, + taker_fee not None: Decimal, + uint64_t ts_event, + uint64_t ts_init, + Currency base_currency: Currency | None = None, + Quantity lot_size: Quantity | None = None, + Quantity max_quantity: Quantity | None = None, + Quantity min_quantity: Quantity | None = None, + Money max_notional: Money | None = None, + Money min_notional: Money | None = None, + Price max_price: Price | None = None, + Price min_price: Price | None = None, + str tick_scheme_name = None, + dict info = None, + ): + + super().__init__( + instrument_id=instrument_id, + raw_symbol=raw_symbol, + asset_class=asset_class, + instrument_class=InstrumentClass.SPOT, + quote_currency=quote_currency, + is_inverse=False, + price_precision=price_precision, + size_precision=size_precision, + price_increment=price_increment, + size_increment=size_increment, + multiplier=Quantity.from_int_c(1), + lot_size=lot_size, + max_quantity=max_quantity, + min_quantity=min_quantity, + max_notional=max_notional, + min_notional=min_notional, + max_price=max_price, + min_price=min_price, + margin_init=margin_init, + margin_maint=margin_maint, + maker_fee=maker_fee, + taker_fee=taker_fee, + tick_scheme_name=tick_scheme_name, + ts_event=ts_event, + ts_init=ts_init, + info=info, + ) + + @staticmethod + cdef Commodity from_dict_c(dict values): + Condition.not_none(values, "values") + cdef str lot_s = values["lot_size"] + cdef str max_q = values["max_quantity"] + cdef str min_q = values["min_quantity"] + cdef str max_n = values["max_notional"] + cdef str min_n = values["min_notional"] + cdef str max_p = values["max_price"] + cdef str min_p = values["min_price"] + return Commodity( + instrument_id=InstrumentId.from_str_c(values["id"]), + raw_symbol=Symbol(values["raw_symbol"]), + asset_class=asset_class_from_str(values["asset_class"]), + quote_currency=Currency.from_str_c(values["quote_currency"]), + price_precision=values["price_precision"], + size_precision=values["size_precision"], + price_increment=Price.from_str_c(values["price_increment"]), + size_increment=Quantity.from_str_c(values["size_increment"]), + lot_size=Quantity.from_str_c(lot_s) if lot_s is not None else None, + max_quantity=Quantity.from_str_c(max_q) if max_q is not None else None, + min_quantity=Quantity.from_str_c(min_q) if min_q is not None else None, + max_notional=Money.from_str_c(max_n) if max_n is not None else None, + min_notional=Money.from_str_c(min_n) if min_n is not None else None, + max_price=Price.from_str_c(max_p) if max_p is not None else None, + min_price=Price.from_str_c(min_p) if min_p is not None else None, + margin_init=Decimal(values["margin_init"]), + margin_maint=Decimal(values["margin_maint"]), + maker_fee=Decimal(values["maker_fee"]), + taker_fee=Decimal(values["taker_fee"]), + ts_event=values["ts_event"], + ts_init=values["ts_init"], + info=values["info"], + ) + + @staticmethod + cdef dict to_dict_c(Commodity obj): + Condition.not_none(obj, "obj") + return { + "type": "Commodity", + "id": obj.id.to_str(), + "raw_symbol": obj.raw_symbol.to_str(), + "asset_class": asset_class_to_str(obj.asset_class), + "quote_currency": obj.quote_currency.code, + "price_precision": obj.price_precision, + "price_increment": str(obj.price_increment), + "size_precision": obj.size_precision, + "size_increment": str(obj.size_increment), + "lot_size": str(obj.lot_size) if obj.lot_size is not None else None, + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, + "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, + "margin_init": str(obj.margin_init), + "margin_maint": str(obj.margin_maint), + "maker_fee": str(obj.maker_fee), + "taker_fee": str(obj.taker_fee), + "ts_event": obj.ts_event, + "ts_init": obj.ts_init, + "info": obj.info, + } + + @staticmethod + def from_dict(dict values) -> Commodity: + """ + Return an instrument from the given initialization values. + + Parameters + ---------- + values : dict[str, object] + The values to initialize the instrument with. + + Returns + ------- + Commodity + + """ + return Commodity.from_dict_c(values) + + @staticmethod + def to_dict(Commodity obj) -> dict[str, object]: + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, object] + + """ + return Commodity.to_dict_c(obj) + + @staticmethod + cdef Commodity from_pyo3_c(pyo3_instrument): + return Commodity( + instrument_id=InstrumentId.from_str_c(pyo3_instrument.id.value), + raw_symbol=Symbol(pyo3_instrument.id.symbol.value), + asset_class=asset_class_from_str(str(pyo3_instrument.asset_class)), + quote_currency=Currency.from_str_c(pyo3_instrument.quote_currency.code), + price_precision=pyo3_instrument.price_precision, + size_precision=pyo3_instrument.size_precision, + price_increment=Price.from_raw_c(pyo3_instrument.price_increment.raw, pyo3_instrument.price_precision), + size_increment=Quantity.from_raw_c(pyo3_instrument.size_increment.raw, pyo3_instrument.size_precision), + lot_size=Quantity.from_str_c(pyo3_instrument.lot_size) if pyo3_instrument.lot_size is not None else None, + max_quantity=Quantity.from_raw_c(pyo3_instrument.max_quantity.raw, pyo3_instrument.max_quantity.precision) if pyo3_instrument.max_quantity is not None else None, + min_quantity=Quantity.from_raw_c(pyo3_instrument.min_quantity.raw, pyo3_instrument.min_quantity.precision) if pyo3_instrument.min_quantity is not None else None, + max_notional=Money.from_str_c(str(pyo3_instrument.max_notional)) if pyo3_instrument.max_notional is not None else None, + min_notional=Money.from_str_c(str(pyo3_instrument.min_notional)) if pyo3_instrument.min_notional is not None else None, + max_price=Price.from_raw_c(pyo3_instrument.max_price.raw,pyo3_instrument.max_price.precision) if pyo3_instrument.max_price is not None else None, + min_price=Price.from_raw_c(pyo3_instrument.min_price.raw,pyo3_instrument.min_price.precision) if pyo3_instrument.min_price is not None else None, + margin_init=Decimal(pyo3_instrument.margin_init), + margin_maint=Decimal(pyo3_instrument.margin_maint), + maker_fee=Decimal(pyo3_instrument.maker_fee), + taker_fee=Decimal(pyo3_instrument.taker_fee), + ts_event=pyo3_instrument.ts_event, + ts_init=pyo3_instrument.ts_init, + info=pyo3_instrument.info, + ) + + @staticmethod + def from_pyo3(pyo3_instrument): + return Commodity.from_pyo3_c(pyo3_instrument) diff --git a/nautilus_trader/serialization/arrow/implementations/instruments.py b/nautilus_trader/serialization/arrow/implementations/instruments.py index 1ed01afea2f0..7efd14ae3eee 100644 --- a/nautilus_trader/serialization/arrow/implementations/instruments.py +++ b/nautilus_trader/serialization/arrow/implementations/instruments.py @@ -17,6 +17,8 @@ import pyarrow as pa from nautilus_trader.model.instruments import BettingInstrument +from nautilus_trader.model.instruments import Cfd +from nautilus_trader.model.instruments import Commodity from nautilus_trader.model.instruments import CryptoFuture from nautilus_trader.model.instruments import CryptoPerpetual from nautilus_trader.model.instruments import CurrencyPair @@ -245,6 +247,42 @@ "ts_init": pa.uint64(), }, ), + Cfd: pa.schema( + { + "id": pa.dictionary(pa.int64(), pa.string()), + "raw_symbol": pa.string(), + "asset_class": pa.dictionary(pa.int8(), pa.string()), + "currency": pa.dictionary(pa.int16(), pa.string()), + "base_currency": pa.dictionary(pa.int16(), pa.string()), + "quote_currency": pa.dictionary(pa.int16(), pa.string()), + "price_precision": pa.uint8(), + "size_precision": pa.uint8(), + "price_increment": pa.dictionary(pa.int16(), pa.string()), + "size_increment": pa.dictionary(pa.int16(), pa.string()), + "multiplier": pa.dictionary(pa.int16(), pa.string()), + "lot_size": pa.dictionary(pa.int16(), pa.string()), + "underlying": pa.dictionary(pa.int16(), pa.string()), + "ts_event": pa.uint64(), + "ts_init": pa.uint64(), + }, + ), + Commodity: pa.schema( + { + "id": pa.dictionary(pa.int64(), pa.string()), + "raw_symbol": pa.string(), + "asset_class": pa.dictionary(pa.int8(), pa.string()), + "currency": pa.dictionary(pa.int16(), pa.string()), + "quote_currency": pa.dictionary(pa.int16(), pa.string()), + "price_precision": pa.uint8(), + "size_precision": pa.uint8(), + "price_increment": pa.dictionary(pa.int16(), pa.string()), + "size_increment": pa.dictionary(pa.int16(), pa.string()), + "multiplier": pa.dictionary(pa.int16(), pa.string()), + "lot_size": pa.dictionary(pa.int16(), pa.string()), + "ts_event": pa.uint64(), + "ts_init": pa.uint64(), + }, + ), } From b80d8813c9cc39221bf9d6c65f4e8c98a40c4fec Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 24 Apr 2024 07:53:38 +1000 Subject: [PATCH 033/193] Update instrument docstrings --- nautilus_trader/model/instruments/cfd.pyx | 9 +++++++-- nautilus_trader/model/instruments/commodity.pyx | 4 +--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/nautilus_trader/model/instruments/cfd.pyx b/nautilus_trader/model/instruments/cfd.pyx index 20c35197331d..ba9a8d5ab304 100644 --- a/nautilus_trader/model/instruments/cfd.pyx +++ b/nautilus_trader/model/instruments/cfd.pyx @@ -34,7 +34,7 @@ from nautilus_trader.model.objects cimport Quantity cdef class Cfd(Instrument): """ - Represents a generic currency pair instrument in a spot/cash market. + Represents a Contract for Difference (CFD) instrument. Can represent both Fiat FX and Cryptocurrency pairs. @@ -45,7 +45,7 @@ cdef class Cfd(Instrument): raw_symbol : Symbol The raw/local/native symbol for the instrument, assigned by the venue. asset_class : AssetClass - The cfd contract asset class. + The CFD contract asset class. base_currency : Currency, optional The base currency. quote_currency : Currency @@ -119,6 +119,11 @@ cdef class Cfd(Instrument): If `max_price` is not positive (> 0). ValueError If `min_price` is negative (< 0). + + References + ---------- + https://en.wikipedia.org/wiki/Contract_for_difference + """ def __init__( diff --git a/nautilus_trader/model/instruments/commodity.pyx b/nautilus_trader/model/instruments/commodity.pyx index 32a28094d8b0..5f2f4b312ed3 100644 --- a/nautilus_trader/model/instruments/commodity.pyx +++ b/nautilus_trader/model/instruments/commodity.pyx @@ -34,9 +34,7 @@ from nautilus_trader.model.objects cimport Quantity cdef class Commodity(Instrument): """ - Represents a generic currency pair instrument in a spot/cash market. - - Can represent both Fiat FX and Cryptocurrency pairs. + Represents a commodity instrument in a spot/cash market. Parameters ---------- From 536d647f1b35c81204bcb2cd480ad482ea6edbf4 Mon Sep 17 00:00:00 2001 From: imemo88 <167953507+imemo88@users.noreply.github.com> Date: Wed, 24 Apr 2024 13:07:08 +0200 Subject: [PATCH 034/193] Betfair bugfix: Reload cache as a dict of venue and client order ids (#1608) --- nautilus_trader/adapters/betfair/execution.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/adapters/betfair/execution.py b/nautilus_trader/adapters/betfair/execution.py index 502f9d08cdfd..64a10ecf5069 100644 --- a/nautilus_trader/adapters/betfair/execution.py +++ b/nautilus_trader/adapters/betfair/execution.py @@ -602,7 +602,9 @@ async def load_venue_id_mapping_from_cache(self) -> None: self._log.info("Loading venue_id mapping from cache") raw = self._cache.get("betfair_execution_client.venue_order_id_to_client_order_id") or b"{}" self._log.info(f"venue_id_mapping: {raw.decode()=}") - self.venue_order_id_to_client_order_id = msgspec.json.decode(raw) + self.venue_order_id_to_client_order_id = { + VenueOrderId(k): ClientOrderId(v) for k, v in msgspec.json.decode(raw).items() + } def set_venue_id_mapping( self, From e39591a866459147e881150c3e89ef1e501e5dab Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 24 Apr 2024 21:54:28 +1000 Subject: [PATCH 035/193] Add contract activation and expiration handling --- nautilus_trader/backtest/matching_engine.pyx | 20 ++ nautilus_trader/model/instruments/base.pxd | 3 + nautilus_trader/model/instruments/base.pyx | 8 + nautilus_trader/model/orders/base.pxd | 6 +- .../unit_tests/backtest/test_exchange_glbx.py | 230 ++++++++++++++++++ 5 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/backtest/test_exchange_glbx.py diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index c8af523a07c4..1f73bfc10f39 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -31,6 +31,8 @@ from nautilus_trader.common.component cimport TestClock from nautilus_trader.common.component cimport is_logging_initialized from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.data cimport Data +from nautilus_trader.core.datetime cimport format_iso8601 +from nautilus_trader.core.datetime cimport unix_nanos_to_dt from nautilus_trader.core.rust.model cimport AccountType from nautilus_trader.core.rust.model cimport AggressorSide from nautilus_trader.core.rust.model cimport BookType @@ -81,6 +83,7 @@ from nautilus_trader.model.identifiers cimport StrategyId from nautilus_trader.model.identifiers cimport TradeId from nautilus_trader.model.identifiers cimport TraderId from nautilus_trader.model.identifiers cimport VenueOrderId +from nautilus_trader.model.instruments.base cimport EXPIRING_INSTRUMENT_TYPES from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.equity cimport Equity from nautilus_trader.model.objects cimport Money @@ -675,6 +678,23 @@ cdef class OrderMatchingEngine: # Index identifiers self._account_ids[order.trader_id] = account_id + cdef uint64_t now_ns = self._clock.timestamp_ns() + if self.instrument.instrument_class in EXPIRING_INSTRUMENT_TYPES: + if now_ns < self.instrument.activation_ns: + self._generate_order_rejected( + order, + f"Contract {self.instrument.id} not yet active, " + f"activation {format_iso8601(unix_nanos_to_dt(self.instrument.activation_ns))}" + ) + return + elif now_ns > self.instrument.expiration_ns: + self._generate_order_rejected( + order, + f"Contract {self.instrument.id} has expired, " + f"expiration {format_iso8601(unix_nanos_to_dt(self.instrument.expiration_ns))}" + ) + return + cdef: Order parent Order contingenct_order diff --git a/nautilus_trader/model/instruments/base.pxd b/nautilus_trader/model/instruments/base.pxd index d055a43ee72d..987cf9d5bddc 100644 --- a/nautilus_trader/model/instruments/base.pxd +++ b/nautilus_trader/model/instruments/base.pxd @@ -27,6 +27,9 @@ from nautilus_trader.model.objects cimport Quantity from nautilus_trader.model.tick_scheme.base cimport TickScheme +cdef set[InstrumentClass] EXPIRING_INSTRUMENT_TYPES + + cdef class Instrument(Data): cdef TickScheme _tick_scheme diff --git a/nautilus_trader/model/instruments/base.pyx b/nautilus_trader/model/instruments/base.pyx index e625631479fc..030a54fa19fe 100644 --- a/nautilus_trader/model/instruments/base.pyx +++ b/nautilus_trader/model/instruments/base.pyx @@ -37,6 +37,14 @@ from nautilus_trader.model.tick_scheme.base cimport TICK_SCHEMES from nautilus_trader.model.tick_scheme.base cimport get_tick_scheme +EXPIRING_INSTRUMENT_TYPES = { + InstrumentClass.FUTURE, + InstrumentClass.FUTURE_SPREAD, + InstrumentClass.OPTION, + InstrumentClass.OPTION_SPREAD, +} + + cdef class Instrument(Data): """ The base class for all instruments. diff --git a/nautilus_trader/model/orders/base.pxd b/nautilus_trader/model/orders/base.pxd index b10f3a6018ab..90e0dce869fd 100644 --- a/nautilus_trader/model/orders/base.pxd +++ b/nautilus_trader/model/orders/base.pxd @@ -50,9 +50,9 @@ from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity -cdef set STOP_ORDER_TYPES -cdef set LIMIT_ORDER_TYPES -cdef set LOCAL_ACTIVE_ORDER_STATUS +cdef set[OrderType] STOP_ORDER_TYPES +cdef set[OrderType] LIMIT_ORDER_TYPES +cdef set[OrderStatus] LOCAL_ACTIVE_ORDER_STATUS cdef class Order: diff --git a/tests/unit_tests/backtest/test_exchange_glbx.py b/tests/unit_tests/backtest/test_exchange_glbx.py new file mode 100644 index 000000000000..b7fb2cd40ad8 --- /dev/null +++ b/tests/unit_tests/backtest/test_exchange_glbx.py @@ -0,0 +1,230 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +from decimal import Decimal + +from nautilus_trader.backtest.exchange import SimulatedExchange +from nautilus_trader.backtest.execution_client import BacktestExecClient +from nautilus_trader.backtest.models import FillModel +from nautilus_trader.backtest.models import LatencyModel +from nautilus_trader.backtest.models import MakerTakerFeeModel +from nautilus_trader.common.component import MessageBus +from nautilus_trader.common.component import TestClock +from nautilus_trader.config import ExecEngineConfig +from nautilus_trader.config import RiskEngineConfig +from nautilus_trader.data.engine import DataEngine +from nautilus_trader.execution.engine import ExecutionEngine +from nautilus_trader.model.currencies import USD +from nautilus_trader.model.enums import AccountType +from nautilus_trader.model.enums import OmsType +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import OrderStatus +from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.objects import Money +from nautilus_trader.model.objects import Price +from nautilus_trader.model.objects import Quantity +from nautilus_trader.portfolio.portfolio import Portfolio +from nautilus_trader.risk.engine import RiskEngine +from nautilus_trader.test_kit.providers import TestInstrumentProvider +from nautilus_trader.test_kit.stubs.component import TestComponentStubs +from nautilus_trader.test_kit.stubs.data import TestDataStubs +from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs +from nautilus_trader.trading import Strategy + + +_ESH4_GLBX = TestInstrumentProvider.es_future(2024, 3) + + +class TestSimulatedExchangeGlbx: + def setup(self) -> None: + # Fixture Setup + self.clock = TestClock() + self.trader_id = TestIdStubs.trader_id() + + self.msgbus = MessageBus( + trader_id=self.trader_id, + clock=self.clock, + ) + + self.cache = TestComponentStubs.cache() + + self.portfolio = Portfolio( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + self.data_engine = DataEngine( + msgbus=self.msgbus, + clock=self.clock, + cache=self.cache, + ) + + self.exec_engine = ExecutionEngine( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + config=ExecEngineConfig(debug=True), + ) + + self.risk_engine = RiskEngine( + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + config=RiskEngineConfig(debug=True), + ) + + self.exchange = SimulatedExchange( + venue=Venue("GLBX"), + oms_type=OmsType.HEDGING, + account_type=AccountType.MARGIN, + base_currency=USD, + starting_balances=[Money(1_000_000, USD)], + default_leverage=Decimal(10), + leverages={}, + instruments=[_ESH4_GLBX], + modules=[], + fill_model=FillModel(), + fee_model=MakerTakerFeeModel(), + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + latency_model=LatencyModel(0), + ) + + self.exec_client = BacktestExecClient( + exchange=self.exchange, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + # Wire up components + self.exec_engine.register_client(self.exec_client) + self.exchange.register_client(self.exec_client) + + self.cache.add_instrument(_ESH4_GLBX) + + # Create mock strategy + self.strategy = Strategy() + self.strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + # Start components + self.exchange.reset() + self.data_engine.start() + self.exec_engine.start() + self.strategy.start() + + def test_repr(self) -> None: + # Arrange, Act, Assert + assert ( + repr(self.exchange) + == "SimulatedExchange(id=GLBX, oms_type=HEDGING, account_type=MARGIN)" + ) + + def test_process_order_within_expiration_submits(self) -> None: + # Arrange: Prepare market + one_nano_past_activation = _ESH4_GLBX.activation_ns + 1 + tick = TestDataStubs.quote_tick( + instrument=_ESH4_GLBX, + bid_price=4010.00, + ask_price=4011.00, + ts_init=one_nano_past_activation, + ) + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + + order = self.strategy.order_factory.limit( + _ESH4_GLBX.id, + OrderSide.BUY, + Quantity.from_int(10), + Price.from_str("4000.00"), + ) + + # Act + self.strategy.submit_order(order) + self.exchange.process(one_nano_past_activation) + + # Assert + assert self.clock.timestamp_ns() == 1_630_704_600_000_000_001 + assert order.status == OrderStatus.ACCEPTED + + def test_process_order_prior_to_activation_rejects(self) -> None: + # Arrange: Prepare market + tick = TestDataStubs.quote_tick( + instrument=_ESH4_GLBX, + bid_price=4010.00, + ask_price=4011.00, + ) + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + + order = self.strategy.order_factory.limit( + _ESH4_GLBX.id, + OrderSide.BUY, + Quantity.from_int(10), + Price.from_str("4000.00"), + ) + + # Act + self.strategy.submit_order(order) + self.exchange.process(0) + + # Assert + assert order.status == OrderStatus.REJECTED + assert ( + order.last_event.reason + == "Contract ESH4.GLBX not yet active, activation 2021-09-03T21:30:00.000Z" + ) + + def test_process_order_after_expiration_rejects(self) -> None: + # Arrange: Prepare market + one_nano_past_expiration = _ESH4_GLBX.expiration_ns + 1 + + tick = TestDataStubs.quote_tick( + instrument=_ESH4_GLBX, + bid_price=4010.00, + ask_price=4011.00, + ts_init=one_nano_past_expiration, + ) + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + + order = self.strategy.order_factory.limit( + _ESH4_GLBX.id, + OrderSide.BUY, + Quantity.from_int(10), + Price.from_str("4000.00"), + ) + + # Act + self.strategy.submit_order(order) + self.exchange.process(one_nano_past_expiration) + + # Assert + assert self.clock.timestamp_ns() == 1_710_513_000_000_000_001 + assert order.status == OrderStatus.REJECTED + assert ( + order.last_event.reason + == "Contract ESH4.GLBX has expired, expiration 2024-03-15T14:30:00.000Z" + ) From d3149c50762182b570d43d687595a2f4877fcc11 Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Thu, 25 Apr 2024 03:29:57 +0300 Subject: [PATCH 036/193] Remove outdated symbology handling for CBOT instruments (#1609) --- .../interactive_brokers/parsing/instruments.py | 18 +++++------------- .../interactive_brokers/test_parsing.py | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index b982baaf8911..5f7e3e57d86d 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -578,19 +578,11 @@ def instrument_id_to_ib_contract_simplified_symbology( # noqa: C901 (too comple ) elif instrument_id.venue.value in VENUES_FUT: if m := RE_FUT.match(instrument_id.symbol.value): - if instrument_id.venue.value == "CBOT": - # IB still using old symbology after merger of CBOT with CME - return IBContract( - secType="FUT", - exchange=instrument_id.venue.value, - localSymbol=f"{m['symbol'].ljust(4)} {FUTURES_CODE_TO_MONTH[m['month']]} {m['year']}", - ) - else: - return IBContract( - secType="FUT", - exchange=instrument_id.venue.value, - localSymbol=f"{m['symbol']}{m['month']}{m['year'][-1]}", - ) + return IBContract( + secType="FUT", + exchange=instrument_id.venue.value, + localSymbol=f"{m['symbol']}{m['month']}{m['year'][-1]}", + ) elif m := RE_IND.match(instrument_id.symbol.value): return IBContract( secType="CONTFUT", diff --git a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py index 49f2d160f256..f2b309e8c4e1 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py @@ -56,7 +56,7 @@ (IBContract(secType="CONTFUT", exchange="SNFE", symbol="SPI"), "SPI.SNFE"), (IBContract(secType="FUT", exchange="CME", localSymbol="ESH3"), "ESH23.CME"), (IBContract(secType="FUT", exchange="CME", localSymbol="M6EH3"), "M6EH23.CME"), - (IBContract(secType="FUT", exchange="CBOT", localSymbol="MYM JUN 23"), "MYMM23.CBOT"), + (IBContract(secType="FUT", exchange="CBOT", localSymbol="MYMM3"), "MYMM23.CBOT"), (IBContract(secType="FUT", exchange="NYMEX", localSymbol="MCLV3"), "MCLV23.NYMEX"), (IBContract(secType="FUT", exchange="SNFE", localSymbol="APH3"), "APH23.SNFE"), (IBContract(secType="FOP", exchange="NYBOT", localSymbol="EX2G3 P4080"), "EX2G23P4080.NYBOT"), @@ -75,7 +75,7 @@ (IBContract(secType="OPT", exchange="SMART", localSymbol="AAPL 230217P00155000"), "AAPL 230217P00155000=OPT.SMART"), (IBContract(secType="FUT", exchange="CME", localSymbol="ESH3"), "ESH3=FUT.CME"), (IBContract(secType="FUT", exchange="CME", localSymbol="M6EH3"), "M6EH3=FUT.CME"), - (IBContract(secType="FUT", exchange="CBOT", localSymbol="MYM JUN 23"), "MYM JUN 23=FUT.CBOT"), + (IBContract(secType="FUT", exchange="CBOT", localSymbol="MYMM3"), "MYMM3=FUT.CBOT"), (IBContract(secType="FUT", exchange="NYMEX", localSymbol="MCLV3"), "MCLV3=FUT.NYMEX"), (IBContract(secType="FUT", exchange="SNFE", localSymbol="APH3"), "APH3=FUT.SNFE"), (IBContract(secType="FOP", exchange="NYBOT", localSymbol="EX2G3 P4080"), "EX2G3 P4080=FOP.NYBOT"), From e5b5ba59f9856502e3c8be5bf18ea04f671f2a8d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 24 Apr 2024 19:22:45 +1000 Subject: [PATCH 037/193] Update github workflows --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 368789794a1a..0b860df992cb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,7 +35,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v3.1 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -43,7 +43,7 @@ jobs: - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v7.0.7 + uses: tj-actions/branch-names@v8.0.1 - name: Build nautilus_trader image (nightly) if: ${{ steps.branch-name.outputs.current_branch == 'nightly' }} From 2c27426e135b3dd1bc228049ff8bfb6f2a8cfb16 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 25 Apr 2024 18:14:22 +1000 Subject: [PATCH 038/193] Update dependencies --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 14 +-- poetry.lock | 225 ++++++++++++++++++++------------------- pyproject.toml | 4 +- 4 files changed, 123 insertions(+), 122 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1486b0a7a918..4fbae3b7a567 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -73,7 +73,7 @@ repos: types: [python] - repo: https://github.com/psf/black - rev: 24.4.0 + rev: 24.4.1 hooks: - id: black types_or: [python, pyi] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 0c9fb6ca0d07..588d4ba3d425 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4035,9 +4035,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" [[package]] name = "rustls-webpki" @@ -5476,11 +5476,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -5716,9 +5716,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "63381fa6624bf92130a6b87c0d07380116f80b565c42cf0d754136f0238359ef" [[package]] name = "zstd" diff --git a/poetry.lock b/poetry.lock index b023bb044376..c9e00de50fe3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,33 +202,33 @@ msgspec = ">=0.18.5" [[package]] name = "black" -version = "24.4.0" +version = "24.4.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ad001a9ddd9b8dfd1b434d566be39b1cd502802c8d38bbb1ba612afda2ef436"}, - {file = "black-24.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3a3a092b8b756c643fe45f4624dbd5a389f770a4ac294cf4d0fce6af86addaf"}, - {file = "black-24.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dae79397f367ac8d7adb6c779813328f6d690943f64b32983e896bcccd18cbad"}, - {file = "black-24.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71d998b73c957444fb7c52096c3843875f4b6b47a54972598741fe9a7f737fcb"}, - {file = "black-24.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5537f456a22cf5cfcb2707803431d2feeb82ab3748ade280d6ccd0b40ed2e8"}, - {file = "black-24.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64e60a7edd71fd542a10a9643bf369bfd2644de95ec71e86790b063aa02ff745"}, - {file = "black-24.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd5b4f76056cecce3e69b0d4c228326d2595f506797f40b9233424e2524c070"}, - {file = "black-24.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:64578cf99b6b46a6301bc28bdb89f9d6f9b592b1c5837818a177c98525dbe397"}, - {file = "black-24.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f95cece33329dc4aa3b0e1a771c41075812e46cf3d6e3f1dfe3d91ff09826ed2"}, - {file = "black-24.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4396ca365a4310beef84d446ca5016f671b10f07abdba3e4e4304218d2c71d33"}, - {file = "black-24.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d99dfdf37a2a00a6f7a8dcbd19edf361d056ee51093b2445de7ca09adac965"}, - {file = "black-24.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:21f9407063ec71c5580b8ad975653c66508d6a9f57bd008bb8691d273705adcd"}, - {file = "black-24.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:652e55bb722ca026299eb74e53880ee2315b181dfdd44dca98e43448620ddec1"}, - {file = "black-24.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f2966b9b2b3b7104fca9d75b2ee856fe3fdd7ed9e47c753a4bb1a675f2caab8"}, - {file = "black-24.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb9ca06e556a09f7f7177bc7cb604e5ed2d2df1e9119e4f7d2f1f7071c32e5d"}, - {file = "black-24.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4e71cdebdc8efeb6deaf5f2deb28325f8614d48426bed118ecc2dcaefb9ebf3"}, - {file = "black-24.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6644f97a7ef6f401a150cca551a1ff97e03c25d8519ee0bbc9b0058772882665"}, - {file = "black-24.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75a2d0b4f5eb81f7eebc31f788f9830a6ce10a68c91fbe0fade34fff7a2836e6"}, - {file = "black-24.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb949f56a63c5e134dfdca12091e98ffb5fd446293ebae123d10fc1abad00b9e"}, - {file = "black-24.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:7852b05d02b5b9a8c893ab95863ef8986e4dda29af80bbbda94d7aee1abf8702"}, - {file = "black-24.4.0-py3-none-any.whl", hash = "sha256:74eb9b5420e26b42c00a3ff470dc0cd144b80a766128b1771d07643165e08d0e"}, - {file = "black-24.4.0.tar.gz", hash = "sha256:f07b69fda20578367eaebbd670ff8fc653ab181e1ff95d84497f9fa20e7d0641"}, + {file = "black-24.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f7749fd0d97ff9415975a1432fac7df89bf13c3833cea079e55fa004d5f28c0"}, + {file = "black-24.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859f3cc5d2051adadf8fd504a01e02b0fd866d7549fff54bc9202d524d2e8bd7"}, + {file = "black-24.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59271c9c29dfa97f7fda51f56c7809b3f78e72fd8d2205189bbd23022a0618b6"}, + {file = "black-24.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:5ed9c34cba223149b5a0144951a0f33d65507cf82c5449cb3c35fe4b515fea9a"}, + {file = "black-24.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dae3ae59d6f2dc93700fd5034a3115434686e66fd6e63d4dcaa48d19880f2b0"}, + {file = "black-24.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5f8698974a81af83283eb47644f2711b5261138d6d9180c863fce673cbe04b13"}, + {file = "black-24.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f404b6e77043b23d0321fb7772522b876b6de737ad3cb97d6b156638d68ce81"}, + {file = "black-24.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:c94e52b766477bdcd010b872ba0714d5458536dc9d0734eff6583ba7266ffd89"}, + {file = "black-24.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:962d9e953872cdb83b97bb737ad47244ce2938054dc946685a4cad98520dab38"}, + {file = "black-24.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d8e3b2486b7dd522b1ab2ba1ec4907f0aa8f5e10a33c4271fb331d1d10b70c"}, + {file = "black-24.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed77e214b785148f57e43ca425b6e0850165144aa727d66ac604e56a70bb7825"}, + {file = "black-24.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4ef4764437d7eba8386689cd06e1fb5341ee0ae2e9e22582b21178782de7ed94"}, + {file = "black-24.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:92b183f8eef5baf7b20a513abcf982ad616f544f593f6688bb2850d2982911f1"}, + {file = "black-24.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:945abd7b3572add997757c94295bb3e73c6ffaf3366b1f26cb2356a4bffd1dc3"}, + {file = "black-24.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db5154b9e5b478031371d8bc41ff37b33855fa223a6cfba456c9b73fb96f77d4"}, + {file = "black-24.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:afc84c33c1a9aaf3d73140cee776b4ddf73ff429ffe6b7c56dc1c9c10725856d"}, + {file = "black-24.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0889f4eb8b3bdf8b189e41a71cf0dbb8141a98346cd1a2695dea5995d416e940"}, + {file = "black-24.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5bb0143f175db45a55227eefd63e90849d96c266330ba31719e9667d0d5ec3b9"}, + {file = "black-24.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:713a04a78e78f28ef7e8df7a16fe075670ea164860fcef3885e4f3dffc0184b3"}, + {file = "black-24.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:171959bc879637a8cdbc53dc3fddae2a83e151937a28cf605fd175ce61e0e94a"}, + {file = "black-24.4.1-py3-none-any.whl", hash = "sha256:ecbab810604fe02c70b3a08afd39beb599f7cc9afd13e81f5336014133b4fe35"}, + {file = "black-24.4.1.tar.gz", hash = "sha256:5241612dc8cad5b6fd47432b8bd04db80e07cfbc53bb69e9ae18985063bcb8dd"}, ] [package.dependencies] @@ -394,63 +394,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, + {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, + {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [package.dependencies] @@ -1304,38 +1304,38 @@ files = [ [[package]] name = "mypy" -version = "1.9.0" +version = "1.10.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] @@ -1587,18 +1587,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" @@ -2542,13 +2543,13 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "virtualenv" -version = "20.25.3" +version = "20.26.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.3-py3-none-any.whl", hash = "sha256:8aac4332f2ea6ef519c648d0bc48a5b1d324994753519919bddbb1aff25a104e"}, - {file = "virtualenv-20.25.3.tar.gz", hash = "sha256:7bb554bbdfeaacc3349fa614ea5bff6ac300fc7c335e9facf3a3bcfc703f45be"}, + {file = "virtualenv-20.26.0-py3-none-any.whl", hash = "sha256:0846377ea76e818daaa3e00a4365c018bc3ac9760cbb3544de542885aad61fb3"}, + {file = "virtualenv-20.26.0.tar.gz", hash = "sha256:ec25a9671a5102c8d2657f62792a27b48f016664c6873f6beed3800008577210"}, ] [package.dependencies] @@ -2685,4 +2686,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "fbc78c9e9804eed52bab970597c88434a4cb25eb4c9b39635d8d751b69830dc7" +content-hash = "af1b6cb1fb038e1cf2648c5cb47e117917332d1f0bd574d417fae9c06a97aa68" diff --git a/pyproject.toml b/pyproject.toml index 6e6ac0b15ab1..881d6e0e849a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ ib = ["nautilus_ibapi", "async-timeout", "defusedxml"] optional = true [tool.poetry.group.dev.dependencies] -black = "^24.4.0" +black = "^24.4.1" docformatter = "^1.7.5" mypy = "^1.9.0" pandas-stubs = "^2.2.1" @@ -92,7 +92,7 @@ types-toml = "^0.10.2" optional = true [tool.poetry.group.test.dependencies] -coverage = "^7.4.4" +coverage = "^7.5.0" pytest = "^7.4.4" pytest-aiohttp = "^1.0.5" pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_scoped_event_loop' attribute of immutable type From 319342d7f03555509c52056abda40555425d2ebb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 25 Apr 2024 20:23:08 +1000 Subject: [PATCH 039/193] Add contract expiration order and position closing --- nautilus_trader/backtest/matching_engine.pxd | 1 + nautilus_trader/backtest/matching_engine.pyx | 31 +++++++++- tests/conftest.py | 2 +- .../unit_tests/backtest/test_exchange_glbx.py | 59 +++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/backtest/matching_engine.pxd b/nautilus_trader/backtest/matching_engine.pxd index ba279db45133..450cb78d6922 100644 --- a/nautilus_trader/backtest/matching_engine.pxd +++ b/nautilus_trader/backtest/matching_engine.pxd @@ -79,6 +79,7 @@ cdef class OrderMatchingEngine: cdef FillModel _fill_model cdef FeeModel _fee_model # cdef object _auction_match_algo + cdef bint _instrument_has_expiration cdef bint _bar_execution cdef bint _reject_stop_orders cdef bint _support_gtd_orders diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 1f73bfc10f39..8114289cd8ed 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -185,6 +185,7 @@ cdef class OrderMatchingEngine: self.account_type = account_type self.market_status = MarketStatus.OPEN + self._instrument_has_expiration = instrument.instrument_class in EXPIRING_INSTRUMENT_TYPES self._bar_execution = bar_execution self._reject_stop_orders = reject_stop_orders self._support_gtd_orders = support_gtd_orders @@ -678,8 +679,9 @@ cdef class OrderMatchingEngine: # Index identifiers self._account_ids[order.trader_id] = account_id - cdef uint64_t now_ns = self._clock.timestamp_ns() - if self.instrument.instrument_class in EXPIRING_INSTRUMENT_TYPES: + cdef uint64_t + if self._instrument_has_expiration: + now_ns = self._clock.timestamp_ns() if now_ns < self.instrument.activation_ns: self._generate_order_rejected( order, @@ -1311,6 +1313,31 @@ cdef class OrderMatchingEngine: self._target_last = 0 self._has_targets = False + # Instrument expiration + if self._instrument_has_expiration and timestamp_ns >= self.instrument.expiration_ns: + self._log.info(f"{self.instrument.id} reached expiration") + + # Cancel all open orders + for order in self.get_open_orders(): + self.cancel_order(order) + + # Close all open positions + for position in self.cache.positions(None, self.instrument.id): + order = MarketOrder( + trader_id=position.trader_id, + strategy_id=position.strategy_id, + instrument_id=position.instrument_id, + client_order_id=ClientOrderId(str(uuid.uuid4())), + order_side=Order.closing_side_c(position.side), + quantity=position.quantity, + init_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + reduce_only=True, + tags=[f"EXPIRATION_{self.venue}_CLOSE"], + ) + self.cache.add_order(order, position_id=position.id) + self.fill_market_order(order) + cpdef list determine_limit_price_and_volume(self, Order order): """ Return the projected fills for the given *limit* order filling passively diff --git a/tests/conftest.py b/tests/conftest.py index 6f1c32df080a..93fe18629e66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,7 @@ def bypass_logging() -> None: """ init_logging( - level_stdout=LogLevel.WARNING, + level_stdout=LogLevel.DEBUG, bypass=True, # Set this to False to see logging in tests ) diff --git a/tests/unit_tests/backtest/test_exchange_glbx.py b/tests/unit_tests/backtest/test_exchange_glbx.py index b7fb2cd40ad8..9241e898c97a 100644 --- a/tests/unit_tests/backtest/test_exchange_glbx.py +++ b/tests/unit_tests/backtest/test_exchange_glbx.py @@ -228,3 +228,62 @@ def test_process_order_after_expiration_rejects(self) -> None: order.last_event.reason == "Contract ESH4.GLBX has expired, expiration 2024-03-15T14:30:00.000Z" ) + + def test_process_exchange_past_instrument_expiration_cancels_open_order(self) -> None: + # Arrange: Prepare market + one_nano_past_activation = _ESH4_GLBX.activation_ns + 1 + tick = TestDataStubs.quote_tick( + instrument=_ESH4_GLBX, + bid_price=4010.00, + ask_price=4011.00, + ts_init=one_nano_past_activation, + ) + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + + order = self.strategy.order_factory.limit( + _ESH4_GLBX.id, + OrderSide.BUY, + Quantity.from_int(10), + Price.from_str("4000.00"), + ) + + self.strategy.submit_order(order) + self.exchange.process(one_nano_past_activation) + + # Act + self.exchange.get_matching_engine(_ESH4_GLBX.id).iterate(_ESH4_GLBX.expiration_ns) + + # Assert + assert self.clock.timestamp_ns() == _ESH4_GLBX.expiration_ns == 1_710_513_000_000_000_000 + assert order.status == OrderStatus.CANCELED + + def test_process_exchange_past_instrument_expiration_closed_open_position(self) -> None: + # Arrange: Prepare market + one_nano_past_activation = _ESH4_GLBX.activation_ns + 1 + tick = TestDataStubs.quote_tick( + instrument=_ESH4_GLBX, + bid_price=4010.00, + ask_price=4011.00, + ts_init=one_nano_past_activation, + ) + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + + order = self.strategy.order_factory.market( + _ESH4_GLBX.id, + OrderSide.BUY, + Quantity.from_int(10), + ) + + self.strategy.submit_order(order) + self.exchange.process(one_nano_past_activation) + + # Act + self.exchange.get_matching_engine(_ESH4_GLBX.id).iterate(_ESH4_GLBX.expiration_ns) + + # Assert + assert self.clock.timestamp_ns() == _ESH4_GLBX.expiration_ns == 1_710_513_000_000_000_000 + assert order.status == OrderStatus.FILLED + position = self.cache.positions()[0] + assert position.is_closed From 2fd9337bf406631c3492592580b5803c706d0452 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 25 Apr 2024 21:24:23 +1000 Subject: [PATCH 040/193] Update flags description --- nautilus_core/model/src/data/delta.rs | 2 +- nautilus_core/model/src/data/deltas.rs | 2 +- nautilus_core/model/src/data/depth.rs | 2 +- nautilus_core/model/src/enums.rs | 2 +- nautilus_trader/core/includes/model.h | 6 +++--- nautilus_trader/core/rust/model.pxd | 6 +++--- nautilus_trader/model/book.pyx | 6 +++--- nautilus_trader/model/data.pyx | 6 +++--- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs index a844949de8b7..de521981e4ce 100644 --- a/nautilus_core/model/src/data/delta.rs +++ b/nautilus_core/model/src/data/delta.rs @@ -47,7 +47,7 @@ pub struct OrderBookDelta { pub action: BookAction, /// The order to apply. pub order: BookOrder, - /// The record flags bit field, indicating packet end and data information. + /// The record flags bit field, indicating event end and data information. pub flags: u8, /// The message sequence number assigned at the venue. pub sequence: u64, diff --git a/nautilus_core/model/src/data/deltas.rs b/nautilus_core/model/src/data/deltas.rs index 2d5778787885..c5b0e02d7697 100644 --- a/nautilus_core/model/src/data/deltas.rs +++ b/nautilus_core/model/src/data/deltas.rs @@ -39,7 +39,7 @@ pub struct OrderBookDeltas { pub instrument_id: InstrumentId, /// The order book deltas. pub deltas: Vec, - /// The record flags bit field, indicating packet end and data information. + /// The record flags bit field, indicating event end and data information. pub flags: u8, /// The message sequence number assigned at the venue. pub sequence: u64, diff --git a/nautilus_core/model/src/data/depth.rs b/nautilus_core/model/src/data/depth.rs index 087fe8d0f2b6..b88796de9539 100644 --- a/nautilus_core/model/src/data/depth.rs +++ b/nautilus_core/model/src/data/depth.rs @@ -56,7 +56,7 @@ pub struct OrderBookDepth10 { pub bid_counts: [u32; DEPTH10_LEN], /// The count of ask orders per level for the depth update. pub ask_counts: [u32; DEPTH10_LEN], - /// The record flags bit field, indicating packet end and data information. + /// The record flags bit field, indicating event end and data information. pub flags: u8, /// The message sequence number assigned at the venue. pub sequence: u64, diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index 5ec1ae7c421a..2c861edfca0b 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -907,7 +907,7 @@ pub enum PriceType { Last = 4, } -/// A record flag bit field, indicating packet end and data information. +/// A record flag bit field, indicating event end and data information. #[repr(C)] #[derive( Copy, diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index c185e6009de6..ac0bba909df2 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -543,7 +543,7 @@ typedef enum PriceType { } PriceType; /** - * A record flag bit field, indicating packet end and data information. + * A record flag bit field, indicating event end and data information. */ typedef enum RecordFlag { /** @@ -806,7 +806,7 @@ typedef struct OrderBookDelta_t { */ struct BookOrder_t order; /** - * The record flags bit field, indicating packet end and data information. + * The record flags bit field, indicating event end and data information. */ uint8_t flags; /** @@ -870,7 +870,7 @@ typedef struct OrderBookDepth10_t { */ uint32_t ask_counts[DEPTH10_LEN]; /** - * The record flags bit field, indicating packet end and data information. + * The record flags bit field, indicating event end and data information. */ uint8_t flags; /** diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 185108e6d0dc..7559d3c76604 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -294,7 +294,7 @@ cdef extern from "../includes/model.h": # The last price at which a trade was made for an instrument. LAST # = 4, - # A record flag bit field, indicating packet end and data information. + # A record flag bit field, indicating event end and data information. cpdef enum RecordFlag: # Last message in the packet from the venue for a given `instrument_id`. F_LAST # = (1 << 7), @@ -442,7 +442,7 @@ cdef extern from "../includes/model.h": BookAction action; # The order to apply. BookOrder_t order; - # The record flags bit field, indicating packet end and data information. + # The record flags bit field, indicating event end and data information. uint8_t flags; # The message sequence number assigned at the venue. uint64_t sequence; @@ -482,7 +482,7 @@ cdef extern from "../includes/model.h": uint32_t bid_counts[DEPTH10_LEN]; # The count of ask orders per level for the depth update. uint32_t ask_counts[DEPTH10_LEN]; - # The record flags bit field, indicating packet end and data information. + # The record flags bit field, indicating event end and data information. uint8_t flags; # The message sequence number assigned at the venue. uint64_t sequence; diff --git a/nautilus_trader/model/book.pyx b/nautilus_trader/model/book.pyx index 8c7d8500144e..c628a8cf65dd 100644 --- a/nautilus_trader/model/book.pyx +++ b/nautilus_trader/model/book.pyx @@ -245,7 +245,7 @@ cdef class OrderBook(Data): ts_event : uint64_t The UNIX timestamp (nanoseconds) when the book event occurred. flags : uint8_t, default 0 - The record flags bit field, indicating packet end and data information. + The record flags bit field, indicating event end and data information. sequence : uint64_t, default 0 The unique sequence number for the update. If default 0 then will increment the `sequence`. @@ -273,7 +273,7 @@ cdef class OrderBook(Data): ts_event : uint64_t The UNIX timestamp (nanoseconds) when the book event occurred. flags : uint8_t, default 0 - The record flags bit field, indicating packet end and data information. + The record flags bit field, indicating event end and data information. sequence : uint64_t, default 0 The unique sequence number for the update. If default 0 then will increment the `sequence`. @@ -293,7 +293,7 @@ cdef class OrderBook(Data): ts_event : uint64_t The UNIX timestamp (nanoseconds) when the book event occurred. flags : uint8_t, default 0 - The record flags bit field, indicating packet end and data information. + The record flags bit field, indicating event end and data information. sequence : uint64_t, default 0 The unique sequence number for the update. If default 0 then will increment the `sequence`. diff --git a/nautilus_trader/model/data.pyx b/nautilus_trader/model/data.pyx index 3846a24679c4..1c405f158cdb 100644 --- a/nautilus_trader/model/data.pyx +++ b/nautilus_trader/model/data.pyx @@ -1619,7 +1619,7 @@ cdef class OrderBookDelta(Data): order : BookOrder, optional with no default so ``None`` must be passed explicitly The book order for the delta. flags : uint8_t - The record flags bit field, indicating packet end and data information. + The record flags bit field, indicating event end and data information. A value of zero indicates no flags. sequence : uint64_t The unique sequence number for the update. @@ -2027,7 +2027,7 @@ cdef class OrderBookDelta(Data): order_id : uint64_t The order ID. flags : uint8_t - The record flags bit field, indicating packet end and data information. + The record flags bit field, indicating event end and data information. A value of zero indicates no flags. sequence : uint64_t The unique sequence number for the update. @@ -2481,7 +2481,7 @@ cdef class OrderBookDepth10(Data): ask_counts : list[uint32_t] The count of ask orders per level for the update. Can be zeros if data not available. flags : uint8_t - The record flags bit field, indicating packet end and data information. + The record flags bit field, indicating event end and data information. A value of zero indicates no flags. sequence : uint64_t The unique sequence number for the update. From 6513d2057b823472f68617b1efe38e88ed4fc0b3 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Fri, 26 Apr 2024 08:44:31 +0200 Subject: [PATCH 041/193] Add Postgres cache database (#1607) --- .github/workflows/build.yml | 62 ++++- nautilus_core/cli/Cargo.toml | 2 +- nautilus_core/cli/src/database/postgres.rs | 177 +------------- nautilus_core/cli/src/opt.rs | 3 + nautilus_core/infrastructure/Cargo.toml | 2 +- nautilus_core/infrastructure/src/lib.rs | 2 +- .../infrastructure/src/python/mod.rs | 5 + .../src/python/sql/cache_database.rs | 90 +++++++ .../infrastructure/src/python/sql/mod.rs | 16 ++ nautilus_core/infrastructure/src/sql/cache.rs | 100 -------- .../infrastructure/src/sql/cache_database.rs | 144 +++++++++++ .../infrastructure/src/sql/database.rs | 226 ------------------ nautilus_core/infrastructure/src/sql/mod.rs | 10 +- .../src/sql/{schema.rs => models/general.rs} | 6 +- .../infrastructure/src/sql/models/mod.rs | 2 + .../infrastructure/src/sql/models/types.rs | 43 ++++ nautilus_core/infrastructure/src/sql/pg.rs | 199 +++++++++++++++ .../infrastructure/src/sql/queries.rs | 89 +++++++ .../tests/test_cache_database_postgres.rs | 91 +++++++ nautilus_core/persistence/Cargo.toml | 6 +- nautilus_trader/cache/postgres/__init__.py | 14 ++ nautilus_trader/cache/postgres/adapter.py | 57 +++++ .../cache/postgres/transformers.py | 41 ++++ nautilus_trader/core/nautilus_pyo3.pyi | 20 ++ .../test_cache_database_postgres.py | 125 ++++++++++ 25 files changed, 1012 insertions(+), 520 deletions(-) create mode 100644 nautilus_core/infrastructure/src/python/sql/cache_database.rs create mode 100644 nautilus_core/infrastructure/src/python/sql/mod.rs delete mode 100644 nautilus_core/infrastructure/src/sql/cache.rs create mode 100644 nautilus_core/infrastructure/src/sql/cache_database.rs delete mode 100644 nautilus_core/infrastructure/src/sql/database.rs rename nautilus_core/infrastructure/src/sql/{schema.rs => models/general.rs} (91%) create mode 100644 nautilus_core/infrastructure/src/sql/models/types.rs create mode 100644 nautilus_core/infrastructure/src/sql/queries.rs create mode 100644 nautilus_core/infrastructure/tests/test_cache_database_postgres.rs create mode 100644 nautilus_trader/cache/postgres/__init__.py create mode 100644 nautilus_trader/cache/postgres/adapter.py create mode 100644 nautilus_trader/cache/postgres/transformers.py create mode 100644 tests/integration_tests/infrastructure/test_cache_database_postgres.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e53f546cadd..8c0dae0bb979 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,25 @@ jobs: env: BUILD_MODE: debug RUST_BACKTRACE: 1 + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: pass + POSTGRES_DB: nautilus + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Free disk space (Ubuntu) @@ -105,10 +124,16 @@ jobs: # pre-commit run --hook-stage manual gitlint-ci pre-commit run --all-files - - name: Install Redis (Linux) + - name: Install Nautilus CLI and run init postgres run: | - sudo apt-get install redis-server - redis-server --daemonize yes + make install-cli + nautilus database init --schema ${{ github.workspace }}/schema + env: + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: pass + POSTGRES_DATABASE: nautilus - name: Run nautilus_core cargo tests (Linux) run: | @@ -224,6 +249,25 @@ jobs: env: BUILD_MODE: debug RUST_BACKTRACE: 1 + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: pass + POSTGRES_DB: nautilus + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout repository @@ -290,10 +334,16 @@ jobs: # pre-commit run --hook-stage manual gitlint-ci pre-commit run --all-files - - name: Install Redis (macOS) + - name: Install Nautilus CLI and run init postgres run: | - brew install redis - redis-server --daemonize yes + make install-cli + nautilus database init --schema ${{ github.workspace }}/schema + env: + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: pass + POSTGRES_DATABASE: nautilus - name: Run nautilus_core cargo tests (macOS) run: | diff --git a/nautilus_core/cli/Cargo.toml b/nautilus_core/cli/Cargo.toml index 941f013ea46e..163f78e96bb8 100644 --- a/nautilus_core/cli/Cargo.toml +++ b/nautilus_core/cli/Cargo.toml @@ -14,7 +14,7 @@ path = "src/bin/cli.rs" nautilus-common = { path = "../common"} nautilus-model = { path = "../model" } nautilus-core = { path = "../core" } -nautilus-infrastructure = { path = "../infrastructure" , features = ['sql']} +nautilus-infrastructure = { path = "../infrastructure" , features = ['postgres']} anyhow = { workspace = true } tokio = {workspace = true} log = { workspace = true } diff --git a/nautilus_core/cli/src/database/postgres.rs b/nautilus_core/cli/src/database/postgres.rs index 5536276b5499..39f374881df0 100644 --- a/nautilus_core/cli/src/database/postgres.rs +++ b/nautilus_core/cli/src/database/postgres.rs @@ -13,182 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use log::{error, info}; -use nautilus_infrastructure::sql::pg::{connect_pg, get_postgres_connect_options}; -use sqlx::PgPool; +use nautilus_infrastructure::sql::pg::{ + connect_pg, drop_postgres, get_postgres_connect_options, init_postgres, +}; use crate::opt::{DatabaseCommand, DatabaseOpt}; -/// Scans current path with keyword `nautilus_trader` and build schema dir -fn get_schema_dir() -> anyhow::Result { - std::env::var("SCHEMA_DIR").or_else(|_| { - let nautilus_git_repo_name = "nautilus_trader"; - let binding = std::env::current_dir().unwrap(); - let current_dir = binding.to_str().unwrap(); - match current_dir.find(nautilus_git_repo_name){ - Some(index) => { - let schema_path = current_dir[0..index + nautilus_git_repo_name.len()].to_string() + "/schema"; - Ok(schema_path) - } - None => anyhow::bail!("Could not calculate schema dir from current directory path or SCHEMA_DIR env variable") - } - }) -} - -pub async fn init_postgres(pg: &PgPool, database: String, password: String) -> anyhow::Result<()> { - info!("Initializing Postgres database with target permissions and schema"); - // create public schema - match sqlx::query("CREATE SCHEMA IF NOT EXISTS public;") - .execute(pg) - .await - { - Ok(_) => info!("Schema public created successfully"), - Err(err) => error!("Error creating schema public: {:?}", err), - } - // create role if not exists - match sqlx::query(format!("CREATE ROLE {database} PASSWORD '{password}' LOGIN;").as_str()) - .execute(pg) - .await - { - Ok(_) => info!("Role {} created successfully", database), - Err(err) => { - if err.to_string().contains("already exists") { - info!("Role {} already exists", database); - } else { - error!("Error creating role {}: {:?}", database, err); - } - } - } - // execute all the sql files in schema dir - let schema_dir = get_schema_dir()?; - let mut sql_files = - std::fs::read_dir(schema_dir)?.collect::, std::io::Error>>()?; - for file in &mut sql_files { - let file_name = file.file_name(); - info!("Executing schema file: {:?}", file_name); - let file_path = file.path(); - let sql_content = std::fs::read_to_string(file_path.clone())?; - for sql_statement in sql_content.split(';').filter(|s| !s.trim().is_empty()) { - sqlx::query(sql_statement).execute(pg).await?; - } - } - // grant connect - match sqlx::query(format!("GRANT CONNECT ON DATABASE {database} TO {database};").as_str()) - .execute(pg) - .await - { - Ok(_) => info!("Connect privileges granted to role {}", database), - Err(err) => error!( - "Error granting connect privileges to role {}: {:?}", - database, err - ), - } - // grant all schema privileges to the role - match sqlx::query(format!("GRANT ALL PRIVILEGES ON SCHEMA public TO {database};").as_str()) - .execute(pg) - .await - { - Ok(_) => info!("All schema privileges granted to role {}", database), - Err(err) => error!( - "Error granting all privileges to role {}: {:?}", - database, err - ), - } - // grant all table privileges to the role - match sqlx::query( - format!("GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {database};").as_str(), - ) - .execute(pg) - .await - { - Ok(_) => info!("All tables privileges granted to role {}", database), - Err(err) => error!( - "Error granting all privileges to role {}: {:?}", - database, err - ), - } - // grant all sequence privileges to the role - match sqlx::query( - format!("GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {database};").as_str(), - ) - .execute(pg) - .await - { - Ok(_) => info!("All sequences privileges granted to role {}", database), - Err(err) => error!( - "Error granting all privileges to role {}: {:?}", - database, err - ), - } - // grant all function privileges to the role - match sqlx::query( - format!("GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO {database};").as_str(), - ) - .execute(pg) - .await - { - Ok(_) => info!("All functions privileges granted to role {}", database), - Err(err) => error!( - "Error granting all privileges to role {}: {:?}", - database, err - ), - } - - Ok(()) -} - -pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> { - // execute drop owned - match sqlx::query(format!("DROP OWNED BY {database}").as_str()) - .execute(pg) - .await - { - Ok(_) => info!("Dropped owned objects by role {}", database), - Err(err) => error!("Error dropping owned by role {}: {:?}", database, err), - } - // revoke connect - match sqlx::query(format!("REVOKE CONNECT ON DATABASE {database} FROM {database};").as_str()) - .execute(pg) - .await - { - Ok(_) => info!("Revoked connect privileges from role {}", database), - Err(err) => error!( - "Error revoking connect privileges from role {}: {:?}", - database, err - ), - } - // revoke privileges - match sqlx::query( - format!("REVOKE ALL PRIVILEGES ON DATABASE {database} FROM {database};").as_str(), - ) - .execute(pg) - .await - { - Ok(_) => info!("Revoked all privileges from role {}", database), - Err(err) => error!( - "Error revoking all privileges from role {}: {:?}", - database, err - ), - } - // execute drop schema - match sqlx::query("DROP SCHEMA IF EXISTS public CASCADE") - .execute(pg) - .await - { - Ok(_) => info!("Dropped schema public"), - Err(err) => error!("Error dropping schema public: {:?}", err), - } - // drop role - match sqlx::query(format!("DROP ROLE IF EXISTS {database};").as_str()) - .execute(pg) - .await - { - Ok(_) => info!("Dropped role {}", database), - Err(err) => error!("Error dropping role {}: {:?}", database, err), - } - Ok(()) -} - pub async fn run_database_command(opt: DatabaseOpt) -> anyhow::Result<()> { let command = opt.command.clone(); @@ -207,6 +37,7 @@ pub async fn run_database_command(opt: DatabaseOpt) -> anyhow::Result<()> { &pg, pg_connect_options.database, pg_connect_options.password, + config.schema, ) .await?; } diff --git a/nautilus_core/cli/src/opt.rs b/nautilus_core/cli/src/opt.rs index 8ef5b9c2b1d2..1df35a0e78f2 100644 --- a/nautilus_core/cli/src/opt.rs +++ b/nautilus_core/cli/src/opt.rs @@ -51,6 +51,9 @@ pub struct DatabaseConfig { /// Password for connecting to the database #[arg(long)] pub password: Option, + /// Directory path to the schema files + #[arg(long)] + pub schema: Option, } #[derive(Parser, Debug, Clone)] diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 95a2ad9eff91..594ca1448b37 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -50,4 +50,4 @@ extension-module = [ ] python = ["pyo3"] redis = ["dep:redis"] -sql = ["dep:sqlx"] +postgres = ["dep:sqlx"] diff --git a/nautilus_core/infrastructure/src/lib.rs b/nautilus_core/infrastructure/src/lib.rs index 6f87ec9dc619..e34994a90fa7 100644 --- a/nautilus_core/infrastructure/src/lib.rs +++ b/nautilus_core/infrastructure/src/lib.rs @@ -34,5 +34,5 @@ pub mod python; #[cfg(feature = "redis")] pub mod redis; -#[cfg(feature = "sql")] +#[cfg(feature = "postgres")] pub mod sql; diff --git a/nautilus_core/infrastructure/src/python/mod.rs b/nautilus_core/infrastructure/src/python/mod.rs index 6c07e5f0dbfc..6bf67c7c8340 100644 --- a/nautilus_core/infrastructure/src/python/mod.rs +++ b/nautilus_core/infrastructure/src/python/mod.rs @@ -18,6 +18,9 @@ #[cfg(feature = "redis")] pub mod redis; +#[cfg(feature = "postgres")] +pub mod sql; + use pyo3::{prelude::*, pymodule}; #[pymodule] @@ -26,5 +29,7 @@ pub fn infrastructure(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; #[cfg(feature = "redis")] m.add_class::()?; + #[cfg(feature = "postgres")] + m.add_class::()?; Ok(()) } diff --git a/nautilus_core/infrastructure/src/python/sql/cache_database.rs b/nautilus_core/infrastructure/src/python/sql/cache_database.rs new file mode 100644 index 000000000000..f1c7b181eb33 --- /dev/null +++ b/nautilus_core/infrastructure/src/python/sql/cache_database.rs @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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::collections::HashMap; + +use nautilus_common::runtime::get_runtime; +use nautilus_core::python::to_pyruntime_err; +use nautilus_model::types::currency::Currency; +use pyo3::prelude::*; + +use crate::sql::{ + cache_database::PostgresCacheDatabase, pg::delete_nautilus_postgres_tables, + queries::DatabaseQueries, +}; + +#[pymethods] +impl PostgresCacheDatabase { + #[staticmethod] + #[pyo3(name = "connect")] + fn py_connect( + host: Option, + port: Option, + username: Option, + password: Option, + database: Option, + ) -> PyResult { + let result = get_runtime().block_on(async { + PostgresCacheDatabase::connect(host, port, username, password, database).await + }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "load")] + fn py_load(slf: PyRef<'_, Self>) -> PyResult>> { + let result = get_runtime().block_on(async { slf.load().await }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "load_currency")] + fn py_load_currency(slf: PyRef<'_, Self>, code: &str) -> PyResult> { + let result = + get_runtime().block_on(async { DatabaseQueries::load_currency(&slf.pool, code).await }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "load_currencies")] + fn py_load_currencies(slf: PyRef<'_, Self>) -> PyResult> { + let result = + get_runtime().block_on(async { DatabaseQueries::load_currencies(&slf.pool).await }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "add")] + fn py_add(slf: PyRef<'_, Self>, key: String, value: Vec) -> PyResult<()> { + let result = get_runtime().block_on(async { slf.add(key, value).await }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "add_currency")] + fn py_add_currency(slf: PyRef<'_, Self>, currency: Currency) -> PyResult<()> { + let result = get_runtime().block_on(async { slf.add_currency(currency).await }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "flush_db")] + fn py_drop_schema(slf: PyRef<'_, Self>) -> PyResult<()> { + let result = + get_runtime().block_on(async { delete_nautilus_postgres_tables(&slf.pool).await }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "truncate")] + fn py_truncate(slf: PyRef<'_, Self>, table: String) -> PyResult<()> { + let result = + get_runtime().block_on(async { DatabaseQueries::truncate(&slf.pool, table).await }); + result.map_err(to_pyruntime_err) + } +} diff --git a/nautilus_core/infrastructure/src/python/sql/mod.rs b/nautilus_core/infrastructure/src/python/sql/mod.rs new file mode 100644 index 000000000000..454f4be6bd37 --- /dev/null +++ b/nautilus_core/infrastructure/src/python/sql/mod.rs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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. +// ------------------------------------------------------------------------------------------------- + +pub mod cache_database; diff --git a/nautilus_core/infrastructure/src/sql/cache.rs b/nautilus_core/infrastructure/src/sql/cache.rs deleted file mode 100644 index fecf1072d482..000000000000 --- a/nautilus_core/infrastructure/src/sql/cache.rs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (C) 2015-2024 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 nautilus_model::identifiers::trader_id::TraderId; -use sqlx::Error; - -use crate::sql::{database::Database, schema::GeneralItem}; - -pub struct SqlCacheDatabase { - trader_id: TraderId, - db: Database, -} - -impl SqlCacheDatabase { - #[must_use] - pub fn new(trader_id: TraderId, database: Database) -> Self { - Self { - trader_id, - db: database, - } - } - #[must_use] - pub fn key_trader(&self) -> String { - format!("trader-{}", self.trader_id) - } - - #[must_use] - pub fn key_general(&self) -> String { - format!("{}:general:", self.key_trader()) - } - - pub async fn add(&self, key: String, value: String) -> Result { - let query = format!( - "INSERT INTO general (key, value) VALUES ('{key}', '{value}') ON CONFLICT (key) DO NOTHING;" - ); - self.db.execute(query.as_str()).await - } - - pub async fn get(&self, key: String) -> Vec { - let query = format!("SELECT * FROM general WHERE key = '{key}'"); - self.db - .fetch_all::(query.as_str()) - .await - .unwrap() - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Tests -//////////////////////////////////////////////////////////////////////////////// -#[cfg(test)] -mod tests { - use nautilus_model::identifiers::stubs::trader_id; - - use super::SqlCacheDatabase; - use crate::sql::database::{init_db_schema, setup_test_database}; - - async fn setup_sql_cache_database() -> SqlCacheDatabase { - let db = setup_test_database().await; - let schema_dir = "../../schema"; - init_db_schema(&db, schema_dir) - .await - .expect("Failed to init db schema"); - let trader = trader_id(); - SqlCacheDatabase::new(trader, db) - } - - #[tokio::test] - async fn test_keys() { - let cache = setup_sql_cache_database().await; - assert_eq!(cache.key_trader(), "trader-TRADER-001"); - assert_eq!(cache.key_general(), "trader-TRADER-001:general:"); - } - - #[tokio::test] - async fn test_add_get_general() { - let cache = setup_sql_cache_database().await; - cache - .add(String::from("key1"), String::from("value1")) - .await - .expect("Failed to add key"); - let value = cache.get(String::from("key1")).await; - assert_eq!(value.len(), 1); - let item = value.first().unwrap(); - assert_eq!(item.key, "key1"); - assert_eq!(item.value, "value1"); - } -} diff --git a/nautilus_core/infrastructure/src/sql/cache_database.rs b/nautilus_core/infrastructure/src/sql/cache_database.rs new file mode 100644 index 000000000000..ed6ab08a9e48 --- /dev/null +++ b/nautilus_core/infrastructure/src/sql/cache_database.rs @@ -0,0 +1,144 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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::{ + collections::{HashMap, VecDeque}, + time::{Duration, Instant}, +}; + +use nautilus_model::types::currency::Currency; +use sqlx::{postgres::PgConnectOptions, PgPool}; +use tokio::{ + sync::mpsc::{channel, error::TryRecvError, Receiver, Sender}, + time::sleep, +}; + +use crate::sql::{ + models::general::GeneralRow, + pg::{connect_pg, get_postgres_connect_options}, + queries::DatabaseQueries, +}; + +#[derive(Debug)] +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.persistence") +)] +pub struct PostgresCacheDatabase { + pub pool: PgPool, + tx: Sender, +} + +#[derive(Debug, Clone)] +pub enum DatabaseQuery { + Add(String, Vec), + AddCurrency(Currency), +} + +fn get_buffer_interval() -> Duration { + Duration::from_millis(0) +} + +async fn drain_buffer(pool: &PgPool, buffer: &mut VecDeque) { + for cmd in buffer.drain(..) { + match cmd { + DatabaseQuery::Add(key, value) => { + DatabaseQueries::add(pool, key, value).await.unwrap(); + } + DatabaseQuery::AddCurrency(currency) => { + DatabaseQueries::add_currency(pool, currency).await.unwrap(); + } + } + } +} + +impl PostgresCacheDatabase { + pub async fn connect( + host: Option, + port: Option, + username: Option, + password: Option, + database: Option, + ) -> Result { + let pg_connect_options = + get_postgres_connect_options(host, port, username, password, database).unwrap(); + let pool = connect_pg(pg_connect_options.clone().into()).await.unwrap(); + let (tx, rx) = channel::(1000); + // spawn a thread to handle messages + let _join_handle = tokio::spawn(async move { + PostgresCacheDatabase::handle_message(rx, pg_connect_options.clone().into()).await; + }); + Ok(PostgresCacheDatabase { pool, tx }) + } + + async fn handle_message(mut rx: Receiver, pg_connect_options: PgConnectOptions) { + let pool = connect_pg(pg_connect_options).await.unwrap(); + // Buffering + let mut buffer: VecDeque = VecDeque::new(); + let mut last_drain = Instant::now(); + let buffer_interval = get_buffer_interval(); + let recv_interval = Duration::from_millis(1); + + loop { + if last_drain.elapsed() >= buffer_interval && !buffer.is_empty() { + // drain buffer + drain_buffer(&pool, &mut buffer).await; + last_drain = Instant::now(); + } else { + // Continue to receive and handle messages until channel is hung up + match rx.try_recv() { + Ok(msg) => buffer.push_back(msg), + Err(TryRecvError::Empty) => sleep(recv_interval).await, + Err(TryRecvError::Disconnected) => break, + } + } + } + // rain any remaining message + if !buffer.is_empty() { + drain_buffer(&pool, &mut buffer).await; + } + } + + pub async fn load(&self) -> Result>, sqlx::Error> { + let query = sqlx::query_as::<_, GeneralRow>("SELECT * FROM general"); + let result = query.fetch_all(&self.pool).await; + match result { + Ok(rows) => { + let mut cache: HashMap> = HashMap::new(); + for row in rows { + cache.insert(row.key, row.value); + } + Ok(cache) + } + Err(err) => { + panic!("Failed to load general table: {err}") + } + } + } + + pub async fn add(&self, key: String, value: Vec) -> anyhow::Result<()> { + let query = DatabaseQuery::Add(key, value); + self.tx.send(query).await.map_err(|err| { + anyhow::anyhow!("Failed to send query to database message handler: {err}") + }) + } + + pub async fn add_currency(&self, currency: Currency) -> anyhow::Result<()> { + let query = DatabaseQuery::AddCurrency(currency); + self.tx.send(query).await.map_err(|err| { + anyhow::anyhow!("Failed to query add_currency to database message handler: {err}") + }) + } +} diff --git a/nautilus_core/infrastructure/src/sql/database.rs b/nautilus_core/infrastructure/src/sql/database.rs deleted file mode 100644 index f757025b99ae..000000000000 --- a/nautilus_core/infrastructure/src/sql/database.rs +++ /dev/null @@ -1,226 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (C) 2015-2024 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::{path::Path, str::FromStr}; - -use sqlx::{ - any::{install_default_drivers, AnyConnectOptions}, - sqlite::SqliteConnectOptions, - Error, Pool, SqlitePool, -}; - -#[derive(Clone)] -pub struct Database { - pub pool: Pool, -} - -pub enum DatabaseEngine { - POSTGRES, - SQLITE, -} - -fn str_to_database_engine(engine_str: &str) -> DatabaseEngine { - match engine_str { - "POSTGRES" | "postgres" => DatabaseEngine::POSTGRES, - "SQLITE" | "sqlite" => DatabaseEngine::SQLITE, - _ => panic!("Invalid database engine: {engine_str}"), - } -} - -impl Database { - pub async fn new(engine: Option, conn_string: Option<&str>) -> Self { - install_default_drivers(); - let db_options = Self::get_db_options(engine, conn_string); - let db = sqlx::pool::PoolOptions::new() - .max_connections(20) - .connect_with(db_options) - .await; - match db { - Ok(pool) => Self { pool }, - Err(err) => { - panic!("Failed to connect to database: {err}") - } - } - } - - #[must_use] - pub fn get_db_options( - engine: Option, - conn_string: Option<&str>, - ) -> AnyConnectOptions { - let connection_string = match conn_string { - Some(conn_string) => Ok(conn_string.to_string()), - None => std::env::var("DATABASE_URL"), - }; - let database_engine: DatabaseEngine = match engine { - Some(engine) => engine, - None => str_to_database_engine( - std::env::var("DATABASE_ENGINE") - .unwrap_or("SQLITE".to_string()) - .as_str(), - ), - }; - match connection_string { - Ok(connection_string) => match database_engine { - DatabaseEngine::POSTGRES => AnyConnectOptions::from_str(connection_string.as_str()) - .expect("Invalid PostgresSQL connection string"), - DatabaseEngine::SQLITE => AnyConnectOptions::from_str(connection_string.as_str()) - .expect("Invalid SQLITE connection string"), - }, - Err(err) => { - panic!("Failed to connect to database: {err}") - } - } - } - - pub async fn execute(&self, query_str: &str) -> Result { - let result = sqlx::query(query_str).execute(&self.pool).await?; - - Ok(result.rows_affected()) - } - - pub async fn fetch_all(&self, query_str: &str) -> Result, Error> - where - T: for<'r> sqlx::FromRow<'r, sqlx::any::AnyRow> + Unpin, - { - let rows = sqlx::query(query_str).fetch_all(&self.pool).await?; - - let mut objects = Vec::new(); - for row in rows { - let obj = T::from_row(&row)?; - objects.push(obj); - } - - Ok(objects) - } -} - -pub async fn init_db_schema(db: &Database, schema_dir: &str) -> anyhow::Result<()> { - // scan all the files in the current directory - let mut sql_files = - std::fs::read_dir(schema_dir)?.collect::, std::io::Error>>()?; - - for file in &mut sql_files { - let file_name = file.file_name(); - println!("Executing SQL file: {file_name:?}"); - let file_path = file.path(); - let sql_content = std::fs::read_to_string(file_path.clone())?; - for sql_statement in sql_content.split(';').filter(|s| !s.trim().is_empty()) { - db.execute(sql_statement).await.unwrap_or_else(|e| { - panic!( - "Failed to execute SQL statement: {} with reason {}", - file_path.display(), - e - ) - }); - } - } - Ok(()) -} - -pub async fn setup_test_database() -> Database { - // check if test_db.sqlite exists,if not, create it - let db_path = std::env::var("TEST_DB_PATH").unwrap_or("test_db.sqlite".to_string()); - let db_file_path = Path::new(db_path.as_str()); - let exists = db_file_path.exists(); - if !exists { - SqlitePool::connect_with( - SqliteConnectOptions::new() - .filename(db_file_path) - .create_if_missing(true), - ) - .await - .expect("Failed to create test_db.sqlite"); - } - Database::new(Some(DatabaseEngine::SQLITE), Some("sqlite:test_db.sqlite")).await -} - -//////////////////////////////////////////////////////////////////////////////// -// Tests -//////////////////////////////////////////////////////////////////////////////// -#[cfg(test)] -mod tests { - - use sqlx::{FromRow, Row}; - - use crate::sql::database::{setup_test_database, Database}; - - async fn init_item_table(database: &Database) { - database - .execute("CREATE TABLE IF NOT EXISTS items (key TEXT PRIMARY KEY, value TEXT)") - .await - .expect("Failed to create table item"); - } - - async fn drop_table(database: &Database) { - database - .execute("DROP TABLE items") - .await - .expect("Failed to drop table items"); - } - - #[tokio::test] - async fn test_database() { - let db = setup_test_database().await; - let rows_affected = db.execute("SELECT 1").await.unwrap(); - // it will not fail and give 0 rows affected - assert_eq!(rows_affected, 0); - } - - #[tokio::test] - async fn test_database_fetch_all() { - let db = setup_test_database().await; - struct SimpleValue { - value: i32, - } - impl FromRow<'_, sqlx::any::AnyRow> for SimpleValue { - fn from_row(row: &sqlx::any::AnyRow) -> Result { - Ok(Self { - value: row.try_get(0)?, - }) - } - } - let result = db.fetch_all::("SELECT 3").await.unwrap(); - assert_eq!(result[0].value, 3); - } - - #[tokio::test] - async fn test_insert_and_select() { - let db = setup_test_database().await; - init_item_table(&db).await; - // insert some value - db.execute("INSERT INTO items (key, value) VALUES ('key1', 'value1')") - .await - .unwrap(); - // fetch item, impl Data struct - struct Item { - key: String, - value: String, - } - impl FromRow<'_, sqlx::any::AnyRow> for Item { - fn from_row(row: &sqlx::any::AnyRow) -> Result { - Ok(Self { - key: row.try_get(0)?, - value: row.try_get(1)?, - }) - } - } - let result = db.fetch_all::("SELECT * FROM items").await.unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].key, "key1"); - assert_eq!(result[0].value, "value1"); - drop_table(&db).await; - } -} diff --git a/nautilus_core/infrastructure/src/sql/mod.rs b/nautilus_core/infrastructure/src/sql/mod.rs index f098c3cf9d49..6e14caa333d3 100644 --- a/nautilus_core/infrastructure/src/sql/mod.rs +++ b/nautilus_core/infrastructure/src/sql/mod.rs @@ -13,12 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -/// Be careful about ordering and foreign key constraints when deleting data. -/// We can use this list for manual truncation of tables. -pub const NAUTILUS_TABLES: [&str; 5] = - ["general", "instrument", "currency", "order", "order_event"]; +// Be careful about ordering and foreign key constraints when deleting data. +pub const NAUTILUS_TABLES: [&str; 2] = ["general", "currency"]; -pub mod database; +pub mod cache_database; pub mod models; pub mod pg; -pub mod schema; +pub mod queries; diff --git a/nautilus_core/infrastructure/src/sql/schema.rs b/nautilus_core/infrastructure/src/sql/models/general.rs similarity index 91% rename from nautilus_core/infrastructure/src/sql/schema.rs rename to nautilus_core/infrastructure/src/sql/models/general.rs index 2a551437f16c..824714a2c0d3 100644 --- a/nautilus_core/infrastructure/src/sql/schema.rs +++ b/nautilus_core/infrastructure/src/sql/models/general.rs @@ -13,8 +13,8 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -#[derive(sqlx::FromRow)] -pub struct GeneralItem { +#[derive(Debug, sqlx::FromRow)] +pub struct GeneralRow { pub key: String, - pub value: String, + pub value: Vec, } diff --git a/nautilus_core/infrastructure/src/sql/models/mod.rs b/nautilus_core/infrastructure/src/sql/models/mod.rs index 02d78ede908a..4fe4acea056d 100644 --- a/nautilus_core/infrastructure/src/sql/models/mod.rs +++ b/nautilus_core/infrastructure/src/sql/models/mod.rs @@ -13,4 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +pub mod general; pub mod instruments; +pub mod types; diff --git a/nautilus_core/infrastructure/src/sql/models/types.rs b/nautilus_core/infrastructure/src/sql/models/types.rs new file mode 100644 index 000000000000..a2d98170f723 --- /dev/null +++ b/nautilus_core/infrastructure/src/sql/models/types.rs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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_model::{enums::CurrencyType, types::currency::Currency}; +use sqlx::{postgres::PgRow, FromRow, Row}; + +pub struct CurrencyModel(pub Currency); + +impl<'r> FromRow<'r, PgRow> for CurrencyModel { + fn from_row(row: &'r PgRow) -> Result { + let code = row.try_get::("code")?; + let precision = row.try_get::("precision")?; + let iso4217 = row.try_get::("iso4217")?; + let name = row.try_get::("name")?; + let currency_type = row + .try_get::("currency_type") + .map(|res| CurrencyType::from_str(res.as_str()).unwrap())?; + + let currency = Currency::new( + code.as_str(), + precision as u8, + iso4217 as u16, + name.as_str(), + currency_type, + ) + .unwrap(); + Ok(CurrencyModel(currency)) + } +} diff --git a/nautilus_core/infrastructure/src/sql/pg.rs b/nautilus_core/infrastructure/src/sql/pg.rs index c86a5245e0d3..fd31fea4c917 100644 --- a/nautilus_core/infrastructure/src/sql/pg.rs +++ b/nautilus_core/infrastructure/src/sql/pg.rs @@ -14,6 +14,7 @@ // ------------------------------------------------------------------------------------------------- use sqlx::{postgres::PgConnectOptions, query, ConnectOptions, PgPool}; +use tracing::log::{error, info}; use crate::sql::NAUTILUS_TABLES; @@ -112,3 +113,201 @@ pub async fn delete_nautilus_postgres_tables(db: &PgPool) -> anyhow::Result<()> pub async fn connect_pg(options: PgConnectOptions) -> anyhow::Result { Ok(PgPool::connect_with(options).await.unwrap()) } + +/// Scans current path with keyword nautilus_trader and build schema dir +fn get_schema_dir() -> anyhow::Result { + std::env::var("SCHEMA_DIR").or_else(|_| { + let nautilus_git_repo_name = "nautilus_trader"; + let binding = std::env::current_dir().unwrap(); + let current_dir = binding.to_str().unwrap(); + match current_dir.find(nautilus_git_repo_name){ + Some(index) => { + let schema_path = current_dir[0..index + nautilus_git_repo_name.len()].to_string() + "/schema"; + Ok(schema_path) + } + None => anyhow::bail!("Could not calculate schema dir from current directory path or SCHEMA_DIR env variable") + } + }) +} + +pub async fn init_postgres( + pg: &PgPool, + database: String, + password: String, + schema_dir: Option, +) -> anyhow::Result<()> { + info!("Initializing Postgres database with target permissions and schema"); + // create public schema + match sqlx::query("CREATE SCHEMA IF NOT EXISTS public;") + .execute(pg) + .await + { + Ok(_) => info!("Schema public created successfully"), + Err(err) => error!("Error creating schema public: {:?}", err), + } + // create role if not exists + match sqlx::query(format!("CREATE ROLE {} PASSWORD '{}' LOGIN;", database, password).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Role {} created successfully", database), + Err(err) => { + if err.to_string().contains("already exists") { + info!("Role {} already exists", database); + } else { + error!("Error creating role {}: {:?}", database, err); + } + } + } + // execute all the sql files in schema dir + let schema_dir = schema_dir.unwrap_or_else(|| get_schema_dir().unwrap()); + let mut sql_files = + std::fs::read_dir(schema_dir)?.collect::, std::io::Error>>()?; + for file in &mut sql_files { + let file_name = file.file_name(); + info!("Executing schema file: {:?}", file_name); + let file_path = file.path(); + let sql_content = std::fs::read_to_string(file_path.clone())?; + for sql_statement in sql_content.split(';').filter(|s| !s.trim().is_empty()) { + sqlx::query(sql_statement) + .execute(pg) + .await + .map_err(|err| { + if err.to_string().contains("already exists") { + info!("Already exists error on statement, skipping"); + } else { + panic!( + "Error executing statement {} with error: {:?}", + sql_statement, err + ) + } + }) + .unwrap(); + } + } + // grant connect + match sqlx::query(format!("GRANT CONNECT ON DATABASE {0} TO {0};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Connect privileges granted to role {}", database), + Err(err) => error!( + "Error granting connect privileges to role {}: {:?}", + database, err + ), + } + // grant all schema privileges to the role + match sqlx::query(format!("GRANT ALL PRIVILEGES ON SCHEMA public TO {};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("All schema privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + // grant all table privileges to the role + match sqlx::query( + format!( + "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {};", + database + ) + .as_str(), + ) + .execute(pg) + .await + { + Ok(_) => info!("All tables privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + // grant all sequence privileges to the role + match sqlx::query( + format!( + "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {};", + database + ) + .as_str(), + ) + .execute(pg) + .await + { + Ok(_) => info!("All sequences privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + // grant all function privileges to the role + match sqlx::query( + format!( + "GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO {};", + database + ) + .as_str(), + ) + .execute(pg) + .await + { + Ok(_) => info!("All functions privileges granted to role {}", database), + Err(err) => error!( + "Error granting all privileges to role {}: {:?}", + database, err + ), + } + + Ok(()) +} + +pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> { + // execute drop owned + match sqlx::query(format!("DROP OWNED BY {}", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Dropped owned objects by role {}", database), + Err(err) => error!("Error dropping owned by role {}: {:?}", database, err), + } + // revoke connect + match sqlx::query(format!("REVOKE CONNECT ON DATABASE {0} FROM {0};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Revoked connect privileges from role {}", database), + Err(err) => error!( + "Error revoking connect privileges from role {}: {:?}", + database, err + ), + } + // revoke privileges + match sqlx::query(format!("REVOKE ALL PRIVILEGES ON DATABASE {0} FROM {0};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Revoked all privileges from role {}", database), + Err(err) => error!( + "Error revoking all privileges from role {}: {:?}", + database, err + ), + } + // execute drop schema + match sqlx::query("DROP SCHEMA IF EXISTS public CASCADE") + .execute(pg) + .await + { + Ok(_) => info!("Dropped schema public"), + Err(err) => error!("Error dropping schema public: {:?}", err), + } + // drop role + match sqlx::query(format!("DROP ROLE IF EXISTS {};", database).as_str()) + .execute(pg) + .await + { + Ok(_) => info!("Dropped role {}", database), + Err(err) => error!("Error dropping role {}: {:?}", database, err), + } + Ok(()) +} diff --git a/nautilus_core/infrastructure/src/sql/queries.rs b/nautilus_core/infrastructure/src/sql/queries.rs new file mode 100644 index 000000000000..c0db3edc4bc3 --- /dev/null +++ b/nautilus_core/infrastructure/src/sql/queries.rs @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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::collections::HashMap; + +use nautilus_model::types::currency::Currency; +use sqlx::PgPool; + +use crate::sql::models::{general::GeneralRow, types::CurrencyModel}; + +pub struct DatabaseQueries; + +impl DatabaseQueries { + pub async fn add(pool: &PgPool, key: String, value: Vec) -> anyhow::Result<()> { + sqlx::query("INSERT INTO general (key, value) VALUES ($1, $2)") + .bind(key) + .bind(value) + .execute(pool) + .await + .map(|_| ()) + .map_err(|err| anyhow::anyhow!("Failed to insert into general table: {err}")) + } + + pub async fn load(pool: &PgPool) -> anyhow::Result>> { + sqlx::query_as::<_, GeneralRow>("SELECT * FROM general") + .fetch_all(pool) + .await + .map(|rows| { + let mut cache: HashMap> = HashMap::new(); + for row in rows { + cache.insert(row.key, row.value); + } + cache + }) + .map_err(|err| anyhow::anyhow!("Failed to load general table: {err}")) + } + + pub async fn add_currency(pool: &PgPool, currency: Currency) -> anyhow::Result<()> { + sqlx::query( + "INSERT INTO currency (code, precision, iso4217, name, currency_type) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (code) DO NOTHING" + ) + .bind(currency.code.as_str()) + .bind(currency.precision as i32) + .bind(currency.iso4217 as i32) + .bind(currency.name.as_str()) + .bind(currency.currency_type.to_string()) + .execute(pool) + .await + .map(|_| ()) + .map_err(|err| anyhow::anyhow!("Failed to insert into currency table: {err}")) + } + + pub async fn load_currencies(pool: &PgPool) -> anyhow::Result> { + sqlx::query_as::<_, CurrencyModel>("SELECT * FROM currency ORDER BY code ASC") + .fetch_all(pool) + .await + .map(|rows| rows.into_iter().map(|row| row.0).collect()) + .map_err(|err| anyhow::anyhow!("Failed to load currencies: {err}")) + } + + pub async fn load_currency(pool: &PgPool, code: &str) -> anyhow::Result> { + sqlx::query_as::<_, CurrencyModel>("SELECT * FROM currency WHERE code = $1") + .bind(code) + .fetch_optional(pool) + .await + .map(|currency| currency.map(|row| row.0)) + .map_err(|err| anyhow::anyhow!("Failed to load currency: {err}")) + } + + pub async fn truncate(pool: &PgPool, table: String) -> anyhow::Result<()> { + sqlx::query(format!("TRUNCATE TABLE {} CASCADE", table).as_str()) + .execute(pool) + .await + .map(|_| ()) + .map_err(|err| anyhow::anyhow!("Failed to truncate table: {err}")) + } +} diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs new file mode 100644 index 000000000000..05d3cc39ff1a --- /dev/null +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 nautilus_infrastructure::sql::{ + cache_database::PostgresCacheDatabase, + pg::{connect_pg, delete_nautilus_postgres_tables, PostgresConnectOptions}, +}; +use sqlx::PgPool; + +pub fn get_test_pg_connect_options(username: &str) -> PostgresConnectOptions { + PostgresConnectOptions::new( + "localhost".to_string(), + 5432, + username.to_string(), + "pass".to_string(), + "nautilus".to_string(), + ) +} +pub async fn get_pg(username: &str) -> PgPool { + let pg_connect_options = get_test_pg_connect_options(username); + connect_pg(pg_connect_options.into()).await.unwrap() +} + +pub async fn initialize() -> anyhow::Result<()> { + // get pg pool with root postgres user to drop & create schema + let pg_pool = get_pg("postgres").await; + delete_nautilus_postgres_tables(&pg_pool).await.unwrap(); + Ok(()) +} + +pub async fn get_pg_cache_database() -> anyhow::Result { + initialize().await.unwrap(); + // run tests as nautilus user + let connect_options = get_test_pg_connect_options("nautilus"); + Ok(PostgresCacheDatabase::connect( + Some(connect_options.host), + Some(connect_options.port), + Some(connect_options.username), + Some(connect_options.password), + Some(connect_options.database), + ) + .await + .unwrap()) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::get_pg_cache_database; + + #[tokio::test] + async fn test_load_general_objects_when_nothing_in_cache_returns_empty_hashmap() { + let pg_cache = get_pg_cache_database().await.unwrap(); + let result = pg_cache.load().await.unwrap(); + println!("1: {:?}", result); + assert_eq!(result.len(), 0); + } + + #[tokio::test] + async fn test_add_general_object_adds_to_cache() { + let pg_cache = get_pg_cache_database().await.unwrap(); + let test_id_value = String::from("test_value").into_bytes(); + pg_cache + .add(String::from("test_id"), test_id_value.clone()) + .await + .unwrap(); + // sleep with tokio + tokio::time::sleep(Duration::from_secs(1)).await; + let result = pg_cache.load().await.unwrap(); + println!("2: {:?}", result); + assert_eq!(result.keys().len(), 1); + assert_eq!( + result.keys().cloned().collect::>(), + vec![String::from("test_id")] + ); // assert_eq!(result.get(&test_id_key).unwrap().to_owned(),&test_id_value.clone()); + assert_eq!(result.get("test_id").unwrap().to_owned(), test_id_value); + } +} diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index 59e60ca1c617..d95388b88c95 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["rlib", "staticlib", "cdylib"] [dependencies] nautilus-core = { path = "../core" } -nautilus-model = { path = "../model" } +nautilus-model = { path = "../model", features = ["stubs"] } anyhow = { workspace = true } futures = { workspace = true } pyo3 = { workspace = true, optional = true } @@ -35,8 +35,8 @@ procfs = "0.16.0" [features] default = ["ffi", "python"] extension-module = [ - "pyo3/extension-module", - "nautilus-core/extension-module", + "pyo3/extension-module", + "nautilus-core/extension-module", "nautilus-model/extension-module", ] ffi = ["nautilus-core/ffi", "nautilus-model/ffi"] diff --git a/nautilus_trader/cache/postgres/__init__.py b/nautilus_trader/cache/postgres/__init__.py new file mode 100644 index 000000000000..3d34cab4588e --- /dev/null +++ b/nautilus_trader/cache/postgres/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- diff --git a/nautilus_trader/cache/postgres/adapter.py b/nautilus_trader/cache/postgres/adapter.py new file mode 100644 index 000000000000..fe67593bc242 --- /dev/null +++ b/nautilus_trader/cache/postgres/adapter.py @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.cache.config import CacheConfig +from nautilus_trader.cache.facade import CacheDatabaseFacade +from nautilus_trader.cache.postgres.transformers import transform_currency_from_pyo3 +from nautilus_trader.cache.postgres.transformers import transform_currency_to_pyo3 +from nautilus_trader.core.nautilus_pyo3 import PostgresCacheDatabase +from nautilus_trader.model.objects import Currency + + +class CachePostgresAdapter(CacheDatabaseFacade): + + def __init__( + self, + config: CacheConfig | None = None, + ): + if config: + config = CacheConfig() + super().__init__(config) + self._backing: PostgresCacheDatabase = PostgresCacheDatabase.connect() + + def flush(self): + self._backing.flush_db() + + def load(self): + data = self._backing.load() + return {key: bytes(value) for key, value in data.items()} + + def add(self, key: str, value: bytes): + self._backing.add(key, value) + + def add_currency(self, currency: Currency): + currency_pyo3 = transform_currency_to_pyo3(currency) + self._backing.add_currency(currency_pyo3) + + def load_currencies(self) -> dict[str, Currency]: + currencies = self._backing.load_currencies() + return {currency.code: transform_currency_from_pyo3(currency) for currency in currencies} + + def load_currency(self, code: str) -> Currency | None: + currency_pyo3 = self._backing.load_currency(code) + if currency_pyo3: + return transform_currency_from_pyo3(currency_pyo3) + return None diff --git a/nautilus_trader/cache/postgres/transformers.py b/nautilus_trader/cache/postgres/transformers.py new file mode 100644 index 000000000000..65542490015d --- /dev/null +++ b/nautilus_trader/cache/postgres/transformers.py @@ -0,0 +1,41 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.enums import CurrencyType +from nautilus_trader.model.objects import Currency + + +################################################################################ +# Currency +################################################################################ +def transform_currency_from_pyo3(currency: nautilus_pyo3.Currency) -> Currency: + return Currency( + code=currency.code, + precision=currency.precision, + iso4217=currency.iso4217, + name=currency.name, + currency_type=CurrencyType(currency.currency_type.value), + ) + + +def transform_currency_to_pyo3(currency: Currency) -> nautilus_pyo3.Currency: + return nautilus_pyo3.Currency( + code=currency.code, + precision=currency.precision, + iso4217=currency.iso4217, + name=currency.name, + currency_type=nautilus_pyo3.CurrencyType.from_str(currency.currency_type.name), + ) diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index ea41259eb7cf..d027c8213435 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -682,6 +682,8 @@ class CurrencyType(Enum): CRYPTO = "CRYPTO" FIAT = "FIAT" COMMODITY_BACKED = "COMMODITY_BACKED" + @classmethod + def from_str(cls, value: str) -> CurrencyType: ... class InstrumentCloseType(Enum): END_OF_SESSION = "END_OF_SESSION" @@ -2250,6 +2252,24 @@ class RedisCacheDatabase: config: dict[str, Any], ) -> None: ... +class PostgresCacheDatabase: + @classmethod + def connect( + cls, + host: str | None = None, + port: str | None = None, + username: str | None = None, + password: str | None = None, + database: str | None = None, + )-> PostgresCacheDatabase: ... + def load(self) -> dict[str,str]: ... + def add(self, key: str, value: bytes) -> None: ... + def add_currency(self,currency: Currency) -> None: ... + def load_currency(self, code: str) -> Currency | None: ... + def load_currencies(self) -> list[Currency]: ... + def flush_db(self) -> None: ... + def truncate(self, table: str) -> None: ... + ################################################################################################### # Network ################################################################################################### diff --git a/tests/integration_tests/infrastructure/test_cache_database_postgres.py b/tests/integration_tests/infrastructure/test_cache_database_postgres.py new file mode 100644 index 000000000000..759a3de3fa75 --- /dev/null +++ b/tests/integration_tests/infrastructure/test_cache_database_postgres.py @@ -0,0 +1,125 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + +import os + +import pytest + +from nautilus_trader.cache.postgres.adapter import CachePostgresAdapter +from nautilus_trader.common.component import MessageBus +from nautilus_trader.common.component import TestClock +from nautilus_trader.model.enums import CurrencyType +from nautilus_trader.model.objects import Currency +from nautilus_trader.portfolio.portfolio import Portfolio +from nautilus_trader.test_kit.functions import eventually +from nautilus_trader.test_kit.providers import TestInstrumentProvider +from nautilus_trader.test_kit.stubs.component import TestComponentStubs +from nautilus_trader.test_kit.stubs.data import TestDataStubs +from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs +from nautilus_trader.trading.strategy import Strategy + + +AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") + + +class TestCachePostgresAdapter: + def setup(self): + # set envs + os.environ["POSTGRES_HOST"] = "localhost" + os.environ["POSTGRES_PORT"] = "5432" + os.environ["POSTGRES_USERNAME"] = "nautilus" + os.environ["POSTGRES_PASSWORD"] = "pass" + os.environ["POSTGRES_DATABASE"] = "nautilus" + self.database: CachePostgresAdapter = CachePostgresAdapter() + # reset database + self.database.flush() + self.clock = TestClock() + + self.trader_id = TestIdStubs.trader_id() + + self.msgbus = MessageBus( + trader_id=self.trader_id, + clock=self.clock, + ) + + self.cache = TestComponentStubs.cache() + + self.portfolio = Portfolio( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + # Init strategy + self.strategy = Strategy() + self.strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + def teardown(self): + self.database.flush() + + @pytest.mark.asyncio + async def test_load_general_objects_when_nothing_in_cache_returns_empty_dict(self): + # Arrange, Act + result = self.database.load() + + # Assert + assert result == {} + + @pytest.mark.asyncio + async def test_add_general_object_adds_to_cache(self): + # Arrange + bar = TestDataStubs.bar_5decimal() + key = str(bar.bar_type) + "-" + str(bar.ts_event) + + # Act + self.database.add(key, str(bar).encode()) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load()) + + # Assert + assert self.database.load() == {key: str(bar).encode()} + + ################################################################################ + # Currency + ################################################################################ + @pytest.mark.asyncio + async def test_add_currency(self): + # Arrange + currency = Currency( + code="BTC", + precision=8, + iso4217=0, + name="BTC", + currency_type=CurrencyType.CRYPTO, + ) + + # Act + self.database.add_currency(currency) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_currency(currency.code)) + + # Assert + assert self.database.load_currency(currency.code) == currency + + currencies = self.database.load_currencies() + assert list(currencies.keys()) == ["BTC"] From c22bf00fe948cf6adc6ae7810badc629e97f2480 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 25 Apr 2024 21:34:46 +1000 Subject: [PATCH 042/193] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 0419baf231a7..d6d14fac28fc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,7 @@ Released on TBD (UTC). ### Enhancements - Added Nautilus CLI (see [docs](https://docs.nautilustrader.io/nightly/developer_guide/index.html)), many thanks @filipmacek +- Added futures and options contract activation and expiration simulation ### Breaking Changes - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) From b5a9360bda391bee170e8141da72d59c0d987309 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 26 Apr 2024 16:55:13 +1000 Subject: [PATCH 043/193] Update dependencies --- .pre-commit-config.yaml | 6 +-- nautilus_core/Cargo.lock | 31 +++++++++------ poetry.lock | 84 ++++++++++++++++++++-------------------- pyproject.toml | 6 +-- 4 files changed, 68 insertions(+), 59 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fbae3b7a567..2a1c9594e300 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -73,7 +73,7 @@ repos: types: [python] - repo: https://github.com/psf/black - rev: 24.4.1 + rev: 24.4.2 hooks: - id: black types_or: [python, pyi] @@ -82,7 +82,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.1 + rev: v0.4.2 hooks: - id: ruff args: ["--fix"] @@ -111,7 +111,7 @@ repos: ] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.0 hooks: - id: mypy args: [ diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 588d4ba3d425..32c8c268dd00 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2423,9 +2423,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -3133,9 +3133,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -3143,15 +3143,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -3675,6 +3675,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -5454,7 +5463,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "redox_syscall", + "redox_syscall 0.4.1", "wasite", ] @@ -5716,9 +5725,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63381fa6624bf92130a6b87c0d07380116f80b565c42cf0d754136f0238359ef" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zstd" diff --git a/poetry.lock b/poetry.lock index c9e00de50fe3..efc012cf78a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,33 +202,33 @@ msgspec = ">=0.18.5" [[package]] name = "black" -version = "24.4.1" +version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f7749fd0d97ff9415975a1432fac7df89bf13c3833cea079e55fa004d5f28c0"}, - {file = "black-24.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859f3cc5d2051adadf8fd504a01e02b0fd866d7549fff54bc9202d524d2e8bd7"}, - {file = "black-24.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59271c9c29dfa97f7fda51f56c7809b3f78e72fd8d2205189bbd23022a0618b6"}, - {file = "black-24.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:5ed9c34cba223149b5a0144951a0f33d65507cf82c5449cb3c35fe4b515fea9a"}, - {file = "black-24.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dae3ae59d6f2dc93700fd5034a3115434686e66fd6e63d4dcaa48d19880f2b0"}, - {file = "black-24.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5f8698974a81af83283eb47644f2711b5261138d6d9180c863fce673cbe04b13"}, - {file = "black-24.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f404b6e77043b23d0321fb7772522b876b6de737ad3cb97d6b156638d68ce81"}, - {file = "black-24.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:c94e52b766477bdcd010b872ba0714d5458536dc9d0734eff6583ba7266ffd89"}, - {file = "black-24.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:962d9e953872cdb83b97bb737ad47244ce2938054dc946685a4cad98520dab38"}, - {file = "black-24.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d8e3b2486b7dd522b1ab2ba1ec4907f0aa8f5e10a33c4271fb331d1d10b70c"}, - {file = "black-24.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed77e214b785148f57e43ca425b6e0850165144aa727d66ac604e56a70bb7825"}, - {file = "black-24.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4ef4764437d7eba8386689cd06e1fb5341ee0ae2e9e22582b21178782de7ed94"}, - {file = "black-24.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:92b183f8eef5baf7b20a513abcf982ad616f544f593f6688bb2850d2982911f1"}, - {file = "black-24.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:945abd7b3572add997757c94295bb3e73c6ffaf3366b1f26cb2356a4bffd1dc3"}, - {file = "black-24.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db5154b9e5b478031371d8bc41ff37b33855fa223a6cfba456c9b73fb96f77d4"}, - {file = "black-24.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:afc84c33c1a9aaf3d73140cee776b4ddf73ff429ffe6b7c56dc1c9c10725856d"}, - {file = "black-24.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0889f4eb8b3bdf8b189e41a71cf0dbb8141a98346cd1a2695dea5995d416e940"}, - {file = "black-24.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5bb0143f175db45a55227eefd63e90849d96c266330ba31719e9667d0d5ec3b9"}, - {file = "black-24.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:713a04a78e78f28ef7e8df7a16fe075670ea164860fcef3885e4f3dffc0184b3"}, - {file = "black-24.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:171959bc879637a8cdbc53dc3fddae2a83e151937a28cf605fd175ce61e0e94a"}, - {file = "black-24.4.1-py3-none-any.whl", hash = "sha256:ecbab810604fe02c70b3a08afd39beb599f7cc9afd13e81f5336014133b4fe35"}, - {file = "black-24.4.1.tar.gz", hash = "sha256:5241612dc8cad5b6fd47432b8bd04db80e07cfbc53bb69e9ae18985063bcb8dd"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -2020,28 +2020,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.1" +version = "0.4.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2d9ef6231e3fbdc0b8c72404a1a0c46fd0dcea84efca83beb4681c318ea6a953"}, - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9485f54a7189e6f7433e0058cf8581bee45c31a25cd69009d2a040d1bd4bfaef"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2921ac03ce1383e360e8a95442ffb0d757a6a7ddd9a5be68561a671e0e5807e"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eec8d185fe193ad053eda3a6be23069e0c8ba8c5d20bc5ace6e3b9e37d246d3f"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa27d9d72a94574d250f42b7640b3bd2edc4c58ac8ac2778a8c82374bb27984"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f1ee41580bff1a651339eb3337c20c12f4037f6110a36ae4a2d864c52e5ef954"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0926cefb57fc5fced629603fbd1a23d458b25418681d96823992ba975f050c2b"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6e37f2e3cd74496a74af9a4fa67b547ab3ca137688c484749189bf3a686ceb"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd703a5975ac1998c2cc5e9494e13b28f31e66c616b0a76e206de2562e0843c"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b92f03b4aa9fa23e1799b40f15f8b95cdc418782a567d6c43def65e1bbb7f1cf"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c859f294f8633889e7d77de228b203eb0e9a03071b72b5989d89a0cf98ee262"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b34510141e393519a47f2d7b8216fec747ea1f2c81e85f076e9f2910588d4b64"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6e68d248ed688b9d69fd4d18737edcbb79c98b251bba5a2b031ce2470224bdf9"}, - {file = "ruff-0.4.1-py3-none-win32.whl", hash = "sha256:b90506f3d6d1f41f43f9b7b5ff845aeefabed6d2494307bc7b178360a8805252"}, - {file = "ruff-0.4.1-py3-none-win_amd64.whl", hash = "sha256:c7d391e5936af5c9e252743d767c564670dc3889aff460d35c518ee76e4b26d7"}, - {file = "ruff-0.4.1-py3-none-win_arm64.whl", hash = "sha256:a1eaf03d87e6a7cd5e661d36d8c6e874693cb9bc3049d110bc9a97b350680c43"}, - {file = "ruff-0.4.1.tar.gz", hash = "sha256:d592116cdbb65f8b1b7e2a2b48297eb865f6bdc20641879aa9d7b9c11d86db79"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, + {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, + {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, + {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, + {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, ] [[package]] @@ -2686,4 +2686,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "af1b6cb1fb038e1cf2648c5cb47e117917332d1f0bd574d417fae9c06a97aa68" +content-hash = "279104839c15b07ca74c197e5490a56059ac17c8099a0ab5b4c01a41d5761921" diff --git a/pyproject.toml b/pyproject.toml index 881d6e0e849a..87300168d4bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,12 +78,12 @@ ib = ["nautilus_ibapi", "async-timeout", "defusedxml"] optional = true [tool.poetry.group.dev.dependencies] -black = "^24.4.1" +black = "^24.4.2" docformatter = "^1.7.5" -mypy = "^1.9.0" +mypy = "^1.10.0" pandas-stubs = "^2.2.1" pre-commit = "^3.7.0" -ruff = "^0.4.1" +ruff = "^0.4.2" types-pytz = "^2023.3" types-requests = "^2.31" types-toml = "^0.10.2" From 2423a3db9c57c3f44a2786d2d96490ec3e611567 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 26 Apr 2024 16:55:37 +1000 Subject: [PATCH 044/193] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index d6d14fac28fc..8f3e076c85fe 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,7 @@ Released on TBD (UTC). ### Enhancements - Added Nautilus CLI (see [docs](https://docs.nautilustrader.io/nightly/developer_guide/index.html)), many thanks @filipmacek +- Added `Cfd` and `Commodity` instruments with Interactive Brokers support, thanks @DracheShiki - Added futures and options contract activation and expiration simulation ### Breaking Changes From 0b08c51596f99fe4da41e6d4d1bc688d6690b818 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Fri, 26 Apr 2024 23:33:43 +0200 Subject: [PATCH 045/193] Add *.iml files to .gitignore (#1611) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7ea9a3fc400b..c5ebf2cfcc27 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ *.env *.tar.gz* *.zip +*.iml *.dbz *.dbn From 7896810c88f4fe345cde836e1a6d8590c4962e96 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 10:13:18 +1000 Subject: [PATCH 046/193] Continue Cache in Rust --- nautilus_core/common/src/cache/mod.rs | 616 ++++++++++++++++++++---- nautilus_core/model/src/orders/base.rs | 84 +++- nautilus_core/model/src/polymorphism.rs | 22 +- 3 files changed, 613 insertions(+), 109 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 54cd41f8e50b..9d833a0d8d71 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -31,7 +31,7 @@ use nautilus_model::{ quote::QuoteTick, trade::TradeTick, }, - enums::{OrderSide, PositionSide, PriceType}, + enums::{AggregationSource, OrderSide, PositionSide, PriceType}, identifiers::{ account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, @@ -42,11 +42,11 @@ use nautilus_model::{ orderbook::book::OrderBook, orders::{base::OrderAny, list::OrderList}, polymorphism::{ - GetClientOrderId, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetOrderSide, - GetStrategyId, + GetClientOrderId, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetOrderFilledQty, + GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetStrategyId, IsClosed, }, position::Position, - types::{currency::Currency, price::Price}, + types::{currency::Currency, price::Price, quantity::Quantity}, }; use ustr::Ustr; @@ -122,7 +122,7 @@ pub struct CacheIndex { strategy_orders: HashMap>, strategy_positions: HashMap>, exec_algorithm_orders: HashMap>, - exec_spawn_orders: HashMap>, + exec_spawn_orders: HashMap>, orders: HashSet, orders_open: HashSet, orders_closed: HashSet, @@ -185,7 +185,7 @@ pub struct Cache { synthetics: HashMap, accounts: HashMap>, orders: HashMap, - order_lists: HashMap>, + order_lists: HashMap, positions: HashMap, position_snapshots: HashMap>, } @@ -249,7 +249,7 @@ impl Cache { } } - // -- COMMANDS ------------------------------------------------------------ + // -- COMMANDS -------------------------------------------------------------------------------- pub fn cache_general(&mut self) -> anyhow::Result<()> { self.general = match &self.database { @@ -684,13 +684,13 @@ impl Cache { Ok(()) } - // -- IDENTIFIER QUERIES -------------------------------------------------- + // -- IDENTIFIER QUERIES ---------------------------------------------------------------------- fn build_order_query_filter_set( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> Option> { let mut query: Option> = None; @@ -698,7 +698,7 @@ impl Cache { query = Some( self.index .venue_orders - .get(&venue) + .get(venue) .map_or(HashSet::new(), |o| o.iter().copied().collect()), ); }; @@ -707,7 +707,7 @@ impl Cache { let instrument_orders = self .index .instrument_orders - .get(&instrument_id) + .get(instrument_id) .map_or(HashSet::new(), |o| o.iter().copied().collect()); if let Some(existing_query) = &mut query { @@ -724,7 +724,7 @@ impl Cache { let strategy_orders = self .index .strategy_orders - .get(&strategy_id) + .get(strategy_id) .map_or(HashSet::new(), |o| o.iter().copied().collect()); if let Some(existing_query) = &mut query { @@ -742,9 +742,9 @@ impl Cache { fn build_position_query_filter_set( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> Option> { let mut query: Option> = None; @@ -752,7 +752,7 @@ impl Cache { query = Some( self.index .venue_positions - .get(&venue) + .get(venue) .map_or(HashSet::new(), |p| p.iter().copied().collect()), ); }; @@ -761,7 +761,7 @@ impl Cache { let instrument_positions = self .index .instrument_positions - .get(&instrument_id) + .get(instrument_id) .map_or(HashSet::new(), |p| p.iter().copied().collect()); if let Some(existing_query) = query { @@ -780,7 +780,7 @@ impl Cache { let strategy_positions = self .index .strategy_positions - .get(&strategy_id) + .get(strategy_id) .map_or(HashSet::new(), |p| p.iter().copied().collect()); if let Some(existing_query) = query { @@ -800,7 +800,7 @@ impl Cache { fn get_orders_for_ids( &self, - client_order_ids: HashSet, + client_order_ids: &HashSet, side: Option, ) -> Vec<&OrderAny> { let side = side.unwrap_or(OrderSide::NoOrderSide); @@ -809,7 +809,7 @@ impl Cache { for client_order_id in client_order_ids { let order = self .orders - .get(&client_order_id) + .get(client_order_id) .unwrap_or_else(|| panic!("Order {client_order_id} not found")); if side == OrderSide::NoOrderSide || side == order.order_side() { orders.push(order); @@ -821,7 +821,7 @@ impl Cache { fn get_positions_for_ids( &self, - position_ids: HashSet<&PositionId>, + position_ids: &HashSet, side: Option, ) -> Vec<&Position> { let side = side.unwrap_or(PositionSide::NoPositionSide); @@ -843,9 +843,9 @@ impl Cache { #[must_use] pub fn client_order_ids( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -857,9 +857,9 @@ impl Cache { #[must_use] pub fn client_order_ids_open( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -876,9 +876,9 @@ impl Cache { #[must_use] pub fn client_order_ids_closed( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -895,9 +895,9 @@ impl Cache { #[must_use] pub fn client_order_ids_emulated( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -914,9 +914,9 @@ impl Cache { #[must_use] pub fn client_order_ids_inflight( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -933,9 +933,9 @@ impl Cache { #[must_use] pub fn position_ids( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -947,9 +947,9 @@ impl Cache { #[must_use] pub fn position_open_ids( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -966,9 +966,9 @@ impl Cache { #[must_use] pub fn position_closed_ids( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, ) -> HashSet { let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); match query { @@ -997,88 +997,88 @@ impl Cache { self.index.exec_algorithms.clone() } - // -- ORDER QUERIES ------------------------------------------------------- + // -- ORDER QUERIES --------------------------------------------------------------------------- #[must_use] - pub fn order(&self, client_order_id: ClientOrderId) -> Option<&OrderAny> { - self.orders.get(&client_order_id) + pub fn order(&self, client_order_id: &ClientOrderId) -> Option<&OrderAny> { + self.orders.get(client_order_id) } #[must_use] - pub fn client_order_id(&self, venue_order_id: VenueOrderId) -> Option<&ClientOrderId> { - self.index.order_ids.get(&venue_order_id) + pub fn client_order_id(&self, venue_order_id: &VenueOrderId) -> Option<&ClientOrderId> { + self.index.order_ids.get(venue_order_id) } #[must_use] - pub fn venue_order_id(&self, client_order_id: ClientOrderId) -> Option { + pub fn venue_order_id(&self, client_order_id: &ClientOrderId) -> Option { self.orders - .get(&client_order_id) + .get(client_order_id) .and_then(nautilus_model::polymorphism::GetVenueOrderId::venue_order_id) } #[must_use] - pub fn client_id(&self, client_order_id: ClientOrderId) -> Option<&ClientId> { - self.index.order_client.get(&client_order_id) + pub fn client_id(&self, client_order_id: &ClientOrderId) -> Option<&ClientId> { + self.index.order_client.get(client_order_id) } #[must_use] pub fn orders( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> Vec<&OrderAny> { let client_order_ids = self.client_order_ids(venue, instrument_id, strategy_id); - self.get_orders_for_ids(client_order_ids, side) + self.get_orders_for_ids(&client_order_ids, side) } #[must_use] pub fn orders_open( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> Vec<&OrderAny> { let client_order_ids = self.client_order_ids_open(venue, instrument_id, strategy_id); - self.get_orders_for_ids(client_order_ids, side) + self.get_orders_for_ids(&client_order_ids, side) } #[must_use] pub fn orders_closed( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> Vec<&OrderAny> { let client_order_ids = self.client_order_ids_closed(venue, instrument_id, strategy_id); - self.get_orders_for_ids(client_order_ids, side) + self.get_orders_for_ids(&client_order_ids, side) } #[must_use] pub fn orders_emulated( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> Vec<&OrderAny> { let client_order_ids = self.client_order_ids_emulated(venue, instrument_id, strategy_id); - self.get_orders_for_ids(client_order_ids, side) + self.get_orders_for_ids(&client_order_ids, side) } #[must_use] pub fn orders_inflight( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> Vec<&OrderAny> { let client_order_ids = self.client_order_ids_inflight(venue, instrument_id, strategy_id); - self.get_orders_for_ids(client_order_ids, side) + self.get_orders_for_ids(&client_order_ids, side) } #[must_use] @@ -1086,48 +1086,48 @@ impl Cache { let client_order_ids = self.index.position_orders.get(&position_id); match client_order_ids { Some(client_order_ids) => { - self.get_orders_for_ids(client_order_ids.iter().copied().collect(), None) + self.get_orders_for_ids(&client_order_ids.iter().cloned().collect(), None) } None => Vec::new(), } } #[must_use] - pub fn order_exists(&self, client_order_id: ClientOrderId) -> bool { - self.index.orders.contains(&client_order_id) + pub fn order_exists(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders.contains(client_order_id) } #[must_use] - pub fn is_order_open(&self, client_order_id: ClientOrderId) -> bool { - self.index.orders_open.contains(&client_order_id) + pub fn is_order_open(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_open.contains(client_order_id) } #[must_use] - pub fn is_order_closed(&self, client_order_id: ClientOrderId) -> bool { - self.index.orders_closed.contains(&client_order_id) + pub fn is_order_closed(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_closed.contains(client_order_id) } #[must_use] - pub fn is_order_emulated(&self, client_order_id: ClientOrderId) -> bool { - self.index.orders_emulated.contains(&client_order_id) + pub fn is_order_emulated(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_emulated.contains(client_order_id) } #[must_use] - pub fn is_order_inflight(&self, client_order_id: ClientOrderId) -> bool { - self.index.orders_inflight.contains(&client_order_id) + pub fn is_order_inflight(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_inflight.contains(client_order_id) } #[must_use] - pub fn is_order_pending_cancel_local(&self, client_order_id: ClientOrderId) -> bool { - self.index.orders_pending_cancel.contains(&client_order_id) + pub fn is_order_pending_cancel_local(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_pending_cancel.contains(client_order_id) } #[must_use] pub fn orders_open_count( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> usize { self.orders_open(venue, instrument_id, strategy_id, side) @@ -1137,9 +1137,9 @@ impl Cache { #[must_use] pub fn orders_closed_count( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> usize { self.orders_closed(venue, instrument_id, strategy_id, side) @@ -1149,9 +1149,9 @@ impl Cache { #[must_use] pub fn orders_emulated_count( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> usize { self.orders_emulated(venue, instrument_id, strategy_id, side) @@ -1161,9 +1161,9 @@ impl Cache { #[must_use] pub fn orders_inflight_count( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> usize { self.orders_inflight(venue, instrument_id, strategy_id, side) @@ -1173,15 +1173,275 @@ impl Cache { #[must_use] pub fn orders_total_count( &self, - venue: Option, - instrument_id: Option, - strategy_id: Option, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, side: Option, ) -> usize { self.orders(venue, instrument_id, strategy_id, side).len() } - // -- GENERAL ------------------------------------------------------------- + #[must_use] + pub fn order_list(&self, order_list_id: &OrderListId) -> Option<&OrderList> { + self.order_lists.get(order_list_id) + } + + #[must_use] + pub fn order_lists( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> Vec<&OrderList> { + let mut order_lists = self.order_lists.values().collect::>(); + + if let Some(venue) = venue { + order_lists.retain(|ol| ol.instrument_id.venue == *venue); + } + + if let Some(instrument_id) = instrument_id { + order_lists.retain(|ol| &ol.instrument_id == instrument_id); + } + + if let Some(strategy_id) = strategy_id { + order_lists.retain(|ol| &ol.strategy_id == strategy_id); + } + + order_lists + } + + #[must_use] + pub fn order_list_exists(&self, order_list_id: &OrderListId) -> bool { + self.order_lists.contains_key(order_list_id) + } + + // -- EXEC ALGORITHM QUERIES ------------------------------------------------------------------ + + #[must_use] + pub fn orders_for_exec_algorithm( + &self, + exec_algorithm_id: &ExecAlgorithmId, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&OrderAny> { + let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); + let exec_algorithm_order_ids = self.index.exec_algorithm_orders.get(exec_algorithm_id); + + if let Some(query) = query { + if let Some(exec_algorithm_order_ids) = exec_algorithm_order_ids { + let exec_algorithm_order_ids = exec_algorithm_order_ids.intersection(&query); + } + } + + if let Some(exec_algorithm_order_ids) = exec_algorithm_order_ids { + self.get_orders_for_ids(exec_algorithm_order_ids, side) + } else { + Vec::new() + } + } + + #[must_use] + pub fn orders_for_exec_spawn(&self, exec_spawn_id: &ClientOrderId) -> Vec<&OrderAny> { + self.get_orders_for_ids( + self.index + .exec_spawn_orders + .get(exec_spawn_id) + .unwrap_or(&HashSet::new()), + None, + ) + } + + #[must_use] + pub fn exec_spawn_total_quantity( + &self, + exec_spawn_id: &ClientOrderId, + active_only: bool, + ) -> Option { + let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); + + let mut total_quantity: Option = None; + + for spawn_order in exec_spawn_orders { + if !active_only || !spawn_order.is_closed() { + if let Some(mut total_quantity) = total_quantity { + total_quantity += spawn_order.quantity() + } + } else { + total_quantity = Some(spawn_order.quantity()) + } + } + + total_quantity + } + + #[must_use] + pub fn exec_spawn_total_filled_qty( + &self, + exec_spawn_id: &ClientOrderId, + active_only: bool, + ) -> Option { + let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); + + let mut total_quantity: Option = None; + + for spawn_order in exec_spawn_orders { + if !active_only || !spawn_order.is_closed() { + if let Some(mut total_quantity) = total_quantity { + total_quantity += spawn_order.filled_qty() + } + } else { + total_quantity = Some(spawn_order.filled_qty()) + } + } + + total_quantity + } + + #[must_use] + pub fn exec_spawn_total_leaves_qty( + &self, + exec_spawn_id: &ClientOrderId, + active_only: bool, + ) -> Option { + let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); + + let mut total_quantity: Option = None; + + for spawn_order in exec_spawn_orders { + if !active_only || !spawn_order.is_closed() { + if let Some(mut total_quantity) = total_quantity { + total_quantity += spawn_order.leaves_qty() + } + } else { + total_quantity = Some(spawn_order.leaves_qty()) + } + } + + total_quantity + } + + // -- POSITION QUERIES ------------------------------------------------------------------------ + + #[must_use] + pub fn position(&self, position_id: &PositionId) -> Option<&Position> { + self.positions.get(position_id) + } + + #[must_use] + pub fn position_for_order(&self, client_order_id: &ClientOrderId) -> Option<&Position> { + self.index + .order_position + .get(client_order_id) + .and_then(|position_id| self.positions.get(position_id)) + } + + #[must_use] + pub fn position_id(&self, client_order_id: &ClientOrderId) -> Option<&PositionId> { + self.index.order_position.get(client_order_id) + } + + #[must_use] + pub fn positions( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&Position> { + let position_ids = self.position_ids(venue, instrument_id, strategy_id); + self.get_positions_for_ids(&position_ids, side) + } + + #[must_use] + pub fn positions_open( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&Position> { + let position_ids = self.position_open_ids(venue, instrument_id, strategy_id); + self.get_positions_for_ids(&position_ids, side) + } + + #[must_use] + pub fn positions_closed( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&Position> { + let position_ids = self.position_closed_ids(venue, instrument_id, strategy_id); + self.get_positions_for_ids(&position_ids, side) + } + + #[must_use] + pub fn position_exists(&self, position_id: &PositionId) -> bool { + self.index.positions.contains(position_id) + } + + #[must_use] + pub fn is_position_open(&self, position_id: &PositionId) -> bool { + self.index.positions_open.contains(position_id) + } + + #[must_use] + pub fn is_position_closed(&self, position_id: &PositionId) -> bool { + self.index.positions_closed.contains(position_id) + } + + #[must_use] + pub fn positions_open_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> u64 { + self.positions_open(venue, instrument_id, strategy_id, side) + .len() as u64 + } + + #[must_use] + pub fn positions_closed_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> u64 { + self.positions_closed(venue, instrument_id, strategy_id, side) + .len() as u64 + } + + #[must_use] + pub fn positions_total_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> u64 { + self.positions(venue, instrument_id, strategy_id, side) + .len() as u64 + } + + // -- STRATEGY QUERIES ------------------------------------------------------------------------ + + #[must_use] + pub fn strategy_id_for_order(&self, client_order_id: &ClientOrderId) -> Option<&StrategyId> { + self.index.order_strategy.get(client_order_id) + } + + #[must_use] + pub fn strategy_id_for_position(&self, position_id: &PositionId) -> Option<&StrategyId> { + self.index.position_strategy.get(position_id) + } + + // -- GENERAL --------------------------------------------------------------------------------- pub fn get(&self, key: &str) -> anyhow::Result> { check_valid_string(key, stringify!(key))?; @@ -1189,7 +1449,7 @@ impl Cache { Ok(self.general.get(key).map(std::vec::Vec::as_slice)) } - // -- DATA QUERIES -------------------------------------------------------- + // -- DATA QUERIES ---------------------------------------------------------------------------- #[must_use] pub fn price(&self, instrument_id: &InstrumentId, price_type: PriceType) -> Option { @@ -1262,6 +1522,150 @@ impl Cache { pub fn bar(&self, bar_type: &BarType) -> Option<&Bar> { self.bars.get(bar_type).and_then(|bars| bars.front()) } + + #[must_use] + pub fn book_update_count(&self, instrument_id: &InstrumentId) -> u64 { + self.books + .get(instrument_id) + .map(|book| book.count) + .unwrap_or(0) + } + + #[must_use] + pub fn quote_tick_count(&self, instrument_id: &InstrumentId) -> u64 { + self.quotes + .get(instrument_id) + .map(|quotes| quotes.len()) + .unwrap_or(0) as u64 + } + + #[must_use] + pub fn trade_tick_count(&self, instrument_id: &InstrumentId) -> u64 { + self.trades + .get(instrument_id) + .map(|trades| trades.len()) + .unwrap_or(0) as u64 + } + + #[must_use] + pub fn bar_count(&self, bar_type: &BarType) -> u64 { + self.bars.get(bar_type).map(|bars| bars.len()).unwrap_or(0) as u64 + } + + #[must_use] + pub fn has_order_book(&self, instrument_id: &InstrumentId) -> bool { + self.books.contains_key(instrument_id) + } + + #[must_use] + pub fn has_quote_ticks(&self, instrument_id: &InstrumentId) -> bool { + self.quote_tick_count(instrument_id) > 0 + } + + #[must_use] + pub fn has_trade_ticks(&self, instrument_id: &InstrumentId) -> bool { + self.trade_tick_count(instrument_id) > 0 + } + + #[must_use] + pub fn has_bars(&self, bar_type: &BarType) -> bool { + self.bar_count(bar_type) > 0 + } + + // -- INSTRUMENT QUERIES ---------------------------------------------------------------------- + + #[must_use] + pub fn instrument(&self, instrument_id: &InstrumentId) -> Option<&InstrumentAny> { + self.instruments.get(instrument_id) + } + + #[must_use] + pub fn instrument_ids(&self, venue: &Venue) -> Vec<&InstrumentId> { + self.instruments + .keys() + .filter(|i| &i.venue == venue) + .collect() + } + + #[must_use] + pub fn instruments(&self, venue: &Venue) -> Vec<&InstrumentAny> { + self.instruments + .values() + .filter(|i| &i.id().venue == venue) + .collect() + } + + #[must_use] + pub fn bar_types( + &self, + instrument_id: Option<&InstrumentId>, + price_type: Option<&PriceType>, + aggregation_source: AggregationSource, + ) -> Vec<&BarType> { + let mut bar_types = self + .bars + .keys() + .filter(|bar_type| bar_type.aggregation_source == aggregation_source) + .collect::>(); + + if let Some(instrument_id) = instrument_id { + bar_types.retain(|bar_type| &bar_type.instrument_id == instrument_id); + } + + if let Some(price_type) = price_type { + bar_types.retain(|bar_type| &bar_type.spec.price_type == price_type); + } + + bar_types + } + + // -- SYNTHETIC QUERIES ----------------------------------------------------------------------- + + #[must_use] + pub fn synthetic(&self, instrument_id: &InstrumentId) -> Option<&SyntheticInstrument> { + self.synthetics.get(instrument_id) + } + + #[must_use] + pub fn synthetic_ids(&self) -> Vec<&InstrumentId> { + self.synthetics.keys().collect() + } + + #[must_use] + pub fn synthetics(&self) -> Vec<&SyntheticInstrument> { + self.synthetics.values().collect() + } + + // -- ACCOUNT QUERIES ----------------------------------------------------------------------- + + #[must_use] + pub fn account(&self, account_id: &AccountId) -> Option<&dyn Account> { + self.accounts + .get(account_id) + .map(|box_account| box_account.as_ref()) + } + + #[must_use] + pub fn account_for_venue(&self, venue: &Venue) -> Option<&dyn Account> { + self.index + .venue_account + .get(venue) + .and_then(|account_id| self.accounts.get(account_id)) + .map(|box_account| box_account.as_ref()) + } + + #[must_use] + pub fn account_id(&self, venue: &Venue) -> Option<&AccountId> { + self.index.venue_account.get(venue) + } + + #[must_use] + pub fn accounts(&self, account_id: &AccountId) -> Vec<&dyn Account> { + self.accounts + .values() + .map(|box_account| box_account.as_ref()) + .collect() + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 846aa2c462c9..a6097e0620a0 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -47,8 +47,8 @@ use crate::{ }, polymorphism::{ GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, - GetLimitPrice, GetOrderSide, GetOrderSideSpecified, GetStopPrice, GetStrategyId, - GetVenueOrderId, + GetLimitPrice, GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, + GetOrderSideSpecified, GetStopPrice, GetStrategyId, GetVenueOrderId, IsClosed, IsOpen, }, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; @@ -261,6 +261,54 @@ impl GetOrderSide for OrderAny { } } +impl GetOrderQuantity for OrderAny { + fn quantity(&self) -> Quantity { + match self { + Self::Limit(order) => order.quantity, + Self::LimitIfTouched(order) => order.quantity, + Self::Market(order) => order.quantity, + Self::MarketIfTouched(order) => order.quantity, + Self::MarketToLimit(order) => order.quantity, + Self::StopLimit(order) => order.quantity, + Self::StopMarket(order) => order.quantity, + Self::TrailingStopLimit(order) => order.quantity, + Self::TrailingStopMarket(order) => order.quantity, + } + } +} + +impl GetOrderFilledQty for OrderAny { + fn filled_qty(&self) -> Quantity { + match self { + Self::Limit(order) => order.filled_qty(), + Self::LimitIfTouched(order) => order.filled_qty(), + Self::Market(order) => order.filled_qty(), + Self::MarketIfTouched(order) => order.filled_qty(), + Self::MarketToLimit(order) => order.filled_qty(), + Self::StopLimit(order) => order.filled_qty(), + Self::StopMarket(order) => order.filled_qty(), + Self::TrailingStopLimit(order) => order.filled_qty(), + Self::TrailingStopMarket(order) => order.filled_qty(), + } + } +} + +impl GetOrderLeavesQty for OrderAny { + fn leaves_qty(&self) -> Quantity { + match self { + Self::Limit(order) => order.leaves_qty(), + Self::LimitIfTouched(order) => order.leaves_qty(), + Self::Market(order) => order.leaves_qty(), + Self::MarketIfTouched(order) => order.leaves_qty(), + Self::MarketToLimit(order) => order.leaves_qty(), + Self::StopLimit(order) => order.leaves_qty(), + Self::StopMarket(order) => order.leaves_qty(), + Self::TrailingStopLimit(order) => order.leaves_qty(), + Self::TrailingStopMarket(order) => order.leaves_qty(), + } + } +} + impl GetOrderSideSpecified for OrderAny { fn order_side_specified(&self) -> OrderSideSpecified { match self { @@ -293,6 +341,38 @@ impl GetEmulationTrigger for OrderAny { } } +impl IsOpen for OrderAny { + fn is_open(&self) -> bool { + match self { + Self::Limit(order) => order.is_open(), + Self::LimitIfTouched(order) => order.is_open(), + Self::Market(order) => order.is_open(), + Self::MarketIfTouched(order) => order.is_open(), + Self::MarketToLimit(order) => order.is_open(), + Self::StopLimit(order) => order.is_open(), + Self::StopMarket(order) => order.is_open(), + Self::TrailingStopLimit(order) => order.is_open(), + Self::TrailingStopMarket(order) => order.is_open(), + } + } +} + +impl IsClosed for OrderAny { + fn is_closed(&self) -> bool { + match self { + Self::Limit(order) => order.is_closed(), + Self::LimitIfTouched(order) => order.is_closed(), + Self::Market(order) => order.is_closed(), + Self::MarketIfTouched(order) => order.is_closed(), + Self::MarketToLimit(order) => order.is_closed(), + Self::StopLimit(order) => order.is_closed(), + Self::StopMarket(order) => order.is_closed(), + Self::TrailingStopLimit(order) => order.is_closed(), + Self::TrailingStopMarket(order) => order.is_closed(), + } + } +} + #[derive(Clone, Debug)] pub enum PassiveOrderAny { Limit(LimitOrderAny), diff --git a/nautilus_core/model/src/polymorphism.rs b/nautilus_core/model/src/polymorphism.rs index a1e703c69f64..77e340acb4db 100644 --- a/nautilus_core/model/src/polymorphism.rs +++ b/nautilus_core/model/src/polymorphism.rs @@ -23,7 +23,7 @@ use crate::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, strategy_id::StrategyId, venue_order_id::VenueOrderId, }, - types::price::Price, + types::{price::Price, quantity::Quantity}, }; pub trait GetTsInit { @@ -58,6 +58,18 @@ pub trait GetOrderSide { fn order_side(&self) -> OrderSide; } +pub trait GetOrderQuantity { + fn quantity(&self) -> Quantity; +} + +pub trait GetOrderFilledQty { + fn filled_qty(&self) -> Quantity; +} + +pub trait GetOrderLeavesQty { + fn leaves_qty(&self) -> Quantity; +} + pub trait GetOrderSideSpecified { fn order_side_specified(&self) -> OrderSideSpecified; } @@ -73,3 +85,11 @@ pub trait GetLimitPrice { pub trait GetStopPrice { fn stop_px(&self) -> Price; } + +pub trait IsOpen { + fn is_open(&self) -> bool; +} + +pub trait IsClosed { + fn is_closed(&self) -> bool; +} From 0c1bea36493463403d9b556f0f5936f5b8068eaa Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 10:17:28 +1000 Subject: [PATCH 047/193] Fix clippy lints --- nautilus_core/common/src/cache/mod.rs | 35 +++++++++---------- .../tests/test_cache_database_postgres.rs | 5 +-- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 9d833a0d8d71..4ed2bcfa8f15 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -1086,7 +1086,7 @@ impl Cache { let client_order_ids = self.index.position_orders.get(&position_id); match client_order_ids { Some(client_order_ids) => { - self.get_orders_for_ids(&client_order_ids.iter().cloned().collect(), None) + self.get_orders_for_ids(&client_order_ids.iter().copied().collect(), None) } None => Vec::new(), } @@ -1266,10 +1266,10 @@ impl Cache { for spawn_order in exec_spawn_orders { if !active_only || !spawn_order.is_closed() { if let Some(mut total_quantity) = total_quantity { - total_quantity += spawn_order.quantity() + total_quantity += spawn_order.quantity(); } } else { - total_quantity = Some(spawn_order.quantity()) + total_quantity = Some(spawn_order.quantity()); } } @@ -1289,10 +1289,10 @@ impl Cache { for spawn_order in exec_spawn_orders { if !active_only || !spawn_order.is_closed() { if let Some(mut total_quantity) = total_quantity { - total_quantity += spawn_order.filled_qty() + total_quantity += spawn_order.filled_qty(); } } else { - total_quantity = Some(spawn_order.filled_qty()) + total_quantity = Some(spawn_order.filled_qty()); } } @@ -1312,10 +1312,10 @@ impl Cache { for spawn_order in exec_spawn_orders { if !active_only || !spawn_order.is_closed() { if let Some(mut total_quantity) = total_quantity { - total_quantity += spawn_order.leaves_qty() + total_quantity += spawn_order.leaves_qty(); } } else { - total_quantity = Some(spawn_order.leaves_qty()) + total_quantity = Some(spawn_order.leaves_qty()); } } @@ -1525,31 +1525,28 @@ impl Cache { #[must_use] pub fn book_update_count(&self, instrument_id: &InstrumentId) -> u64 { - self.books - .get(instrument_id) - .map(|book| book.count) - .unwrap_or(0) + self.books.get(instrument_id).map_or(0, |book| book.count) } #[must_use] pub fn quote_tick_count(&self, instrument_id: &InstrumentId) -> u64 { self.quotes .get(instrument_id) - .map(|quotes| quotes.len()) - .unwrap_or(0) as u64 + .map_or(0, std::collections::VecDeque::len) as u64 } #[must_use] pub fn trade_tick_count(&self, instrument_id: &InstrumentId) -> u64 { self.trades .get(instrument_id) - .map(|trades| trades.len()) - .unwrap_or(0) as u64 + .map_or(0, std::collections::VecDeque::len) as u64 } #[must_use] pub fn bar_count(&self, bar_type: &BarType) -> u64 { - self.bars.get(bar_type).map(|bars| bars.len()).unwrap_or(0) as u64 + self.bars + .get(bar_type) + .map_or(0, std::collections::VecDeque::len) as u64 } #[must_use] @@ -1642,7 +1639,7 @@ impl Cache { pub fn account(&self, account_id: &AccountId) -> Option<&dyn Account> { self.accounts .get(account_id) - .map(|box_account| box_account.as_ref()) + .map(std::convert::AsRef::as_ref) } #[must_use] @@ -1651,7 +1648,7 @@ impl Cache { .venue_account .get(venue) .and_then(|account_id| self.accounts.get(account_id)) - .map(|box_account| box_account.as_ref()) + .map(std::convert::AsRef::as_ref) } #[must_use] @@ -1663,7 +1660,7 @@ impl Cache { pub fn accounts(&self, account_id: &AccountId) -> Vec<&dyn Account> { self.accounts .values() - .map(|box_account| box_account.as_ref()) + .map(std::convert::AsRef::as_ref) .collect() } } diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs index 05d3cc39ff1a..19dda20af452 100644 --- a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -19,6 +19,7 @@ use nautilus_infrastructure::sql::{ }; use sqlx::PgPool; +#[must_use] pub fn get_test_pg_connect_options(username: &str) -> PostgresConnectOptions { PostgresConnectOptions::new( "localhost".to_string(), @@ -65,7 +66,7 @@ mod tests { async fn test_load_general_objects_when_nothing_in_cache_returns_empty_hashmap() { let pg_cache = get_pg_cache_database().await.unwrap(); let result = pg_cache.load().await.unwrap(); - println!("1: {:?}", result); + println!("1: {result:?}"); assert_eq!(result.len(), 0); } @@ -80,7 +81,7 @@ mod tests { // sleep with tokio tokio::time::sleep(Duration::from_secs(1)).await; let result = pg_cache.load().await.unwrap(); - println!("2: {:?}", result); + println!("2: {result:?}"); assert_eq!(result.keys().len(), 1); assert_eq!( result.keys().cloned().collect::>(), From 792ccf60e4b8ad2943219f422966b6f6c4db5fe9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 15:46:21 +1000 Subject: [PATCH 048/193] Update core dependencies --- nautilus_core/Cargo.lock | 20 ++++++++++---------- nautilus_core/Cargo.toml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 32c8c268dd00..c9b4a4fff89c 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -389,9 +389,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" +checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" dependencies = [ "bzip2", "flate2", @@ -1649,9 +1649,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" dependencies = [ "crc32fast", "miniz_oxide", @@ -4138,18 +4138,18 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", @@ -5231,9 +5231,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode_categories" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 5c6c37ea814a..ab218ab2dfd3 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -41,7 +41,7 @@ rand = "0.8.5" rmp-serde = "1.2.0" rust_decimal = "1.35.0" rust_decimal_macros = "1.34.2" -serde = { version = "1.0.198", features = ["derive"] } +serde = { version = "1.0.199", features = ["derive"] } serde_json = "1.0.116" strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.59" From fbf778f4bb94634bf7d1f4ffa85e014b4aea5179 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 15:51:33 +1000 Subject: [PATCH 049/193] Rename TimedeltaNanos to DurationNanos --- nautilus_core/common/src/timer.rs | 4 ++-- nautilus_core/core/src/nanos.rs | 10 ++-------- nautilus_core/model/src/events/position/closed.rs | 4 ++-- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 919b1541e7e3..03159405b2d4 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -28,7 +28,7 @@ use std::{ use nautilus_core::{ correctness::{check_positive_u64, check_valid_string}, datetime::floor_to_nearest_microsecond, - nanos::{TimedeltaNanos, UnixNanos}, + nanos::{DurationNanos, UnixNanos}, time::get_atomic_clock_realtime, uuid::UUID4, }; @@ -123,7 +123,7 @@ impl Ord for TimeEventHandler { pub trait Timer { fn new( name: Ustr, - interval_ns: TimedeltaNanos, + interval_ns: DurationNanos, start_time_ns: UnixNanos, stop_time_ns: Option, ) -> Self; diff --git a/nautilus_core/core/src/nanos.rs b/nautilus_core/core/src/nanos.rs index 622b8b6a7b07..f77f412ea5b8 100644 --- a/nautilus_core/core/src/nanos.rs +++ b/nautilus_core/core/src/nanos.rs @@ -175,14 +175,8 @@ impl Display for UnixNanos { } } -/// Represents an event timestamp in nanoseconds since UNIX epoch. -pub type TsEvent = UnixNanos; - -/// Represents an initialization timestamp in nanoseconds since UNIX epoch. -pub type TsInit = UnixNanos; - -/// Represents a timedelta in nanoseconds. -pub type TimedeltaNanos = i64; +/// Represents a duration in nanoseconds. +pub type DurationNanos = u64; //////////////////////////////////////////////////////////////////////////////// // Tests diff --git a/nautilus_core/model/src/events/position/closed.rs b/nautilus_core/model/src/events/position/closed.rs index 1619f2adf7e3..a25272035fac 100644 --- a/nautilus_core/model/src/events/position/closed.rs +++ b/nautilus_core/model/src/events/position/closed.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use nautilus_core::nanos::{TimedeltaNanos, UnixNanos}; +use nautilus_core::nanos::{DurationNanos, UnixNanos}; use crate::{ enums::{OrderSide, PositionSide}, @@ -46,7 +46,7 @@ pub struct PositionClosed { pub realized_return: f64, pub realized_pnl: Money, pub unrealized_pnl: Money, - pub duration: TimedeltaNanos, + pub duration: DurationNanos, pub ts_opened: UnixNanos, pub ts_closed: UnixNanos, pub ts_event: UnixNanos, From 4efcdc3956ed4d0b0f0afe7359e72df5a8c47c67 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 16:19:01 +1000 Subject: [PATCH 050/193] Add OrderList impl and tests --- nautilus_core/model/src/orders/list.rs | 115 ++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 3 deletions(-) diff --git a/nautilus_core/model/src/orders/list.rs b/nautilus_core/model/src/orders/list.rs index 5c6f25e7f8c5..7f9d19745761 100644 --- a/nautilus_core/model/src/orders/list.rs +++ b/nautilus_core/model/src/orders/list.rs @@ -13,12 +13,17 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use nautilus_core::nanos::UnixNanos; +use std::fmt::Display; + +use nautilus_core::{correctness::check_slice_not_empty, nanos::UnixNanos}; use serde::{Deserialize, Serialize}; use super::base::OrderAny; -use crate::identifiers::{ - instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, +use crate::{ + identifiers::{ + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + }, + polymorphism::{GetInstrumentId, GetStrategyId}, }; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -34,8 +39,112 @@ pub struct OrderList { pub ts_init: UnixNanos, } +impl OrderList { + pub fn new( + order_list_id: OrderListId, + instrument_id: InstrumentId, + strategy_id: StrategyId, + orders: Vec, + ts_init: UnixNanos, + ) -> anyhow::Result { + check_slice_not_empty(orders.as_slice(), stringify!(orders))?; + for order in &orders { + assert_eq!(instrument_id, order.instrument_id()); + assert_eq!(strategy_id, order.strategy_id()); + } + + Ok(Self { + id: order_list_id, + instrument_id, + strategy_id, + orders, + ts_init, + }) + } +} + impl PartialEq for OrderList { fn eq(&self, other: &Self) -> bool { self.id == other.id } } + +impl Display for OrderList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "OrderList(\ + id={}, \ + instrument_id={}, \ + strategy_id={}, \ + orders={:?}, \ + ts_init={}, \ + )", + self.id, self.instrument_id, self.strategy_id, self.orders, self.ts_init, + ) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + use crate::{ + enums::OrderSide, + identifiers::{order_list_id::OrderListId, strategy_id::StrategyId}, + instruments::{currency_pair::CurrencyPair, stubs::*}, + orders::stubs::TestOrderStubs, + types::{price::Price, quantity::Quantity}, + }; + + #[rstest] + fn test_display(audusd_sim: CurrencyPair) { + let order1 = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + let order2 = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + let order3 = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + + let orders = vec![ + OrderAny::Limit(order1), + OrderAny::Limit(order2), + OrderAny::Limit(order3), + ]; + + let order_list = OrderList::new( + OrderListId::from("OL-001"), + audusd_sim.id, + StrategyId::from("EMACross-001"), + orders, + UnixNanos::default(), + ) + .unwrap(); + + assert!(order_list.to_string().starts_with( + "OrderList(id=OL-001, instrument_id=AUD/USD.SIM, strategy_id=EMACross-001, orders=" + )); + } +} From d2e2399973ef4d1771926cbae2cea8d255014ad1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 16:22:01 +1000 Subject: [PATCH 051/193] Update test name --- nautilus_core/model/src/orders/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_core/model/src/orders/list.rs b/nautilus_core/model/src/orders/list.rs index 7f9d19745761..1593aa5ebb99 100644 --- a/nautilus_core/model/src/orders/list.rs +++ b/nautilus_core/model/src/orders/list.rs @@ -102,7 +102,7 @@ mod tests { }; #[rstest] - fn test_display(audusd_sim: CurrencyPair) { + fn test_new_and_display(audusd_sim: CurrencyPair) { let order1 = TestOrderStubs::limit_order( audusd_sim.id, OrderSide::Buy, From 335cbae92cb106d9c4adddf34d13ea02117eda00 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 16:24:28 +1000 Subject: [PATCH 052/193] Consistently use reference in retain --- nautilus_core/common/src/cache/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 4ed2bcfa8f15..11d0d7255eb5 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -1196,7 +1196,7 @@ impl Cache { let mut order_lists = self.order_lists.values().collect::>(); if let Some(venue) = venue { - order_lists.retain(|ol| ol.instrument_id.venue == *venue); + order_lists.retain(|ol| &ol.instrument_id.venue == venue); } if let Some(instrument_id) = instrument_id { From 7b9815f288198b9df4172768c448065a3e8eb599 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 16:28:58 +1000 Subject: [PATCH 053/193] Add Cache tests --- nautilus_core/common/src/cache/mod.rs | 40 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 11d0d7255eb5..da161b48abf4 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -1701,14 +1701,50 @@ mod tests { } #[rstest] - fn test_general_when_no_value() { + fn test_cache_general_load_when_no_database() { + let mut cache = Cache::default(); + assert!(cache.cache_general().is_ok()); + } + + #[rstest] + fn test_cache_currencies_load_when_no_database() { + let mut cache = Cache::default(); + assert!(cache.cache_currencies().is_ok()); + } + + #[rstest] + fn test_cache_instruments_load_when_no_database() { + let mut cache = Cache::default(); + assert!(cache.cache_instruments().is_ok()); + } + + #[rstest] + fn test_cache_synthetics_when_no_database() { + let mut cache = Cache::default(); + assert!(cache.cache_synthetics().is_ok()); + } + + #[rstest] + fn test_cache_orders_when_no_database() { + let mut cache = Cache::default(); + assert!(cache.cache_orders().is_ok()); + } + + #[rstest] + fn test_cache_positions_when_no_database() { + let mut cache = Cache::default(); + assert!(cache.cache_positions().is_ok()); + } + + #[rstest] + fn test_get_general_when_no_value() { let cache = Cache::default(); let result = cache.get("A").unwrap(); assert_eq!(result, None); } #[rstest] - fn test_general_when_value() { + fn test_add_general_when_value() { let mut cache = Cache::default(); let key = "A"; From 9f34244ee0866a9f8444a5febf0290ce14759e83 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 16:50:23 +1000 Subject: [PATCH 054/193] Add AccountId methods --- .../model/src/identifiers/account_id.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index 43a6ae37368b..fd3bcbe332a0 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -21,6 +21,8 @@ use std::{ use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; +use super::venue::Venue; + /// Represents a valid account ID. /// /// Must be correctly formatted with two valid strings either side of a hyphen '-'. @@ -65,6 +67,20 @@ impl AccountId { pub fn as_str(&self) -> &str { self.0.as_str() } + + /// Returns the account issuer for this identifier. + #[must_use] + pub fn get_issuer(&self) -> Venue { + // SAFETY: Account ID is guaranteed to have chars either side of a hyphen + Venue::from_str_unchecked(self.0.split('-').collect::>().first().unwrap()) + } + + /// Returns the account ID assigned by the issuer. + #[must_use] + pub fn get_issuers_id(&self) -> &str { + // SAFETY: Account ID is guaranteed to have chars either side of a hyphen + self.0.split('-').collect::>().last().unwrap() + } } impl Default for AccountId { @@ -128,4 +144,14 @@ mod tests { fn test_string_reprs(account_ib: AccountId) { assert_eq!(account_ib.as_str(), "IB-1234567890"); } + + #[rstest] + fn test_get_issuer(account_ib: AccountId) { + assert_eq!(account_ib.get_issuer(), Venue::new("IB").unwrap()); + } + + #[rstest] + fn test_get_issuers_id(account_ib: AccountId) { + assert_eq!(account_ib.get_issuers_id(), "1234567890"); + } } From 53693282d5ea43ffafe94fae2f14250ec5d1a12f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 17:48:41 +1000 Subject: [PATCH 055/193] Add Cache index build --- nautilus_core/common/src/cache/mod.rs | 215 +++++++++++++++++++++++- nautilus_core/model/src/orders/base.rs | 35 +++- nautilus_core/model/src/polymorphism.rs | 11 +- 3 files changed, 254 insertions(+), 7 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index da161b48abf4..9bed4ad73399 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -31,7 +31,7 @@ use nautilus_model::{ quote::QuoteTick, trade::TradeTick, }, - enums::{AggregationSource, OrderSide, PositionSide, PriceType}, + enums::{AggregationSource, OrderSide, PositionSide, PriceType, TriggerType}, identifiers::{ account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, @@ -42,8 +42,9 @@ use nautilus_model::{ orderbook::book::OrderBook, orders::{base::OrderAny, list::OrderList}, polymorphism::{ - GetClientOrderId, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetOrderFilledQty, - GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetStrategyId, IsClosed, + GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, + GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetPositionId, + GetStrategyId, GetVenueOrderId, IsClosed, IsInflight, IsOpen, }, position::Position, types::{currency::Currency, price::Price, quantity::Quantity}, @@ -330,8 +331,212 @@ impl Cache { Ok(()) } - pub fn build_index(&self) { - todo!() // Needs order query methods + pub fn build_index(&mut self) { + self.index.clear(); + debug!("Building index"); + + // Index accounts + for account_id in self.accounts.keys() { + self.index + .venue_account + .insert(account_id.get_issuer(), *account_id); + } + + // Index orders + for (client_order_id, order) in &self.orders { + let instrument_id = order.instrument_id(); + let venue = instrument_id.venue; + let strategy_id = order.strategy_id(); + + // 1: Build _index_venue_orders -> {Venue, {ClientOrderId}} + if let Some(venue_orders) = self.index.venue_orders.get_mut(&venue) { + venue_orders.insert(*client_order_id); + } else { + let mut venue_orders = HashSet::new(); + venue_orders.insert(*client_order_id); + self.index.venue_orders.insert(venue, venue_orders); + } + + // 2: Build _index_order_ids -> {VenueOrderId, ClientOrderId} + if let Some(venue_order_id) = order.venue_order_id() { + self.index + .order_ids + .insert(venue_order_id, *client_order_id); + } + + // 3: Build _index_order_position -> {ClientOrderId, PositionId} + if let Some(position_id) = order.position_id() { + self.index + .order_position + .insert(*client_order_id, position_id); + } + + // 4: Build _index_order_strategy -> {ClientOrderId, StrategyId} + self.index + .order_strategy + .insert(*client_order_id, order.strategy_id()); + + // 5: Build _index_instrument_orders -> {InstrumentId, {ClientOrderId}} + if let Some(instrument_orders) = self.index.instrument_orders.get_mut(&instrument_id) { + instrument_orders.insert(*client_order_id); + } else { + let mut instrument_orders = HashSet::new(); + instrument_orders.insert(*client_order_id); + self.index + .instrument_orders + .insert(instrument_id, instrument_orders); + } + + // 6: Build _index_strategy_orders -> {StrategyId, {ClientOrderId}} + if let Some(strategy_orders) = self.index.strategy_orders.get_mut(&strategy_id) { + strategy_orders.insert(*client_order_id); + } else { + let mut strategy_orders = HashSet::new(); + strategy_orders.insert(*client_order_id); + self.index + .strategy_orders + .insert(strategy_id, strategy_orders); + } + + // 7: Build _index_exec_algorithm_orders -> {ExecAlgorithmId, {ClientOrderId}} + if let Some(exec_algorithm_id) = order.exec_algorithm_id() { + if let Some(exec_algorithm_orders) = + self.index.exec_algorithm_orders.get_mut(&exec_algorithm_id) + { + exec_algorithm_orders.insert(*client_order_id); + } else { + let mut exec_algorithm_orders = HashSet::new(); + exec_algorithm_orders.insert(*client_order_id); + self.index + .exec_algorithm_orders + .insert(exec_algorithm_id, exec_algorithm_orders); + } + } + + // 8: Build _index_exec_spawn_orders -> {ClientOrderId, {ClientOrderId}} + if let Some(exec_spawn_id) = order.exec_spawn_id() { + if let Some(exec_spawn_orders) = + self.index.exec_spawn_orders.get_mut(&exec_spawn_id) + { + exec_spawn_orders.insert(*client_order_id); + } else { + let mut exec_spawn_orders = HashSet::new(); + exec_spawn_orders.insert(*client_order_id); + self.index + .exec_spawn_orders + .insert(exec_spawn_id, exec_spawn_orders); + } + } + + // 9: Build _index_orders -> {ClientOrderId} + self.index.orders.insert(*client_order_id); + + // 10: Build _index_orders_open -> {ClientOrderId} + if order.is_open() { + self.index.orders_open.insert(*client_order_id); + } + + // 11: Build _index_orders_closed -> {ClientOrderId} + if order.is_closed() { + self.index.orders_closed.insert(*client_order_id); + } + + // 12: Build _index_orders_emulated -> {ClientOrderId} + if let Some(emulation_trigger) = order.emulation_trigger() { + if emulation_trigger != TriggerType::NoTrigger && !order.is_closed() { + self.index.orders_emulated.insert(*client_order_id); + } + } + + // 13: Build _index_orders_inflight -> {ClientOrderId} + if order.is_inflight() { + self.index.orders_inflight.insert(*client_order_id); + } + + // 14: Build _index_strategies -> {StrategyId} + self.index.strategies.insert(strategy_id); + + // 15: Build _index_strategies -> {ExecAlgorithmId} + if let Some(exec_algorithm_id) = order.exec_algorithm_id() { + self.index.exec_algorithms.insert(exec_algorithm_id); + } + } + + // Index positions + for (position_id, position) in &self.positions { + let instrument_id = position.instrument_id; + let venue = instrument_id.venue; + let strategy_id = position.strategy_id; + + // 1: Build _index_venue_positions -> {Venue, {PositionId}} + if let Some(venue_positions) = self.index.venue_positions.get_mut(&venue) { + venue_positions.insert(*position_id); + } else { + let mut venue_positions = HashSet::new(); + venue_positions.insert(*position_id); + self.index.venue_positions.insert(venue, venue_positions); + } + + // 2: Build _index_position_strategy -> {PositionId, StrategyId} + self.index + .position_strategy + .insert(*position_id, position.strategy_id); + + // 3: Build _index_position_orders -> {PositionId, {ClientOrderId}} + if let Some(position_orders) = self.index.position_orders.get_mut(position_id) { + for client_order_id in position.client_order_ids() { + position_orders.insert(client_order_id); + } + } else { + let mut position_orders = HashSet::new(); + for client_order_id in position.client_order_ids() { + position_orders.insert(client_order_id); + } + self.index + .position_orders + .insert(*position_id, position_orders); + } + + // 4: Build _index_instrument_positions -> {InstrumentId, {PositionId}} + if let Some(instrument_positions) = + self.index.instrument_positions.get_mut(&instrument_id) + { + instrument_positions.insert(*position_id); + } else { + let mut instrument_positions = HashSet::new(); + instrument_positions.insert(*position_id); + self.index + .instrument_positions + .insert(instrument_id, instrument_positions); + } + + // 5: Build _index_strategy_positions -> {StrategyId, {PositionId}} + if let Some(strategy_positions) = self.index.strategy_positions.get_mut(&strategy_id) { + strategy_positions.insert(*position_id); + } else { + let mut strategy_positions = HashSet::new(); + strategy_positions.insert(*position_id); + self.index + .strategy_positions + .insert(strategy_id, strategy_positions); + } + + // 6: Build _index_positions -> {PositionId} + self.index.positions.insert(*position_id); + + // 7: Build _index_positions_open -> {PositionId} + if position.is_open() { + self.index.positions_open.insert(*position_id); + } + + // 8: Build _index_positions_closed -> {PositionId} + if position.is_closed() { + self.index.positions_closed.insert(*position_id); + } + + // 9: Build _index_strategies -> {StrategyId} + self.index.strategies.insert(strategy_id); + } } #[must_use] diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index a6097e0620a0..63aa0770f139 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -48,7 +48,8 @@ use crate::{ polymorphism::{ GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetLimitPrice, GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, - GetOrderSideSpecified, GetStopPrice, GetStrategyId, GetVenueOrderId, IsClosed, IsOpen, + GetOrderSideSpecified, GetPositionId, GetStopPrice, GetStrategyId, GetVenueOrderId, + IsClosed, IsInflight, IsOpen, }, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; @@ -213,6 +214,22 @@ impl GetStrategyId for OrderAny { } } +impl GetPositionId for OrderAny { + fn position_id(&self) -> Option { + match self { + Self::Limit(order) => order.position_id, + Self::LimitIfTouched(order) => order.position_id, + Self::Market(order) => order.position_id, + Self::MarketIfTouched(order) => order.position_id, + Self::MarketToLimit(order) => order.position_id, + Self::StopLimit(order) => order.position_id, + Self::StopMarket(order) => order.position_id, + Self::TrailingStopLimit(order) => order.position_id, + Self::TrailingStopMarket(order) => order.position_id, + } + } +} + impl GetExecAlgorithmId for OrderAny { fn exec_algorithm_id(&self) -> Option { match self { @@ -373,6 +390,22 @@ impl IsClosed for OrderAny { } } +impl IsInflight for OrderAny { + fn is_inflight(&self) -> bool { + match self { + Self::Limit(order) => order.is_inflight(), + Self::LimitIfTouched(order) => order.is_inflight(), + Self::Market(order) => order.is_inflight(), + Self::MarketIfTouched(order) => order.is_inflight(), + Self::MarketToLimit(order) => order.is_inflight(), + Self::StopLimit(order) => order.is_inflight(), + Self::StopMarket(order) => order.is_inflight(), + Self::TrailingStopLimit(order) => order.is_inflight(), + Self::TrailingStopMarket(order) => order.is_inflight(), + } + } +} + #[derive(Clone, Debug)] pub enum PassiveOrderAny { Limit(LimitOrderAny), diff --git a/nautilus_core/model/src/polymorphism.rs b/nautilus_core/model/src/polymorphism.rs index 77e340acb4db..420d8bd81af0 100644 --- a/nautilus_core/model/src/polymorphism.rs +++ b/nautilus_core/model/src/polymorphism.rs @@ -21,7 +21,8 @@ use crate::{ enums::{OrderSide, OrderSideSpecified, TriggerType}, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, - instrument_id::InstrumentId, strategy_id::StrategyId, venue_order_id::VenueOrderId, + instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, + venue_order_id::VenueOrderId, }, types::{price::Price, quantity::Quantity}, }; @@ -46,6 +47,10 @@ pub trait GetStrategyId { fn strategy_id(&self) -> StrategyId; } +pub trait GetPositionId { + fn position_id(&self) -> Option; +} + pub trait GetExecAlgorithmId { fn exec_algorithm_id(&self) -> Option; } @@ -93,3 +98,7 @@ pub trait IsOpen { pub trait IsClosed { fn is_closed(&self) -> bool; } + +pub trait IsInflight { + fn is_inflight(&self) -> bool; +} From 6fe3ae341434b1f1fa02a805c7e7a67a4a072cfa Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 27 Apr 2024 19:39:25 +1000 Subject: [PATCH 056/193] Fix Money.from_str underscore parsing --- nautilus_core/model/src/types/money.rs | 2 ++ tests/unit_tests/model/objects/test_money.py | 1 + 2 files changed, 3 insertions(+) diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 56b54a8f030b..fd0d5b90e8bb 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -102,6 +102,7 @@ impl FromStr for Money { // Parse amount let amount = parts[0] + .replace('_', "") .parse::() .map_err(|e| format!("Cannot parse amount '{}' as `f64`: {:?}", parts[0], e))?; @@ -388,6 +389,7 @@ mod tests { #[case("0 USD", Currency::USD(), dec!(0.00))] #[case("1.1 AUD", Currency::AUD(), dec!(1.10))] #[case("1.12345678 BTC", Currency::BTC(), dec!(1.12345678))] + #[case("10_000.10 USD", Currency::USD(), dec!(10000.10))] fn test_from_str_valid_input( #[case] input: &str, #[case] expected_currency: Currency, diff --git a/tests/unit_tests/model/objects/test_money.py b/tests/unit_tests/model/objects/test_money.py index 7594e0141259..64d2838b9753 100644 --- a/tests/unit_tests/model/objects/test_money.py +++ b/tests/unit_tests/model/objects/test_money.py @@ -200,6 +200,7 @@ def test_from_raw_given_valid_values_returns_expected_result( ["1.00 USDT", Money(1.00, USDT)], ["1.00 USD", Money(1.00, USD)], ["1.001 AUD", Money(1.00, AUD)], + ["10_001.01 AUD", Money(10001.01, AUD)], ], ) def test_from_str_given_valid_strings_returns_expected_result( From 2e23d215ad06200f3cd32a3fecc8248314455b23 Mon Sep 17 00:00:00 2001 From: David Blom Date: Sat, 27 Apr 2024 23:25:47 +0200 Subject: [PATCH 057/193] Fix sandbox account initialization (#1613) --- nautilus_trader/adapters/sandbox/execution.py | 4 +++- tests/integration_tests/adapters/sandbox/conftest.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/adapters/sandbox/execution.py b/nautilus_trader/adapters/sandbox/execution.py index c145d08eacb5..18d112a52329 100644 --- a/nautilus_trader/adapters/sandbox/execution.py +++ b/nautilus_trader/adapters/sandbox/execution.py @@ -86,6 +86,7 @@ def __init__( balance: int, oms_type: OmsType = OmsType.NETTING, account_type: AccountType = AccountType.MARGIN, + default_leverage: Decimal = Decimal(10), ) -> None: self._currency = Currency.from_str(currency) money = Money(value=balance, currency=self._currency) @@ -112,7 +113,7 @@ def __init__( account_type=self._account_type, base_currency=self._currency, starting_balances=[self.balance.free], - default_leverage=Decimal(10), + default_leverage=default_leverage, leverages={}, instruments=self.INSTRUMENTS, modules=[], @@ -132,6 +133,7 @@ def __init__( clock=self.test_clock, ) self.exchange.register_client(self._client) + self.exchange.initialize_account() def connect(self) -> None: """ diff --git a/tests/integration_tests/adapters/sandbox/conftest.py b/tests/integration_tests/adapters/sandbox/conftest.py index b171d2958438..7eacad869e03 100644 --- a/tests/integration_tests/adapters/sandbox/conftest.py +++ b/tests/integration_tests/adapters/sandbox/conftest.py @@ -13,9 +13,12 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from decimal import Decimal + import pytest from nautilus_trader.adapters.sandbox.execution import SandboxExecutionClient +from nautilus_trader.model.enums import AccountType from nautilus_trader.model.events import AccountState from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import Venue @@ -48,6 +51,8 @@ def exec_client( venue=venue.value, currency="USD", balance=100_000, + account_type=AccountType.CASH, + default_leverage=Decimal(1), ) From 9852a41bef8972499ee90f2164372dca17d7867b Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 27 Apr 2024 23:30:23 +0200 Subject: [PATCH 058/193] Add psql cache database - instruments (#1614) --- nautilus_core/infrastructure/Cargo.toml | 2 +- .../src/python/sql/cache_database.rs | 50 ++++- .../infrastructure/src/sql/cache_database.rs | 94 ++++++++- nautilus_core/infrastructure/src/sql/mod.rs | 2 +- .../src/sql/models/instruments.rs | 96 ++++----- nautilus_core/infrastructure/src/sql/pg.rs | 2 +- .../infrastructure/src/sql/queries.rs | 91 +++++++- .../tests/test_cache_database_postgres.rs | 155 +++++++++++++- nautilus_core/model/src/instruments/mod.rs | 6 + nautilus_core/model/src/instruments/stubs.rs | 17 +- .../python/instruments/crypto_perpetual.rs | 4 +- .../model/src/python/instruments/mod.rs | 2 + nautilus_core/model/src/types/money.rs | 19 +- nautilus_trader/cache/postgres/adapter.py | 14 ++ .../cache/postgres/transformers.py | 55 +++++ nautilus_trader/core/nautilus_pyo3.pyi | 5 +- .../test_kit/rust/instruments_pyo3.py | 6 +- .../test_cache_database_postgres.py | 195 +++++++++++++++++- 18 files changed, 725 insertions(+), 90 deletions(-) diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 594ca1448b37..4ba4d069c4ef 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["rlib", "cdylib"] [dependencies] nautilus-common = { path = "../common", features = ["python"] } nautilus-core = { path = "../core" , features = ["python"] } -nautilus-model = { path = "../model" , features = ["python"] } +nautilus-model = { path = "../model" , features = ["python", "stubs"] } anyhow = { workspace = true } pyo3 = { workspace = true, optional = true } rmp-serde = { workspace = true } diff --git a/nautilus_core/infrastructure/src/python/sql/cache_database.rs b/nautilus_core/infrastructure/src/python/sql/cache_database.rs index f1c7b181eb33..9a4eb0147a56 100644 --- a/nautilus_core/infrastructure/src/python/sql/cache_database.rs +++ b/nautilus_core/infrastructure/src/python/sql/cache_database.rs @@ -17,7 +17,11 @@ use std::collections::HashMap; use nautilus_common::runtime::get_runtime; use nautilus_core::python::to_pyruntime_err; -use nautilus_model::types::currency::Currency; +use nautilus_model::{ + identifiers::instrument_id::InstrumentId, + python::instruments::{convert_instrument_any_to_pyobject, convert_pyobject_to_instrument_any}, + types::currency::Currency, +}; use pyo3::prelude::*; use crate::sql::{ @@ -74,6 +78,50 @@ impl PostgresCacheDatabase { result.map_err(to_pyruntime_err) } + #[pyo3(name = "load_instrument")] + fn py_load_instrument( + slf: PyRef<'_, Self>, + instrument_id: InstrumentId, + py: Python<'_>, + ) -> PyResult> { + get_runtime().block_on(async { + let result = DatabaseQueries::load_instrument(&slf.pool, instrument_id) + .await + .unwrap(); + match result { + Some(instrument) => { + let py_object = convert_instrument_any_to_pyobject(py, instrument)?; + Ok(Some(py_object)) + } + None => Ok(None), + } + }) + } + + #[pyo3(name = "load_instruments")] + fn py_load_instruments(slf: PyRef<'_, Self>, py: Python<'_>) -> PyResult> { + get_runtime().block_on(async { + let result = DatabaseQueries::load_instruments(&slf.pool).await.unwrap(); + let mut instruments = Vec::new(); + for instrument in result { + let py_object = convert_instrument_any_to_pyobject(py, instrument)?; + instruments.push(py_object); + } + Ok(instruments) + }) + } + + #[pyo3(name = "add_instrument")] + fn py_add_instrument( + slf: PyRef<'_, Self>, + instrument: PyObject, + py: Python<'_>, + ) -> PyResult<()> { + let instrument_any = convert_pyobject_to_instrument_any(py, instrument)?; + let result = get_runtime().block_on(async { slf.add_instrument(instrument_any).await }); + result.map_err(to_pyruntime_err) + } + #[pyo3(name = "flush_db")] fn py_drop_schema(slf: PyRef<'_, Self>) -> PyResult<()> { let result = diff --git a/nautilus_core/infrastructure/src/sql/cache_database.rs b/nautilus_core/infrastructure/src/sql/cache_database.rs index ed6ab08a9e48..c512ae12b145 100644 --- a/nautilus_core/infrastructure/src/sql/cache_database.rs +++ b/nautilus_core/infrastructure/src/sql/cache_database.rs @@ -18,7 +18,9 @@ use std::{ time::{Duration, Instant}, }; -use nautilus_model::types::currency::Currency; +use nautilus_model::{ + identifiers::instrument_id::InstrumentId, instruments::InstrumentAny, types::currency::Currency, +}; use sqlx::{postgres::PgConnectOptions, PgPool}; use tokio::{ sync::mpsc::{channel, error::TryRecvError, Receiver, Sender}, @@ -34,17 +36,19 @@ use crate::sql::{ #[derive(Debug)] #[cfg_attr( feature = "python", - pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.persistence") + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.infrastructure") )] pub struct PostgresCacheDatabase { pub pool: PgPool, tx: Sender, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum DatabaseQuery { Add(String, Vec), AddCurrency(Currency), + AddInstrument(InstrumentAny), } fn get_buffer_interval() -> Duration { @@ -60,6 +64,64 @@ async fn drain_buffer(pool: &PgPool, buffer: &mut VecDeque) { DatabaseQuery::AddCurrency(currency) => { DatabaseQueries::add_currency(pool, currency).await.unwrap(); } + DatabaseQuery::AddInstrument(instrument) => match instrument { + InstrumentAny::CryptoFuture(crypto_future) => { + DatabaseQueries::add_instrument(pool, "CRYPTO_FUTURE", Box::new(crypto_future)) + .await + .unwrap() + } + InstrumentAny::CryptoPerpetual(crypto_perpetual) => { + DatabaseQueries::add_instrument( + pool, + "CRYPTO_PERPETUAL", + Box::new(crypto_perpetual), + ) + .await + .unwrap() + } + InstrumentAny::CurrencyPair(currency_pair) => { + DatabaseQueries::add_instrument(pool, "CURRENCY_PAIR", Box::new(currency_pair)) + .await + .unwrap() + } + InstrumentAny::Equity(equity) => { + DatabaseQueries::add_instrument(pool, "EQUITY", Box::new(equity)) + .await + .unwrap() + } + InstrumentAny::FuturesContract(futures_contract) => { + DatabaseQueries::add_instrument( + pool, + "FUTURES_CONTRACT", + Box::new(futures_contract), + ) + .await + .unwrap() + } + InstrumentAny::FuturesSpread(futures_spread) => DatabaseQueries::add_instrument( + pool, + "FUTURES_SPREAD", + Box::new(futures_spread), + ) + .await + .unwrap(), + InstrumentAny::OptionsContract(options_contract) => { + DatabaseQueries::add_instrument( + pool, + "OPTIONS_CONTRACT", + Box::new(options_contract), + ) + .await + .unwrap() + } + InstrumentAny::OptionsSpread(options_spread) => DatabaseQueries::add_instrument( + pool, + "OPTIONS_SPREAD", + Box::new(options_spread), + ) + .await + .unwrap(), + }, } } } @@ -141,4 +203,32 @@ impl PostgresCacheDatabase { anyhow::anyhow!("Failed to query add_currency to database message handler: {err}") }) } + + pub async fn load_currencies(&self) -> anyhow::Result> { + DatabaseQueries::load_currencies(&self.pool).await + } + + pub async fn load_currency(&self, code: &str) -> anyhow::Result> { + DatabaseQueries::load_currency(&self.pool, code).await + } + + pub async fn add_instrument(&self, instrument: InstrumentAny) -> anyhow::Result<()> { + let query = DatabaseQuery::AddInstrument(instrument); + self.tx.send(query).await.map_err(|err| { + anyhow::anyhow!( + "Failed to send query add_instrument to database message handler: {err}" + ) + }) + } + + pub async fn load_instrument( + &self, + instrument_id: InstrumentId, + ) -> anyhow::Result> { + DatabaseQueries::load_instrument(&self.pool, instrument_id).await + } + + pub async fn load_instruments(&self) -> anyhow::Result> { + DatabaseQueries::load_instruments(&self.pool).await + } } diff --git a/nautilus_core/infrastructure/src/sql/mod.rs b/nautilus_core/infrastructure/src/sql/mod.rs index 6e14caa333d3..9c375a71cb53 100644 --- a/nautilus_core/infrastructure/src/sql/mod.rs +++ b/nautilus_core/infrastructure/src/sql/mod.rs @@ -14,7 +14,7 @@ // ------------------------------------------------------------------------------------------------- // Be careful about ordering and foreign key constraints when deleting data. -pub const NAUTILUS_TABLES: [&str; 2] = ["general", "currency"]; +pub const NAUTILUS_TABLES: [&str; 3] = ["general", "instrument", "currency"]; pub mod cache_database; pub mod models; diff --git a/nautilus_core/infrastructure/src/sql/models/instruments.rs b/nautilus_core/infrastructure/src/sql/models/instruments.rs index 46959076f3c2..e78882a9d82e 100644 --- a/nautilus_core/infrastructure/src/sql/models/instruments.rs +++ b/nautilus_core/infrastructure/src/sql/models/instruments.rs @@ -35,55 +35,57 @@ use rust_decimal::Decimal; use sqlx::{postgres::PgRow, FromRow, Row}; use ustr::Ustr; -struct InstrumentAnyModel(InstrumentAny); -struct CryptoFutureModel(CryptoFuture); -struct CryptoPerpetualModel(CryptoPerpetual); -struct CurrencyPairModel(CurrencyPair); -struct EquityModel(Equity); -struct FuturesContractModel(FuturesContract); -struct FuturesSpreadModel(FuturesSpread); -struct OptionsContractModel(OptionsContract); -struct OptionsSpreadModel(OptionsSpread); +pub struct InstrumentAnyModel(pub InstrumentAny); +pub struct CryptoFutureModel(pub CryptoFuture); +pub struct CryptoPerpetualModel(pub CryptoPerpetual); +pub struct CurrencyPairModel(pub CurrencyPair); +pub struct EquityModel(pub Equity); +pub struct FuturesContractModel(pub FuturesContract); +pub struct FuturesSpreadModel(pub FuturesSpread); +pub struct OptionsContractModel(pub OptionsContract); +pub struct OptionsSpreadModel(pub OptionsSpread); // TBD -// impl<'r> FromRow<'r, PgRow> for InstrumentAnyModel { -// fn from_row(row: &'r PgRow) -> Result { -// let kind = row.get::("kind"); -// if kind == "CRYPTO_FUTURE" { -// Ok(InstrumentAny::CryptoFuture( -// CryptoFutureModel::from_row(row).unwrap().0, -// )) -// } else if kind == "CRYPTO_PERPETUAL" { -// Ok(InstrumentAny::CryptoPerpetual( -// CryptoPerpetual::from_row(row).unwrap(), -// )) -// } else if kind == "CURRENCY_PAIR" { -// Ok(InstrumentAny::CurrencyPair( -// CurrencyPair::from_row(row).unwrap(), -// )) -// } else if kind == "EQUITY" { -// Ok(InstrumentAny::Equity(Equity::from_row(row).unwrap())) -// } else if kind == "FUTURES_CONTRACT" { -// Ok(InstrumentAny::FuturesContract( -// FuturesContract::from_row(row).unwrap(), -// )) -// } else if kind == "FUTURES_SPREAD" { -// Ok(InstrumentAny::FuturesSpread( -// FuturesSpread::from_row(row).unwrap(), -// )) -// } else if kind == "OPTIONS_CONTRACT" { -// Ok(InstrumentAny::OptionsContract( -// OptionsContract::from_row(row).unwrap(), -// )) -// } else if kind == "OPTIONS_SPREAD" { -// Ok(InstrumentAny::OptionsSpread( -// OptionsSpread::from_row(row).unwrap(), -// )) -// } else { -// panic!("Unknown instrument type") -// } -// } -// } +impl<'r> FromRow<'r, PgRow> for InstrumentAnyModel { + fn from_row(row: &'r PgRow) -> Result { + let kind = row.get::("kind"); + if kind == "CRYPTO_FUTURE" { + Ok(InstrumentAnyModel(InstrumentAny::CryptoFuture( + CryptoFutureModel::from_row(row).unwrap().0, + ))) + } else if kind == "CRYPTO_PERPETUAL" { + Ok(InstrumentAnyModel(InstrumentAny::CryptoPerpetual( + CryptoPerpetualModel::from_row(row).unwrap().0, + ))) + } else if kind == "CURRENCY_PAIR" { + Ok(InstrumentAnyModel(InstrumentAny::CurrencyPair( + CurrencyPairModel::from_row(row).unwrap().0, + ))) + } else if kind == "EQUITY" { + Ok(InstrumentAnyModel(InstrumentAny::Equity( + EquityModel::from_row(row).unwrap().0, + ))) + } else if kind == "FUTURES_CONTRACT" { + Ok(InstrumentAnyModel(InstrumentAny::FuturesContract( + FuturesContractModel::from_row(row).unwrap().0, + ))) + } else if kind == "FUTURES_SPREAD" { + Ok(InstrumentAnyModel(InstrumentAny::FuturesSpread( + FuturesSpreadModel::from_row(row).unwrap().0, + ))) + } else if kind == "OPTIONS_CONTRACT" { + Ok(InstrumentAnyModel(InstrumentAny::OptionsContract( + OptionsContractModel::from_row(row).unwrap().0, + ))) + } else if kind == "OPTIONS_SPREAD" { + Ok(InstrumentAnyModel(InstrumentAny::OptionsSpread( + OptionsSpreadModel::from_row(row).unwrap().0, + ))) + } else { + panic!("Unknown instrument type") + } + } +} impl<'r> FromRow<'r, PgRow> for CryptoFutureModel { fn from_row(row: &'r PgRow) -> Result { diff --git a/nautilus_core/infrastructure/src/sql/pg.rs b/nautilus_core/infrastructure/src/sql/pg.rs index fd31fea4c917..710589279696 100644 --- a/nautilus_core/infrastructure/src/sql/pg.rs +++ b/nautilus_core/infrastructure/src/sql/pg.rs @@ -105,7 +105,7 @@ pub async fn delete_nautilus_postgres_tables(db: &PgPool) -> anyhow::Result<()> query(format!("DELETE FROM \"{}\" WHERE true", table).as_str()) .execute(db) .await - .unwrap_or_else(|_| panic!("Failed to delete table {}", table)); + .unwrap_or_else(|err| panic!("Failed to delete table {} because: {}", table, err)); } Ok(()) } diff --git a/nautilus_core/infrastructure/src/sql/queries.rs b/nautilus_core/infrastructure/src/sql/queries.rs index c0db3edc4bc3..751a73b84067 100644 --- a/nautilus_core/infrastructure/src/sql/queries.rs +++ b/nautilus_core/infrastructure/src/sql/queries.rs @@ -15,10 +15,16 @@ use std::collections::HashMap; -use nautilus_model::types::currency::Currency; +use nautilus_model::{ + identifiers::instrument_id::InstrumentId, + instruments::{Instrument, InstrumentAny}, + types::currency::Currency, +}; use sqlx::PgPool; -use crate::sql::models::{general::GeneralRow, types::CurrencyModel}; +use crate::sql::models::{ + general::GeneralRow, instruments::InstrumentAnyModel, types::CurrencyModel, +}; pub struct DatabaseQueries; @@ -86,4 +92,85 @@ impl DatabaseQueries { .map(|_| ()) .map_err(|err| anyhow::anyhow!("Failed to truncate table: {err}")) } + + pub async fn add_instrument( + pool: &PgPool, + kind: &str, + instrument: Box, + ) -> anyhow::Result<()> { + sqlx::query(r#" + INSERT INTO "instrument" ( + id, kind, raw_symbol, base_currency, underlying, quote_currency, settlement_currency, isin, asset_class, exchange, + multiplier, option_kind, is_inverse, strike_price, activation_ns, expiration_ns, price_precision, size_precision, + price_increment, size_increment, maker_fee, taker_fee, margin_init, margin_maint, lot_size, max_quantity, min_quantity, max_notional, + min_notional, max_price, min_price, ts_init, ts_event, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (id) + DO UPDATE + SET + kind = $2, raw_symbol = $3, base_currency= $4, underlying = $5, quote_currency = $6, settlement_currency = $7, isin = $8, asset_class = $9, exchange = $10, + multiplier = $11, option_kind = $12, is_inverse = $13, strike_price = $14, activation_ns = $15, expiration_ns = $16 , price_precision = $17, size_precision = $18, + price_increment = $19, size_increment = $20, maker_fee = $21, taker_fee = $22, margin_init = $23, margin_maint = $24, lot_size = $25, max_quantity = $26, + min_quantity = $27, max_notional = $28, min_notional = $29, max_price = $30, min_price = $31, ts_init = $32, ts_event = $33, updated_at = CURRENT_TIMESTAMP + "#) + .bind(instrument.id().to_string()) + .bind(kind) + .bind(instrument.raw_symbol().to_string()) + .bind(instrument.base_currency().map(|x| x.code.as_str())) + .bind(instrument.underlying().map(|x| x.to_string())) + .bind(instrument.quote_currency().code.as_str()) + .bind(instrument.settlement_currency().code.as_str()) + .bind(instrument.isin().map(|x| x.to_string())) + .bind(instrument.asset_class().to_string()) + .bind(instrument.exchange().map(|x| x.to_string())) + .bind(instrument.multiplier().to_string()) + .bind(instrument.option_kind().map(|x| x.to_string())) + .bind(instrument.is_inverse()) + .bind(instrument.strike_price().map(|x| x.to_string())) + .bind(instrument.activation_ns().map(|x| x.to_string())) + .bind(instrument.expiration_ns().map(|x| x.to_string())) + .bind(instrument.price_precision() as i32) + .bind(instrument.size_precision() as i32) + .bind(instrument.price_increment().to_string()) + .bind(instrument.size_increment().to_string()) + .bind(instrument.maker_fee().to_string()) + .bind(instrument.taker_fee().to_string()) + .bind(instrument.margin_init().to_string()) + .bind(instrument.margin_maint().to_string()) + .bind(instrument.lot_size().map(|x| x.to_string())) + .bind(instrument.max_quantity().map(|x| x.to_string())) + .bind(instrument.min_quantity().map(|x| x.to_string())) + .bind(instrument.max_notional().map(|x| x.to_string())) + .bind(instrument.min_notional().map(|x| x.to_string())) + .bind(instrument.max_price().map(|x| x.to_string())) + .bind(instrument.min_price().map(|x| x.to_string())) + .bind(instrument.ts_init().to_string()) + .bind(instrument.ts_event().to_string()) + .execute(pool) + .await + .map(|_| ()) + .map_err(|err| anyhow::anyhow!(format!("Failed to insert item {} into instrument table: {:?}", instrument.id().to_string(), err))) + } + + pub async fn load_instrument( + pool: &PgPool, + instrument_id: InstrumentId, + ) -> anyhow::Result> { + sqlx::query_as::<_, InstrumentAnyModel>("SELECT * FROM instrument WHERE id = $1") + .bind(instrument_id.to_string()) + .fetch_optional(pool) + .await + .map(|instrument| instrument.map(|row| row.0)) + .map_err(|err| { + anyhow::anyhow!("Failed to load instrument with id {instrument_id},error is: {err}") + }) + } + + pub async fn load_instruments(pool: &PgPool) -> anyhow::Result> { + sqlx::query_as::<_, InstrumentAnyModel>("SELECT * FROM instrument") + .fetch_all(pool) + .await + .map(|rows| rows.into_iter().map(|row| row.0).collect()) + .map_err(|err| anyhow::anyhow!("Failed to load instruments: {err}")) + } } diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs index 19dda20af452..a165f046e6e6 100644 --- a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -60,13 +60,25 @@ pub async fn get_pg_cache_database() -> anyhow::Result { mod tests { use std::time::Duration; + use nautilus_model::{ + enums::CurrencyType, + identifiers::instrument_id::InstrumentId, + instruments::{ + stubs::{ + crypto_future_btcusdt, crypto_perpetual_ethusdt, currency_pair_ethusdt, + equity_aapl, futures_contract_es, options_contract_appl, + }, + Instrument, InstrumentAny, + }, + types::{currency::Currency, price::Price, quantity::Quantity}, + }; + use crate::get_pg_cache_database; #[tokio::test] async fn test_load_general_objects_when_nothing_in_cache_returns_empty_hashmap() { let pg_cache = get_pg_cache_database().await.unwrap(); let result = pg_cache.load().await.unwrap(); - println!("1: {result:?}"); assert_eq!(result.len(), 0); } @@ -78,15 +90,148 @@ mod tests { .add(String::from("test_id"), test_id_value.clone()) .await .unwrap(); - // sleep with tokio - tokio::time::sleep(Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_millis(500)).await; let result = pg_cache.load().await.unwrap(); - println!("2: {result:?}"); assert_eq!(result.keys().len(), 1); assert_eq!( result.keys().cloned().collect::>(), vec![String::from("test_id")] - ); // assert_eq!(result.get(&test_id_key).unwrap().to_owned(),&test_id_value.clone()); + ); assert_eq!(result.get("test_id").unwrap().to_owned(), test_id_value); } + + #[tokio::test] + async fn test_add_currency_and_instruments() { + // 1. first define and add currencies as they are contain foreign keys for instruments + let pg_cache = get_pg_cache_database().await.unwrap(); + // Define currencies + let btc = Currency::new("BTC", 8, 0, "BTC", CurrencyType::Crypto).unwrap(); + let eth = Currency::new("ETH", 2, 0, "ETH", CurrencyType::Crypto).unwrap(); + let usd = Currency::new("USD", 2, 0, "USD", CurrencyType::Fiat).unwrap(); + let usdt = Currency::new("USDT", 2, 0, "USDT", CurrencyType::Crypto).unwrap(); + // Insert all the currencies + pg_cache.add_currency(btc).await.unwrap(); + pg_cache.add_currency(eth).await.unwrap(); + pg_cache.add_currency(usd).await.unwrap(); + pg_cache.add_currency(usdt).await.unwrap(); + // Define all the instruments + let crypto_future = + crypto_future_btcusdt(2, 6, Price::from("0.01"), Quantity::from("0.000001")); + let crypto_perpetual = crypto_perpetual_ethusdt(); + let currency_pair = currency_pair_ethusdt(); + let equity = equity_aapl(); + let futures_contract = futures_contract_es(); + let options_contract = options_contract_appl(); + // Insert all the instruments + pg_cache + .add_instrument(InstrumentAny::CryptoFuture(crypto_future)) + .await + .unwrap(); + pg_cache + .add_instrument(InstrumentAny::CryptoPerpetual(crypto_perpetual)) + .await + .unwrap(); + pg_cache + .add_instrument(InstrumentAny::CurrencyPair(currency_pair)) + .await + .unwrap(); + pg_cache + .add_instrument(InstrumentAny::Equity(equity)) + .await + .unwrap(); + pg_cache + .add_instrument(InstrumentAny::FuturesContract(futures_contract)) + .await + .unwrap(); + pg_cache + .add_instrument(InstrumentAny::OptionsContract(options_contract)) + .await + .unwrap(); + tokio::time::sleep(Duration::from_secs(2)).await; + // Check that currency list is correct + let currencies = pg_cache.load_currencies().await.unwrap(); + assert_eq!(currencies.len(), 4); + assert_eq!( + currencies + .into_iter() + .map(|c| c.code.to_string()) + .collect::>(), + vec![ + String::from("BTC"), + String::from("ETH"), + String::from("USD"), + String::from("USDT") + ] + ); + // Check individual currencies + assert_eq!(pg_cache.load_currency("BTC").await.unwrap().unwrap(), btc); + assert_eq!(pg_cache.load_currency("ETH").await.unwrap().unwrap(), eth); + assert_eq!(pg_cache.load_currency("USDT").await.unwrap().unwrap(), usdt); + // Check individual instruments + assert_eq!( + pg_cache + .load_instrument(crypto_future.id()) + .await + .unwrap() + .unwrap(), + InstrumentAny::CryptoFuture(crypto_future) + ); + assert_eq!( + pg_cache + .load_instrument(crypto_perpetual.id()) + .await + .unwrap() + .unwrap(), + InstrumentAny::CryptoPerpetual(crypto_perpetual) + ); + assert_eq!( + pg_cache + .load_instrument(currency_pair.id()) + .await + .unwrap() + .unwrap(), + InstrumentAny::CurrencyPair(currency_pair) + ); + assert_eq!( + pg_cache + .load_instrument(equity.id()) + .await + .unwrap() + .unwrap(), + InstrumentAny::Equity(equity) + ); + assert_eq!( + pg_cache + .load_instrument(futures_contract.id()) + .await + .unwrap() + .unwrap(), + InstrumentAny::FuturesContract(futures_contract) + ); + assert_eq!( + pg_cache + .load_instrument(options_contract.id()) + .await + .unwrap() + .unwrap(), + InstrumentAny::OptionsContract(options_contract) + ); + // Check that instrument list is correct + let instruments = pg_cache.load_instruments().await.unwrap(); + assert_eq!(instruments.len(), 6); + assert_eq!( + instruments + .into_iter() + .map(|i| i.id()) + .collect::>(), + vec![ + crypto_future.id(), + crypto_perpetual.id(), + currency_pair.id(), + equity.id(), + futures_contract.id(), + options_contract.id() + ] + ); + } } diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index 9e5f43adb069..52e38ab063f5 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -275,6 +275,12 @@ impl InstrumentAny { } } +impl PartialEq for InstrumentAny { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + pub trait Instrument: 'static + Send { fn into_any(self) -> InstrumentAny; fn id(&self) -> InstrumentId; diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index acd64aa6ac32..eb05beaacef8 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -15,7 +15,7 @@ use chrono::{TimeZone, Utc}; use nautilus_core::nanos::UnixNanos; -use rstest::fixture; +use rstest::*; use rust_decimal_macros::dec; use ustr::Ustr; @@ -36,7 +36,12 @@ use crate::{ //////////////////////////////////////////////////////////////////////////////// #[fixture] -pub fn crypto_future_btcusdt() -> CryptoFuture { +pub fn crypto_future_btcusdt( + #[default(2)] price_precision: u8, + #[default(6)] size_precision: u8, + #[default(Price::from("0.01"))] price_increment: Price, + #[default(Quantity::from("0.000001"))] size_increment: Quantity, +) -> CryptoFuture { let activation = Utc.with_ymd_and_hms(2014, 4, 8, 0, 0, 0).unwrap(); let expiration = Utc.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap(); CryptoFuture::new( @@ -48,10 +53,10 @@ pub fn crypto_future_btcusdt() -> CryptoFuture { false, UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64), UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64), - 2, - 6, - Price::from("0.01"), - Quantity::from("0.000001"), + price_precision, + size_precision, + price_increment, + size_increment, dec!(0), dec!(0), dec!(0), diff --git a/nautilus_core/model/src/python/instruments/crypto_perpetual.rs b/nautilus_core/model/src/python/instruments/crypto_perpetual.rs index a406f11734c3..0e00f511c1fd 100644 --- a/nautilus_core/model/src/python/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/python/instruments/crypto_perpetual.rs @@ -34,7 +34,7 @@ impl CryptoPerpetual { #[new] fn py_new( id: InstrumentId, - symbol: Symbol, + raw_symbol: Symbol, base_currency: Currency, quote_currency: Currency, settlement_currency: Currency, @@ -59,7 +59,7 @@ impl CryptoPerpetual { ) -> PyResult { Self::new( id, - symbol, + raw_symbol, base_currency, quote_currency, settlement_currency, diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index b23296c47fc2..902b9610b3fc 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -29,6 +29,8 @@ pub fn convert_instrument_any_to_pyobject( instrument: InstrumentAny, ) -> PyResult { match instrument { + InstrumentAny::CryptoFuture(inst) => Ok(inst.into_py(py)), + InstrumentAny::CryptoPerpetual(inst) => Ok(inst.into_py(py)), InstrumentAny::CurrencyPair(inst) => Ok(inst.into_py(py)), InstrumentAny::Equity(inst) => Ok(inst.into_py(py)), InstrumentAny::FuturesContract(inst) => Ok(inst.into_py(py)), diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index fd0d5b90e8bb..8337f7aaab0c 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -279,23 +279,8 @@ impl<'de> Deserialize<'de> for Money { D: Deserializer<'de>, { let money_str: &str = Deserialize::deserialize(deserializer)?; - - let parts: Vec<&str> = money_str.splitn(2, ' ').collect(); - if parts.len() != 2 { - return Err(serde::de::Error::custom("Invalid Money format")); - } - - let amount_str = parts[0]; - let currency_str = parts[1]; - - let amount = amount_str - .parse::() - .map_err(|_| serde::de::Error::custom("Failed to parse Money amount"))?; - - let currency = Currency::from_str(currency_str) - .map_err(|_| serde::de::Error::custom("Invalid currency"))?; - - Ok(Self::new(amount, currency).unwrap()) // TODO: Properly handle the error + Money::from_str(money_str) + .map_err(|_| serde::de::Error::custom("Failed to parse Money amount")) } } diff --git a/nautilus_trader/cache/postgres/adapter.py b/nautilus_trader/cache/postgres/adapter.py index fe67593bc242..60a54ffc892e 100644 --- a/nautilus_trader/cache/postgres/adapter.py +++ b/nautilus_trader/cache/postgres/adapter.py @@ -17,7 +17,12 @@ from nautilus_trader.cache.facade import CacheDatabaseFacade from nautilus_trader.cache.postgres.transformers import transform_currency_from_pyo3 from nautilus_trader.cache.postgres.transformers import transform_currency_to_pyo3 +from nautilus_trader.cache.postgres.transformers import transform_instrument_from_pyo3 +from nautilus_trader.cache.postgres.transformers import transform_instrument_to_pyo3 +from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.core.nautilus_pyo3 import PostgresCacheDatabase +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.instruments import Instrument from nautilus_trader.model.objects import Currency @@ -55,3 +60,12 @@ def load_currency(self, code: str) -> Currency | None: if currency_pyo3: return transform_currency_from_pyo3(currency_pyo3) return None + + def add_instrument(self, instrument: Instrument): + instrument_pyo3 = transform_instrument_to_pyo3(instrument) + self._backing.add_instrument(instrument_pyo3) + + def load_instrument(self, instrument_id: InstrumentId) -> Instrument: + instrument_id_pyo3 = nautilus_pyo3.InstrumentId.from_str(str(instrument_id)) + instrument_pyo3 = self._backing.load_instrument(instrument_id_pyo3) + return transform_instrument_from_pyo3(instrument_pyo3) diff --git a/nautilus_trader/cache/postgres/transformers.py b/nautilus_trader/cache/postgres/transformers.py index 65542490015d..674d3e7f2673 100644 --- a/nautilus_trader/cache/postgres/transformers.py +++ b/nautilus_trader/cache/postgres/transformers.py @@ -15,6 +15,15 @@ from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.model.enums import CurrencyType +from nautilus_trader.model.instruments import CryptoFuture +from nautilus_trader.model.instruments import CryptoPerpetual +from nautilus_trader.model.instruments import CurrencyPair +from nautilus_trader.model.instruments import Equity +from nautilus_trader.model.instruments import FuturesContract +from nautilus_trader.model.instruments import FuturesSpread +from nautilus_trader.model.instruments import Instrument +from nautilus_trader.model.instruments import OptionsContract +from nautilus_trader.model.instruments import OptionsSpread from nautilus_trader.model.objects import Currency @@ -39,3 +48,49 @@ def transform_currency_to_pyo3(currency: Currency) -> nautilus_pyo3.Currency: name=currency.name, currency_type=nautilus_pyo3.CurrencyType.from_str(currency.currency_type.name), ) + + +################################################################################ +# Instruments +################################################################################ + + +def transform_instrument_to_pyo3(instrument: Instrument): + if isinstance(instrument, CryptoFuture): + return nautilus_pyo3.CryptoFuture.from_dict(CryptoFuture.to_dict(instrument)) + elif isinstance(instrument, CryptoPerpetual): + return nautilus_pyo3.CryptoPerpetual.from_dict(CryptoPerpetual.to_dict(instrument)) + elif isinstance(instrument, CurrencyPair): + currency_pair_dict = CurrencyPair.to_dict(instrument) + return nautilus_pyo3.CurrencyPair.from_dict(currency_pair_dict) + elif isinstance(instrument, Equity): + return nautilus_pyo3.Equity.from_dict(Equity.to_dict(instrument)) + elif isinstance(instrument, FuturesContract): + return nautilus_pyo3.FuturesContract.from_dict(FuturesContract.to_dict(instrument)) + elif isinstance(instrument, OptionsContract): + return nautilus_pyo3.OptionsContract.from_dict(OptionsContract.to_dict(instrument)) + else: + raise ValueError(f"Unknown instrument type: {instrument}") + + +def transform_instrument_from_pyo3(instrument_pyo3) -> Instrument | None: + if instrument_pyo3 is None: + return None + if isinstance(instrument_pyo3, nautilus_pyo3.CryptoFuture): + return CryptoFuture.from_pyo3(instrument_pyo3) + elif isinstance(instrument_pyo3, nautilus_pyo3.CryptoPerpetual): + return CryptoPerpetual.from_pyo3(instrument_pyo3) + elif isinstance(instrument_pyo3, nautilus_pyo3.CurrencyPair): + return CurrencyPair.from_pyo3(instrument_pyo3) + elif isinstance(instrument_pyo3, nautilus_pyo3.Equity): + return Equity.from_pyo3(instrument_pyo3) + elif isinstance(instrument_pyo3, nautilus_pyo3.FuturesContract): + return FuturesContract.from_pyo3(instrument_pyo3) + elif isinstance(instrument_pyo3, nautilus_pyo3.FuturesSpread): + return FuturesSpread.from_pyo3(instrument_pyo3) + elif isinstance(instrument_pyo3, nautilus_pyo3.OptionsContract): + return OptionsContract.from_pyo3(instrument_pyo3) + elif isinstance(instrument_pyo3, nautilus_pyo3.OptionsSpread): + return OptionsSpread.from_pyo3(instrument_pyo3) + else: + raise ValueError(f"Unknown instrument type: {instrument_pyo3}") diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index d027c8213435..c66d6c608ac8 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1509,7 +1509,7 @@ class CryptoPerpetual: def __init__( self, id: InstrumentId, - symbol: Symbol, + raw_symbol: Symbol, base_currency: Currency, quote_currency: Currency, settlement_currency: Currency, @@ -2265,8 +2265,11 @@ class PostgresCacheDatabase: def load(self) -> dict[str,str]: ... def add(self, key: str, value: bytes) -> None: ... def add_currency(self,currency: Currency) -> None: ... + def add_instrument(self, instrument: object) -> None: ... def load_currency(self, code: str) -> Currency | None: ... def load_currencies(self) -> list[Currency]: ... + def load_instrument(self, instrument_id: InstrumentId) -> Instrument | None: ... + def load_instruments(self) -> list[Instrument]: ... def flush_db(self) -> None: ... def truncate(self, table: str) -> None: ... diff --git a/nautilus_trader/test_kit/rust/instruments_pyo3.py b/nautilus_trader/test_kit/rust/instruments_pyo3.py index 4538196fec5d..8b70af3d6df5 100644 --- a/nautilus_trader/test_kit/rust/instruments_pyo3.py +++ b/nautilus_trader/test_kit/rust/instruments_pyo3.py @@ -91,7 +91,7 @@ def audusd_sim(): def ethusdt_perp_binance() -> CryptoPerpetual: return CryptoPerpetual( id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), - symbol=Symbol("ETHUSDT-PERP"), + raw_symbol=Symbol("ETHUSDT-PERP"), base_currency=_ETH, quote_currency=_USDT, settlement_currency=_USDT, @@ -149,7 +149,7 @@ def xbtusd_bitmex() -> CryptoPerpetual: symbol=Symbol("BTC/USD"), venue=Venue("BITMEX"), ), - symbol=Symbol("XBTUSD"), + raw_symbol=Symbol("XBTUSD"), base_currency=_BTC, quote_currency=_USD, settlement_currency=_BTC, @@ -179,7 +179,7 @@ def ethusd_bitmex() -> CryptoPerpetual: symbol=Symbol("ETH/USD"), venue=Venue("BITMEX"), ), - symbol=Symbol("ETHUSD"), + raw_symbol=Symbol("ETHUSD"), base_currency=_ETH, quote_currency=_USD, settlement_currency=_ETH, diff --git a/tests/integration_tests/infrastructure/test_cache_database_postgres.py b/tests/integration_tests/infrastructure/test_cache_database_postgres.py index 759a3de3fa75..70348c7ca3eb 100644 --- a/tests/integration_tests/infrastructure/test_cache_database_postgres.py +++ b/tests/integration_tests/infrastructure/test_cache_database_postgres.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------------------------- - +import asyncio import os import pytest @@ -21,7 +21,9 @@ from nautilus_trader.common.component import MessageBus from nautilus_trader.common.component import TestClock from nautilus_trader.model.enums import CurrencyType +from nautilus_trader.model.instruments import CurrencyPair from nautilus_trader.model.objects import Currency +from nautilus_trader.model.objects import Price from nautilus_trader.portfolio.portfolio import Portfolio from nautilus_trader.test_kit.functions import eventually from nautilus_trader.test_kit.providers import TestInstrumentProvider @@ -75,6 +77,10 @@ def setup(self): def teardown(self): self.database.flush() + ################################################################################ + # General + ################################################################################ + @pytest.mark.asyncio async def test_load_general_objects_when_nothing_in_cache_returns_empty_dict(self): # Arrange, Act @@ -123,3 +129,190 @@ async def test_add_currency(self): currencies = self.database.load_currencies() assert list(currencies.keys()) == ["BTC"] + + ################################################################################ + # Instrument - Crypto Future + ################################################################################ + @pytest.mark.asyncio + async def test_add_instrument_crypto_future(self): + # Arrange, Act + btc_usdt_crypto_future = TestInstrumentProvider.btcusdt_future_binance() + self.database.add_currency(btc_usdt_crypto_future.underlying) + self.database.add_currency(btc_usdt_crypto_future.quote_currency) + self.database.add_currency(btc_usdt_crypto_future.settlement_currency) + + await asyncio.sleep(0.5) + # Check that we have added target currencies, because of foreign key constraints + await eventually(lambda: self.database.load_currencies()) + + currencies = self.database.load_currencies() + assert list(currencies.keys()) == ["BTC", "USDT"] + + # add instrument + self.database.add_instrument(btc_usdt_crypto_future) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_instrument(btc_usdt_crypto_future.id)) + + # Assert + result = self.database.load_instrument(btc_usdt_crypto_future.id) + assert result == btc_usdt_crypto_future + + ################################################################################ + # Instrument - Crypto Perpetual + ################################################################################ + @pytest.mark.asyncio + async def test_add_instrument_crypto_perpetual(self): + eth_usdt_crypto_perpetual = TestInstrumentProvider.ethusdt_perp_binance() + self.database.add_currency(eth_usdt_crypto_perpetual.base_currency) + self.database.add_currency(eth_usdt_crypto_perpetual.quote_currency) + self.database.add_currency(eth_usdt_crypto_perpetual.settlement_currency) + + await asyncio.sleep(0.5) + # Check that we have added target currencies, because of foreign key constraints + await eventually(lambda: self.database.load_currencies()) + + currencies = self.database.load_currencies() + assert list(currencies.keys()) == ["ETH", "USDT"] + + # add instrument + self.database.add_instrument(eth_usdt_crypto_perpetual) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_instrument(eth_usdt_crypto_perpetual.id)) + + # Assert + result = self.database.load_instrument(eth_usdt_crypto_perpetual.id) + assert result == eth_usdt_crypto_perpetual + + ################################################################################ + # Instrument - Currency Pair + ################################################################################ + + @pytest.mark.asyncio + async def test_add_instrument_currency_pair(self): + self.database.add_currency(AUDUSD_SIM.base_currency) + self.database.add_currency(AUDUSD_SIM.quote_currency) + await asyncio.sleep(0.6) + + # Check that we have added target currencies, because of foreign key constraints + await eventually(lambda: self.database.load_currencies()) + currencies = self.database.load_currencies() + assert list(currencies.keys()) == ["AUD", "USD"] + + self.database.add_instrument(AUDUSD_SIM) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_instrument(AUDUSD_SIM.id)) + + # Assert + assert AUDUSD_SIM == self.database.load_instrument(AUDUSD_SIM.id) + + # Update some fields, to check that add_instrument is idempotent + aud_usd_currency_pair_updated = CurrencyPair( + instrument_id=AUDUSD_SIM.id, + raw_symbol=AUDUSD_SIM.raw_symbol, + base_currency=AUDUSD_SIM.base_currency, + quote_currency=AUDUSD_SIM.quote_currency, + price_precision=AUDUSD_SIM.price_precision, + size_precision=AUDUSD_SIM.size_precision, + price_increment=AUDUSD_SIM.price_increment, + size_increment=AUDUSD_SIM.size_increment, + lot_size=AUDUSD_SIM.lot_size, + max_quantity=AUDUSD_SIM.max_quantity, + min_quantity=AUDUSD_SIM.min_quantity, + max_price=AUDUSD_SIM.max_price, + min_price=Price.from_str("111"), # <-- changed this + max_notional=AUDUSD_SIM.max_notional, + min_notional=AUDUSD_SIM.min_notional, + margin_init=AUDUSD_SIM.margin_init, + margin_maint=AUDUSD_SIM.margin_maint, + maker_fee=AUDUSD_SIM.maker_fee, + taker_fee=AUDUSD_SIM.taker_fee, + tick_scheme_name=AUDUSD_SIM.tick_scheme_name, + ts_event=123, # <-- changed this + ts_init=456, # <-- changed this + ) + + self.database.add_instrument(aud_usd_currency_pair_updated) + + # We have to manually sleep and not use eventually + await asyncio.sleep(0.5) + + # Assert + result = self.database.load_instrument(AUDUSD_SIM.id) + assert result.id == AUDUSD_SIM.id + assert result.ts_event == 123 + assert result.ts_init == 456 + assert result.min_price == Price.from_str("111") + + ################################################################################ + # Instrument - Equity + ################################################################################ + + @pytest.mark.asyncio + async def test_add_instrument_equity(self): + appl_equity = TestInstrumentProvider.equity() + self.database.add_currency(appl_equity.quote_currency) + + await asyncio.sleep(0.1) + # Check that we have added target currencies, because of foreign key constraints + await eventually(lambda: self.database.load_currencies()) + + currencies = self.database.load_currencies() + assert list(currencies.keys()) == ["USD"] + + # add instrument + self.database.add_instrument(appl_equity) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_instrument(appl_equity.id)) + + # Assert + assert appl_equity == self.database.load_instrument(appl_equity.id) + + ################################################################################ + # Instrument - Futures Contract + ################################################################################ + @pytest.mark.asyncio + async def test_add_instrument_futures_contract(self): + es_futures = TestInstrumentProvider.es_future(expiry_year=2023, expiry_month=12) + self.database.add_currency(es_futures.quote_currency) + + # Check that we have added target currencies, because of foreign key constraints + await eventually(lambda: self.database.load_currencies()) + + currencies = self.database.load_currencies() + assert list(currencies.keys()) == ["USD"] + + # add instrument + self.database.add_instrument(es_futures) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_instrument(es_futures.id)) + + # Assert + assert es_futures == self.database.load_instrument(es_futures.id) + + ################################################################################ + # Instrument - Options Contract + ################################################################################ + @pytest.mark.asyncio + async def test_add_instrument_options_contract(self): + aapl_option = TestInstrumentProvider.aapl_option() + self.database.add_currency(aapl_option.quote_currency) + + # Check that we have added target currencies, because of foreign key constraints + await eventually(lambda: self.database.load_currencies()) + + currencies = self.database.load_currencies() + assert list(currencies.keys()) == ["USD"] + + # add instrument + self.database.add_instrument(aapl_option) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_instrument(aapl_option.id)) + + # Assert + assert aapl_option == self.database.load_instrument(aapl_option.id) From 71af97d3ad7e09de3f4f15c6b0790b24f3813f31 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 07:33:51 +1000 Subject: [PATCH 059/193] Improve Money error messages --- nautilus_core/model/src/types/money.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 8337f7aaab0c..1b669f339fe1 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -96,7 +96,7 @@ impl FromStr for Money { // Ensure we have both the amount and currency if parts.len() != 2 { return Err(format!( - "Invalid input format: '{input}'. Expected ' '" + "Error invalid input format '{input}'. Expected ' '" )); } @@ -104,7 +104,7 @@ impl FromStr for Money { let amount = parts[0] .replace('_', "") .parse::() - .map_err(|e| format!("Cannot parse amount '{}' as `f64`: {:?}", parts[0], e))?; + .map_err(|e| format!("Error parsing amount '{}' as `f64`: {:?}", parts[0], e))?; // Parse currency let currency = Currency::from_str(parts[1]).map_err(|e: anyhow::Error| e.to_string())?; From f33369eb9ce4dcf5a3617f4d75243f93b0c7754e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 07:36:34 +1000 Subject: [PATCH 060/193] Cleanup imports --- .../infrastructure/test_cache_database_postgres.py | 1 + .../{test_cache_database.py => test_cache_database_redis.py} | 0 2 files changed, 1 insertion(+) rename tests/integration_tests/infrastructure/{test_cache_database.py => test_cache_database_redis.py} (100%) diff --git a/tests/integration_tests/infrastructure/test_cache_database_postgres.py b/tests/integration_tests/infrastructure/test_cache_database_postgres.py index 70348c7ca3eb..e80d504b22b9 100644 --- a/tests/integration_tests/infrastructure/test_cache_database_postgres.py +++ b/tests/integration_tests/infrastructure/test_cache_database_postgres.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------------------------- + import asyncio import os diff --git a/tests/integration_tests/infrastructure/test_cache_database.py b/tests/integration_tests/infrastructure/test_cache_database_redis.py similarity index 100% rename from tests/integration_tests/infrastructure/test_cache_database.py rename to tests/integration_tests/infrastructure/test_cache_database_redis.py From 50451b5c4380540c9b40e8621d947087237a2f48 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 07:42:33 +1000 Subject: [PATCH 061/193] Make error variable consistent and formatting --- nautilus_core/cli/src/bin/cli.rs | 4 +- .../infrastructure/src/sql/cache_database.rs | 4 +- nautilus_core/infrastructure/src/sql/pg.rs | 82 +++++++++++-------- .../persistence/src/python/backend/session.rs | 2 +- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/nautilus_core/cli/src/bin/cli.rs b/nautilus_core/cli/src/bin/cli.rs index a7ceaf08953b..66599d45550f 100644 --- a/nautilus_core/cli/src/bin/cli.rs +++ b/nautilus_core/cli/src/bin/cli.rs @@ -24,7 +24,7 @@ async fn main() { .with_module_level("sqlx", LevelFilter::Off) .init() .unwrap(); - if let Err(err) = nautilus_cli::run(NautilusCli::parse()).await { - error!("Error executing Nautilus CLI: {}", err); + if let Err(e) = nautilus_cli::run(NautilusCli::parse()).await { + error!("Error executing Nautilus CLI: {}", e); } } diff --git a/nautilus_core/infrastructure/src/sql/cache_database.rs b/nautilus_core/infrastructure/src/sql/cache_database.rs index c512ae12b145..de99609a62a4 100644 --- a/nautilus_core/infrastructure/src/sql/cache_database.rs +++ b/nautilus_core/infrastructure/src/sql/cache_database.rs @@ -184,8 +184,8 @@ impl PostgresCacheDatabase { } Ok(cache) } - Err(err) => { - panic!("Failed to load general table: {err}") + Err(e) => { + panic!("Failed to load general table: {e}") } } } diff --git a/nautilus_core/infrastructure/src/sql/pg.rs b/nautilus_core/infrastructure/src/sql/pg.rs index 710589279696..3a68c871cad2 100644 --- a/nautilus_core/infrastructure/src/sql/pg.rs +++ b/nautilus_core/infrastructure/src/sql/pg.rs @@ -100,7 +100,7 @@ pub fn get_postgres_connect_options( } pub async fn delete_nautilus_postgres_tables(db: &PgPool) -> anyhow::Result<()> { - // iterate over NAUTILUS_TABLES and delete all rows + // Iterate over NAUTILUS_TABLES and delete all rows for table in NAUTILUS_TABLES { query(format!("DELETE FROM \"{}\" WHERE true", table).as_str()) .execute(db) @@ -137,29 +137,32 @@ pub async fn init_postgres( schema_dir: Option, ) -> anyhow::Result<()> { info!("Initializing Postgres database with target permissions and schema"); - // create public schema + + // Create public schema match sqlx::query("CREATE SCHEMA IF NOT EXISTS public;") .execute(pg) .await { Ok(_) => info!("Schema public created successfully"), - Err(err) => error!("Error creating schema public: {:?}", err), + Err(e) => error!("Error creating schema public: {:?}", e), } - // create role if not exists + + // Create role if not exists match sqlx::query(format!("CREATE ROLE {} PASSWORD '{}' LOGIN;", database, password).as_str()) .execute(pg) .await { Ok(_) => info!("Role {} created successfully", database), - Err(err) => { - if err.to_string().contains("already exists") { + Err(e) => { + if e.to_string().contains("already exists") { info!("Role {} already exists", database); } else { - error!("Error creating role {}: {:?}", database, err); + error!("Error creating role {}: {:?}", database, e); } } } - // execute all the sql files in schema dir + + // Execute all the sql files in schema dir let schema_dir = schema_dir.unwrap_or_else(|| get_schema_dir().unwrap()); let mut sql_files = std::fs::read_dir(schema_dir)?.collect::, std::io::Error>>()?; @@ -185,29 +188,32 @@ pub async fn init_postgres( .unwrap(); } } - // grant connect + + // Grant connect match sqlx::query(format!("GRANT CONNECT ON DATABASE {0} TO {0};", database).as_str()) .execute(pg) .await { Ok(_) => info!("Connect privileges granted to role {}", database), - Err(err) => error!( + Err(e) => error!( "Error granting connect privileges to role {}: {:?}", - database, err + database, e ), } - // grant all schema privileges to the role + + // Grant all schema privileges to the role match sqlx::query(format!("GRANT ALL PRIVILEGES ON SCHEMA public TO {};", database).as_str()) .execute(pg) .await { Ok(_) => info!("All schema privileges granted to role {}", database), - Err(err) => error!( + Err(e) => error!( "Error granting all privileges to role {}: {:?}", - database, err + database, e ), } - // grant all table privileges to the role + + // Grant all table privileges to the role match sqlx::query( format!( "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO {};", @@ -219,12 +225,13 @@ pub async fn init_postgres( .await { Ok(_) => info!("All tables privileges granted to role {}", database), - Err(err) => error!( + Err(e) => error!( "Error granting all privileges to role {}: {:?}", - database, err + database, e ), } - // grant all sequence privileges to the role + + // Grant all sequence privileges to the role match sqlx::query( format!( "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO {};", @@ -236,12 +243,13 @@ pub async fn init_postgres( .await { Ok(_) => info!("All sequences privileges granted to role {}", database), - Err(err) => error!( + Err(e) => error!( "Error granting all privileges to role {}: {:?}", - database, err + database, e ), } - // grant all function privileges to the role + + // Grant all function privileges to the role match sqlx::query( format!( "GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO {};", @@ -253,9 +261,9 @@ pub async fn init_postgres( .await { Ok(_) => info!("All functions privileges granted to role {}", database), - Err(err) => error!( + Err(e) => error!( "Error granting all privileges to role {}: {:?}", - database, err + database, e ), } @@ -263,51 +271,55 @@ pub async fn init_postgres( } pub async fn drop_postgres(pg: &PgPool, database: String) -> anyhow::Result<()> { - // execute drop owned + // Execute drop owned match sqlx::query(format!("DROP OWNED BY {}", database).as_str()) .execute(pg) .await { Ok(_) => info!("Dropped owned objects by role {}", database), - Err(err) => error!("Error dropping owned by role {}: {:?}", database, err), + Err(e) => error!("Error dropping owned by role {}: {:?}", database, e), } - // revoke connect + + // Revoke connect match sqlx::query(format!("REVOKE CONNECT ON DATABASE {0} FROM {0};", database).as_str()) .execute(pg) .await { Ok(_) => info!("Revoked connect privileges from role {}", database), - Err(err) => error!( + Err(e) => error!( "Error revoking connect privileges from role {}: {:?}", - database, err + database, e ), } - // revoke privileges + + // Revoke privileges match sqlx::query(format!("REVOKE ALL PRIVILEGES ON DATABASE {0} FROM {0};", database).as_str()) .execute(pg) .await { Ok(_) => info!("Revoked all privileges from role {}", database), - Err(err) => error!( + Err(e) => error!( "Error revoking all privileges from role {}: {:?}", - database, err + database, e ), } - // execute drop schema + + // Execute drop schema match sqlx::query("DROP SCHEMA IF EXISTS public CASCADE") .execute(pg) .await { Ok(_) => info!("Dropped schema public"), - Err(err) => error!("Error dropping schema public: {:?}", err), + Err(e) => error!("Error dropping schema public: {:?}", e), } - // drop role + + // Drop role match sqlx::query(format!("DROP ROLE IF EXISTS {};", database).as_str()) .execute(pg) .await { Ok(_) => info!("Dropped role {}", database), - Err(err) => error!("Error dropping role {}: {:?}", database, err), + Err(e) => error!("Error dropping role {}: {:?}", database, e), } Ok(()) } diff --git a/nautilus_core/persistence/src/python/backend/session.rs b/nautilus_core/persistence/src/python/backend/session.rs index a4e451f2276d..38136be22300 100644 --- a/nautilus_core/persistence/src/python/backend/session.rs +++ b/nautilus_core/persistence/src/python/backend/session.rs @@ -103,7 +103,7 @@ impl DataQueryResult { let cvec = slf.set_chunk(acc); Python::with_gil(|py| match PyCapsule::new::(py, cvec, None) { Ok(capsule) => Ok(Some(capsule.into_py(py))), - Err(err) => Err(to_pyruntime_err(err)), + Err(e) => Err(to_pyruntime_err(e)), }) } _ => Ok(None), From ee174b78ae079260c99940f2f989614ca80623ef Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 07:45:27 +1000 Subject: [PATCH 062/193] Fix OrderList display --- nautilus_core/model/src/orders/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_core/model/src/orders/list.rs b/nautilus_core/model/src/orders/list.rs index 1593aa5ebb99..bebb27cebdb9 100644 --- a/nautilus_core/model/src/orders/list.rs +++ b/nautilus_core/model/src/orders/list.rs @@ -78,7 +78,7 @@ impl Display for OrderList { instrument_id={}, \ strategy_id={}, \ orders={:?}, \ - ts_init={}, \ + ts_init={}\ )", self.id, self.instrument_id, self.strategy_id, self.orders, self.ts_init, ) From 7a88a5015203c0cbce0c1419513d1703f1c139de Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 08:11:07 +1000 Subject: [PATCH 063/193] Refine Cache index building --- nautilus_core/common/src/cache/mod.rs | 184 ++++++++++---------------- 1 file changed, 71 insertions(+), 113 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 9bed4ad73399..f95a99852d5e 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -343,198 +343,156 @@ impl Cache { } // Index orders - for (client_order_id, order) in &self.orders { + for (client_order_id, order) in self.orders.iter() { let instrument_id = order.instrument_id(); let venue = instrument_id.venue; let strategy_id = order.strategy_id(); - // 1: Build _index_venue_orders -> {Venue, {ClientOrderId}} - if let Some(venue_orders) = self.index.venue_orders.get_mut(&venue) { - venue_orders.insert(*client_order_id); - } else { - let mut venue_orders = HashSet::new(); - venue_orders.insert(*client_order_id); - self.index.venue_orders.insert(venue, venue_orders); - } + // 1: Build index.venue_orders -> {Venue, {ClientOrderId}} + self.index + .venue_orders + .entry(venue) + .or_default() + .insert(*client_order_id); - // 2: Build _index_order_ids -> {VenueOrderId, ClientOrderId} + // 2: Build index.order_ids -> {VenueOrderId, ClientOrderId} if let Some(venue_order_id) = order.venue_order_id() { self.index .order_ids .insert(venue_order_id, *client_order_id); } - // 3: Build _index_order_position -> {ClientOrderId, PositionId} + // 3: Build index.order_position -> {ClientOrderId, PositionId} if let Some(position_id) = order.position_id() { self.index .order_position .insert(*client_order_id, position_id); } - // 4: Build _index_order_strategy -> {ClientOrderId, StrategyId} + // 4: Build index.order_strategy -> {ClientOrderId, StrategyId} self.index .order_strategy .insert(*client_order_id, order.strategy_id()); - // 5: Build _index_instrument_orders -> {InstrumentId, {ClientOrderId}} - if let Some(instrument_orders) = self.index.instrument_orders.get_mut(&instrument_id) { - instrument_orders.insert(*client_order_id); - } else { - let mut instrument_orders = HashSet::new(); - instrument_orders.insert(*client_order_id); - self.index - .instrument_orders - .insert(instrument_id, instrument_orders); - } + // 5: Build index.instrument_orders -> {InstrumentId, {ClientOrderId}} + self.index + .instrument_orders + .entry(instrument_id) + .or_default() + .insert(*client_order_id); - // 6: Build _index_strategy_orders -> {StrategyId, {ClientOrderId}} - if let Some(strategy_orders) = self.index.strategy_orders.get_mut(&strategy_id) { - strategy_orders.insert(*client_order_id); - } else { - let mut strategy_orders = HashSet::new(); - strategy_orders.insert(*client_order_id); - self.index - .strategy_orders - .insert(strategy_id, strategy_orders); - } + // 6: Build index.strategy_orders -> {StrategyId, {ClientOrderId}} + self.index + .strategy_orders + .entry(strategy_id) + .or_default() + .insert(*client_order_id); - // 7: Build _index_exec_algorithm_orders -> {ExecAlgorithmId, {ClientOrderId}} + // 7: Build index.exec_algorithm_orders -> {ExecAlgorithmId, {ClientOrderId}} if let Some(exec_algorithm_id) = order.exec_algorithm_id() { - if let Some(exec_algorithm_orders) = - self.index.exec_algorithm_orders.get_mut(&exec_algorithm_id) - { - exec_algorithm_orders.insert(*client_order_id); - } else { - let mut exec_algorithm_orders = HashSet::new(); - exec_algorithm_orders.insert(*client_order_id); - self.index - .exec_algorithm_orders - .insert(exec_algorithm_id, exec_algorithm_orders); - } + self.index + .exec_algorithm_orders + .entry(exec_algorithm_id) + .or_default() + .insert(*client_order_id); } - // 8: Build _index_exec_spawn_orders -> {ClientOrderId, {ClientOrderId}} + // 8: Build index.exec_spawn_orders -> {ClientOrderId, {ClientOrderId}} if let Some(exec_spawn_id) = order.exec_spawn_id() { - if let Some(exec_spawn_orders) = - self.index.exec_spawn_orders.get_mut(&exec_spawn_id) - { - exec_spawn_orders.insert(*client_order_id); - } else { - let mut exec_spawn_orders = HashSet::new(); - exec_spawn_orders.insert(*client_order_id); - self.index - .exec_spawn_orders - .insert(exec_spawn_id, exec_spawn_orders); - } + self.index + .exec_spawn_orders + .entry(exec_spawn_id) + .or_default() + .insert(*client_order_id); } - // 9: Build _index_orders -> {ClientOrderId} + // 9: Build index.orders -> {ClientOrderId} self.index.orders.insert(*client_order_id); - // 10: Build _index_orders_open -> {ClientOrderId} + // 10: Build index.orders_open -> {ClientOrderId} if order.is_open() { self.index.orders_open.insert(*client_order_id); } - // 11: Build _index_orders_closed -> {ClientOrderId} + // 11: Build index.orders_closed -> {ClientOrderId} if order.is_closed() { self.index.orders_closed.insert(*client_order_id); } - // 12: Build _index_orders_emulated -> {ClientOrderId} + // 12: Build index.orders_emulated -> {ClientOrderId} if let Some(emulation_trigger) = order.emulation_trigger() { if emulation_trigger != TriggerType::NoTrigger && !order.is_closed() { self.index.orders_emulated.insert(*client_order_id); } } - // 13: Build _index_orders_inflight -> {ClientOrderId} + // 13: Build index.orders_inflight -> {ClientOrderId} if order.is_inflight() { self.index.orders_inflight.insert(*client_order_id); } - // 14: Build _index_strategies -> {StrategyId} + // 14: Build index.strategies -> {StrategyId} self.index.strategies.insert(strategy_id); - // 15: Build _index_strategies -> {ExecAlgorithmId} + // 15: Build index.strategies -> {ExecAlgorithmId} if let Some(exec_algorithm_id) = order.exec_algorithm_id() { self.index.exec_algorithms.insert(exec_algorithm_id); } } // Index positions - for (position_id, position) in &self.positions { + for (position_id, position) in self.positions.iter() { let instrument_id = position.instrument_id; let venue = instrument_id.venue; let strategy_id = position.strategy_id; - // 1: Build _index_venue_positions -> {Venue, {PositionId}} - if let Some(venue_positions) = self.index.venue_positions.get_mut(&venue) { - venue_positions.insert(*position_id); - } else { - let mut venue_positions = HashSet::new(); - venue_positions.insert(*position_id); - self.index.venue_positions.insert(venue, venue_positions); - } + // 1: Build index.venue_positions -> {Venue, {PositionId}} + self.index + .venue_positions + .entry(venue) + .or_default() + .insert(*position_id); - // 2: Build _index_position_strategy -> {PositionId, StrategyId} + // 2: Build index.position_strategy -> {PositionId, StrategyId} self.index .position_strategy .insert(*position_id, position.strategy_id); - // 3: Build _index_position_orders -> {PositionId, {ClientOrderId}} - if let Some(position_orders) = self.index.position_orders.get_mut(position_id) { - for client_order_id in position.client_order_ids() { - position_orders.insert(client_order_id); - } - } else { - let mut position_orders = HashSet::new(); - for client_order_id in position.client_order_ids() { - position_orders.insert(client_order_id); - } - self.index - .position_orders - .insert(*position_id, position_orders); - } + // 3: Build index.position_orders -> {PositionId, {ClientOrderId}} + self.index + .position_orders + .entry(*position_id) + .or_default() + .extend(position.client_order_ids().into_iter()); - // 4: Build _index_instrument_positions -> {InstrumentId, {PositionId}} - if let Some(instrument_positions) = - self.index.instrument_positions.get_mut(&instrument_id) - { - instrument_positions.insert(*position_id); - } else { - let mut instrument_positions = HashSet::new(); - instrument_positions.insert(*position_id); - self.index - .instrument_positions - .insert(instrument_id, instrument_positions); - } + // 4: Build index.instrument_positions -> {InstrumentId, {PositionId}} + self.index + .instrument_positions + .entry(instrument_id) + .or_default() + .insert(*position_id); - // 5: Build _index_strategy_positions -> {StrategyId, {PositionId}} - if let Some(strategy_positions) = self.index.strategy_positions.get_mut(&strategy_id) { - strategy_positions.insert(*position_id); - } else { - let mut strategy_positions = HashSet::new(); - strategy_positions.insert(*position_id); - self.index - .strategy_positions - .insert(strategy_id, strategy_positions); - } + // 5: Build index.strategy_positions -> {StrategyId, {PositionId}} + self.index + .strategy_positions + .entry(strategy_id) + .or_default() + .insert(*position_id); - // 6: Build _index_positions -> {PositionId} + // 6: Build index.positions -> {PositionId} self.index.positions.insert(*position_id); - // 7: Build _index_positions_open -> {PositionId} + // 7: Build index.positions_open -> {PositionId} if position.is_open() { self.index.positions_open.insert(*position_id); } - // 8: Build _index_positions_closed -> {PositionId} + // 8: Build index.positions_closed -> {PositionId} if position.is_closed() { self.index.positions_closed.insert(*position_id); } - // 9: Build _index_strategies -> {StrategyId} + // 9: Build index.strategies -> {StrategyId} self.index.strategies.insert(strategy_id); } } From a56c912036f519cf44c4449b5d879e5ce8a895c2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 09:31:03 +1000 Subject: [PATCH 064/193] Add Cache methods in Rust --- nautilus_core/common/src/cache/database.rs | 2 +- nautilus_core/common/src/cache/mod.rs | 679 +++++++++++++++++++-- 2 files changed, 620 insertions(+), 61 deletions(-) diff --git a/nautilus_core/common/src/cache/database.rs b/nautilus_core/common/src/cache/database.rs index 6eb5c019e66d..50025b35f19c 100644 --- a/nautilus_core/common/src/cache/database.rs +++ b/nautilus_core/common/src/cache/database.rs @@ -261,7 +261,7 @@ impl CacheDatabaseAdapter { todo!() // TODO } - pub fn update_account(&self) -> anyhow::Result<()> { + pub fn update_account(&self, account: &dyn Account) -> anyhow::Result<()> { todo!() // TODO } diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index f95a99852d5e..2acea0f8db98 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -21,9 +21,12 @@ pub mod database; -use std::collections::{HashMap, HashSet, VecDeque}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + time::{SystemTime, UNIX_EPOCH}, +}; -use log::{debug, info}; +use log::{debug, error, info, warn}; use nautilus_core::correctness::{check_key_not_in_map, check_slice_not_empty, check_valid_string}; use nautilus_model::{ data::{ @@ -31,7 +34,7 @@ use nautilus_model::{ quote::QuoteTick, trade::TradeTick, }, - enums::{AggregationSource, OrderSide, PositionSide, PriceType, TriggerType}, + enums::{AggregationSource, OmsType, OrderSide, PositionSide, PriceType, TriggerType}, identifiers::{ account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, @@ -252,6 +255,7 @@ impl Cache { // -- COMMANDS -------------------------------------------------------------------------------- + /// Clear the current general cache and load the general objects from the cache database. pub fn cache_general(&mut self) -> anyhow::Result<()> { self.general = match &self.database { Some(db) => db.load()?, @@ -265,6 +269,7 @@ impl Cache { Ok(()) } + /// Clear the current currencies cache and load currencies from the cache database. pub fn cache_currencies(&mut self) -> anyhow::Result<()> { self.currencies = match &self.database { Some(db) => db.load_currencies()?, @@ -275,6 +280,7 @@ impl Cache { Ok(()) } + /// Clear the current instruments cache and load instruments from the cache database. pub fn cache_instruments(&mut self) -> anyhow::Result<()> { self.instruments = match &self.database { Some(db) => db.load_instruments()?, @@ -285,6 +291,8 @@ impl Cache { Ok(()) } + /// Clear the current synthetic instruments cache and load synthetic instruments from the cache + /// database. pub fn cache_synthetics(&mut self) -> anyhow::Result<()> { self.synthetics = match &self.database { Some(db) => db.load_synthetics()?, @@ -298,6 +306,7 @@ impl Cache { Ok(()) } + /// Clear the current accounts cache and load accounts from the cache database. pub fn cache_accounts(&mut self) -> anyhow::Result<()> { self.accounts = match &self.database { Some(db) => db.load_accounts()?, @@ -311,6 +320,7 @@ impl Cache { Ok(()) } + /// Clear the current orders cache and load orders from the cache database. pub fn cache_orders(&mut self) -> anyhow::Result<()> { self.orders = match &self.database { Some(db) => db.load_orders()?, @@ -321,6 +331,7 @@ impl Cache { Ok(()) } + /// Clear the current positions cache and load positions from the cache database. pub fn cache_positions(&mut self) -> anyhow::Result<()> { self.positions = match &self.database { Some(db) => db.load_positions()?, @@ -331,6 +342,7 @@ impl Cache { Ok(()) } + /// Clear the current cache index and re-build. pub fn build_index(&mut self) { self.index.clear(); debug!("Building index"); @@ -343,7 +355,7 @@ impl Cache { } // Index orders - for (client_order_id, order) in self.orders.iter() { + for (client_order_id, order) in &self.orders { let instrument_id = order.instrument_id(); let venue = instrument_id.venue; let strategy_id = order.strategy_id(); @@ -441,7 +453,7 @@ impl Cache { } // Index positions - for (position_id, position) in self.positions.iter() { + for (position_id, position) in &self.positions { let instrument_id = position.instrument_id; let venue = instrument_id.venue; let strategy_id = position.strategy_id; @@ -497,15 +509,394 @@ impl Cache { } } + /// Check integrity of data within the cache. + /// + /// All data should be loaded from the database prior to this call. + /// If an error is found then a log error message will also be produced. #[must_use] - pub fn check_integrity(&self) -> bool { - true // TODO + fn check_integrity(&mut self) -> bool { + let mut error_count = 0; + let failure = "Integrity failure"; + + // Get current timestamp in microseconds + let timestamp_us = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros(); + + info!("Checking data integrity"); + + // Check object caches + for account_id in self.accounts.keys() { + if !self + .index + .venue_account + .contains_key(&account_id.get_issuer()) + { + error!( + "{} in accounts: {} not found in `self.index.venue_account`", + failure, account_id + ); + error_count += 1; + } + } + + for (client_order_id, order) in &self.orders { + if !self.index.order_strategy.contains_key(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.order_strategy`", + failure, client_order_id + ); + error_count += 1; + } + if !self.index.orders.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders`", + failure, client_order_id + ); + error_count += 1; + } + if order.is_inflight() && !self.index.orders_inflight.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders_inflight`", + failure, client_order_id + ); + error_count += 1; + } + if order.is_open() && !self.index.orders_open.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders_open`", + failure, client_order_id + ); + error_count += 1; + } + if order.is_closed() && !self.index.orders_closed.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders_closed`", + failure, client_order_id + ); + error_count += 1; + } + if let Some(exec_algorithm_id) = order.exec_algorithm_id() { + if !self + .index + .exec_algorithm_orders + .contains_key(&exec_algorithm_id) + { + error!( + "{} in orders: {} not found in `self.index.exec_algorithm_orders`", + failure, exec_algorithm_id + ); + error_count += 1; + } + if order.exec_spawn_id().is_none() + && !self.index.exec_spawn_orders.contains_key(client_order_id) + { + error!( + "{} in orders: {} not found in `self.index.exec_spawn_orders`", + failure, exec_algorithm_id + ); + error_count += 1; + } + } + } + + for (position_id, position) in &self.positions { + if !self.index.position_strategy.contains_key(position_id) { + error!( + "{} in positions: {} not found in `self.index.position_strategy`", + failure, position_id + ); + error_count += 1; + } + if !self.index.position_orders.contains_key(position_id) { + error!( + "{} in positions: {} not found in `self.index.position_orders`", + failure, position_id + ); + error_count += 1; + } + if !self.index.positions.contains(position_id) { + error!( + "{} in positions: {} not found in `self.index.positions`", + failure, position_id + ); + error_count += 1; + } + if position.is_open() && !self.index.positions_open.contains(position_id) { + error!( + "{} in positions: {} not found in `self.index.positions_open`", + failure, position_id + ); + error_count += 1; + } + if position.is_closed() && !self.index.positions_closed.contains(position_id) { + error!( + "{} in positions: {} not found in `self.index.positions_closed`", + failure, position_id + ); + error_count += 1; + } + } + + // Check indexes + for account_id in self.index.venue_account.values() { + if !self.accounts.contains_key(account_id) { + error!( + "{} in `index.venue_account`: {} not found in `self.accounts`", + failure, account_id + ); + error_count += 1; + } + } + + for client_order_id in self.index.order_ids.values() { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.order_ids`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in self.index.order_position.keys() { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.order_position`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + // Check indexes + for client_order_id in self.index.order_strategy.keys() { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.order_strategy`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for position_id in self.index.position_strategy.keys() { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.position_strategy`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for position_id in self.index.position_orders.keys() { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.position_orders`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for (instrument_id, client_order_ids) in &self.index.instrument_orders { + for client_order_id in client_order_ids { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.instrument_orders`: {} not found in `self.orders`", + failure, instrument_id + ); + error_count += 1; + } + } + } + + for instrument_id in self.index.instrument_positions.keys() { + if !self.index.instrument_orders.contains_key(instrument_id) { + error!( + "{} in `index.instrument_positions`: {} not found in `index.instrument_orders`", + failure, instrument_id + ); + error_count += 1; + } + } + + for client_order_ids in self.index.strategy_orders.values() { + for client_order_id in client_order_ids { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.strategy_orders`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + } + + for position_ids in self.index.strategy_positions.values() { + for position_id in position_ids { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.strategy_positions`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + } + + for client_order_id in &self.index.orders { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_emulated { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_emulated`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_inflight { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_inflight`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_open { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_open`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_closed { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_closed`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for position_id in &self.index.positions { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.positions`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for position_id in &self.index.positions_open { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.positions_open`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for position_id in &self.index.positions_closed { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.positions_closed`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for strategy_id in &self.index.strategies { + if !self.index.strategy_orders.contains_key(strategy_id) { + error!( + "{} in `index.strategies`: {} not found in `index.strategy_orders`", + failure, strategy_id + ); + error_count += 1; + } + } + + for exec_algorithm_id in &self.index.exec_algorithms { + if !self + .index + .exec_algorithm_orders + .contains_key(exec_algorithm_id) + { + error!( + "{} in `index.exec_algorithms`: {} not found in `index.exec_algorithm_orders`", + failure, exec_algorithm_id + ); + error_count += 1; + } + } + + // Finally + let total_us = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros() + - timestamp_us; + + if error_count == 0 { + info!("Integrity check passed in {}μs", total_us); + true + } else { + error!( + "Integrity check failed with {} error{} in {}μs", + error_count, + if error_count == 1 { "" } else { "s" }, + total_us + ); + false + } } - pub fn check_residuals(&self) { - todo!() // Needs order query methods + /// Check for any residual open state and log warnings if any are found. + /// + ///'Open state' is considered to be open orders and open positions. + #[must_use] + pub fn check_residuals(&self) -> bool { + debug!("Checking residuals"); + + let mut residuals = false; + + // Check for any open orders + for order in self.orders_open(None, None, None, None) { + residuals = true; + warn!("Residual {:?}", order); + } + + // Check for any open positions + for position in self.positions_open(None, None, None, None) { + residuals = true; + warn!("Residual {}", position); + } + + residuals } + /// Clear the caches index. pub fn clear_index(&mut self) { self.index.clear(); debug!("Cleared index"); @@ -535,6 +926,7 @@ impl Cache { info!("Reset cache"); } + /// Dispose of the cache which will close any underlying database adapter. pub fn dispose(&self) -> anyhow::Result<()> { if let Some(database) = &self.database { // TODO: Log operations in database adapter @@ -543,6 +935,7 @@ impl Cache { Ok(()) } + /// Flush the caches database which permanently removes all persisted data. pub fn flush_db(&self) -> anyhow::Result<()> { if let Some(database) = &self.database { // TODO: Log operations in database adapter @@ -551,6 +944,10 @@ impl Cache { Ok(()) } + /// Add the given general object to the cache. + /// + /// The cache is agnostic to what the object actually is (and how it may be serialized), + /// offering maximum flexibility. pub fn add(&mut self, key: &str, value: Vec) -> anyhow::Result<()> { check_valid_string(key, stringify!(key))?; check_slice_not_empty(value.as_slice(), stringify!(value))?; @@ -564,12 +961,14 @@ impl Cache { Ok(()) } + /// Add the given order `book` to the cache. pub fn add_order_book(&mut self, book: OrderBook) -> anyhow::Result<()> { debug!("Add `OrderBook` {}", book.instrument_id); self.books.insert(book.instrument_id, book); Ok(()) } + /// Add the given `quote` tick to the cache. pub fn add_quote(&mut self, quote: QuoteTick) -> anyhow::Result<()> { debug!("Add `QuoteTick` {}", quote.instrument_id); let quotes_deque = self @@ -580,6 +979,7 @@ impl Cache { Ok(()) } + /// Add the given `quotes` to the cache. pub fn add_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> { check_slice_not_empty(quotes, stringify!(quotes))?; @@ -596,6 +996,7 @@ impl Cache { Ok(()) } + /// Add the given `trade` tick to the cache. pub fn add_trade(&mut self, trade: TradeTick) -> anyhow::Result<()> { debug!("Add `TradeTick` {}", trade.instrument_id); let trades_deque = self @@ -606,6 +1007,7 @@ impl Cache { Ok(()) } + /// Add the give `trades` to the cache. pub fn add_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> { check_slice_not_empty(trades, stringify!(trades))?; @@ -622,6 +1024,7 @@ impl Cache { Ok(()) } + /// Add the given `bar` to the cache. pub fn add_bar(&mut self, bar: Bar) -> anyhow::Result<()> { debug!("Add `Bar` {}", bar.bar_type); let bars = self @@ -632,6 +1035,7 @@ impl Cache { Ok(()) } + /// Add the given `bars` to the cache. pub fn add_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> { check_slice_not_empty(bars, stringify!(bars))?; @@ -648,6 +1052,7 @@ impl Cache { Ok(()) } + /// Add the given `currency` to the cache. pub fn add_currency(&mut self, currency: Currency) -> anyhow::Result<()> { debug!("Add `Currency` {}", currency.code); @@ -659,6 +1064,7 @@ impl Cache { Ok(()) } + /// Add the given `instrument` to the cache. pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> { debug!("Add `Instrument` {}", instrument.id()); @@ -670,6 +1076,7 @@ impl Cache { Ok(()) } + /// Add the given `synthetic` instrument to the cache. pub fn add_synthetic(&mut self, synthetic: SyntheticInstrument) -> anyhow::Result<()> { debug!("Add `SyntheticInstrument` {}", synthetic.id); @@ -681,6 +1088,7 @@ impl Cache { Ok(()) } + /// Add the given `account` to the cache. pub fn add_account(&mut self, account: Box) -> anyhow::Result<()> { debug!("Add `Account` {}", account.id()); @@ -715,7 +1123,7 @@ impl Cache { let client_order_id = order.client_order_id(); let strategy_id = order.strategy_id(); let exec_algorithm_id = order.exec_algorithm_id(); - let _exec_spawn_id = order.exec_spawn_id(); + let exec_spawn_id = order.exec_spawn_id(); if !replace_existing { check_key_not_in_map( @@ -753,56 +1161,42 @@ impl Cache { self.index.strategies.insert(strategy_id); // Update venue -> orders index - if let Some(venue_orders) = self.index.venue_orders.get_mut(&venue) { - venue_orders.insert(client_order_id); - } else { - let mut new_set = HashSet::new(); - new_set.insert(client_order_id); - self.index.venue_orders.insert(venue, new_set); - } + self.index + .venue_orders + .entry(venue) + .or_default() + .insert(client_order_id); // Update instrument -> orders index - if let Some(instrument_orders) = self.index.instrument_orders.get_mut(&instrument_id) { - instrument_orders.insert(client_order_id); - } else { - let mut new_set = HashSet::new(); - new_set.insert(client_order_id); - self.index.instrument_orders.insert(instrument_id, new_set); - } + self.index + .instrument_orders + .entry(instrument_id) + .or_default() + .insert(client_order_id); // Update strategy -> orders index - if let Some(strategy_orders) = self.index.strategy_orders.get_mut(&strategy_id) { - strategy_orders.insert(client_order_id); - } else { - let mut new_set = HashSet::new(); - new_set.insert(client_order_id); - self.index.strategy_orders.insert(strategy_id, new_set); - } + self.index + .strategy_orders + .entry(strategy_id) + .or_default() + .insert(client_order_id); // Update exec_algorithm -> orders index if let Some(exec_algorithm_id) = exec_algorithm_id { self.index.exec_algorithms.insert(exec_algorithm_id); - if let Some(exec_algorithm_orders) = - self.index.exec_algorithm_orders.get_mut(&exec_algorithm_id) - { - exec_algorithm_orders.insert(client_order_id); - } else { - let mut new_set = HashSet::new(); - new_set.insert(client_order_id); - self.index - .exec_algorithm_orders - .insert(exec_algorithm_id, new_set); - } + self.index + .exec_algorithm_orders + .entry(exec_algorithm_id) + .or_default() + .insert(client_order_id); - // TODO: Implement - // if let Some(exec_spawn_orders) = self.index.exec_spawn_orders.get_mut(&exec_spawn_id) { - // exec_spawn_orders.insert(client_order_id.clone()); - // } else { - // let mut new_set = HashSet::new(); - // new_set.insert(client_order_id.clone()); - // self.index.exec_spawn_orders.insert(exec_spawn_id, new_set); - // } + // SAFETY: We can guarantee the `exec_spawn_id` is Some + self.index + .exec_spawn_orders + .entry(exec_spawn_id.unwrap()) + .or_default() + .insert(client_order_id); } // TODO: Change emulation trigger setup @@ -816,16 +1210,15 @@ impl Cache { // } // } - // TODO: Implement // Index position ID if provided - // if let Some(position_id) = position_id { - // self.add_position_id( - // position_id, - // order.instrument_id().venue, - // client_order_id.clone(), - // strategy_id, - // ); - // } + if let Some(position_id) = order.position_id() { + self.add_position_id( + &position_id, + &order.instrument_id().venue, + &client_order_id, + &strategy_id, + )?; + } // Index client ID if provided if let Some(client_id) = client_id { @@ -833,7 +1226,6 @@ impl Cache { log::debug!("Indexed {:?}", client_id); } - // Update database if available if let Some(database) = &mut self.database { database.add_order(&order)?; // TODO: Implement @@ -847,6 +1239,173 @@ impl Cache { Ok(()) } + /// Index the given `position_id` with the other given IDs. + pub fn add_position_id( + &mut self, + position_id: &PositionId, + venue: &Venue, + client_order_id: &ClientOrderId, + strategy_id: &StrategyId, + ) -> anyhow::Result<()> { + self.index + .order_position + .insert(*client_order_id, *position_id); + + // Index: ClientOrderId -> PositionId + if let Some(database) = &mut self.database { + database.index_order_position(*client_order_id, *position_id)?; + } + + // Index: PositionId -> StrategyId + self.index + .position_strategy + .insert(*position_id, *strategy_id); + + // Index: PositionId -> set[ClientOrderId] + self.index + .position_orders + .entry(*position_id) + .or_default() + .insert(*client_order_id); + + // Index: StrategyId -> set[PositionId] + self.index + .strategy_positions + .entry(*strategy_id) + .or_default() + .insert(*position_id); + + Ok(()) + } + + pub fn add_position(&mut self, position: Position, oms_type: OmsType) -> anyhow::Result<()> { + self.positions.insert(position.id, position.clone()); + self.index.positions.insert(position.id); + self.index.positions_open.insert(position.id); + + self.add_position_id( + &position.id, + &position.instrument_id.venue, + &position.opening_order_id, + &position.strategy_id, + )?; + + let venue = position.instrument_id.venue; + let venue_positions = self.index.venue_positions.entry(venue).or_default(); + venue_positions.insert(position.id); + + // Index: InstrumentId -> HashSet + let instrument_id = position.instrument_id; + let instrument_positions = self + .index + .instrument_positions + .entry(instrument_id) + .or_default(); + instrument_positions.insert(position.id); + + log::debug!( + "Added Position(id={}, strategy_id={})", + position.id, + position.strategy_id, + ); + + if let Some(database) = &mut self.database { + database.add_position(&position)?; + // TODO: Implement position snapshots + // if self.snapshot_positions { + // database.snapshot_position_state( + // position, + // position.ts_last, + // self.calculate_unrealized_pnl(&position), + // )?; + // } + } + + Ok(()) + } + + /// Update the given `account` in the cache. + pub fn update_account(&mut self, account: &dyn Account) -> anyhow::Result<()> { + if let Some(database) = &mut self.database { + database.update_account(account)?; + } + Ok(()) + } + + /// Update the given `order` in the cache. + pub fn update_order(&mut self, order: &OrderAny) -> anyhow::Result<()> { + let client_order_id = order.client_order_id(); + + // Update venue order ID + if let Some(venue_order_id) = order.venue_order_id() { + // Assumes order_id does not change + self.index.order_ids.insert(venue_order_id, client_order_id); + } + + // Update in-flight state + if order.is_inflight() { + self.index.orders_inflight.insert(client_order_id); + } else { + self.index.orders_inflight.remove(&client_order_id); + } + + // Update open/closed state + if order.is_open() { + self.index.orders_closed.remove(&client_order_id); + self.index.orders_open.insert(client_order_id); + } else if order.is_closed() { + self.index.orders_open.remove(&client_order_id); + self.index.orders_pending_cancel.remove(&client_order_id); + self.index.orders_closed.insert(client_order_id); + } + + // Update emulation + if let Some(emulation_trigger) = order.emulation_trigger() { + match emulation_trigger { + TriggerType::NoTrigger => self.index.orders_emulated.remove(&client_order_id), + _ => self.index.orders_emulated.insert(client_order_id), + }; + } + + if let Some(database) = &mut self.database { + database.update_order(order)?; + // TODO: Implement order snapshots + // if self.snapshot_orders { + // database.snapshot_order_state(order)?; + // } + } + + Ok(()) + } + + /// Update the given `order` as pending cancel locally. + pub fn update_order_pending_cancel_local(&mut self, order: &OrderAny) { + self.index + .orders_pending_cancel + .insert(order.client_order_id()); + } + + /// Update the given `position` in the cache. + pub fn update_position(&mut self, position: &Position) -> anyhow::Result<()> { + // Update open/closed state + if position.is_open() { + self.index.positions_open.insert(position.id); + self.index.positions_closed.remove(&position.id); + } else { + self.index.positions_closed.insert(position.id); + self.index.positions_open.remove(&position.id); + } + + if let Some(database) = &mut self.database { + database.update_position(position)?; + // TODO: Implement order snapshots + // if self.snapshot_orders { + // database.snapshot_order_state(order)?; + // } + } + Ok(()) + } + // -- IDENTIFIER QUERIES ---------------------------------------------------------------------- fn build_order_query_filter_set( From 3df7693d333db531d820a3779eb79ad7904e95e5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 10:33:26 +1000 Subject: [PATCH 065/193] Improve Quantity and Price string parsing --- nautilus_core/model/src/types/price.rs | 3 ++- nautilus_core/model/src/types/quantity.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index f6173658af76..e83c8b48b059 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -117,8 +117,9 @@ impl FromStr for Price { fn from_str(input: &str) -> Result { let float_from_input = input + .replace('_', "") .parse::() - .map_err(|err| format!("Cannot parse `input` string '{input}' as f64: {err}"))?; + .map_err(|err| format!("Error parsing `input` string '{input}' as f64: {err}"))?; Self::new(float_from_input, precision_from_str(input)) .map_err(|e: anyhow::Error| e.to_string()) diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index 0f45df8d5031..7400e1a30599 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -110,8 +110,9 @@ impl FromStr for Quantity { fn from_str(input: &str) -> Result { let float_from_input = input + .replace('_', "") .parse::() - .map_err(|e| format!("Cannot parse `input` string '{input}' as f64: {e}"))?; + .map_err(|e| format!("Error parsing `input` string '{input}' as f64: {e}"))?; Self::new(float_from_input, precision_from_str(input)) .map_err(|e: anyhow::Error| e.to_string()) From 4f8d463870979595aad67957994ca57ed8ab643b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 10:34:14 +1000 Subject: [PATCH 066/193] Add another quote tick stub --- nautilus_core/model/src/data/quote.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 909d42bc6a36..9d63160ed184 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -175,9 +175,23 @@ pub mod stubs { use crate::{ data::quote::QuoteTick, identifiers::instrument_id::InstrumentId, + instruments::{currency_pair::CurrencyPair, stubs::*}, types::{price::Price, quantity::Quantity}, }; + #[fixture] + pub fn quote_tick_audusd_sim(audusd_sim: CurrencyPair) -> QuoteTick { + QuoteTick { + instrument_id: audusd_sim.id, + bid_price: Price::from("1.00000"), + ask_price: Price::from("1.00000"), + bid_size: Quantity::from(100_000), + ask_size: Quantity::from(100_000), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::from(1), + } + } + #[fixture] pub fn quote_tick_ethusdt_binance() -> QuoteTick { QuoteTick { @@ -186,7 +200,7 @@ pub mod stubs { ask_price: Price::from("10001.0000"), bid_size: Quantity::from("1.00000000"), ask_size: Quantity::from("1.00000000"), - ts_event: UnixNanos::from(0), + ts_event: UnixNanos::default(), ts_init: UnixNanos::from(1), } } From 73bbc44aa36d089f968ff4b86f7e213aa7b349a9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 10:38:08 +1000 Subject: [PATCH 067/193] Consistently use UnixNanos default --- nautilus_core/core/src/nanos.rs | 4 ++-- nautilus_core/model/src/data/bar.rs | 10 +++++----- nautilus_core/model/src/data/trade.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nautilus_core/core/src/nanos.rs b/nautilus_core/core/src/nanos.rs index f77f412ea5b8..fb32ba6ef406 100644 --- a/nautilus_core/core/src/nanos.rs +++ b/nautilus_core/core/src/nanos.rs @@ -296,13 +296,13 @@ mod tests { #[rstest] #[should_panic(expected = "Error subtracting with underflow")] fn test_overflow_sub() { - let _ = UnixNanos::from(0) - UnixNanos::from(1); // This should panic due to underflow + let _ = UnixNanos::default() - UnixNanos::from(1); // This should panic due to underflow } #[rstest] #[should_panic(expected = "Error subtracting with underflow")] fn test_overflow_sub_u64() { - let _ = UnixNanos::from(0) - 1_u64; // This should panic due to underflow + let _ = UnixNanos::default() - 1_u64; // This should panic due to underflow } #[rstest] diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 42fea573f0c4..8d3d50a0774b 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -333,7 +333,7 @@ pub mod stubs { low: Price::from("1.00002"), close: Price::from("1.00003"), volume: Quantity::from("100000"), - ts_event: UnixNanos::from(0), + ts_event: UnixNanos::default(), ts_init: UnixNanos::from(1), } } @@ -544,8 +544,8 @@ mod tests { low: Price::from("1.00002"), close: Price::from("1.00003"), volume: Quantity::from("100000"), - ts_event: UnixNanos::from(0), - ts_init: UnixNanos::from(0), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::from(1), }; let bar2 = Bar { @@ -555,8 +555,8 @@ mod tests { low: Price::from("1.00002"), close: Price::from("1.00003"), volume: Quantity::from("100000"), - ts_event: UnixNanos::from(0), - ts_init: UnixNanos::from(0), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::from(1), }; assert_eq!(bar1, bar1); assert_ne!(bar1, bar2); diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index 98db02bc08c7..ae1b4a5a0012 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -147,7 +147,7 @@ pub mod stubs { size: Quantity::from("1.00000000"), aggressor_side: AggressorSide::Buyer, trade_id: TradeId::new("123456789").unwrap(), - ts_event: UnixNanos::from(0), + ts_event: UnixNanos::default(), ts_init: UnixNanos::from(1), } } From d303679e6c544273c1b3d6e322e57ef7d04e9302 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 10:38:40 +1000 Subject: [PATCH 068/193] Continue Cache tests in Rust --- nautilus_core/common/src/cache/mod.rs | 66 +++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 2acea0f8db98..249b1c6a7e8a 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -2392,31 +2392,47 @@ impl Cache { //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { + use nautilus_model::data::quote::stubs::*; + use nautilus_model::data::quote::QuoteTick; + use nautilus_model::instruments::{currency_pair::CurrencyPair, stubs::*}; use rstest::*; use super::Cache; #[rstest] - fn test_reset_index() { + fn test_build_index_when_empty() { + let mut cache = Cache::default(); + cache.build_index(); + } + + #[rstest] + fn test_clear_index_when_empty() { let mut cache = Cache::default(); cache.clear_index(); } #[rstest] - fn test_reset() { + fn test_reset_when_empty() { let mut cache = Cache::default(); cache.reset(); } #[rstest] - fn test_dispose() { + fn test_dispose_when_empty() { let cache = Cache::default(); let result = cache.dispose(); assert!(result.is_ok()); } #[rstest] - fn test_flushdb() { + fn test_flush_db_when_empty() { + let cache = Cache::default(); + let result = cache.flush_db(); + assert!(result.is_ok()); + } + + #[rstest] + fn test_check_residuals_when_empty() { let cache = Cache::default(); let result = cache.flush_db(); assert!(result.is_ok()); @@ -2459,8 +2475,9 @@ mod tests { } #[rstest] - fn test_get_general_when_no_value() { + fn test_get_general_when_empty() { let cache = Cache::default(); + let result = cache.get("A").unwrap(); assert_eq!(result, None); } @@ -2476,4 +2493,43 @@ mod tests { let result = cache.get(key).unwrap(); assert_eq!(result, Some(&value.as_slice()).copied()); } + + #[rstest] + fn test_quote_tick_when_empty(audusd_sim: CurrencyPair) { + let cache = Cache::default(); + + let result = cache.quote_tick(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_quote_tick_when_some(quote_tick_audusd_sim: QuoteTick) { + let mut cache = Cache::default(); + cache.add_quote(quote_tick_audusd_sim).unwrap(); + + let result = cache.quote_tick("e_tick_audusd_sim.instrument_id); + assert_eq!(result, Some("e_tick_audusd_sim)); + } + + #[rstest] + fn test_quote_ticks_when_empty(audusd_sim: CurrencyPair) { + let cache = Cache::default(); + + let result = cache.quote_ticks(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_quote_ticks_when_some(quote_tick_audusd_sim: QuoteTick) { + let mut cache = Cache::default(); + let quotes = vec![ + quote_tick_audusd_sim, + quote_tick_audusd_sim, + quote_tick_audusd_sim, + ]; + cache.add_quotes("es).unwrap(); + + let result = cache.quote_ticks("e_tick_audusd_sim.instrument_id); + assert_eq!(result, Some(quotes)); + } } From f8976caaba6e2a24bb06c0f7ba3a46895ac0008e Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:54:44 +0300 Subject: [PATCH 069/193] Fix IBContractDetails parsing (#1615) --- .../adapters/interactive_brokers/parsing/instruments.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index 5f7e3e57d86d..b79f1a47d319 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -164,6 +164,10 @@ def parse_instrument( def contract_details_to_dict(details: IBContractDetails) -> dict: dict_details = details.dict().copy() dict_details["contract"] = details.contract.dict().copy() + if dict_details.get("secIdList"): + dict_details["secIdList"] = { + tag_value.tag: tag_value.value for tag_value in dict_details["secIdList"] + } return dict_details From 6d807d3c059a897f343e340457717db6bffc20b7 Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:58:48 +0300 Subject: [PATCH 070/193] Fix Portfolio registration for IB (#1616) --- nautilus_trader/live/node_builder.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/nautilus_trader/live/node_builder.py b/nautilus_trader/live/node_builder.py index 057172e534be..0a8b06a22b40 100644 --- a/nautilus_trader/live/node_builder.py +++ b/nautilus_trader/live/node_builder.py @@ -197,10 +197,6 @@ def build_data_clients( venue = Venue(venue) self._data_engine.register_venue_routing(client, venue) - # Temporary handling for setting specific 'venue' for portfolio - if name == "InteractiveBrokers": - self._portfolio.set_specific_venue(Venue("InteractiveBrokers")) - def build_exec_clients( # noqa: C901 (too complex) self, config: dict[str, LiveExecClientConfig], @@ -264,5 +260,5 @@ def build_exec_clients( # noqa: C901 (too complex) self._exec_engine.register_venue_routing(client, venue) # Temporary handling for setting specific 'venue' for portfolio - if name == "InteractiveBrokers": - self._portfolio.set_specific_venue(Venue("InteractiveBrokers")) + if factory.__name__ == "InteractiveBrokersLiveExecClientFactory": + self._portfolio.set_specific_venue(Venue("INTERACTIVE_BROKERS")) From 729b4ecf981ac7b6ae8b7d165592f8636ee4b368 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 21:55:06 +1000 Subject: [PATCH 071/193] Fix docker/login-action version specifier --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0b860df992cb..cc320208f6c9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,7 +35,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to GHCR - uses: docker/login-action@v3.1 + uses: docker/login-action@v3.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From f78b7c9c90c46099142dfb026be23aaba0172982 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 22:02:57 +1000 Subject: [PATCH 072/193] Update release notes --- RELEASES.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 8f3e076c85fe..c68b350f43f1 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,15 +3,18 @@ Released on TBD (UTC). ### Enhancements -- Added Nautilus CLI (see [docs](https://docs.nautilustrader.io/nightly/developer_guide/index.html)), many thanks @filipmacek -- Added `Cfd` and `Commodity` instruments with Interactive Brokers support, thanks @DracheShiki -- Added futures and options contract activation and expiration simulation +- Added Nautilus CLI (see [docs](https://docs.nautilustrader.io/nightly/developer_guide/index.html)) (#1602), many thanks @filipmacek +- Added `Cfd` and `Commodity` instruments with Interactive Brokers support (#1604), thanks @DracheShiki +- Added `OrderMatchingEngine` futures and options contract activation and expiration simulation ### Breaking Changes - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) ### Fixes - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) +- Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z +- Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z +- Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek --- From de66c168ed51726f8a8a1889db358e47b9645072 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 22:11:02 +1000 Subject: [PATCH 073/193] Skip database infrastructure tests unless Linux --- .../test_cache_database_postgres.py | 63 +++++----- .../test_cache_database_redis.py | 110 +++++++++--------- 2 files changed, 91 insertions(+), 82 deletions(-) diff --git a/tests/integration_tests/infrastructure/test_cache_database_postgres.py b/tests/integration_tests/infrastructure/test_cache_database_postgres.py index e80d504b22b9..40ee328388dc 100644 --- a/tests/integration_tests/infrastructure/test_cache_database_postgres.py +++ b/tests/integration_tests/infrastructure/test_cache_database_postgres.py @@ -15,6 +15,7 @@ import asyncio import os +import sys import pytest @@ -34,7 +35,15 @@ from nautilus_trader.trading.strategy import Strategy -AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") +_AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") + +# Requirements: +# - A Postgres service listening on the default port 5432 + +pytestmark = pytest.mark.skipif( + sys.platform != "linux", + reason="databases only supported on Linux", +) class TestCachePostgresAdapter: @@ -192,8 +201,8 @@ async def test_add_instrument_crypto_perpetual(self): @pytest.mark.asyncio async def test_add_instrument_currency_pair(self): - self.database.add_currency(AUDUSD_SIM.base_currency) - self.database.add_currency(AUDUSD_SIM.quote_currency) + self.database.add_currency(_AUDUSD_SIM.base_currency) + self.database.add_currency(_AUDUSD_SIM.quote_currency) await asyncio.sleep(0.6) # Check that we have added target currencies, because of foreign key constraints @@ -201,36 +210,36 @@ async def test_add_instrument_currency_pair(self): currencies = self.database.load_currencies() assert list(currencies.keys()) == ["AUD", "USD"] - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) # Allow MPSC thread to insert - await eventually(lambda: self.database.load_instrument(AUDUSD_SIM.id)) + await eventually(lambda: self.database.load_instrument(_AUDUSD_SIM.id)) # Assert - assert AUDUSD_SIM == self.database.load_instrument(AUDUSD_SIM.id) + assert _AUDUSD_SIM == self.database.load_instrument(_AUDUSD_SIM.id) # Update some fields, to check that add_instrument is idempotent aud_usd_currency_pair_updated = CurrencyPair( - instrument_id=AUDUSD_SIM.id, - raw_symbol=AUDUSD_SIM.raw_symbol, - base_currency=AUDUSD_SIM.base_currency, - quote_currency=AUDUSD_SIM.quote_currency, - price_precision=AUDUSD_SIM.price_precision, - size_precision=AUDUSD_SIM.size_precision, - price_increment=AUDUSD_SIM.price_increment, - size_increment=AUDUSD_SIM.size_increment, - lot_size=AUDUSD_SIM.lot_size, - max_quantity=AUDUSD_SIM.max_quantity, - min_quantity=AUDUSD_SIM.min_quantity, - max_price=AUDUSD_SIM.max_price, + instrument_id=_AUDUSD_SIM.id, + raw_symbol=_AUDUSD_SIM.raw_symbol, + base_currency=_AUDUSD_SIM.base_currency, + quote_currency=_AUDUSD_SIM.quote_currency, + price_precision=_AUDUSD_SIM.price_precision, + size_precision=_AUDUSD_SIM.size_precision, + price_increment=_AUDUSD_SIM.price_increment, + size_increment=_AUDUSD_SIM.size_increment, + lot_size=_AUDUSD_SIM.lot_size, + max_quantity=_AUDUSD_SIM.max_quantity, + min_quantity=_AUDUSD_SIM.min_quantity, + max_price=_AUDUSD_SIM.max_price, min_price=Price.from_str("111"), # <-- changed this - max_notional=AUDUSD_SIM.max_notional, - min_notional=AUDUSD_SIM.min_notional, - margin_init=AUDUSD_SIM.margin_init, - margin_maint=AUDUSD_SIM.margin_maint, - maker_fee=AUDUSD_SIM.maker_fee, - taker_fee=AUDUSD_SIM.taker_fee, - tick_scheme_name=AUDUSD_SIM.tick_scheme_name, + max_notional=_AUDUSD_SIM.max_notional, + min_notional=_AUDUSD_SIM.min_notional, + margin_init=_AUDUSD_SIM.margin_init, + margin_maint=_AUDUSD_SIM.margin_maint, + maker_fee=_AUDUSD_SIM.maker_fee, + taker_fee=_AUDUSD_SIM.taker_fee, + tick_scheme_name=_AUDUSD_SIM.tick_scheme_name, ts_event=123, # <-- changed this ts_init=456, # <-- changed this ) @@ -241,8 +250,8 @@ async def test_add_instrument_currency_pair(self): await asyncio.sleep(0.5) # Assert - result = self.database.load_instrument(AUDUSD_SIM.id) - assert result.id == AUDUSD_SIM.id + result = self.database.load_instrument(_AUDUSD_SIM.id) + assert result.id == _AUDUSD_SIM.id assert result.ts_event == 123 assert result.ts_init == 456 assert result.min_price == Price.from_str("111") diff --git a/tests/integration_tests/infrastructure/test_cache_database_redis.py b/tests/integration_tests/infrastructure/test_cache_database_redis.py index d5e6aa7362c2..0742ee958c5d 100644 --- a/tests/integration_tests/infrastructure/test_cache_database_redis.py +++ b/tests/integration_tests/infrastructure/test_cache_database_redis.py @@ -68,14 +68,14 @@ from nautilus_trader.trading.strategy import Strategy -AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") +_AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") # Requirements: -# - A Redis instance listening on the default port 6379 +# - A Redis service listening on the default port 6379 pytestmark = pytest.mark.skipif( - sys.platform == "win32", - reason="not longer testing with Memurai database", + sys.platform != "linux", + reason="databases only supported on Linux", ) @@ -199,19 +199,19 @@ async def test_add_account(self): @pytest.mark.asyncio async def test_add_instrument(self): # Arrange, Act - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) # Allow MPSC thread to insert - await eventually(lambda: self.database.load_instrument(AUDUSD_SIM.id)) + await eventually(lambda: self.database.load_instrument(_AUDUSD_SIM.id)) # Assert - assert self.database.load_instrument(AUDUSD_SIM.id) == AUDUSD_SIM + assert self.database.load_instrument(_AUDUSD_SIM.id) == _AUDUSD_SIM @pytest.mark.asyncio async def test_add_order(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -229,12 +229,12 @@ async def test_add_order(self): async def test_add_position(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) self.database.add_order(order) # Allow MPSC thread to insert @@ -243,12 +243,12 @@ async def test_add_position(self): position_id = PositionId("P-1") fill = TestEventStubs.order_filled( order, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, position_id=position_id, last_px=Price.from_str("1.00000"), ) - position = Position(instrument=AUDUSD_SIM, fill=fill) + position = Position(instrument=_AUDUSD_SIM, fill=fill) # Act self.database.add_position(position) @@ -278,7 +278,7 @@ async def test_update_account(self): async def test_update_order_when_not_already_exists_logs(self): # Arrange order = self.strategy.order_factory.stop_market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), Price.from_str("1.00000"), @@ -294,7 +294,7 @@ async def test_update_order_when_not_already_exists_logs(self): async def test_update_order_for_open_order(self): # Arrange order = self.strategy.order_factory.stop_market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), Price.from_str("1.00000"), @@ -320,7 +320,7 @@ async def test_update_order_for_open_order(self): async def test_update_order_for_closed_order(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -338,7 +338,7 @@ async def test_update_order_for_closed_order(self): fill = TestEventStubs.order_filled( order, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, last_px=Price.from_str("1.00001"), ) @@ -356,13 +356,13 @@ async def test_update_order_for_closed_order(self): @pytest.mark.asyncio async def test_update_position_for_closed_position(self): # Arrange - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) # Allow MPSC thread to insert - await eventually(lambda: self.database.load_instrument(AUDUSD_SIM.id)) + await eventually(lambda: self.database.load_instrument(_AUDUSD_SIM.id)) order1 = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -382,7 +382,7 @@ async def test_update_position_for_closed_position(self): order1.apply( TestEventStubs.order_filled( order1, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, position_id=position_id, last_px=Price.from_str("1.00001"), trade_id=TradeId("1"), @@ -394,14 +394,14 @@ async def test_update_position_for_closed_position(self): await eventually(lambda: self.database.load_order(order1.client_order_id)) # Act - position = Position(instrument=AUDUSD_SIM, fill=order1.last_event) + position = Position(instrument=_AUDUSD_SIM, fill=order1.last_event) self.database.add_position(position) # Allow MPSC thread to insert await eventually(lambda: self.database.load_position(position.id)) order2 = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.SELL, Quantity.from_int(100_000), ) @@ -419,7 +419,7 @@ async def test_update_position_for_closed_position(self): filled = TestEventStubs.order_filled( order2, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, position_id=position_id, last_px=Price.from_str("1.00001"), trade_id=TradeId("2"), @@ -443,7 +443,7 @@ async def test_update_position_for_closed_position(self): async def test_update_position_when_not_already_exists_logs(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -456,12 +456,12 @@ async def test_update_position_when_not_already_exists_logs(self): position_id = PositionId("P-1") fill = TestEventStubs.order_filled( order, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, position_id=position_id, last_px=Price.from_str("1.00000"), ) - position = Position(instrument=AUDUSD_SIM, fill=fill) + position = Position(instrument=_AUDUSD_SIM, fill=fill) # Act self.database.update_position(position) @@ -555,7 +555,7 @@ async def test_load_currencies_when_currencies_in_database_returns_expected(self @pytest.mark.asyncio async def test_load_instrument_when_no_instrument_in_database_returns_none(self): # Arrange, Act - result = self.database.load_instrument(AUDUSD_SIM.id) + result = self.database.load_instrument(_AUDUSD_SIM.id) # Assert assert result is None @@ -563,21 +563,21 @@ async def test_load_instrument_when_no_instrument_in_database_returns_none(self) @pytest.mark.asyncio async def test_load_instrument_when_instrument_in_database_returns_expected(self): # Arrange - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) # Allow MPSC thread to insert - await eventually(lambda: self.database.load_instrument(AUDUSD_SIM.id)) + await eventually(lambda: self.database.load_instrument(_AUDUSD_SIM.id)) # Act - result = self.database.load_instrument(AUDUSD_SIM.id) + result = self.database.load_instrument(_AUDUSD_SIM.id) # Assert - assert result == AUDUSD_SIM + assert result == _AUDUSD_SIM @pytest.mark.asyncio async def test_load_instruments_when_instrument_in_database_returns_expected(self): # Arrange - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) # Allow MPSC thread to insert await eventually(lambda: self.database.load_instruments()) @@ -586,7 +586,7 @@ async def test_load_instruments_when_instrument_in_database_returns_expected(sel result = self.database.load_instruments() # Assert - assert result == {AUDUSD_SIM.id: AUDUSD_SIM} + assert result == {_AUDUSD_SIM.id: _AUDUSD_SIM} @pytest.mark.asyncio async def test_load_synthetic_when_no_synethic_instrument_in_database_returns_none(self): @@ -644,7 +644,7 @@ async def test_load_account_when_account_in_database_returns_account(self): async def test_load_order_when_no_order_in_database_returns_none(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -659,7 +659,7 @@ async def test_load_order_when_no_order_in_database_returns_none(self): async def test_load_order_when_market_order_in_database_returns_order(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -680,7 +680,7 @@ async def test_load_order_with_exec_algorithm_params(self): # Arrange exec_algorithm_params = {"horizon_secs": 20, "interval_secs": 2.5} order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), exec_algorithm_id=ExecAlgorithmId("TWAP"), @@ -703,7 +703,7 @@ async def test_load_order_with_exec_algorithm_params(self): async def test_load_order_when_limit_order_in_database_returns_order(self): # Arrange order = self.strategy.order_factory.limit( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), Price.from_str("1.00000"), @@ -724,7 +724,7 @@ async def test_load_order_when_limit_order_in_database_returns_order(self): async def test_load_order_when_transformed_to_market_order_in_database_returns_order(self): # Arrange order = self.strategy.order_factory.limit( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), Price.from_str("1.00000"), @@ -748,7 +748,7 @@ async def test_load_order_when_transformed_to_market_order_in_database_returns_o async def test_load_order_when_transformed_to_limit_order_in_database_returns_order(self): # Arrange order = self.strategy.order_factory.limit_if_touched( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), Price.from_str("1.00000"), @@ -773,7 +773,7 @@ async def test_load_order_when_transformed_to_limit_order_in_database_returns_or async def test_load_order_when_stop_market_order_in_database_returns_order(self): # Arrange order = self.strategy.order_factory.stop_market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), Price.from_str("1.00000"), @@ -794,7 +794,7 @@ async def test_load_order_when_stop_market_order_in_database_returns_order(self) async def test_load_order_when_stop_limit_order_in_database_returns_order(self): # Arrange order = self.strategy.order_factory.stop_limit( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), price=Price.from_str("1.00000"), @@ -829,7 +829,7 @@ async def test_load_position_when_no_position_in_database_returns_none(self): async def test_load_position_when_no_instrument_in_database_returns_none(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -842,12 +842,12 @@ async def test_load_position_when_no_instrument_in_database_returns_none(self): position_id = PositionId("P-1") fill = TestEventStubs.order_filled( order, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, position_id=position_id, last_px=Price.from_str("1.00000"), ) - position = Position(instrument=AUDUSD_SIM, fill=fill) + position = Position(instrument=_AUDUSD_SIM, fill=fill) self.database.add_position(position) # Act @@ -859,13 +859,13 @@ async def test_load_position_when_no_instrument_in_database_returns_none(self): @pytest.mark.asyncio async def test_load_position_when_position_in_database_returns_position(self): # Arrange - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) # Allow MPSC thread to insert - await eventually(lambda: self.database.load_instrument(AUDUSD_SIM.id)) + await eventually(lambda: self.database.load_instrument(_AUDUSD_SIM.id)) order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -878,12 +878,12 @@ async def test_load_position_when_position_in_database_returns_position(self): position_id = PositionId("P-1") fill = TestEventStubs.order_filled( order, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, position_id=position_id, last_px=Price.from_str("1.00000"), ) - position = Position(instrument=AUDUSD_SIM, fill=fill) + position = Position(instrument=_AUDUSD_SIM, fill=fill) self.database.add_position(position) @@ -931,7 +931,7 @@ async def test_load_orders_cache_when_no_orders(self): async def test_load_orders_cache_when_one_order_in_database(self): # Arrange order = self.strategy.order_factory.market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), ) @@ -958,13 +958,13 @@ async def test_load_positions_cache_when_no_positions(self): @pytest.mark.asyncio async def test_load_positions_cache_when_one_position_in_database(self): # Arrange - self.database.add_instrument(AUDUSD_SIM) + self.database.add_instrument(_AUDUSD_SIM) # Allow MPSC thread to insert - await eventually(lambda: self.database.load_instrument(AUDUSD_SIM.id)) + await eventually(lambda: self.database.load_instrument(_AUDUSD_SIM.id)) order1 = self.strategy.order_factory.stop_market( - AUDUSD_SIM.id, + _AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100_000), Price.from_str("1.00000"), @@ -981,13 +981,13 @@ async def test_load_positions_cache_when_one_position_in_database(self): order1.apply( TestEventStubs.order_filled( order1, - instrument=AUDUSD_SIM, + instrument=_AUDUSD_SIM, position_id=position_id, last_px=Price.from_str("1.00001"), ), ) - position = Position(instrument=AUDUSD_SIM, fill=order1.last_event) + position = Position(instrument=_AUDUSD_SIM, fill=order1.last_event) self.database.add_position(position) # Allow MPSC thread to insert From d17c4776ac5cc3393f3451250ece72c7e9aa2cf3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 22:13:22 +1000 Subject: [PATCH 074/193] Remove docker services from mac-os runner --- .github/workflows/build.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c0dae0bb979..b2f45057f5e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -249,25 +249,6 @@ jobs: env: BUILD_MODE: debug RUST_BACKTRACE: 1 - services: - redis: - image: redis - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - postgres: - image: postgres - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: pass - POSTGRES_DB: nautilus - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout repository From 62fc78f670800747fa5744dae5b63b2e812d19d7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 28 Apr 2024 22:14:02 +1000 Subject: [PATCH 075/193] Only run database infrastructure tests on Linux --- .../infrastructure/tests/test_cache_database_postgres.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs index a165f046e6e6..38ad3c35dcf7 100644 --- a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -57,6 +57,7 @@ pub async fn get_pg_cache_database() -> anyhow::Result { } #[cfg(test)] +#[cfg(target_os = "linux")] // Databases only supported on Linux mod tests { use std::time::Duration; From 0821b225acb8e25eddbb4c68ab11e20ef9c38496 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 07:54:02 +1000 Subject: [PATCH 076/193] Fix GitHub workflows --- .github/workflows/build.yml | 11 ----------- .github/workflows/coverage.yml | 31 ++++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2f45057f5e9..48d87953f4dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -315,17 +315,6 @@ jobs: # pre-commit run --hook-stage manual gitlint-ci pre-commit run --all-files - - name: Install Nautilus CLI and run init postgres - run: | - make install-cli - nautilus database init --schema ${{ github.workspace }}/schema - env: - POSTGRES_HOST: localhost - POSTGRES_PORT: 5432 - POSTGRES_USERNAME: postgres - POSTGRES_PASSWORD: pass - POSTGRES_DATABASE: nautilus - - name: Run nautilus_core cargo tests (macOS) run: | cargo install cargo-nextest diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fb4522ae6d57..9f1c4cd1133e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -14,6 +14,25 @@ jobs: python-version: ["3.10"] # Fails on 3.11 due Cython name: build - Python ${{ matrix.python-version }} (${{ matrix.arch }} ${{ matrix.os }}) runs-on: ${{ matrix.os }} + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: pass + POSTGRES_DB: nautilus + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Free disk space (Ubuntu) @@ -90,10 +109,16 @@ jobs: path: ${{ env.POETRY_CACHE_DIR }} key: ${{ runner.os }}-${{ matrix.python-version }}-poetry-${{ hashFiles('**/poetry.lock') }} - - name: Install Redis + - name: Install Nautilus CLI and run init postgres run: | - sudo apt-get install redis-server - redis-server --daemonize yes + make install-cli + nautilus database init --schema ${{ github.workspace }}/schema + env: + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: pass + POSTGRES_DATABASE: nautilus - name: Run tests with coverage run: make pytest-coverage From 6405fa029465b14933ae1ff8eb9094747f511dc3 Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:36:28 +0300 Subject: [PATCH 077/193] Add Sandbox example with Interactive Brokers (#1618) --- examples/live/interactive_brokers/sandbox.py | 140 ++++++++++++++++++ .../examples/strategies/ema_cross.py | 13 +- 2 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 examples/live/interactive_brokers/sandbox.py diff --git a/examples/live/interactive_brokers/sandbox.py b/examples/live/interactive_brokers/sandbox.py new file mode 100644 index 000000000000..cd7c1872da19 --- /dev/null +++ b/examples/live/interactive_brokers/sandbox.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 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. +# ------------------------------------------------------------------------------------------------- + + +from decimal import Decimal + +# fmt: off +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersGatewayConfig +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig +from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveDataClientFactory +from nautilus_trader.adapters.sandbox.config import SandboxExecutionClientConfig +from nautilus_trader.adapters.sandbox.execution import SandboxExecutionClient +from nautilus_trader.adapters.sandbox.factory import SandboxLiveExecClientFactory +from nautilus_trader.config import LiveDataEngineConfig +from nautilus_trader.config import LoggingConfig +from nautilus_trader.config import TradingNodeConfig +from nautilus_trader.examples.strategies.ema_cross import EMACross +from nautilus_trader.examples.strategies.ema_cross import EMACrossConfig +from nautilus_trader.live.node import TradingNode +from nautilus_trader.model.data import BarType +from nautilus_trader.persistence.catalog import ParquetDataCatalog + + +# fmt: on + +# Load instruments from a Parquet catalog +CATALOG_PATH = "/path/to/catalog" +catalog = ParquetDataCatalog(CATALOG_PATH) +SANDBOX_INSTRUMENTS = catalog.instruments(instrument_ids=["EUR/USD.IDEALPRO"]) + +# Need to manually set instruments for sandbox exec client +SandboxExecutionClient.INSTRUMENTS = ( + SANDBOX_INSTRUMENTS # <- ALL INSTRUMENTS MUST HAVE THE SAME VENUE +) + +# Set up the Interactive Brokers gateway configuration, this is applicable only when using Docker. +gateway = InteractiveBrokersGatewayConfig( + start=False, + username=None, + password=None, + trading_mode="paper", + read_only_api=True, +) + +instrument_provider = InteractiveBrokersInstrumentProviderConfig( + build_futures_chain=False, + build_options_chain=False, + min_expiry_days=10, + max_expiry_days=60, + load_ids=frozenset(str(instrument.id) for instrument in SANDBOX_INSTRUMENTS), +) + +# Set up the execution clients (required per venue) +SANDBOX_VENUES = {str(instrument.venue) for instrument in SANDBOX_INSTRUMENTS} +exec_clients = {} +for venue in SANDBOX_VENUES: + exec_clients[venue] = SandboxExecutionClientConfig( + venue=venue, + currency="USD", + balance=1_000_000, + instrument_provider=instrument_provider, + ) + + +# Configure the trading node +config_node = TradingNodeConfig( + trader_id="SANDBOX-001", + logging=LoggingConfig(log_level="INFO"), + data_clients={ + "IB": InteractiveBrokersDataClientConfig( + ibg_host="127.0.0.1", + ibg_port=7497, + ibg_client_id=1, + use_regular_trading_hours=True, + instrument_provider=instrument_provider, + gateway=gateway, + ), + }, + exec_clients=exec_clients, # type: ignore + data_engine=LiveDataEngineConfig( + time_bars_timestamp_on_close=False, + validate_data_sequence=True, + ), + timeout_connection=90.0, + timeout_reconciliation=5.0, + timeout_portfolio=5.0, + timeout_disconnection=5.0, + timeout_post_stop=2.0, +) + +# Instantiate the node with a configuration +node = TradingNode(config=config_node) + +# Instantiate strategies +strategies = {} +for instrument in SANDBOX_INSTRUMENTS: + # Configure your strategy + strategy_config = EMACrossConfig( + instrument_id=instrument.id, + bar_type=BarType.from_str(f"{instrument.id}-30-SECOND-MID-EXTERNAL"), + trade_size=Decimal(100_000), + subscribe_quote_ticks=True, + ) + # Instantiate your strategy + strategy = EMACross(config=strategy_config) + # Add your strategies and modules + node.trader.add_strategy(strategy) + + strategies[str(instrument.id)] = strategy + + +# Register client factories with the node +for data_client in config_node.data_clients: + node.add_data_client_factory(data_client, InteractiveBrokersLiveDataClientFactory) +for exec_client in config_node.exec_clients: + node.add_exec_client_factory(exec_client, SandboxLiveExecClientFactory) + +node.build() + + +# Stop and dispose of the node with SIGINT/CTRL+C +if __name__ == "__main__": + try: + node.run() + finally: + node.dispose() diff --git a/nautilus_trader/examples/strategies/ema_cross.py b/nautilus_trader/examples/strategies/ema_cross.py index 1aa5d01c3f65..d3972b6621cd 100644 --- a/nautilus_trader/examples/strategies/ema_cross.py +++ b/nautilus_trader/examples/strategies/ema_cross.py @@ -57,6 +57,10 @@ class EMACrossConfig(StrategyConfig, frozen=True): The fast EMA period. slow_ema_period : int, default 20 The slow EMA period. + subscribe_trade_ticks : bool, default True + If trade ticks should be subscribed to. + subscribe_quote_ticks : bool, default False + If quote ticks should be subscribed to. close_positions_on_stop : bool, default True If all open positions should be closed on strategy stop. order_id_tag : str @@ -73,6 +77,8 @@ class EMACrossConfig(StrategyConfig, frozen=True): trade_size: Decimal fast_ema_period: PositiveInt = 10 slow_ema_period: PositiveInt = 20 + subscribe_trade_ticks: bool = True + subscribe_quote_ticks: bool = False close_positions_on_stop: bool = True @@ -135,8 +141,11 @@ def on_start(self) -> None: # Subscribe to live data self.subscribe_bars(self.bar_type) - # self.subscribe_quote_ticks(self.instrument_id) - self.subscribe_trade_ticks(self.instrument_id) + if self.config.subscribe_quote_ticks: + self.subscribe_quote_ticks(self.instrument_id) + if self.config.subscribe_trade_ticks: + self.subscribe_trade_ticks(self.instrument_id) + # self.subscribe_ticker(self.instrument_id) # For debugging # self.subscribe_order_book_deltas(self.instrument_id, depth=20) # For debugging # self.subscribe_order_book_snapshots(self.instrument_id, depth=20) # For debugging From 093dd500d6f5724de9dfd606fc5575293acb5c74 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 18:43:30 +1000 Subject: [PATCH 078/193] Update dependencies --- nautilus_core/Cargo.lock | 34 +++++++++++++++++----------------- poetry.lock | 14 +++++++------- pyproject.toml | 2 +- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index c9b4a4fff89c..91d2db3d586e 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -215,7 +215,7 @@ dependencies = [ "chrono", "chrono-tz", "half", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "num", ] @@ -344,7 +344,7 @@ dependencies = [ "arrow-data", "arrow-schema", "half", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1161,7 +1161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1169,9 +1169,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "databento" @@ -1224,7 +1224,7 @@ dependencies = [ "futures", "glob", "half", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "indexmap 2.2.6", "itertools 0.12.1", "log", @@ -1287,7 +1287,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "futures", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "log", "object_store", "parking_lot", @@ -1350,7 +1350,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-physical-expr", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "itertools 0.12.1", "log", "regex-syntax 0.8.3", @@ -1377,7 +1377,7 @@ dependencies = [ "datafusion-execution", "datafusion-expr", "half", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "hex", "indexmap 2.2.6", "itertools 0.12.1", @@ -1411,7 +1411,7 @@ dependencies = [ "datafusion-physical-expr", "futures", "half", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "indexmap 2.2.6", "itertools 0.12.1", "log", @@ -1621,9 +1621,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "finl_unicode" @@ -1912,9 +1912,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", "allocator-api2", @@ -1926,7 +1926,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -2222,7 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -3175,7 +3175,7 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lz4_flex", "num", "num-bigint", diff --git a/poetry.lock b/poetry.lock index efc012cf78a4..821b06dd4ea0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1852,19 +1852,19 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-xdist" -version = "3.5.0" +version = "3.6.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, - {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, ] [package.dependencies] -execnet = ">=1.1" +execnet = ">=2.1" psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} -pytest = ">=6.2.0" +pytest = ">=7.0.0" [package.extras] psutil = ["psutil (>=3.0)"] @@ -2686,4 +2686,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "279104839c15b07ca74c197e5490a56059ac17c8099a0ab5b4c01a41d5761921" +content-hash = "0d885255610e911d220a81a1ac61da2453422a6dc9ef41d0d24c347c58d0d7cf" diff --git a/pyproject.toml b/pyproject.toml index 87300168d4bc..9da95ecc4e2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_s pytest-benchmark = "^4.0.0" pytest-cov = "^4.1.0" pytest-mock = "^3.14.0" -pytest-xdist = { version = "^3.5.0", extras = ["psutil"] } +pytest-xdist = { version = "^3.6.1", extras = ["psutil"] } [tool.poetry.group.docs] optional = true From 0924ef2457db9ad1445cfad31d85b3ebcadff131 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 18:46:59 +1000 Subject: [PATCH 079/193] Add services make targets --- Makefile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 87c27bf3cedd..6272b3656df9 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,14 @@ docker-build-jupyter: docker-push-jupyter: docker push ${IMAGE}:jupyter +.PHONY: start-services +start-services: + docker-compose -f .docker/docker-compose.yml up -d + +.PHONY: stop-services +stop-services: + docker-compose -f .docker/docker-compose.yml down + .PHONY: pytest pytest: bash scripts/test.sh @@ -156,5 +164,3 @@ install-talib: .PHONY: install-cli install-cli: (cd nautilus_core && cargo install --path cli --bin nautilus) - - From 427501544d24f54324478265b2f3dba0267ff28d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 18:49:37 +1000 Subject: [PATCH 080/193] Pause mac-os platform in build workflow --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48d87953f4dd..549521782cf4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -234,7 +234,8 @@ jobs: PARALLEL_BUILD: false build-macos: - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/nightly' + if: github.ref == 'refs/heads/master' + # if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/nightly' strategy: fail-fast: false matrix: From e7d0503aab276ef94a22bfcbedc2e6c86478c11a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 18:54:05 +1000 Subject: [PATCH 081/193] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index c68b350f43f1..65a89bba2d5c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,7 @@ Released on TBD (UTC). - Added Nautilus CLI (see [docs](https://docs.nautilustrader.io/nightly/developer_guide/index.html)) (#1602), many thanks @filipmacek - Added `Cfd` and `Commodity` instruments with Interactive Brokers support (#1604), thanks @DracheShiki - Added `OrderMatchingEngine` futures and options contract activation and expiration simulation +- Added Sandbox example with Interactive Brokers (#1618), thanks @rsmb7z ### Breaking Changes - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) From 0dde491624552b56c2a0354a49000b0ea763b9e8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 20:00:01 +1000 Subject: [PATCH 082/193] Derive QuoteTick and TradeTick builders --- nautilus_core/model/src/data/quote.rs | 32 ++++++++++++++------------- nautilus_core/model/src/data/trade.rs | 18 ++++++++++++++- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 9d63160ed184..142633e2abcc 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -22,6 +22,7 @@ use std::{ hash::Hash, }; +use derive_builder::Builder; use indexmap::IndexMap; use nautilus_core::{correctness::check_equal_u8, nanos::UnixNanos, serialization::Serializable}; use serde::{Deserialize, Serialize}; @@ -34,7 +35,7 @@ use crate::{ /// Represents a single quote tick in a market. #[repr(C)] -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Builder)] #[serde(tag = "type")] #[cfg_attr( feature = "python", @@ -164,6 +165,21 @@ impl Display for QuoteTick { impl Serializable for QuoteTick {} +#[cfg(feature = "stubs")] +impl Default for QuoteTick { + fn default() -> Self { + Self { + instrument_id: InstrumentId::from("AUDUSD.SIM"), + bid_price: Price::from("1.00000"), + ask_price: Price::from("1.00000"), + bid_size: Quantity::from(100_000), + ask_size: Quantity::from(100_000), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } +} + //////////////////////////////////////////////////////////////////////////////// // Stubs //////////////////////////////////////////////////////////////////////////////// @@ -175,23 +191,9 @@ pub mod stubs { use crate::{ data::quote::QuoteTick, identifiers::instrument_id::InstrumentId, - instruments::{currency_pair::CurrencyPair, stubs::*}, types::{price::Price, quantity::Quantity}, }; - #[fixture] - pub fn quote_tick_audusd_sim(audusd_sim: CurrencyPair) -> QuoteTick { - QuoteTick { - instrument_id: audusd_sim.id, - bid_price: Price::from("1.00000"), - ask_price: Price::from("1.00000"), - bid_size: Quantity::from(100_000), - ask_size: Quantity::from(100_000), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::from(1), - } - } - #[fixture] pub fn quote_tick_ethusdt_binance() -> QuoteTick { QuoteTick { diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index ae1b4a5a0012..2765e5246881 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -21,6 +21,7 @@ use std::{ hash::Hash, }; +use derive_builder::Builder; use indexmap::IndexMap; use nautilus_core::{nanos::UnixNanos, serialization::Serializable}; use serde::{Deserialize, Serialize}; @@ -33,7 +34,7 @@ use crate::{ /// Represents a single trade tick in a market. #[repr(C)] -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Builder)] #[serde(tag = "type")] #[cfg_attr( feature = "python", @@ -124,6 +125,21 @@ impl Display for TradeTick { impl Serializable for TradeTick {} +#[cfg(feature = "stubs")] +impl Default for TradeTick { + fn default() -> Self { + TradeTick { + instrument_id: InstrumentId::from("AUDUSD.SIM"), + price: Price::from("1.00000"), + size: Quantity::from(100_000), + aggressor_side: AggressorSide::Buyer, + trade_id: TradeId::new("123456789").unwrap(), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } +} + //////////////////////////////////////////////////////////////////////////////// // Stubs //////////////////////////////////////////////////////////////////////////////// From a83209bc051ac595c0de8a71f07a7e1fe0ebfd3a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 20:04:15 +1000 Subject: [PATCH 083/193] Refine tick stubs --- nautilus_core/model/src/data/quote.rs | 34 +++++++++++++++------------ nautilus_core/model/src/data/trade.rs | 34 +++++++++++++++------------ 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 142633e2abcc..26560d83d460 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -165,21 +165,6 @@ impl Display for QuoteTick { impl Serializable for QuoteTick {} -#[cfg(feature = "stubs")] -impl Default for QuoteTick { - fn default() -> Self { - Self { - instrument_id: InstrumentId::from("AUDUSD.SIM"), - bid_price: Price::from("1.00000"), - ask_price: Price::from("1.00000"), - bid_size: Quantity::from(100_000), - ask_size: Quantity::from(100_000), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::default(), - } - } -} - //////////////////////////////////////////////////////////////////////////////// // Stubs //////////////////////////////////////////////////////////////////////////////// @@ -194,6 +179,25 @@ pub mod stubs { types::{price::Price, quantity::Quantity}, }; + impl Default for QuoteTick { + fn default() -> Self { + Self { + instrument_id: InstrumentId::from("AUDUSD.SIM"), + bid_price: Price::from("1.00000"), + ask_price: Price::from("1.00000"), + bid_size: Quantity::from(100_000), + ask_size: Quantity::from(100_000), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } + } + + #[fixture] + pub fn quote_tick_audusd_sim() -> QuoteTick { + QuoteTick::default() + } + #[fixture] pub fn quote_tick_ethusdt_binance() -> QuoteTick { QuoteTick { diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index 2765e5246881..0b5034f80f5b 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -125,21 +125,6 @@ impl Display for TradeTick { impl Serializable for TradeTick {} -#[cfg(feature = "stubs")] -impl Default for TradeTick { - fn default() -> Self { - TradeTick { - instrument_id: InstrumentId::from("AUDUSD.SIM"), - price: Price::from("1.00000"), - size: Quantity::from(100_000), - aggressor_side: AggressorSide::Buyer, - trade_id: TradeId::new("123456789").unwrap(), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::default(), - } - } -} - //////////////////////////////////////////////////////////////////////////////// // Stubs //////////////////////////////////////////////////////////////////////////////// @@ -155,6 +140,25 @@ pub mod stubs { types::{price::Price, quantity::Quantity}, }; + impl Default for TradeTick { + fn default() -> Self { + TradeTick { + instrument_id: InstrumentId::from("AUDUSD.SIM"), + price: Price::from("1.00000"), + size: Quantity::from(100_000), + aggressor_side: AggressorSide::Buyer, + trade_id: TradeId::new("123456789").unwrap(), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } + } + + #[fixture] + pub fn trade_tick_audusd_sim() -> TradeTick { + TradeTick::default() + } + #[fixture] pub fn stub_trade_tick_ethusdt_buyer() -> TradeTick { TradeTick { From c1b9cb7a0c945366e2578d68d584e84282c82746 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 29 Apr 2024 20:04:52 +1000 Subject: [PATCH 084/193] Continue Cache tests in Rust --- nautilus_core/common/src/cache/mod.rs | 68 ++++++++++++++++++++------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 249b1c6a7e8a..ea258059baaf 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -2392,9 +2392,10 @@ impl Cache { //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { - use nautilus_model::data::quote::stubs::*; - use nautilus_model::data::quote::QuoteTick; - use nautilus_model::instruments::{currency_pair::CurrencyPair, stubs::*}; + use nautilus_model::{ + data::{quote::QuoteTick, trade::TradeTick}, + instruments::{currency_pair::CurrencyPair, stubs::*}, + }; use rstest::*; use super::Cache; @@ -2477,7 +2478,6 @@ mod tests { #[rstest] fn test_get_general_when_empty() { let cache = Cache::default(); - let result = cache.get("A").unwrap(); assert_eq!(result, None); } @@ -2485,7 +2485,6 @@ mod tests { #[rstest] fn test_add_general_when_value() { let mut cache = Cache::default(); - let key = "A"; let value = vec![0_u8]; cache.add(key, value.clone()).unwrap(); @@ -2497,39 +2496,76 @@ mod tests { #[rstest] fn test_quote_tick_when_empty(audusd_sim: CurrencyPair) { let cache = Cache::default(); - let result = cache.quote_tick(&audusd_sim.id); assert_eq!(result, None); } #[rstest] - fn test_quote_tick_when_some(quote_tick_audusd_sim: QuoteTick) { + fn test_quote_tick_when_some() { let mut cache = Cache::default(); - cache.add_quote(quote_tick_audusd_sim).unwrap(); + let quote = QuoteTick::default(); + cache.add_quote(quote).unwrap(); - let result = cache.quote_tick("e_tick_audusd_sim.instrument_id); - assert_eq!(result, Some("e_tick_audusd_sim)); + let result = cache.quote_tick("e.instrument_id); + assert_eq!(result, Some("e)); } #[rstest] fn test_quote_ticks_when_empty(audusd_sim: CurrencyPair) { let cache = Cache::default(); - let result = cache.quote_ticks(&audusd_sim.id); assert_eq!(result, None); } #[rstest] - fn test_quote_ticks_when_some(quote_tick_audusd_sim: QuoteTick) { + fn test_quote_ticks_when_some() { let mut cache = Cache::default(); let quotes = vec![ - quote_tick_audusd_sim, - quote_tick_audusd_sim, - quote_tick_audusd_sim, + QuoteTick::default(), + QuoteTick::default(), + QuoteTick::default(), ]; cache.add_quotes("es).unwrap(); - let result = cache.quote_ticks("e_tick_audusd_sim.instrument_id); + let result = cache.quote_ticks("es[0].instrument_id); assert_eq!(result, Some(quotes)); } + + #[rstest] + fn test_trade_tick_when_empty(audusd_sim: CurrencyPair) { + let cache = Cache::default(); + let result = cache.trade_tick(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_trade_tick_when_some() { + let mut cache = Cache::default(); + let trade = TradeTick::default(); + cache.add_trade(trade).unwrap(); + + let result = cache.trade_tick(&trade.instrument_id); + assert_eq!(result, Some(&trade)); + } + + #[rstest] + fn test_trade_ticks_when_empty(audusd_sim: CurrencyPair) { + let cache = Cache::default(); + let result = cache.trade_ticks(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_trade_ticks_when_some() { + let mut cache = Cache::default(); + let trades = vec![ + TradeTick::default(), + TradeTick::default(), + TradeTick::default(), + ]; + cache.add_trades(&trades).unwrap(); + + let result = cache.trade_ticks(&trades[0].instrument_id); + assert_eq!(result, Some(trades)); + } } From 3482475581d75119576ac6b5ab226329650c23b3 Mon Sep 17 00:00:00 2001 From: Benjamin Singleton Date: Tue, 30 Apr 2024 03:52:03 -0400 Subject: [PATCH 085/193] Support S3 parquet data catalog (#1620) --- .../persistence/catalog/parquet.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index 3068a592254c..d536b503a369 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -138,7 +138,10 @@ def __init__( self.max_rows_per_group = max_rows_per_group self.show_query_paths = show_query_paths - final_path = str(make_path_posix(str(path))) + if self.fs_protocol == "file": + final_path = str(make_path_posix(str(path))) + else: + final_path = str(path) if ( isinstance(self.fs, MemoryFileSystem) @@ -363,7 +366,13 @@ def query( where: str | None = None, **kwargs: Any, ) -> list[Data | CustomData]: - if data_cls in (OrderBookDelta, OrderBookDepth10, QuoteTick, TradeTick, Bar): + if self.fs_protocol == "file" and data_cls in ( + OrderBookDelta, + OrderBookDepth10, + QuoteTick, + TradeTick, + Bar, + ): data = self.query_rust( data_cls=data_cls, instrument_ids=instrument_ids, @@ -377,6 +386,7 @@ def query( data = self.query_pyarrow( data_cls=data_cls, instrument_ids=instrument_ids, + bar_types=bar_types, start=start, end=end, where=where, @@ -484,6 +494,7 @@ def query_pyarrow( self, data_cls: type, instrument_ids: list[str] | None = None, + bar_types: list[str] | None = None, start: TimestampLike | None = None, end: TimestampLike | None = None, filter_expr: str | None = None, @@ -497,6 +508,7 @@ def query_pyarrow( path=dataset_path, filter_expr=filter_expr, instrument_ids=instrument_ids, + bar_types=bar_types, start=start, end=end, ) @@ -515,6 +527,7 @@ def _load_pyarrow_table( path: str, filter_expr: str | None = None, instrument_ids: list[str] | None = None, + bar_types: list[str] | None = None, start: TimestampLike | None = None, end: TimestampLike | None = None, ts_column: str = "ts_init", @@ -533,6 +546,14 @@ def _load_pyarrow_table( ] dataset = pds.dataset(valid_files, filesystem=self.fs) + if bar_types is not None: + if not isinstance(bar_types, list): + bar_types = [bar_types] + valid_files = [ + fn for fn in dataset.files if any(x.replace("/", "") in fn for x in bar_types) + ] + dataset = pds.dataset(valid_files, filesystem=self.fs) + filters: list[pds.Expression] = [filter_expr] if filter_expr is not None else [] if start is not None: filters.append(pds.field(ts_column) >= pd.Timestamp(start).value) @@ -591,7 +612,7 @@ def _handle_table_nautilus( data = ArrowSerializer.deserialize(data_cls=data_cls, batch=table) # TODO (bm/cs) remove when pyo3 objects are used everywhere. module = data[0].__class__.__module__ - if "builtins" in module: + if "nautilus_pyo3" in module: cython_cls = { "OrderBookDelta": OrderBookDelta, "OrderBookDeltas": OrderBookDelta, @@ -600,7 +621,7 @@ def _handle_table_nautilus( "TradeTick": TradeTick, "Bar": Bar, }.get(data_cls.__name__, data_cls.__name__) - data = cython_cls.from_pyo3(data) + data = cython_cls.from_pyo3_list(data) return data def _query_subclasses( From 5d468cca84decb696c926ec218ccd0299b783076 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 30 Apr 2024 18:04:57 +1000 Subject: [PATCH 086/193] Update dependencies --- RELEASES.md | 1 + nautilus_core/Cargo.lock | 20 ++++++++++---------- nautilus_core/Cargo.toml | 4 ++-- poetry.lock | 12 ++++++------ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 65a89bba2d5c..cf73328a0f99 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,6 +7,7 @@ Released on TBD (UTC). - Added `Cfd` and `Commodity` instruments with Interactive Brokers support (#1604), thanks @DracheShiki - Added `OrderMatchingEngine` futures and options contract activation and expiration simulation - Added Sandbox example with Interactive Brokers (#1618), thanks @rsmb7z +- Added `ParquetDataCatalog` S3 support (#1620), thanks benjaminsingleton ### Breaking Changes - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 91d2db3d586e..fdd4bb5c492b 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1649,9 +1649,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -2394,9 +2394,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libm" @@ -3915,9 +3915,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" dependencies = [ "futures", "futures-timer", @@ -3927,9 +3927,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" dependencies = [ "cfg-if", "glob", @@ -4314,9 +4314,9 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index ab218ab2dfd3..103034d92921 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -55,8 +55,8 @@ sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio"] } # dev-dependencies criterion = "0.5.1" float-cmp = "0.9.0" -iai = "0.1" -rstest = "0.18.2" +iai = "0.1.1" +rstest = "0.19.0" tempfile = "3.10.1" # build-dependencies diff --git a/poetry.lock b/poetry.lock index 821b06dd4ea0..f76f75f65911 100644 --- a/poetry.lock +++ b/poetry.lock @@ -639,13 +639,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "filelock" -version = "3.13.4" +version = "3.14.0" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.4-py3-none-any.whl", hash = "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f"}, - {file = "filelock-3.13.4.tar.gz", hash = "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"}, + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, ] [package.extras] @@ -2543,13 +2543,13 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "virtualenv" -version = "20.26.0" +version = "20.26.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.0-py3-none-any.whl", hash = "sha256:0846377ea76e818daaa3e00a4365c018bc3ac9760cbb3544de542885aad61fb3"}, - {file = "virtualenv-20.26.0.tar.gz", hash = "sha256:ec25a9671a5102c8d2657f62792a27b48f016664c6873f6beed3800008577210"}, + {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, + {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, ] [package.dependencies] From 041147a8a6cecaddc3a4bc30cd314775b712f736 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 30 Apr 2024 18:52:39 +1000 Subject: [PATCH 087/193] Refine client base logging --- nautilus_trader/live/data_client.py | 46 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/nautilus_trader/live/data_client.py b/nautilus_trader/live/data_client.py index 662d955e3127..8b34c0cbe1f4 100644 --- a/nautilus_trader/live/data_client.py +++ b/nautilus_trader/live/data_client.py @@ -455,7 +455,7 @@ def subscribe_instruments(self) -> None: self.create_task( self._subscribe_instruments(), log_msg=f"subscribe: instruments {self.venue}", - success_msg=f"Subscribed instruments {self.venue}", + success_msg=f"Subscribed {self.venue} instruments", success_color=LogColor.BLUE, ) @@ -464,7 +464,7 @@ def subscribe_instrument(self, instrument_id: InstrumentId) -> None: self.create_task( self._subscribe_instrument(instrument_id), log_msg=f"subscribe: instrument {instrument_id}", - success_msg=f"Subscribed instrument {instrument_id}", + success_msg=f"Subscribed {instrument_id} instrument", success_color=LogColor.BLUE, ) @@ -484,7 +484,7 @@ def subscribe_order_book_deltas( kwargs=kwargs, ), log_msg=f"subscribe: order_book_deltas {instrument_id}", - success_msg=f"Subscribed order book deltas {instrument_id} depth={depth}", + success_msg=f"Subscribed {instrument_id} order book deltas depth={depth}", success_color=LogColor.BLUE, ) @@ -504,7 +504,7 @@ def subscribe_order_book_snapshots( kwargs=kwargs, ), log_msg=f"subscribe: order_book_snapshots {instrument_id}", - success_msg=f"Subscribed order book snapshots {instrument_id} depth={depth}", + success_msg=f"Subscribed {instrument_id} order book snapshots depth={depth}", success_color=LogColor.BLUE, ) @@ -513,7 +513,7 @@ def subscribe_quote_ticks(self, instrument_id: InstrumentId) -> None: self.create_task( self._subscribe_quote_ticks(instrument_id), log_msg=f"subscribe: quote_ticks {instrument_id}", - success_msg=f"Subscribed quotes {instrument_id}", + success_msg=f"Subscribed {instrument_id} quotes", success_color=LogColor.BLUE, ) @@ -522,7 +522,7 @@ def subscribe_trade_ticks(self, instrument_id: InstrumentId) -> None: self.create_task( self._subscribe_trade_ticks(instrument_id), log_msg=f"subscribe: trade_ticks {instrument_id}", - success_msg=f"Subscribed trades {instrument_id}", + success_msg=f"Subscribed {instrument_id} trades", success_color=LogColor.BLUE, ) @@ -533,7 +533,7 @@ def subscribe_bars(self, bar_type: BarType) -> None: self.create_task( self._subscribe_bars(bar_type), log_msg=f"subscribe: bars {bar_type}", - success_msg=f"Subscribed bars {bar_type}", + success_msg=f"Subscribed {bar_type} bars", success_color=LogColor.BLUE, ) @@ -542,7 +542,7 @@ def subscribe_instrument_status(self, instrument_id: InstrumentId) -> None: self.create_task( self._subscribe_instrument_status(instrument_id), log_msg=f"subscribe: instrument_status {instrument_id}", - success_msg=f"Subscribed instrument status {instrument_id}", + success_msg=f"Subscribed {instrument_id} instrument status ", success_color=LogColor.BLUE, ) @@ -551,7 +551,7 @@ def subscribe_instrument_close(self, instrument_id: InstrumentId) -> None: self.create_task( self._subscribe_instrument_close(instrument_id), log_msg=f"subscribe: instrument_close {instrument_id}", - success_msg=f"Subscribed instrument close {instrument_id}", + success_msg=f"Subscribed {instrument_id} instrument close", success_color=LogColor.BLUE, ) @@ -570,7 +570,7 @@ def unsubscribe_instruments(self) -> None: self.create_task( self._unsubscribe_instruments(), log_msg=f"unsubscribe: instruments {self.venue}", - success_msg=f"Unsubscribed instruments {self.venue}", + success_msg=f"Unsubscribed {self.venue} instruments", success_color=LogColor.BLUE, ) @@ -579,7 +579,7 @@ def unsubscribe_instrument(self, instrument_id: InstrumentId) -> None: self.create_task( self._unsubscribe_instrument(instrument_id), log_msg=f"unsubscribe: instrument {instrument_id}", - success_msg=f"Unsubscribed instrument {instrument_id}", + success_msg=f"Unsubscribed {instrument_id} instrument", success_color=LogColor.BLUE, ) @@ -588,7 +588,7 @@ def unsubscribe_order_book_deltas(self, instrument_id: InstrumentId) -> None: self.create_task( self._unsubscribe_order_book_deltas(instrument_id), log_msg=f"unsubscribe: order_book_deltas {instrument_id}", - success_msg=f"Unsubscribed order book deltas {instrument_id}", + success_msg=f"Unsubscribed {instrument_id} order book deltas", success_color=LogColor.BLUE, ) @@ -597,7 +597,7 @@ def unsubscribe_order_book_snapshots(self, instrument_id: InstrumentId) -> None: self.create_task( self._unsubscribe_order_book_snapshots(instrument_id), log_msg=f"unsubscribe: order_book_snapshots {instrument_id}", - success_msg=f"Unsubscribed order book snapshots {instrument_id}", + success_msg=f"Unsubscribed {instrument_id} order book snapshots", success_color=LogColor.BLUE, ) @@ -606,7 +606,7 @@ def unsubscribe_quote_ticks(self, instrument_id: InstrumentId) -> None: self.create_task( self._unsubscribe_quote_ticks(instrument_id), log_msg=f"unsubscribe: quote_ticks {instrument_id}", - success_msg=f"Unsubscribed quotes {instrument_id}", + success_msg=f"Unsubscribed {instrument_id} quotes", success_color=LogColor.BLUE, ) @@ -615,7 +615,7 @@ def unsubscribe_trade_ticks(self, instrument_id: InstrumentId) -> None: self.create_task( self._unsubscribe_trade_ticks(instrument_id), log_msg=f"unsubscribe: trade_ticks {instrument_id}", - success_msg=f"Unsubscribed trades {instrument_id}", + success_msg=f"Unsubscribed {instrument_id} trades", success_color=LogColor.BLUE, ) @@ -624,7 +624,7 @@ def unsubscribe_bars(self, bar_type: BarType) -> None: self.create_task( self._unsubscribe_bars(bar_type), log_msg=f"unsubscribe: bars {bar_type}", - success_msg=f"Unsubscribed bars {bar_type}", + success_msg=f"Unsubscribed {bar_type} bars", success_color=LogColor.BLUE, ) @@ -633,7 +633,7 @@ def unsubscribe_instrument_status(self, instrument_id: InstrumentId) -> None: self.create_task( self._unsubscribe_instrument_status(instrument_id), log_msg=f"unsubscribe: instrument_status {instrument_id}", - success_msg=f"Unsubscribed instrument status {instrument_id}", + success_msg=f"Unsubscribed {instrument_id} instrument status", success_color=LogColor.BLUE, ) @@ -642,7 +642,7 @@ def unsubscribe_instrument_close(self, instrument_id: InstrumentId) -> None: self.create_task( self._unsubscribe_instrument_close(instrument_id), log_msg=f"unsubscribe: instrument_close {instrument_id}", - success_msg=f"Unsubscribed instrument close {instrument_id}", + success_msg=f"Unsubscribed {instrument_id} instrument close", success_color=LogColor.BLUE, ) @@ -663,7 +663,7 @@ def request_instrument( end: pd.Timestamp | None = None, ) -> None: time_range = f" {start} to {end}" if (start or end) else "" - self._log.info(f"Request instrument {instrument_id}{time_range}", LogColor.BLUE) + self._log.info(f"Request {instrument_id} instrument{time_range}", LogColor.BLUE) self.create_task( self._request_instrument( instrument_id=instrument_id, @@ -683,7 +683,7 @@ def request_instruments( ) -> None: time_range = f" {start} to {end}" if (start or end) else "" self._log.info( - f"Request instruments for {venue}{time_range}", + f"Request {venue} instruments for{time_range}", LogColor.BLUE, ) self.create_task( @@ -706,7 +706,7 @@ def request_quote_ticks( ) -> None: time_range = f" {start} to {end}" if (start or end) else "" limit_str = f" limit={limit}" if limit else "" - self._log.info(f"Request quote ticks {instrument_id}{time_range}{limit_str}", LogColor.BLUE) + self._log.info(f"Request {instrument_id} quote ticks{time_range}{limit_str}", LogColor.BLUE) self.create_task( self._request_quote_ticks( instrument_id=instrument_id, @@ -728,7 +728,7 @@ def request_trade_ticks( ) -> None: time_range = f" {start} to {end}" if (start or end) else "" limit_str = f" limit={limit}" if limit else "" - self._log.info(f"Request trade ticks {instrument_id}{time_range}{limit_str}", LogColor.BLUE) + self._log.info(f"Request {instrument_id} trade ticks{time_range}{limit_str}", LogColor.BLUE) self.create_task( self._request_trade_ticks( instrument_id=instrument_id, @@ -750,7 +750,7 @@ def request_bars( ) -> None: time_range = f" {start} to {end}" if (start or end) else "" limit_str = f" limit={limit}" if limit else "" - self._log.info(f"Request bars {bar_type}{time_range}{limit_str}", LogColor.BLUE) + self._log.info(f"Request {bar_type} bars{time_range}{limit_str}", LogColor.BLUE) self.create_task( self._request_bars( bar_type=bar_type, From 223c4b5b8d6c810b50d81215d330a110f1e77540 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 30 Apr 2024 18:58:01 +1000 Subject: [PATCH 088/193] Fix Binance Futures account balance calculation --- RELEASES.md | 1 + nautilus_trader/adapters/binance/futures/schemas/account.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index cf73328a0f99..a38af0692d85 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -17,6 +17,7 @@ Released on TBD (UTC). - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek +- Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) --- diff --git a/nautilus_trader/adapters/binance/futures/schemas/account.py b/nautilus_trader/adapters/binance/futures/schemas/account.py index 5395ef201dfc..8161a3d55f13 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/account.py +++ b/nautilus_trader/adapters/binance/futures/schemas/account.py @@ -59,8 +59,12 @@ class BinanceFuturesBalanceInfo(msgspec.Struct, frozen=True): def parse_to_account_balance(self) -> AccountBalance: currency = Currency.from_str(self.asset) + # This calculation is currently mixing wallet cash balance and the available balance after + # considering margin collateral. As a temporary measure we're taking the `min` to + # disregard free amounts above the cash balance, but still considering where not all + # balance is available (so locked in some way, i.e. allocated as collateral). total = Decimal(self.walletBalance) - free = Decimal(self.availableBalance) if total != 0 else total + free = min(Decimal(self.availableBalance), total) locked = total - free return AccountBalance( total=Money(total, currency), From cee0e0cb35b5c2985c08d18b558e7f66ff037f04 Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Wed, 1 May 2024 10:59:48 +0300 Subject: [PATCH 089/193] Fix issue in Interactive Brokers get_historical_bars (#1621) --- .../interactive_brokers/client/client.py | 17 ++++++++++++----- .../interactive_brokers/client/market_data.py | 10 +++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 5b29d7cf0372..c07425037cd0 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -437,7 +437,12 @@ def unsubscribe_event(self, name: str) -> None: """ self._event_subscriptions.pop(name) - async def _await_request(self, request: Request, timeout: int) -> Any | None: + async def _await_request( + self, + request: Request, + timeout: int, + default_value: Any | None = None, + ) -> Any: """ Await the completion of a request within a specified timeout. @@ -447,11 +452,13 @@ async def _await_request(self, request: Request, timeout: int) -> Any | None: The request object to await. timeout : int The maximum time to wait for the request to complete, in seconds. + default_value : Any, optional + The default value to return if the request times out or fails. Defaults to None. Returns ------- - Any | ``None`` - The result of the request, or None if the request timed out. + Any + The result of the request, or default_value if the request times out or fails. """ try: @@ -459,11 +466,11 @@ async def _await_request(self, request: Request, timeout: int) -> Any | None: except asyncio.TimeoutError as e: self._log.warning(f"Request timed out for {request}. Ending request.") self._end_request(request.req_id, success=False, exception=e) - return None + return default_value except ConnectionError as e: self._log.error(f"Connection error during {request}. Ending request.") self._end_request(request.req_id, success=False, exception=e) - return None + return default_value def _end_request( self, diff --git a/nautilus_trader/adapters/interactive_brokers/client/market_data.py b/nautilus_trader/adapters/interactive_brokers/client/market_data.py index 7d823cd31123..31cf9d80fdc1 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/market_data.py +++ b/nautilus_trader/adapters/interactive_brokers/client/market_data.py @@ -326,7 +326,7 @@ async def get_historical_bars( end_date_time: pd.Timestamp, duration: str, timeout: int = 60, - ) -> list[Bar] | None: + ) -> list[Bar]: """ Request and retrieve historical bar data for a specified bar type. @@ -347,7 +347,7 @@ async def get_historical_bars( Returns ------- - list[Bar] | ``None`` + list[Bar] """ # Ensure the requested `end_date_time` is in UTC and set formatDate=2 to ensure returned dates are in UTC. @@ -379,13 +379,13 @@ async def get_historical_bars( cancel=functools.partial(self._eclient.cancelHistoricalData, reqId=req_id), ) if not request: - return None + return [] self._log.debug(f"reqHistoricalData: {request.req_id=}, {contract=}") request.handle() - return await self._await_request(request, timeout) + return await self._await_request(request, timeout, default_value=[]) else: self._log.info(f"Request already exist for {request}") - return None + return [] async def get_historical_ticks( self, From 953c188fa9d79b1e057043bcd04008d06b558792 Mon Sep 17 00:00:00 2001 From: Benjamin Singleton Date: Thu, 2 May 2024 05:55:50 -0400 Subject: [PATCH 090/193] Fix IBKR reconnection after gateway/TWS disconnection (#1622) --- .../interactive_brokers/client/client.py | 56 ++++++++++++------- .../interactive_brokers/client/connection.py | 18 +----- .../interactive_brokers/client/test_client.py | 4 +- .../adapters/interactive_brokers/conftest.py | 2 +- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index c07425037cd0..37e00bd0a662 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -134,9 +134,9 @@ def __init__( self._account_ids: set[str] = set() # ConnectionMixin - self._reconnect_attempts: int = 0 - self._max_reconnect_attempts: int = int(os.getenv("IB_MAX_RECONNECT_ATTEMPTS", 0)) - self._indefinite_reconnect: bool = False if self._max_reconnect_attempts else True + self._connection_attempts: int = 0 + self._max_connection_attempts: int = int(os.getenv("IB_MAX_CONNECTION_ATTEMPTS", 0)) + self._indefinite_reconnect: bool = False if self._max_connection_attempts else True self._reconnect_delay: int = 5 # seconds # MarketDataMixin @@ -170,24 +170,38 @@ def _start(self) -> None: self._create_task(self._startup()) async def _startup(self): - try: - self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") - await self._connect() - self._start_tws_incoming_msg_reader() - self._start_internal_msg_queue_processor() - self._eclient.startApi() - # TWS/Gateway will send a managedAccounts message upon successful connection, - # which will set the `_is_ib_connected` event. This typically takes a few - # seconds, so we wait for it here. - await asyncio.wait_for(self._is_ib_connected.wait(), 15) - self._start_connection_watchdog() - self._is_client_ready.set() - except asyncio.TimeoutError: - self._log.error("Client failed to initialize. Connection timeout.") - self._stop() - except Exception as e: - self._log.exception("Unhandled exception in client startup", e) - self._stop() + while not self._is_ib_connected.is_set(): + try: + self._connection_attempts += 1 + if ( + not self._indefinite_reconnect + and self._connection_attempts > self._max_connection_attempts + ): + self._log.error("Max connection attempts reached. Connection failed.") + self._stop() + break + if self._connection_attempts > 1: + self._log.info( + f"Attempt {self._connection_attempts}: Attempting to reconnect in {self._reconnect_delay} seconds...", + ) + await asyncio.sleep(self._reconnect_delay) + self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") + await self._connect() + self._start_tws_incoming_msg_reader() + self._start_internal_msg_queue_processor() + self._eclient.startApi() + # TWS/Gateway will send a managedAccounts message upon successful connection, + # which will set the `_is_ib_connected` event. This typically takes a few + # seconds, so we wait for it here. + await asyncio.wait_for(self._is_ib_connected.wait(), 15) + self._start_connection_watchdog() + except asyncio.TimeoutError: + self._log.error("Client failed to initialize. Connection timeout.") + except Exception as e: + self._log.exception("Unhandled exception in client startup", e) + self._stop() + self._is_client_ready.set() + self._connection_attempts = 0 def _start_tws_incoming_msg_reader(self) -> None: """ diff --git a/nautilus_trader/adapters/interactive_brokers/client/connection.py b/nautilus_trader/adapters/interactive_brokers/client/connection.py index f76d1852f9a0..01f092907d7b 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/connection.py +++ b/nautilus_trader/adapters/interactive_brokers/client/connection.py @@ -92,23 +92,9 @@ async def _handle_reconnect(self) -> None: """ Attempt to reconnect to TWS/Gateway. """ - while not self._is_ib_connected.is_set(): - if ( - not self._indefinite_reconnect - and self._reconnect_attempts > self._max_reconnect_attempts - ): - self._log.error("Max reconnection attempts reached. Connection failed.") - self._stop() - break - self._reconnect_attempts += 1 - self._log.info( - f"Attempt {self._reconnect_attempts}: Attempting to reconnect in {self._reconnect_delay} seconds...", - ) - await asyncio.sleep(self._reconnect_delay) - await self._startup() - + await self._startup() self._log.info("Reconnection successful.") - self._reconnect_attempts = 0 + self._connection_attempts = 0 await self._resubscribe_all() self._resume() diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py index 7ddd17859a28..76ea2a665653 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py @@ -27,9 +27,9 @@ def test_start(ib_client): # Arrange - ib_client._is_ib_connected.set() ib_client._connect = AsyncMock() ib_client._eclient = MagicMock() + ib_client._eclient.startApi = MagicMock(side_effect=ib_client._is_ib_connected.set) # Act ib_client.start() @@ -59,9 +59,9 @@ def test_start_tasks(ib_client): def test_stop(ib_client): # Arrange - ib_client._is_ib_connected.set() ib_client._connect = AsyncMock() ib_client._eclient = MagicMock() + ib_client._eclient.startApi = MagicMock(side_effect=ib_client._is_ib_connected.set) ib_client.start() # Act diff --git a/tests/integration_tests/adapters/interactive_brokers/conftest.py b/tests/integration_tests/adapters/interactive_brokers/conftest.py index e182892f4e52..9b2c9d714c5f 100644 --- a/tests/integration_tests/adapters/interactive_brokers/conftest.py +++ b/tests/integration_tests/adapters/interactive_brokers/conftest.py @@ -103,9 +103,9 @@ def ib_client(data_client_config, event_loop, msgbus, cache, clock): @pytest.fixture() def ib_client_running(ib_client): - ib_client._is_ib_connected.set() ib_client._connect = AsyncMock() ib_client._eclient = MagicMock() + ib_client._eclient.startApi = MagicMock(side_effect=ib_client._is_ib_connected.set) ib_client._account_ids = {"DU123456,"} ib_client.start() yield ib_client From 0d3ddbc07c376136cd2441a2763c9e6ebc084956 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 2 May 2024 19:57:30 +1000 Subject: [PATCH 091/193] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index a38af0692d85..c9dafc661694 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -16,6 +16,7 @@ Released on TBD (UTC). - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z +- Fixed IBKR reconnection after gateway/TWS disconnection (#1622), thanks @benjaminsingleton - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) From d3fca70bcc14dfb8547410126a6e2a2e602f7ef2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 2 May 2024 20:09:11 +1000 Subject: [PATCH 092/193] Update dependencies --- nautilus_core/Cargo.lock | 44 ++++++++++++++++++++-------------------- nautilus_core/Cargo.toml | 4 ++-- poetry.lock | 15 +++----------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index fdd4bb5c492b..35b1c2ce5950 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -242,7 +242,7 @@ dependencies = [ "arrow-schema", "arrow-select", "atoi", - "base64 0.22.0", + "base64 0.22.1", "chrono", "comfy-table", "half", @@ -520,9 +520,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -599,9 +599,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" dependencies = [ "borsh-derive", "cfg_aliases", @@ -609,9 +609,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d" dependencies = [ "once_cell", "proc-macro-crate", @@ -736,9 +736,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" dependencies = [ "jobserver", "libc", @@ -1320,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd221792c666eac174ecc09e606312844772acc12cbec61a420c2fca1ee70959" dependencies = [ "arrow", - "base64 0.22.0", + "base64 0.22.1", "blake2", "blake3", "chrono", @@ -1369,7 +1369,7 @@ dependencies = [ "arrow-ord", "arrow-schema", "arrow-string", - "base64 0.22.0", + "base64 0.22.1", "blake2", "blake3", "chrono", @@ -3168,7 +3168,7 @@ dependencies = [ "arrow-ipc", "arrow-schema", "arrow-select", - "base64 0.22.0", + "base64 0.22.1", "brotli", "bytes", "chrono", @@ -3791,7 +3791,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -3884,9 +3884,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938a142ab806f18b88a97b0dea523d39e0fd730a064b035726adcfc58a8a5188" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ "byteorder", "rmp", @@ -4038,7 +4038,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] @@ -4138,18 +4138,18 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", @@ -5312,9 +5312,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" [[package]] name = "vcpkg" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 103034d92921..997e6e8b79bc 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -38,10 +38,10 @@ log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_ pyo3 = { version = "0.20.3", features = ["rust_decimal"] } pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" -rmp-serde = "1.2.0" +rmp-serde = "1.3.0" rust_decimal = "1.35.0" rust_decimal_macros = "1.34.2" -serde = { version = "1.0.199", features = ["derive"] } +serde = { version = "1.0.200", features = ["derive"] } serde_json = "1.0.116" strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.59" diff --git a/poetry.lock b/poetry.lock index f76f75f65911..ece69f53d73f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -464,7 +464,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = ">=3.6" +python-versions = "*" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -932,6 +932,7 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1510,6 +1511,7 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -1949,7 +1951,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1957,16 +1958,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1983,7 +1976,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1991,7 +1983,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, From c48a3d88354c81efa2a8e63c8b2d3581b9dce39b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 2 May 2024 20:19:57 +1000 Subject: [PATCH 093/193] Add Bar stub and builder --- nautilus_core/model/src/data/bar.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 8d3d50a0774b..14470b334751 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -22,6 +22,7 @@ use std::{ str::FromStr, }; +use derive_builder::Builder; use indexmap::IndexMap; use nautilus_core::{nanos::UnixNanos, serialization::Serializable}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -35,7 +36,7 @@ use crate::{ /// Represents a bar aggregation specification including a step, aggregation /// method/rule and price type. #[repr(C)] -#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] +#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize, Builder)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -310,6 +311,21 @@ pub mod stubs { types::{price::Price, quantity::Quantity}, }; + impl Default for Bar { + fn default() -> Self { + Self { + bar_type: BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL"), + open: Price::from("1.00010"), + high: Price::from("1.00020"), + low: Price::from("1.00000"), + close: Price::from("1.00010"), + volume: Quantity::from(100_000), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } + } + #[fixture] pub fn stub_bar() -> Bar { let instrument_id = InstrumentId { From 300998d3ca9db294a3d435a12af8b698d92372ea Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 2 May 2024 20:32:16 +1000 Subject: [PATCH 094/193] Add Cache tests in Rust --- nautilus_core/common/src/cache/mod.rs | 38 ++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index ea258059baaf..17865720e3c2 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -2393,7 +2393,7 @@ impl Cache { #[cfg(test)] mod tests { use nautilus_model::{ - data::{quote::QuoteTick, trade::TradeTick}, + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, instruments::{currency_pair::CurrencyPair, stubs::*}, }; use rstest::*; @@ -2568,4 +2568,40 @@ mod tests { let result = cache.trade_ticks(&trades[0].instrument_id); assert_eq!(result, Some(trades)); } + + #[rstest] + fn test_bar_when_empty() { + let cache = Cache::default(); + let bar = Bar::default(); + let result = cache.bar(&bar.bar_type); + assert_eq!(result, None); + } + + #[rstest] + fn test_bar_when_some() { + let mut cache = Cache::default(); + let bar = Bar::default(); + cache.add_bar(bar).unwrap(); + + let result = cache.bar(&bar.bar_type); + assert_eq!(result, Some(bar).as_ref()); + } + + #[rstest] + fn test_bars_when_empty() { + let cache = Cache::default(); + let bar = Bar::default(); + let result = cache.bars(&bar.bar_type); + assert_eq!(result, None); + } + + #[rstest] + fn test_bars_when_some() { + let mut cache = Cache::default(); + let bars = vec![Bar::default(), Bar::default(), Bar::default()]; + cache.add_bars(&bars).unwrap(); + + let result = cache.bars(&bars[0].bar_type); + assert_eq!(result, Some(bars)); + } } From bb32dc660696512eccb3a89e5f99e40d6e50eaec Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 11:47:05 +1000 Subject: [PATCH 095/193] Upgrade Rust --- README.md | 8 +++---- nautilus_core/Cargo.lock | 35 ++++++++++++++++++------------- nautilus_core/Cargo.toml | 2 +- nautilus_core/rust-toolchain.toml | 2 +- poetry.lock | 23 +++++++++++++------- pyproject.toml | 2 +- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 6a5ade00169a..2115a8dfb3da 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ | Platform | Rust | Python | | :----------------- | :------ | :----- | -| `Linux (x86_64)` | 1.77.2+ | 3.10+ | -| `macOS (x86_64)` | 1.77.2+ | 3.10+ | -| `macOS (arm64)` | 1.77.2+ | 3.10+ | -| `Windows (x86_64)` | 1.77.2+ | 3.10+ | +| `Linux (x86_64)` | 1.78.0+ | 3.10+ | +| `macOS (x86_64)` | 1.78.0+ | 3.10+ | +| `macOS (arm64)` | 1.78.0+ | 3.10+ | +| `Windows (x86_64)` | 1.78.0+ | 3.10+ | - **Website:** https://nautilustrader.io - **Docs:** https://docs.nautilustrader.io diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 35b1c2ce5950..cb435c6cce3c 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -95,47 +95,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -886,9 +887,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "colored" @@ -2267,6 +2268,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -3191,9 +3198,9 @@ dependencies = [ [[package]] name = "parse-zoneinfo" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" dependencies = [ "regex", ] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 997e6e8b79bc..b8d22e429c68 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -rust-version = "1.77.2" +rust-version = "1.78.0" version = "0.22.0" edition = "2021" authors = ["Nautech Systems "] diff --git a/nautilus_core/rust-toolchain.toml b/nautilus_core/rust-toolchain.toml index e8adc29550cf..2d0746aef1f4 100644 --- a/nautilus_core/rust-toolchain.toml +++ b/nautilus_core/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -version = "1.77.2" +version = "1.78.0" channel = "stable" diff --git a/poetry.lock b/poetry.lock index ece69f53d73f..0a4c54e932d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -464,7 +464,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -932,7 +932,6 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1511,7 +1510,6 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -1951,6 +1949,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1958,8 +1957,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1976,6 +1983,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1983,6 +1991,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2360,13 +2369,13 @@ files = [ [[package]] name = "tqdm" -version = "4.66.2" +version = "4.66.4" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, - {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, + {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, + {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, ] [package.dependencies] @@ -2677,4 +2686,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "0d885255610e911d220a81a1ac61da2453422a6dc9ef41d0d24c347c58d0d7cf" +content-hash = "22271ae8e66964ff23d6975f761241e2707a7e1022126c2da800ff899a1134cf" diff --git a/pyproject.toml b/pyproject.toml index 9da95ecc4e2b..61d350331b9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ msgspec = "^0.18.6" pandas = "^2.2.2" pyarrow = ">=16.0.0" pytz = ">=2023.4.0" -tqdm = "^4.66.2" +tqdm = "^4.66.4" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} async-timeout = {version = "^4.0.3", optional = true} From 617aeb2bd5a06787dbed8934d5515135366dc65d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 17:43:47 +1000 Subject: [PATCH 096/193] Wrap put_nowait with loop.call_soon_threadsafe --- nautilus_trader/adapters/interactive_brokers/client/client.py | 2 +- nautilus_trader/common/executor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 37e00bd0a662..cb9f2da1a24a 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -533,7 +533,7 @@ async def _run_tws_incoming_msg_reader(self) -> None: self._log.debug(f"Msg buffer received: {buf!s}") if msg: # Place msg in the internal queue for processing - self._internal_msg_queue.put_nowait(msg) + self._loop.call_soon_threadsafe(self._internal_msg_queue.put_nowait, msg) else: self._log.debug("More incoming packets are needed.") break diff --git a/nautilus_trader/common/executor.py b/nautilus_trader/common/executor.py index 4f178c457a46..d5bb4c7d7c7d 100644 --- a/nautilus_trader/common/executor.py +++ b/nautilus_trader/common/executor.py @@ -232,7 +232,7 @@ def queue_for_executor( """ task_id = TaskId.create() - self._queue.put_nowait((task_id, func, args, kwargs)) + self._loop.call_soon_threadsafe(self._queue.put_nowait, (task_id, func, args, kwargs)) self._queued_tasks.add(task_id) return task_id From 0726200404bc99eae928c0e8c914e36d21af11d0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 18:18:46 +1000 Subject: [PATCH 097/193] Add SyntheticInstrument stub and builder --- .../model/src/instruments/synthetic.rs | 83 +++++++++---------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/nautilus_core/model/src/instruments/synthetic.rs b/nautilus_core/model/src/instruments/synthetic.rs index bb9f76deb2f6..0f6b4363b5de 100644 --- a/nautilus_core/model/src/instruments/synthetic.rs +++ b/nautilus_core/model/src/instruments/synthetic.rs @@ -18,6 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; +use derive_builder::Builder; use evalexpr::{ContextWithMutableVariables, HashMapContext, Node, Value}; use nautilus_core::nanos::UnixNanos; @@ -28,7 +29,7 @@ use crate::{ /// Represents a synthetic instrument with prices derived from component instruments using a /// formula. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Builder)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -147,82 +148,72 @@ impl Hash for SyntheticInstrument { } } +//////////////////////////////////////////////////////////////////////////////// +// Stubs +/////////////////////////////////////////////////////////////////////////////// +#[cfg(feature = "stubs")] +pub mod stubs { + use super::*; + + impl Default for SyntheticInstrument { + fn default() -> Self { + let btc_binance = InstrumentId::from("BTC.BINANCE"); + let ltc_binance = InstrumentId::from("LTC.BINANCE"); + let formula = "(BTC.BINANCE + LTC.BINANCE) / 2.0".to_string(); + SyntheticInstrument::new( + Symbol::new("BTC-LTC").unwrap(), + 2, + vec![btc_binance, ltc_binance], + formula.clone(), + 0.into(), + 0.into(), + ) + .unwrap() + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +/////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { use rstest::rstest; use super::*; - use crate::identifiers::{instrument_id::InstrumentId, symbol::Symbol}; #[rstest] fn test_calculate_from_map() { - let btc_binance = InstrumentId::from("BTC.BINANCE"); - let ltc_binance = InstrumentId::from("LTC.BINANCE"); - let formula = "(BTC.BINANCE + LTC.BINANCE) / 2".to_string(); - let mut synth = SyntheticInstrument::new( - Symbol::new("BTC-LTC").unwrap(), - 2, - vec![btc_binance, ltc_binance], - formula.clone(), - 0.into(), - 0.into(), - ) - .unwrap(); - + let mut synth = SyntheticInstrument::default(); let mut inputs = HashMap::new(); inputs.insert("BTC.BINANCE".to_string(), 100.0); inputs.insert("LTC.BINANCE".to_string(), 200.0); - let price = synth.calculate_from_map(&inputs).unwrap(); assert_eq!(price.as_f64(), 150.0); - assert_eq!(synth.formula, formula); + assert_eq!( + synth.formula, + "(BTC.BINANCE + LTC.BINANCE) / 2.0".to_string() + ); } #[rstest] fn test_calculate() { - let btc_binance = InstrumentId::from("BTC.BINANCE"); - let ltc_binance = InstrumentId::from("LTC.BINANCE"); - let formula = "(BTC.BINANCE + LTC.BINANCE) / 2.0".to_string(); - let mut synth = SyntheticInstrument::new( - Symbol::new("BTC-LTC").unwrap(), - 2, - vec![btc_binance, ltc_binance], - formula.clone(), - 0.into(), - 0.into(), - ) - .unwrap(); - + let mut synth = SyntheticInstrument::default(); let inputs = vec![100.0, 200.0]; let price = synth.calculate(&inputs).unwrap(); - assert_eq!(price.as_f64(), 150.0); - assert_eq!(synth.formula, formula); } #[rstest] fn test_change_formula() { - let btc_binance = InstrumentId::from("BTC.BINANCE"); - let ltc_binance = InstrumentId::from("LTC.BINANCE"); - let formula = "(BTC.BINANCE + LTC.BINANCE) / 2".to_string(); - let mut synth = SyntheticInstrument::new( - Symbol::new("BTC-LTC").unwrap(), - 2, - vec![btc_binance, ltc_binance], - formula, - 0.into(), - 0.into(), - ) - .unwrap(); - + let mut synth = SyntheticInstrument::default(); let new_formula = "(BTC.BINANCE + LTC.BINANCE) / 4".to_string(); synth.change_formula(new_formula.clone()).unwrap(); let mut inputs = HashMap::new(); inputs.insert("BTC.BINANCE".to_string(), 100.0); inputs.insert("LTC.BINANCE".to_string(), 200.0); - let price = synth.calculate_from_map(&inputs).unwrap(); assert_eq!(price.as_f64(), 75.0); From a1c0200c72e8f7b03d543d71a97310882b8dff7f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 18:19:31 +1000 Subject: [PATCH 098/193] Add Cache tests in Rust --- nautilus_core/common/src/cache/mod.rs | 43 ++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 17865720e3c2..bfbd11a62943 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -2394,7 +2394,9 @@ impl Cache { mod tests { use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, - instruments::{currency_pair::CurrencyPair, stubs::*}, + instruments::{ + currency_pair::CurrencyPair, stubs::*, synthetic::SyntheticInstrument, InstrumentAny, + }, }; use rstest::*; @@ -2493,6 +2495,45 @@ mod tests { assert_eq!(result, Some(&value.as_slice()).copied()); } + #[rstest] + fn test_instrument_when_empty(audusd_sim: CurrencyPair) { + let cache = Cache::default(); + let result = cache.instrument(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_instrument_when_some(audusd_sim: CurrencyPair) { + let mut cache = Cache::default(); + cache + .add_instrument(InstrumentAny::CurrencyPair(audusd_sim)) + .unwrap(); + + let result = cache.instrument(&audusd_sim.id); + assert_eq!( + result, + Some(InstrumentAny::CurrencyPair(audusd_sim)).as_ref() + ); + } + + #[rstest] + fn test_synthetic_when_empty() { + let cache = Cache::default(); + let synth = SyntheticInstrument::default(); + let result = cache.synthetic(&synth.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_synthetic_when_some() { + let mut cache = Cache::default(); + let synth = SyntheticInstrument::default(); + cache.add_synthetic(synth.clone()).unwrap(); + + let result = cache.synthetic(&synth.id); + assert_eq!(result, Some(synth).as_ref()); + } + #[rstest] fn test_quote_tick_when_empty(audusd_sim: CurrencyPair) { let cache = Cache::default(); From 81afa3bef8b7f5a178a05b3505106dfe5b26ef98 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 18:25:33 +1000 Subject: [PATCH 099/193] Fix clippy lints --- .../adapters/src/databento/decode.rs | 44 +++++++++--------- nautilus_core/adapters/src/databento/live.rs | 13 +++--- .../adapters/src/databento/loader.rs | 17 ++++--- .../network/tokio-tungstenite/src/tls.rs | 45 +++++++++---------- .../persistence/src/backend/kmerge_batch.rs | 21 ++++----- 5 files changed, 65 insertions(+), 75 deletions(-) diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index 7720689b5c2f..ddb30cced6dc 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -294,12 +294,11 @@ pub fn decode_options_contract_v1( let currency_str = unsafe { raw_ptr_to_string(msg.currency.as_ptr())? }; let cfi_str = unsafe { raw_ptr_to_string(msg.cfi.as_ptr())? }; let exchange = unsafe { raw_ptr_to_ustr(msg.exchange.as_ptr())? }; - let asset_class_opt = match instrument_id.venue.as_str() { - "OPRA" => Some(AssetClass::Equity), - _ => { - let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; - asset_class - } + let asset_class_opt = if instrument_id.venue.as_str() == "OPRA" { + Some(AssetClass::Equity) + } else { + let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; + asset_class }; let underlying = unsafe { raw_ptr_to_ustr(msg.underlying.as_ptr())? }; let currency = Currency::from_str(¤cy_str)?; @@ -338,12 +337,11 @@ pub fn decode_options_spread_v1( let currency_str = unsafe { raw_ptr_to_string(msg.currency.as_ptr())? }; let cfi_str = unsafe { raw_ptr_to_string(msg.cfi.as_ptr())? }; let exchange = unsafe { raw_ptr_to_ustr(msg.exchange.as_ptr())? }; - let asset_class_opt = match instrument_id.venue.as_str() { - "OPRA" => Some(AssetClass::Equity), - _ => { - let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; - asset_class - } + let asset_class_opt = if instrument_id.venue.as_str() == "OPRA" { + Some(AssetClass::Equity) + } else { + let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; + asset_class }; let underlying = unsafe { raw_ptr_to_ustr(msg.underlying.as_ptr())? }; let strategy_type = unsafe { raw_ptr_to_ustr(msg.secsubtype.as_ptr())? }; @@ -890,12 +888,11 @@ pub fn decode_options_contract( let currency_str = unsafe { raw_ptr_to_string(msg.currency.as_ptr())? }; let cfi_str = unsafe { raw_ptr_to_string(msg.cfi.as_ptr())? }; let exchange = unsafe { raw_ptr_to_ustr(msg.exchange.as_ptr())? }; - let asset_class_opt = match instrument_id.venue.as_str() { - "OPRA" => Some(AssetClass::Equity), - _ => { - let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; - asset_class - } + let asset_class_opt = if instrument_id.venue.as_str() == "OPRA" { + Some(AssetClass::Equity) + } else { + let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; + asset_class }; let underlying = unsafe { raw_ptr_to_ustr(msg.underlying.as_ptr())? }; let currency = Currency::from_str(¤cy_str)?; @@ -933,12 +930,11 @@ pub fn decode_options_spread( ) -> anyhow::Result { let currency_str = unsafe { raw_ptr_to_string(msg.currency.as_ptr())? }; let cfi_str = unsafe { raw_ptr_to_string(msg.cfi.as_ptr())? }; - let asset_class_opt = match instrument_id.venue.as_str() { - "OPRA" => Some(AssetClass::Equity), - _ => { - let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; - asset_class - } + let asset_class_opt = if instrument_id.venue.as_str() == "OPRA" { + Some(AssetClass::Equity) + } else { + let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?; + asset_class }; let exchange = unsafe { raw_ptr_to_ustr(msg.exchange.as_ptr())? }; let underlying = unsafe { raw_ptr_to_ustr(msg.underlying.as_ptr())? }; diff --git a/nautilus_core/adapters/src/databento/live.rs b/nautilus_core/adapters/src/databento/live.rs index 626e1989109d..34563e86c3ec 100644 --- a/nautilus_core/adapters/src/databento/live.rs +++ b/nautilus_core/adapters/src/databento/live.rs @@ -124,13 +124,12 @@ impl DatabentoFeedHandler { .await?; info!("Connected"); - let mut client = match result { - Ok(client) => client, - Err(_) => { - self.msg_tx.send(LiveMessage::Close).await?; - self.cmd_rx.close(); - return Err(anyhow::anyhow!("Timeout connecting to LSG")); - } + let mut client = if let Ok(client) = result { + client + } else { + self.msg_tx.send(LiveMessage::Close).await?; + self.cmd_rx.close(); + return Err(anyhow::anyhow!("Timeout connecting to LSG")); }; // Timeout awaiting the next record before checking for a command diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs index de0631b9f0c3..5a4e679dca9c 100644 --- a/nautilus_core/adapters/src/databento/loader.rs +++ b/nautilus_core/adapters/src/databento/loader.rs @@ -76,15 +76,14 @@ impl DatabentoDataLoader { }; // Load publishers - let publishers_path = match path { - Some(p) => p, - None => { - // Use built-in publishers path - let mut exe_path = env::current_exe()?; - exe_path.pop(); - exe_path.push("publishers.json"); - exe_path - } + let publishers_path = if let Some(p) = path { + p + } else { + // Use built-in publishers path + let mut exe_path = env::current_exe()?; + exe_path.pop(); + exe_path.push("publishers.json"); + exe_path }; loader.load_publishers(publishers_path)?; diff --git a/nautilus_core/network/tokio-tungstenite/src/tls.rs b/nautilus_core/network/tokio-tungstenite/src/tls.rs index 76291c4ff572..5190e62fd22b 100644 --- a/nautilus_core/network/tokio-tungstenite/src/tls.rs +++ b/nautilus_core/network/tokio-tungstenite/src/tls.rs @@ -96,30 +96,29 @@ pub mod encryption { match mode { Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), Mode::Tls => { - let config = match tls_connector { - Some(config) => config, - None => { - #[allow(unused_mut)] - let mut root_store = RootCertStore::empty(); - #[cfg(feature = "rustls-tls-native-roots")] - { - let native_certs = rustls_native_certs::load_native_certs()?; - let total_number = native_certs.len(); - let (number_added, number_ignored) = - root_store.add_parsable_certificates(native_certs); - log::debug!("Added {number_added}/{total_number} native root certificates (ignored {number_ignored})"); - } - #[cfg(feature = "rustls-tls-webpki-roots")] - { - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - } - - Arc::new( - ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(), - ) + let config = if let Some(config) = tls_connector { + config + } else { + #[allow(unused_mut)] + let mut root_store = RootCertStore::empty(); + #[cfg(feature = "rustls-tls-native-roots")] + { + let native_certs = rustls_native_certs::load_native_certs()?; + let total_number = native_certs.len(); + let (number_added, number_ignored) = + root_store.add_parsable_certificates(native_certs); + log::debug!("Added {number_added}/{total_number} native root certificates (ignored {number_ignored})"); } + #[cfg(feature = "rustls-tls-webpki-roots")] + { + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + } + + Arc::new( + ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(), + ) }; let domain = ServerName::try_from(domain.as_str()) .map_err(|_| TlsError::InvalidDnsName)? diff --git a/nautilus_core/persistence/src/backend/kmerge_batch.rs b/nautilus_core/persistence/src/backend/kmerge_batch.rs index e921ab392ce1..7e9834e76259 100644 --- a/nautilus_core/persistence/src/backend/kmerge_batch.rs +++ b/nautilus_core/persistence/src/backend/kmerge_batch.rs @@ -141,8 +141,8 @@ where // Otherwise get the next batch and the element from it // Unless the underlying iterator is exhausted None => loop { - match heap_elem.iter.next() { - Some(mut batch) => match batch.next() { + if let Some(mut batch) = heap_elem.iter.next() { + match batch.next() { Some(mut item) => { heap_elem.batch = batch; std::mem::swap(&mut item, &mut heap_elem.item); @@ -150,17 +150,14 @@ where } // Get next batch from iterator None => continue, - }, - // Iterator has no more batches return current element - // and pop the heap element - None => { - let ElementBatchIter { - item, - batch: _, - iter: _, - } = PeekMut::pop(heap_elem); - break Some(item); } + } else { + let ElementBatchIter { + item, + batch: _, + iter: _, + } = PeekMut::pop(heap_elem); + break Some(item); } }, } From 9fa7aa393b6bf7a135ba5576f23251499c89088f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 18:33:24 +1000 Subject: [PATCH 100/193] Refine Cache tests with fixture --- nautilus_core/common/src/cache/mod.rs | 102 +++++++++----------------- 1 file changed, 35 insertions(+), 67 deletions(-) diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index bfbd11a62943..0f6cf272d413 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -2402,109 +2402,97 @@ mod tests { use super::Cache; + #[fixture] + fn cache() -> Cache { + Cache::default() + } + #[rstest] - fn test_build_index_when_empty() { - let mut cache = Cache::default(); + fn test_build_index_when_empty(mut cache: Cache) { cache.build_index(); } #[rstest] - fn test_clear_index_when_empty() { - let mut cache = Cache::default(); + fn test_clear_index_when_empty(mut cache: Cache) { cache.clear_index(); } #[rstest] - fn test_reset_when_empty() { - let mut cache = Cache::default(); + fn test_reset_when_empty(mut cache: Cache) { cache.reset(); } #[rstest] - fn test_dispose_when_empty() { - let cache = Cache::default(); + fn test_dispose_when_empty(cache: Cache) { let result = cache.dispose(); assert!(result.is_ok()); } #[rstest] - fn test_flush_db_when_empty() { - let cache = Cache::default(); + fn test_flush_db_when_empty(cache: Cache) { let result = cache.flush_db(); assert!(result.is_ok()); } #[rstest] - fn test_check_residuals_when_empty() { - let cache = Cache::default(); + fn test_check_residuals_when_empty(cache: Cache) { let result = cache.flush_db(); assert!(result.is_ok()); } #[rstest] - fn test_cache_general_load_when_no_database() { - let mut cache = Cache::default(); + fn test_cache_general_load_when_no_database(mut cache: Cache) { assert!(cache.cache_general().is_ok()); } #[rstest] - fn test_cache_currencies_load_when_no_database() { - let mut cache = Cache::default(); + fn test_cache_currencies_load_when_no_database(mut cache: Cache) { assert!(cache.cache_currencies().is_ok()); } #[rstest] - fn test_cache_instruments_load_when_no_database() { - let mut cache = Cache::default(); + fn test_cache_instruments_load_when_no_database(mut cache: Cache) { assert!(cache.cache_instruments().is_ok()); } #[rstest] - fn test_cache_synthetics_when_no_database() { - let mut cache = Cache::default(); + fn test_cache_synthetics_when_no_database(mut cache: Cache) { assert!(cache.cache_synthetics().is_ok()); } #[rstest] - fn test_cache_orders_when_no_database() { - let mut cache = Cache::default(); + fn test_cache_orders_when_no_database(mut cache: Cache) { assert!(cache.cache_orders().is_ok()); } #[rstest] - fn test_cache_positions_when_no_database() { - let mut cache = Cache::default(); + fn test_cache_positions_when_no_database(mut cache: Cache) { assert!(cache.cache_positions().is_ok()); } #[rstest] - fn test_get_general_when_empty() { - let cache = Cache::default(); + fn test_get_general_when_empty(cache: Cache) { let result = cache.get("A").unwrap(); assert_eq!(result, None); } #[rstest] - fn test_add_general_when_value() { - let mut cache = Cache::default(); + fn test_add_general_when_value(mut cache: Cache) { let key = "A"; let value = vec![0_u8]; cache.add(key, value.clone()).unwrap(); - let result = cache.get(key).unwrap(); assert_eq!(result, Some(&value.as_slice()).copied()); } #[rstest] - fn test_instrument_when_empty(audusd_sim: CurrencyPair) { - let cache = Cache::default(); + fn test_instrument_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.instrument(&audusd_sim.id); assert_eq!(result, None); } #[rstest] - fn test_instrument_when_some(audusd_sim: CurrencyPair) { - let mut cache = Cache::default(); + fn test_instrument_when_some(mut cache: Cache, audusd_sim: CurrencyPair) { cache .add_instrument(InstrumentAny::CurrencyPair(audusd_sim)) .unwrap(); @@ -2517,131 +2505,111 @@ mod tests { } #[rstest] - fn test_synthetic_when_empty() { - let cache = Cache::default(); + fn test_synthetic_when_empty(cache: Cache) { let synth = SyntheticInstrument::default(); let result = cache.synthetic(&synth.id); assert_eq!(result, None); } #[rstest] - fn test_synthetic_when_some() { + fn test_synthetic_when_some(cache: Cache) { let mut cache = Cache::default(); let synth = SyntheticInstrument::default(); cache.add_synthetic(synth.clone()).unwrap(); - let result = cache.synthetic(&synth.id); assert_eq!(result, Some(synth).as_ref()); } #[rstest] - fn test_quote_tick_when_empty(audusd_sim: CurrencyPair) { - let cache = Cache::default(); + fn test_quote_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.quote_tick(&audusd_sim.id); assert_eq!(result, None); } #[rstest] - fn test_quote_tick_when_some() { - let mut cache = Cache::default(); + fn test_quote_tick_when_some(mut cache: Cache) { let quote = QuoteTick::default(); cache.add_quote(quote).unwrap(); - let result = cache.quote_tick("e.instrument_id); assert_eq!(result, Some("e)); } #[rstest] - fn test_quote_ticks_when_empty(audusd_sim: CurrencyPair) { - let cache = Cache::default(); + fn test_quote_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.quote_ticks(&audusd_sim.id); assert_eq!(result, None); } #[rstest] - fn test_quote_ticks_when_some() { - let mut cache = Cache::default(); + fn test_quote_ticks_when_some(mut cache: Cache) { let quotes = vec![ QuoteTick::default(), QuoteTick::default(), QuoteTick::default(), ]; cache.add_quotes("es).unwrap(); - let result = cache.quote_ticks("es[0].instrument_id); assert_eq!(result, Some(quotes)); } #[rstest] - fn test_trade_tick_when_empty(audusd_sim: CurrencyPair) { - let cache = Cache::default(); + fn test_trade_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.trade_tick(&audusd_sim.id); assert_eq!(result, None); } #[rstest] - fn test_trade_tick_when_some() { - let mut cache = Cache::default(); + fn test_trade_tick_when_some(mut cache: Cache) { let trade = TradeTick::default(); cache.add_trade(trade).unwrap(); - let result = cache.trade_tick(&trade.instrument_id); assert_eq!(result, Some(&trade)); } #[rstest] - fn test_trade_ticks_when_empty(audusd_sim: CurrencyPair) { - let cache = Cache::default(); + fn test_trade_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.trade_ticks(&audusd_sim.id); assert_eq!(result, None); } #[rstest] - fn test_trade_ticks_when_some() { - let mut cache = Cache::default(); + fn test_trade_ticks_when_some(mut cache: Cache) { let trades = vec![ TradeTick::default(), TradeTick::default(), TradeTick::default(), ]; cache.add_trades(&trades).unwrap(); - let result = cache.trade_ticks(&trades[0].instrument_id); assert_eq!(result, Some(trades)); } #[rstest] - fn test_bar_when_empty() { - let cache = Cache::default(); + fn test_bar_when_empty(cache: Cache) { let bar = Bar::default(); let result = cache.bar(&bar.bar_type); assert_eq!(result, None); } #[rstest] - fn test_bar_when_some() { - let mut cache = Cache::default(); + fn test_bar_when_some(mut cache: Cache) { let bar = Bar::default(); cache.add_bar(bar).unwrap(); - let result = cache.bar(&bar.bar_type); assert_eq!(result, Some(bar).as_ref()); } #[rstest] - fn test_bars_when_empty() { - let cache = Cache::default(); + fn test_bars_when_empty(cache: Cache) { let bar = Bar::default(); let result = cache.bars(&bar.bar_type); assert_eq!(result, None); } #[rstest] - fn test_bars_when_some() { - let mut cache = Cache::default(); + fn test_bars_when_some(mut cache: Cache) { let bars = vec![Bar::default(), Bar::default(), Bar::default()]; cache.add_bars(&bars).unwrap(); - let result = cache.bars(&bars[0].bar_type); assert_eq!(result, Some(bars)); } From 88b1229a392bbaec48a6f01fb3db892578d54269 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 19:10:37 +1000 Subject: [PATCH 101/193] Reorganize cache and msgbus modules --- nautilus_core/common/src/cache/core.rs | 2608 +++++++++++++++++ nautilus_core/common/src/cache/mod.rs | 2595 +--------------- nautilus_core/common/src/msgbus/core.rs | 638 ++++ nautilus_core/common/src/msgbus/mod.rs | 625 +--- .../infrastructure/src/redis/msgbus.rs | 2 +- 5 files changed, 3251 insertions(+), 3217 deletions(-) create mode 100644 nautilus_core/common/src/cache/core.rs create mode 100644 nautilus_core/common/src/msgbus/core.rs diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs new file mode 100644 index 000000000000..0519b8440cbd --- /dev/null +++ b/nautilus_core/common/src/cache/core.rs @@ -0,0 +1,2608 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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::{ + collections::{HashMap, HashSet, VecDeque}, + time::{SystemTime, UNIX_EPOCH}, +}; + +use log::{debug, error, info, warn}; +use nautilus_core::correctness::{check_key_not_in_map, check_slice_not_empty, check_valid_string}; +use nautilus_model::{ + data::{ + bar::{Bar, BarType}, + quote::QuoteTick, + trade::TradeTick, + }, + enums::{AggregationSource, OmsType, OrderSide, PositionSide, PriceType, TriggerType}, + identifiers::{ + account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, + component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, + order_list_id::OrderListId, position_id::PositionId, strategy_id::StrategyId, venue::Venue, + venue_order_id::VenueOrderId, + }, + instruments::{synthetic::SyntheticInstrument, InstrumentAny}, + orderbook::book::OrderBook, + orders::{base::OrderAny, list::OrderList}, + polymorphism::{ + GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, + GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetPositionId, + GetStrategyId, GetVenueOrderId, IsClosed, IsInflight, IsOpen, + }, + position::Position, + types::{currency::Currency, price::Price, quantity::Quantity}, +}; +use ustr::Ustr; + +use super::database::CacheDatabaseAdapter; +use crate::{enums::SerializationEncoding, interface::account::Account}; + +/// The configuration for `Cache` instances. +pub struct CacheConfig { + pub encoding: SerializationEncoding, + pub timestamps_as_iso8601: bool, + pub use_trader_prefix: bool, + pub use_instance_id: bool, + pub flush_on_start: bool, + pub drop_instruments_on_reset: bool, + pub tick_capacity: usize, + pub bar_capacity: usize, +} + +impl CacheConfig { + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn new( + encoding: SerializationEncoding, + timestamps_as_iso8601: bool, + use_trader_prefix: bool, + use_instance_id: bool, + flush_on_start: bool, + drop_instruments_on_reset: bool, + tick_capacity: usize, + bar_capacity: usize, + ) -> Self { + Self { + encoding, + timestamps_as_iso8601, + use_trader_prefix, + use_instance_id, + flush_on_start, + drop_instruments_on_reset, + tick_capacity, + bar_capacity, + } + } +} + +impl Default for CacheConfig { + fn default() -> Self { + Self::new( + SerializationEncoding::MsgPack, + false, + true, + false, + false, + true, + 10_000, + 10_000, + ) + } +} + +/// A key-value lookup index for a `Cache`. +pub struct CacheIndex { + venue_account: HashMap, + venue_orders: HashMap>, + venue_positions: HashMap>, + order_ids: HashMap, + order_position: HashMap, + order_strategy: HashMap, + order_client: HashMap, + position_strategy: HashMap, + position_orders: HashMap>, + instrument_orders: HashMap>, + instrument_positions: HashMap>, + strategy_orders: HashMap>, + strategy_positions: HashMap>, + exec_algorithm_orders: HashMap>, + exec_spawn_orders: HashMap>, + orders: HashSet, + orders_open: HashSet, + orders_closed: HashSet, + orders_emulated: HashSet, + orders_inflight: HashSet, + orders_pending_cancel: HashSet, + positions: HashSet, + positions_open: HashSet, + positions_closed: HashSet, + actors: HashSet, + strategies: HashSet, + exec_algorithms: HashSet, +} + +impl CacheIndex { + /// Clear the index which will clear/reset all internal state. + pub fn clear(&mut self) { + self.venue_account.clear(); + self.venue_orders.clear(); + self.venue_positions.clear(); + self.order_ids.clear(); + self.order_position.clear(); + self.order_strategy.clear(); + self.order_client.clear(); + self.position_strategy.clear(); + self.position_orders.clear(); + self.instrument_orders.clear(); + self.instrument_positions.clear(); + self.strategy_orders.clear(); + self.strategy_positions.clear(); + self.exec_algorithm_orders.clear(); + self.exec_spawn_orders.clear(); + self.orders.clear(); + self.orders_open.clear(); + self.orders_closed.clear(); + self.orders_emulated.clear(); + self.orders_inflight.clear(); + self.orders_pending_cancel.clear(); + self.positions.clear(); + self.positions_open.clear(); + self.positions_closed.clear(); + self.actors.clear(); + self.strategies.clear(); + self.exec_algorithms.clear(); + } +} + +/// A common in-memory `Cache` for market and execution related data. +pub struct Cache { + config: CacheConfig, + index: CacheIndex, + database: Option, + general: HashMap>, + quotes: HashMap>, + trades: HashMap>, + books: HashMap, + bars: HashMap>, + currencies: HashMap, + instruments: HashMap, + synthetics: HashMap, + accounts: HashMap>, + orders: HashMap, + order_lists: HashMap, + positions: HashMap, + position_snapshots: HashMap>, +} + +impl Default for Cache { + fn default() -> Self { + Self::new(CacheConfig::default(), None) + } +} + +impl Cache { + #[must_use] + pub fn new(config: CacheConfig, database: Option) -> Self { + let index = CacheIndex { + venue_account: HashMap::new(), + venue_orders: HashMap::new(), + venue_positions: HashMap::new(), + order_ids: HashMap::new(), + order_position: HashMap::new(), + order_strategy: HashMap::new(), + order_client: HashMap::new(), + position_strategy: HashMap::new(), + position_orders: HashMap::new(), + instrument_orders: HashMap::new(), + instrument_positions: HashMap::new(), + strategy_orders: HashMap::new(), + strategy_positions: HashMap::new(), + exec_algorithm_orders: HashMap::new(), + exec_spawn_orders: HashMap::new(), + orders: HashSet::new(), + orders_open: HashSet::new(), + orders_closed: HashSet::new(), + orders_emulated: HashSet::new(), + orders_inflight: HashSet::new(), + orders_pending_cancel: HashSet::new(), + positions: HashSet::new(), + positions_open: HashSet::new(), + positions_closed: HashSet::new(), + actors: HashSet::new(), + strategies: HashSet::new(), + exec_algorithms: HashSet::new(), + }; + + Self { + config, + index, + database, + general: HashMap::new(), + quotes: HashMap::new(), + trades: HashMap::new(), + books: HashMap::new(), + bars: HashMap::new(), + currencies: HashMap::new(), + instruments: HashMap::new(), + synthetics: HashMap::new(), + accounts: HashMap::new(), + orders: HashMap::new(), + order_lists: HashMap::new(), + positions: HashMap::new(), + position_snapshots: HashMap::new(), + } + } + + // -- COMMANDS -------------------------------------------------------------------------------- + + /// Clear the current general cache and load the general objects from the cache database. + pub fn cache_general(&mut self) -> anyhow::Result<()> { + self.general = match &self.database { + Some(db) => db.load()?, + None => HashMap::new(), + }; + + info!( + "Cached {} general object(s) from database", + self.general.len() + ); + Ok(()) + } + + /// Clear the current currencies cache and load currencies from the cache database. + pub fn cache_currencies(&mut self) -> anyhow::Result<()> { + self.currencies = match &self.database { + Some(db) => db.load_currencies()?, + None => HashMap::new(), + }; + + info!("Cached {} currencies from database", self.general.len()); + Ok(()) + } + + /// Clear the current instruments cache and load instruments from the cache database. + pub fn cache_instruments(&mut self) -> anyhow::Result<()> { + self.instruments = match &self.database { + Some(db) => db.load_instruments()?, + None => HashMap::new(), + }; + + info!("Cached {} instruments from database", self.general.len()); + Ok(()) + } + + /// Clear the current synthetic instruments cache and load synthetic instruments from the cache + /// database. + pub fn cache_synthetics(&mut self) -> anyhow::Result<()> { + self.synthetics = match &self.database { + Some(db) => db.load_synthetics()?, + None => HashMap::new(), + }; + + info!( + "Cached {} synthetic instruments from database", + self.general.len() + ); + Ok(()) + } + + /// Clear the current accounts cache and load accounts from the cache database. + pub fn cache_accounts(&mut self) -> anyhow::Result<()> { + self.accounts = match &self.database { + Some(db) => db.load_accounts()?, + None => HashMap::new(), + }; + + info!( + "Cached {} synthetic instruments from database", + self.general.len() + ); + Ok(()) + } + + /// Clear the current orders cache and load orders from the cache database. + pub fn cache_orders(&mut self) -> anyhow::Result<()> { + self.orders = match &self.database { + Some(db) => db.load_orders()?, + None => HashMap::new(), + }; + + info!("Cached {} orders from database", self.general.len()); + Ok(()) + } + + /// Clear the current positions cache and load positions from the cache database. + pub fn cache_positions(&mut self) -> anyhow::Result<()> { + self.positions = match &self.database { + Some(db) => db.load_positions()?, + None => HashMap::new(), + }; + + info!("Cached {} positions from database", self.general.len()); + Ok(()) + } + + /// Clear the current cache index and re-build. + pub fn build_index(&mut self) { + self.index.clear(); + debug!("Building index"); + + // Index accounts + for account_id in self.accounts.keys() { + self.index + .venue_account + .insert(account_id.get_issuer(), *account_id); + } + + // Index orders + for (client_order_id, order) in &self.orders { + let instrument_id = order.instrument_id(); + let venue = instrument_id.venue; + let strategy_id = order.strategy_id(); + + // 1: Build index.venue_orders -> {Venue, {ClientOrderId}} + self.index + .venue_orders + .entry(venue) + .or_default() + .insert(*client_order_id); + + // 2: Build index.order_ids -> {VenueOrderId, ClientOrderId} + if let Some(venue_order_id) = order.venue_order_id() { + self.index + .order_ids + .insert(venue_order_id, *client_order_id); + } + + // 3: Build index.order_position -> {ClientOrderId, PositionId} + if let Some(position_id) = order.position_id() { + self.index + .order_position + .insert(*client_order_id, position_id); + } + + // 4: Build index.order_strategy -> {ClientOrderId, StrategyId} + self.index + .order_strategy + .insert(*client_order_id, order.strategy_id()); + + // 5: Build index.instrument_orders -> {InstrumentId, {ClientOrderId}} + self.index + .instrument_orders + .entry(instrument_id) + .or_default() + .insert(*client_order_id); + + // 6: Build index.strategy_orders -> {StrategyId, {ClientOrderId}} + self.index + .strategy_orders + .entry(strategy_id) + .or_default() + .insert(*client_order_id); + + // 7: Build index.exec_algorithm_orders -> {ExecAlgorithmId, {ClientOrderId}} + if let Some(exec_algorithm_id) = order.exec_algorithm_id() { + self.index + .exec_algorithm_orders + .entry(exec_algorithm_id) + .or_default() + .insert(*client_order_id); + } + + // 8: Build index.exec_spawn_orders -> {ClientOrderId, {ClientOrderId}} + if let Some(exec_spawn_id) = order.exec_spawn_id() { + self.index + .exec_spawn_orders + .entry(exec_spawn_id) + .or_default() + .insert(*client_order_id); + } + + // 9: Build index.orders -> {ClientOrderId} + self.index.orders.insert(*client_order_id); + + // 10: Build index.orders_open -> {ClientOrderId} + if order.is_open() { + self.index.orders_open.insert(*client_order_id); + } + + // 11: Build index.orders_closed -> {ClientOrderId} + if order.is_closed() { + self.index.orders_closed.insert(*client_order_id); + } + + // 12: Build index.orders_emulated -> {ClientOrderId} + if let Some(emulation_trigger) = order.emulation_trigger() { + if emulation_trigger != TriggerType::NoTrigger && !order.is_closed() { + self.index.orders_emulated.insert(*client_order_id); + } + } + + // 13: Build index.orders_inflight -> {ClientOrderId} + if order.is_inflight() { + self.index.orders_inflight.insert(*client_order_id); + } + + // 14: Build index.strategies -> {StrategyId} + self.index.strategies.insert(strategy_id); + + // 15: Build index.strategies -> {ExecAlgorithmId} + if let Some(exec_algorithm_id) = order.exec_algorithm_id() { + self.index.exec_algorithms.insert(exec_algorithm_id); + } + } + + // Index positions + for (position_id, position) in &self.positions { + let instrument_id = position.instrument_id; + let venue = instrument_id.venue; + let strategy_id = position.strategy_id; + + // 1: Build index.venue_positions -> {Venue, {PositionId}} + self.index + .venue_positions + .entry(venue) + .or_default() + .insert(*position_id); + + // 2: Build index.position_strategy -> {PositionId, StrategyId} + self.index + .position_strategy + .insert(*position_id, position.strategy_id); + + // 3: Build index.position_orders -> {PositionId, {ClientOrderId}} + self.index + .position_orders + .entry(*position_id) + .or_default() + .extend(position.client_order_ids().into_iter()); + + // 4: Build index.instrument_positions -> {InstrumentId, {PositionId}} + self.index + .instrument_positions + .entry(instrument_id) + .or_default() + .insert(*position_id); + + // 5: Build index.strategy_positions -> {StrategyId, {PositionId}} + self.index + .strategy_positions + .entry(strategy_id) + .or_default() + .insert(*position_id); + + // 6: Build index.positions -> {PositionId} + self.index.positions.insert(*position_id); + + // 7: Build index.positions_open -> {PositionId} + if position.is_open() { + self.index.positions_open.insert(*position_id); + } + + // 8: Build index.positions_closed -> {PositionId} + if position.is_closed() { + self.index.positions_closed.insert(*position_id); + } + + // 9: Build index.strategies -> {StrategyId} + self.index.strategies.insert(strategy_id); + } + } + + /// Check integrity of data within the cache. + /// + /// All data should be loaded from the database prior to this call. + /// If an error is found then a log error message will also be produced. + #[must_use] + fn check_integrity(&mut self) -> bool { + let mut error_count = 0; + let failure = "Integrity failure"; + + // Get current timestamp in microseconds + let timestamp_us = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros(); + + info!("Checking data integrity"); + + // Check object caches + for account_id in self.accounts.keys() { + if !self + .index + .venue_account + .contains_key(&account_id.get_issuer()) + { + error!( + "{} in accounts: {} not found in `self.index.venue_account`", + failure, account_id + ); + error_count += 1; + } + } + + for (client_order_id, order) in &self.orders { + if !self.index.order_strategy.contains_key(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.order_strategy`", + failure, client_order_id + ); + error_count += 1; + } + if !self.index.orders.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders`", + failure, client_order_id + ); + error_count += 1; + } + if order.is_inflight() && !self.index.orders_inflight.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders_inflight`", + failure, client_order_id + ); + error_count += 1; + } + if order.is_open() && !self.index.orders_open.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders_open`", + failure, client_order_id + ); + error_count += 1; + } + if order.is_closed() && !self.index.orders_closed.contains(client_order_id) { + error!( + "{} in orders: {} not found in `self.index.orders_closed`", + failure, client_order_id + ); + error_count += 1; + } + if let Some(exec_algorithm_id) = order.exec_algorithm_id() { + if !self + .index + .exec_algorithm_orders + .contains_key(&exec_algorithm_id) + { + error!( + "{} in orders: {} not found in `self.index.exec_algorithm_orders`", + failure, exec_algorithm_id + ); + error_count += 1; + } + if order.exec_spawn_id().is_none() + && !self.index.exec_spawn_orders.contains_key(client_order_id) + { + error!( + "{} in orders: {} not found in `self.index.exec_spawn_orders`", + failure, exec_algorithm_id + ); + error_count += 1; + } + } + } + + for (position_id, position) in &self.positions { + if !self.index.position_strategy.contains_key(position_id) { + error!( + "{} in positions: {} not found in `self.index.position_strategy`", + failure, position_id + ); + error_count += 1; + } + if !self.index.position_orders.contains_key(position_id) { + error!( + "{} in positions: {} not found in `self.index.position_orders`", + failure, position_id + ); + error_count += 1; + } + if !self.index.positions.contains(position_id) { + error!( + "{} in positions: {} not found in `self.index.positions`", + failure, position_id + ); + error_count += 1; + } + if position.is_open() && !self.index.positions_open.contains(position_id) { + error!( + "{} in positions: {} not found in `self.index.positions_open`", + failure, position_id + ); + error_count += 1; + } + if position.is_closed() && !self.index.positions_closed.contains(position_id) { + error!( + "{} in positions: {} not found in `self.index.positions_closed`", + failure, position_id + ); + error_count += 1; + } + } + + // Check indexes + for account_id in self.index.venue_account.values() { + if !self.accounts.contains_key(account_id) { + error!( + "{} in `index.venue_account`: {} not found in `self.accounts`", + failure, account_id + ); + error_count += 1; + } + } + + for client_order_id in self.index.order_ids.values() { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.order_ids`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in self.index.order_position.keys() { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.order_position`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + // Check indexes + for client_order_id in self.index.order_strategy.keys() { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.order_strategy`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for position_id in self.index.position_strategy.keys() { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.position_strategy`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for position_id in self.index.position_orders.keys() { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.position_orders`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for (instrument_id, client_order_ids) in &self.index.instrument_orders { + for client_order_id in client_order_ids { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.instrument_orders`: {} not found in `self.orders`", + failure, instrument_id + ); + error_count += 1; + } + } + } + + for instrument_id in self.index.instrument_positions.keys() { + if !self.index.instrument_orders.contains_key(instrument_id) { + error!( + "{} in `index.instrument_positions`: {} not found in `index.instrument_orders`", + failure, instrument_id + ); + error_count += 1; + } + } + + for client_order_ids in self.index.strategy_orders.values() { + for client_order_id in client_order_ids { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.strategy_orders`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + } + + for position_ids in self.index.strategy_positions.values() { + for position_id in position_ids { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.strategy_positions`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + } + + for client_order_id in &self.index.orders { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_emulated { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_emulated`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_inflight { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_inflight`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_open { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_open`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in &self.index.orders_closed { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.orders_closed`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for position_id in &self.index.positions { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.positions`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for position_id in &self.index.positions_open { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.positions_open`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for position_id in &self.index.positions_closed { + if !self.positions.contains_key(position_id) { + error!( + "{} in `index.positions_closed`: {} not found in `self.positions`", + failure, position_id + ); + error_count += 1; + } + } + + for strategy_id in &self.index.strategies { + if !self.index.strategy_orders.contains_key(strategy_id) { + error!( + "{} in `index.strategies`: {} not found in `index.strategy_orders`", + failure, strategy_id + ); + error_count += 1; + } + } + + for exec_algorithm_id in &self.index.exec_algorithms { + if !self + .index + .exec_algorithm_orders + .contains_key(exec_algorithm_id) + { + error!( + "{} in `index.exec_algorithms`: {} not found in `index.exec_algorithm_orders`", + failure, exec_algorithm_id + ); + error_count += 1; + } + } + + // Finally + let total_us = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros() + - timestamp_us; + + if error_count == 0 { + info!("Integrity check passed in {}μs", total_us); + true + } else { + error!( + "Integrity check failed with {} error{} in {}μs", + error_count, + if error_count == 1 { "" } else { "s" }, + total_us + ); + false + } + } + + /// Check for any residual open state and log warnings if any are found. + /// + ///'Open state' is considered to be open orders and open positions. + #[must_use] + pub fn check_residuals(&self) -> bool { + debug!("Checking residuals"); + + let mut residuals = false; + + // Check for any open orders + for order in self.orders_open(None, None, None, None) { + residuals = true; + warn!("Residual {:?}", order); + } + + // Check for any open positions + for position in self.positions_open(None, None, None, None) { + residuals = true; + warn!("Residual {}", position); + } + + residuals + } + + /// Clear the caches index. + pub fn clear_index(&mut self) { + self.index.clear(); + debug!("Cleared index"); + } + + /// Reset the cache. + /// + /// All stateful fields are reset to their initial value. + pub fn reset(&mut self) { + debug!("Resetting cache"); + + self.general.clear(); + self.quotes.clear(); + self.trades.clear(); + self.books.clear(); + self.bars.clear(); + self.instruments.clear(); + self.synthetics.clear(); + self.accounts.clear(); + self.orders.clear(); + // self.order_lists.clear(); // TODO + self.positions.clear(); + self.position_snapshots.clear(); + + self.clear_index(); + + info!("Reset cache"); + } + + /// Dispose of the cache which will close any underlying database adapter. + pub fn dispose(&self) -> anyhow::Result<()> { + if let Some(database) = &self.database { + // TODO: Log operations in database adapter + database.close()?; + } + Ok(()) + } + + /// Flush the caches database which permanently removes all persisted data. + pub fn flush_db(&self) -> anyhow::Result<()> { + if let Some(database) = &self.database { + // TODO: Log operations in database adapter + database.flush()?; + } + Ok(()) + } + + /// Add the given general object to the cache. + /// + /// The cache is agnostic to what the object actually is (and how it may be serialized), + /// offering maximum flexibility. + pub fn add(&mut self, key: &str, value: Vec) -> anyhow::Result<()> { + check_valid_string(key, stringify!(key))?; + check_slice_not_empty(value.as_slice(), stringify!(value))?; + + debug!("Add general {key}"); + self.general.insert(key.to_string(), value.clone()); + + if let Some(database) = &self.database { + database.add(key.to_string(), value)?; + } + Ok(()) + } + + /// Add the given order `book` to the cache. + pub fn add_order_book(&mut self, book: OrderBook) -> anyhow::Result<()> { + debug!("Add `OrderBook` {}", book.instrument_id); + self.books.insert(book.instrument_id, book); + Ok(()) + } + + /// Add the given `quote` tick to the cache. + pub fn add_quote(&mut self, quote: QuoteTick) -> anyhow::Result<()> { + debug!("Add `QuoteTick` {}", quote.instrument_id); + let quotes_deque = self + .quotes + .entry(quote.instrument_id) + .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); + quotes_deque.push_front(quote); + Ok(()) + } + + /// Add the given `quotes` to the cache. + pub fn add_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> { + check_slice_not_empty(quotes, stringify!(quotes))?; + + let instrument_id = quotes[0].instrument_id; + debug!("Add `QuoteTick`[{}] {}", quotes.len(), instrument_id); + let quotes_deque = self + .quotes + .entry(instrument_id) + .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); + + for quote in quotes { + quotes_deque.push_front(*quote); + } + Ok(()) + } + + /// Add the given `trade` tick to the cache. + pub fn add_trade(&mut self, trade: TradeTick) -> anyhow::Result<()> { + debug!("Add `TradeTick` {}", trade.instrument_id); + let trades_deque = self + .trades + .entry(trade.instrument_id) + .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); + trades_deque.push_front(trade); + Ok(()) + } + + /// Add the give `trades` to the cache. + pub fn add_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> { + check_slice_not_empty(trades, stringify!(trades))?; + + let instrument_id = trades[0].instrument_id; + debug!("Add `TradeTick`[{}] {}", trades.len(), instrument_id); + let trades_deque = self + .trades + .entry(instrument_id) + .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); + + for trade in trades { + trades_deque.push_front(*trade); + } + Ok(()) + } + + /// Add the given `bar` to the cache. + pub fn add_bar(&mut self, bar: Bar) -> anyhow::Result<()> { + debug!("Add `Bar` {}", bar.bar_type); + let bars = self + .bars + .entry(bar.bar_type) + .or_insert_with(|| VecDeque::with_capacity(self.config.bar_capacity)); + bars.push_front(bar); + Ok(()) + } + + /// Add the given `bars` to the cache. + pub fn add_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> { + check_slice_not_empty(bars, stringify!(bars))?; + + let bar_type = bars[0].bar_type; + debug!("Add `Bar`[{}] {}", bars.len(), bar_type); + let bars_deque = self + .bars + .entry(bar_type) + .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); + + for bar in bars { + bars_deque.push_front(*bar); + } + Ok(()) + } + + /// Add the given `currency` to the cache. + pub fn add_currency(&mut self, currency: Currency) -> anyhow::Result<()> { + debug!("Add `Currency` {}", currency.code); + + if let Some(database) = &self.database { + database.add_currency(¤cy)?; + } + + self.currencies.insert(currency.code, currency); + Ok(()) + } + + /// Add the given `instrument` to the cache. + pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> { + debug!("Add `Instrument` {}", instrument.id()); + + if let Some(database) = &self.database { + database.add_instrument(&instrument)?; + } + + self.instruments.insert(instrument.id(), instrument); + Ok(()) + } + + /// Add the given `synthetic` instrument to the cache. + pub fn add_synthetic(&mut self, synthetic: SyntheticInstrument) -> anyhow::Result<()> { + debug!("Add `SyntheticInstrument` {}", synthetic.id); + + if let Some(database) = &self.database { + database.add_synthetic(&synthetic)?; + } + + self.synthetics.insert(synthetic.id, synthetic); + Ok(()) + } + + /// Add the given `account` to the cache. + pub fn add_account(&mut self, account: Box) -> anyhow::Result<()> { + debug!("Add `Account` {}", account.id()); + + if let Some(database) = &self.database { + database.add_account(account.as_ref())?; + } + + self.accounts.insert(account.id(), account); + Ok(()) + } + + /// Add the order to the cache indexed with any given identifiers. + /// + /// # Parameters + /// + /// `override_existing`: If the added order should 'override' any existing order and replace + /// it in the cache. This is currently used for emulated orders which are + /// being released and transformed into another type. + /// + /// # Errors + /// + /// If not `replace_existing` and the `order.client_order_id` is already contained in the cache. + pub fn add_order( + &mut self, + order: OrderAny, + _position_id: Option, + client_id: Option, + replace_existing: bool, + ) -> anyhow::Result<()> { + let instrument_id = order.instrument_id(); + let venue = instrument_id.venue; + let client_order_id = order.client_order_id(); + let strategy_id = order.strategy_id(); + let exec_algorithm_id = order.exec_algorithm_id(); + let exec_spawn_id = order.exec_spawn_id(); + + if !replace_existing { + check_key_not_in_map( + &client_order_id, + &self.orders, + stringify!(client_order_id), + stringify!(orders), + )?; + check_key_not_in_map( + &client_order_id, + &self.orders, + stringify!(client_order_id), + stringify!(orders), + )?; + check_key_not_in_map( + &client_order_id, + &self.orders, + stringify!(client_order_id), + stringify!(orders), + )?; + check_key_not_in_map( + &client_order_id, + &self.orders, + stringify!(client_order_id), + stringify!(orders), + )?; + }; + + debug!("Added {:?}", order); + + self.index.orders.insert(client_order_id); + self.index + .order_strategy + .insert(client_order_id, strategy_id); + self.index.strategies.insert(strategy_id); + + // Update venue -> orders index + self.index + .venue_orders + .entry(venue) + .or_default() + .insert(client_order_id); + + // Update instrument -> orders index + self.index + .instrument_orders + .entry(instrument_id) + .or_default() + .insert(client_order_id); + + // Update strategy -> orders index + self.index + .strategy_orders + .entry(strategy_id) + .or_default() + .insert(client_order_id); + + // Update exec_algorithm -> orders index + if let Some(exec_algorithm_id) = exec_algorithm_id { + self.index.exec_algorithms.insert(exec_algorithm_id); + + self.index + .exec_algorithm_orders + .entry(exec_algorithm_id) + .or_default() + .insert(client_order_id); + + // SAFETY: We can guarantee the `exec_spawn_id` is Some + self.index + .exec_spawn_orders + .entry(exec_spawn_id.unwrap()) + .or_default() + .insert(client_order_id); + } + + // TODO: Change emulation trigger setup + // Update emulation index + // match order.emulation_trigger() { + // TriggerType::NoTrigger => { + // self.index.orders_emulated.remove(&client_order_id); + // } + // _ => { + // self.index.orders_emulated.insert(client_order_id.clone()); + // } + // } + + // Index position ID if provided + if let Some(position_id) = order.position_id() { + self.add_position_id( + &position_id, + &order.instrument_id().venue, + &client_order_id, + &strategy_id, + )?; + } + + // Index client ID if provided + if let Some(client_id) = client_id { + self.index.order_client.insert(client_order_id, client_id); + log::debug!("Indexed {:?}", client_id); + } + + if let Some(database) = &mut self.database { + database.add_order(&order)?; + // TODO: Implement + // if self.config.snapshot_orders { + // database.snapshot_order_state(order)?; + // } + } + + self.orders.insert(client_order_id, order); + + Ok(()) + } + + /// Index the given `position_id` with the other given IDs. + pub fn add_position_id( + &mut self, + position_id: &PositionId, + venue: &Venue, + client_order_id: &ClientOrderId, + strategy_id: &StrategyId, + ) -> anyhow::Result<()> { + self.index + .order_position + .insert(*client_order_id, *position_id); + + // Index: ClientOrderId -> PositionId + if let Some(database) = &mut self.database { + database.index_order_position(*client_order_id, *position_id)?; + } + + // Index: PositionId -> StrategyId + self.index + .position_strategy + .insert(*position_id, *strategy_id); + + // Index: PositionId -> set[ClientOrderId] + self.index + .position_orders + .entry(*position_id) + .or_default() + .insert(*client_order_id); + + // Index: StrategyId -> set[PositionId] + self.index + .strategy_positions + .entry(*strategy_id) + .or_default() + .insert(*position_id); + + Ok(()) + } + + pub fn add_position(&mut self, position: Position, oms_type: OmsType) -> anyhow::Result<()> { + self.positions.insert(position.id, position.clone()); + self.index.positions.insert(position.id); + self.index.positions_open.insert(position.id); + + self.add_position_id( + &position.id, + &position.instrument_id.venue, + &position.opening_order_id, + &position.strategy_id, + )?; + + let venue = position.instrument_id.venue; + let venue_positions = self.index.venue_positions.entry(venue).or_default(); + venue_positions.insert(position.id); + + // Index: InstrumentId -> HashSet + let instrument_id = position.instrument_id; + let instrument_positions = self + .index + .instrument_positions + .entry(instrument_id) + .or_default(); + instrument_positions.insert(position.id); + + log::debug!( + "Added Position(id={}, strategy_id={})", + position.id, + position.strategy_id, + ); + + if let Some(database) = &mut self.database { + database.add_position(&position)?; + // TODO: Implement position snapshots + // if self.snapshot_positions { + // database.snapshot_position_state( + // position, + // position.ts_last, + // self.calculate_unrealized_pnl(&position), + // )?; + // } + } + + Ok(()) + } + + /// Update the given `account` in the cache. + pub fn update_account(&mut self, account: &dyn Account) -> anyhow::Result<()> { + if let Some(database) = &mut self.database { + database.update_account(account)?; + } + Ok(()) + } + + /// Update the given `order` in the cache. + pub fn update_order(&mut self, order: &OrderAny) -> anyhow::Result<()> { + let client_order_id = order.client_order_id(); + + // Update venue order ID + if let Some(venue_order_id) = order.venue_order_id() { + // Assumes order_id does not change + self.index.order_ids.insert(venue_order_id, client_order_id); + } + + // Update in-flight state + if order.is_inflight() { + self.index.orders_inflight.insert(client_order_id); + } else { + self.index.orders_inflight.remove(&client_order_id); + } + + // Update open/closed state + if order.is_open() { + self.index.orders_closed.remove(&client_order_id); + self.index.orders_open.insert(client_order_id); + } else if order.is_closed() { + self.index.orders_open.remove(&client_order_id); + self.index.orders_pending_cancel.remove(&client_order_id); + self.index.orders_closed.insert(client_order_id); + } + + // Update emulation + if let Some(emulation_trigger) = order.emulation_trigger() { + match emulation_trigger { + TriggerType::NoTrigger => self.index.orders_emulated.remove(&client_order_id), + _ => self.index.orders_emulated.insert(client_order_id), + }; + } + + if let Some(database) = &mut self.database { + database.update_order(order)?; + // TODO: Implement order snapshots + // if self.snapshot_orders { + // database.snapshot_order_state(order)?; + // } + } + + Ok(()) + } + + /// Update the given `order` as pending cancel locally. + pub fn update_order_pending_cancel_local(&mut self, order: &OrderAny) { + self.index + .orders_pending_cancel + .insert(order.client_order_id()); + } + + /// Update the given `position` in the cache. + pub fn update_position(&mut self, position: &Position) -> anyhow::Result<()> { + // Update open/closed state + if position.is_open() { + self.index.positions_open.insert(position.id); + self.index.positions_closed.remove(&position.id); + } else { + self.index.positions_closed.insert(position.id); + self.index.positions_open.remove(&position.id); + } + + if let Some(database) = &mut self.database { + database.update_position(position)?; + // TODO: Implement order snapshots + // if self.snapshot_orders { + // database.snapshot_order_state(order)?; + // } + } + Ok(()) + } + + // -- IDENTIFIER QUERIES ---------------------------------------------------------------------- + + fn build_order_query_filter_set( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> Option> { + let mut query: Option> = None; + + if let Some(venue) = venue { + query = Some( + self.index + .venue_orders + .get(venue) + .map_or(HashSet::new(), |o| o.iter().copied().collect()), + ); + }; + + if let Some(instrument_id) = instrument_id { + let instrument_orders = self + .index + .instrument_orders + .get(instrument_id) + .map_or(HashSet::new(), |o| o.iter().copied().collect()); + + if let Some(existing_query) = &mut query { + *existing_query = existing_query + .intersection(&instrument_orders) + .copied() + .collect(); + } else { + query = Some(instrument_orders); + }; + }; + + if let Some(strategy_id) = strategy_id { + let strategy_orders = self + .index + .strategy_orders + .get(strategy_id) + .map_or(HashSet::new(), |o| o.iter().copied().collect()); + + if let Some(existing_query) = &mut query { + *existing_query = existing_query + .intersection(&strategy_orders) + .copied() + .collect(); + } else { + query = Some(strategy_orders); + }; + }; + + query + } + + fn build_position_query_filter_set( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> Option> { + let mut query: Option> = None; + + if let Some(venue) = venue { + query = Some( + self.index + .venue_positions + .get(venue) + .map_or(HashSet::new(), |p| p.iter().copied().collect()), + ); + }; + + if let Some(instrument_id) = instrument_id { + let instrument_positions = self + .index + .instrument_positions + .get(instrument_id) + .map_or(HashSet::new(), |p| p.iter().copied().collect()); + + if let Some(existing_query) = query { + query = Some( + existing_query + .intersection(&instrument_positions) + .copied() + .collect(), + ); + } else { + query = Some(instrument_positions); + }; + }; + + if let Some(strategy_id) = strategy_id { + let strategy_positions = self + .index + .strategy_positions + .get(strategy_id) + .map_or(HashSet::new(), |p| p.iter().copied().collect()); + + if let Some(existing_query) = query { + query = Some( + existing_query + .intersection(&strategy_positions) + .copied() + .collect(), + ); + } else { + query = Some(strategy_positions); + }; + }; + + query + } + + fn get_orders_for_ids( + &self, + client_order_ids: &HashSet, + side: Option, + ) -> Vec<&OrderAny> { + let side = side.unwrap_or(OrderSide::NoOrderSide); + let mut orders = Vec::new(); + + for client_order_id in client_order_ids { + let order = self + .orders + .get(client_order_id) + .unwrap_or_else(|| panic!("Order {client_order_id} not found")); + if side == OrderSide::NoOrderSide || side == order.order_side() { + orders.push(order); + }; + } + + orders + } + + fn get_positions_for_ids( + &self, + position_ids: &HashSet, + side: Option, + ) -> Vec<&Position> { + let side = side.unwrap_or(PositionSide::NoPositionSide); + let mut positions = Vec::new(); + + for position_id in position_ids { + let position = self + .positions + .get(position_id) + .unwrap_or_else(|| panic!("Position {position_id} not found")); + if side == PositionSide::NoPositionSide || side == position.side { + positions.push(position); + }; + } + + positions + } + + #[must_use] + pub fn client_order_ids( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self.index.orders.intersection(&query).copied().collect(), + None => self.index.orders.clone(), + } + } + + #[must_use] + pub fn client_order_ids_open( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self + .index + .orders_open + .intersection(&query) + .copied() + .collect(), + None => self.index.orders_open.clone(), + } + } + + #[must_use] + pub fn client_order_ids_closed( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self + .index + .orders_closed + .intersection(&query) + .copied() + .collect(), + None => self.index.orders_closed.clone(), + } + } + + #[must_use] + pub fn client_order_ids_emulated( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self + .index + .orders_emulated + .intersection(&query) + .copied() + .collect(), + None => self.index.orders_emulated.clone(), + } + } + + #[must_use] + pub fn client_order_ids_inflight( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self + .index + .orders_inflight + .intersection(&query) + .copied() + .collect(), + None => self.index.orders_inflight.clone(), + } + } + + #[must_use] + pub fn position_ids( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self.index.positions.intersection(&query).copied().collect(), + None => self.index.positions.clone(), + } + } + + #[must_use] + pub fn position_open_ids( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self + .index + .positions_open + .intersection(&query) + .copied() + .collect(), + None => self.index.positions_open.clone(), + } + } + + #[must_use] + pub fn position_closed_ids( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> HashSet { + let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); + match query { + Some(query) => self + .index + .positions_closed + .intersection(&query) + .copied() + .collect(), + None => self.index.positions_closed.clone(), + } + } + + #[must_use] + pub fn actor_ids(&self) -> HashSet { + self.index.actors.clone() + } + + #[must_use] + pub fn strategy_ids(&self) -> HashSet { + self.index.strategies.clone() + } + + #[must_use] + pub fn exec_algorithm_ids(&self) -> HashSet { + self.index.exec_algorithms.clone() + } + + // -- ORDER QUERIES --------------------------------------------------------------------------- + + #[must_use] + pub fn order(&self, client_order_id: &ClientOrderId) -> Option<&OrderAny> { + self.orders.get(client_order_id) + } + + #[must_use] + pub fn client_order_id(&self, venue_order_id: &VenueOrderId) -> Option<&ClientOrderId> { + self.index.order_ids.get(venue_order_id) + } + + #[must_use] + pub fn venue_order_id(&self, client_order_id: &ClientOrderId) -> Option { + self.orders + .get(client_order_id) + .and_then(nautilus_model::polymorphism::GetVenueOrderId::venue_order_id) + } + + #[must_use] + pub fn client_id(&self, client_order_id: &ClientOrderId) -> Option<&ClientId> { + self.index.order_client.get(client_order_id) + } + + #[must_use] + pub fn orders( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&OrderAny> { + let client_order_ids = self.client_order_ids(venue, instrument_id, strategy_id); + self.get_orders_for_ids(&client_order_ids, side) + } + + #[must_use] + pub fn orders_open( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&OrderAny> { + let client_order_ids = self.client_order_ids_open(venue, instrument_id, strategy_id); + self.get_orders_for_ids(&client_order_ids, side) + } + + #[must_use] + pub fn orders_closed( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&OrderAny> { + let client_order_ids = self.client_order_ids_closed(venue, instrument_id, strategy_id); + self.get_orders_for_ids(&client_order_ids, side) + } + + #[must_use] + pub fn orders_emulated( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&OrderAny> { + let client_order_ids = self.client_order_ids_emulated(venue, instrument_id, strategy_id); + self.get_orders_for_ids(&client_order_ids, side) + } + + #[must_use] + pub fn orders_inflight( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&OrderAny> { + let client_order_ids = self.client_order_ids_inflight(venue, instrument_id, strategy_id); + self.get_orders_for_ids(&client_order_ids, side) + } + + #[must_use] + pub fn orders_for_position(&self, position_id: PositionId) -> Vec<&OrderAny> { + let client_order_ids = self.index.position_orders.get(&position_id); + match client_order_ids { + Some(client_order_ids) => { + self.get_orders_for_ids(&client_order_ids.iter().copied().collect(), None) + } + None => Vec::new(), + } + } + + #[must_use] + pub fn order_exists(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders.contains(client_order_id) + } + + #[must_use] + pub fn is_order_open(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_open.contains(client_order_id) + } + + #[must_use] + pub fn is_order_closed(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_closed.contains(client_order_id) + } + + #[must_use] + pub fn is_order_emulated(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_emulated.contains(client_order_id) + } + + #[must_use] + pub fn is_order_inflight(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_inflight.contains(client_order_id) + } + + #[must_use] + pub fn is_order_pending_cancel_local(&self, client_order_id: &ClientOrderId) -> bool { + self.index.orders_pending_cancel.contains(client_order_id) + } + + #[must_use] + pub fn orders_open_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> usize { + self.orders_open(venue, instrument_id, strategy_id, side) + .len() + } + + #[must_use] + pub fn orders_closed_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> usize { + self.orders_closed(venue, instrument_id, strategy_id, side) + .len() + } + + #[must_use] + pub fn orders_emulated_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> usize { + self.orders_emulated(venue, instrument_id, strategy_id, side) + .len() + } + + #[must_use] + pub fn orders_inflight_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> usize { + self.orders_inflight(venue, instrument_id, strategy_id, side) + .len() + } + + #[must_use] + pub fn orders_total_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> usize { + self.orders(venue, instrument_id, strategy_id, side).len() + } + + #[must_use] + pub fn order_list(&self, order_list_id: &OrderListId) -> Option<&OrderList> { + self.order_lists.get(order_list_id) + } + + #[must_use] + pub fn order_lists( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + ) -> Vec<&OrderList> { + let mut order_lists = self.order_lists.values().collect::>(); + + if let Some(venue) = venue { + order_lists.retain(|ol| &ol.instrument_id.venue == venue); + } + + if let Some(instrument_id) = instrument_id { + order_lists.retain(|ol| &ol.instrument_id == instrument_id); + } + + if let Some(strategy_id) = strategy_id { + order_lists.retain(|ol| &ol.strategy_id == strategy_id); + } + + order_lists + } + + #[must_use] + pub fn order_list_exists(&self, order_list_id: &OrderListId) -> bool { + self.order_lists.contains_key(order_list_id) + } + + // -- EXEC ALGORITHM QUERIES ------------------------------------------------------------------ + + #[must_use] + pub fn orders_for_exec_algorithm( + &self, + exec_algorithm_id: &ExecAlgorithmId, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&OrderAny> { + let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); + let exec_algorithm_order_ids = self.index.exec_algorithm_orders.get(exec_algorithm_id); + + if let Some(query) = query { + if let Some(exec_algorithm_order_ids) = exec_algorithm_order_ids { + let exec_algorithm_order_ids = exec_algorithm_order_ids.intersection(&query); + } + } + + if let Some(exec_algorithm_order_ids) = exec_algorithm_order_ids { + self.get_orders_for_ids(exec_algorithm_order_ids, side) + } else { + Vec::new() + } + } + + #[must_use] + pub fn orders_for_exec_spawn(&self, exec_spawn_id: &ClientOrderId) -> Vec<&OrderAny> { + self.get_orders_for_ids( + self.index + .exec_spawn_orders + .get(exec_spawn_id) + .unwrap_or(&HashSet::new()), + None, + ) + } + + #[must_use] + pub fn exec_spawn_total_quantity( + &self, + exec_spawn_id: &ClientOrderId, + active_only: bool, + ) -> Option { + let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); + + let mut total_quantity: Option = None; + + for spawn_order in exec_spawn_orders { + if !active_only || !spawn_order.is_closed() { + if let Some(mut total_quantity) = total_quantity { + total_quantity += spawn_order.quantity(); + } + } else { + total_quantity = Some(spawn_order.quantity()); + } + } + + total_quantity + } + + #[must_use] + pub fn exec_spawn_total_filled_qty( + &self, + exec_spawn_id: &ClientOrderId, + active_only: bool, + ) -> Option { + let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); + + let mut total_quantity: Option = None; + + for spawn_order in exec_spawn_orders { + if !active_only || !spawn_order.is_closed() { + if let Some(mut total_quantity) = total_quantity { + total_quantity += spawn_order.filled_qty(); + } + } else { + total_quantity = Some(spawn_order.filled_qty()); + } + } + + total_quantity + } + + #[must_use] + pub fn exec_spawn_total_leaves_qty( + &self, + exec_spawn_id: &ClientOrderId, + active_only: bool, + ) -> Option { + let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); + + let mut total_quantity: Option = None; + + for spawn_order in exec_spawn_orders { + if !active_only || !spawn_order.is_closed() { + if let Some(mut total_quantity) = total_quantity { + total_quantity += spawn_order.leaves_qty(); + } + } else { + total_quantity = Some(spawn_order.leaves_qty()); + } + } + + total_quantity + } + + // -- POSITION QUERIES ------------------------------------------------------------------------ + + #[must_use] + pub fn position(&self, position_id: &PositionId) -> Option<&Position> { + self.positions.get(position_id) + } + + #[must_use] + pub fn position_for_order(&self, client_order_id: &ClientOrderId) -> Option<&Position> { + self.index + .order_position + .get(client_order_id) + .and_then(|position_id| self.positions.get(position_id)) + } + + #[must_use] + pub fn position_id(&self, client_order_id: &ClientOrderId) -> Option<&PositionId> { + self.index.order_position.get(client_order_id) + } + + #[must_use] + pub fn positions( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&Position> { + let position_ids = self.position_ids(venue, instrument_id, strategy_id); + self.get_positions_for_ids(&position_ids, side) + } + + #[must_use] + pub fn positions_open( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&Position> { + let position_ids = self.position_open_ids(venue, instrument_id, strategy_id); + self.get_positions_for_ids(&position_ids, side) + } + + #[must_use] + pub fn positions_closed( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> Vec<&Position> { + let position_ids = self.position_closed_ids(venue, instrument_id, strategy_id); + self.get_positions_for_ids(&position_ids, side) + } + + #[must_use] + pub fn position_exists(&self, position_id: &PositionId) -> bool { + self.index.positions.contains(position_id) + } + + #[must_use] + pub fn is_position_open(&self, position_id: &PositionId) -> bool { + self.index.positions_open.contains(position_id) + } + + #[must_use] + pub fn is_position_closed(&self, position_id: &PositionId) -> bool { + self.index.positions_closed.contains(position_id) + } + + #[must_use] + pub fn positions_open_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> u64 { + self.positions_open(venue, instrument_id, strategy_id, side) + .len() as u64 + } + + #[must_use] + pub fn positions_closed_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> u64 { + self.positions_closed(venue, instrument_id, strategy_id, side) + .len() as u64 + } + + #[must_use] + pub fn positions_total_count( + &self, + venue: Option<&Venue>, + instrument_id: Option<&InstrumentId>, + strategy_id: Option<&StrategyId>, + side: Option, + ) -> u64 { + self.positions(venue, instrument_id, strategy_id, side) + .len() as u64 + } + + // -- STRATEGY QUERIES ------------------------------------------------------------------------ + + #[must_use] + pub fn strategy_id_for_order(&self, client_order_id: &ClientOrderId) -> Option<&StrategyId> { + self.index.order_strategy.get(client_order_id) + } + + #[must_use] + pub fn strategy_id_for_position(&self, position_id: &PositionId) -> Option<&StrategyId> { + self.index.position_strategy.get(position_id) + } + + // -- GENERAL --------------------------------------------------------------------------------- + + pub fn get(&self, key: &str) -> anyhow::Result> { + check_valid_string(key, stringify!(key))?; + + Ok(self.general.get(key).map(std::vec::Vec::as_slice)) + } + + // -- DATA QUERIES ---------------------------------------------------------------------------- + + #[must_use] + pub fn price(&self, instrument_id: &InstrumentId, price_type: PriceType) -> Option { + match price_type { + PriceType::Bid => self + .quotes + .get(instrument_id) + .and_then(|quotes| quotes.front().map(|quote| quote.bid_price)), + PriceType::Ask => self + .quotes + .get(instrument_id) + .and_then(|quotes| quotes.front().map(|quote| quote.ask_price)), + PriceType::Mid => self.quotes.get(instrument_id).and_then(|quotes| { + quotes.front().map(|quote| { + Price::new( + (quote.ask_price.as_f64() + quote.bid_price.as_f64()) / 2.0, + quote.bid_price.precision + 1, + ) + .expect("Error calculating mid price") + }) + }), + PriceType::Last => self + .trades + .get(instrument_id) + .and_then(|trades| trades.front().map(|trade| trade.price)), + } + } + + #[must_use] + pub fn quote_ticks(&self, instrument_id: &InstrumentId) -> Option> { + self.quotes + .get(instrument_id) + .map(|quotes| quotes.iter().copied().collect()) + } + + #[must_use] + pub fn trade_ticks(&self, instrument_id: &InstrumentId) -> Option> { + self.trades + .get(instrument_id) + .map(|trades| trades.iter().copied().collect()) + } + + #[must_use] + pub fn bars(&self, bar_type: &BarType) -> Option> { + self.bars + .get(bar_type) + .map(|bars| bars.iter().copied().collect()) + } + + #[must_use] + pub fn order_book(&self, instrument_id: &InstrumentId) -> Option<&OrderBook> { + self.books.get(instrument_id) + } + + #[must_use] + pub fn quote_tick(&self, instrument_id: &InstrumentId) -> Option<&QuoteTick> { + self.quotes + .get(instrument_id) + .and_then(|quotes| quotes.front()) + } + + #[must_use] + pub fn trade_tick(&self, instrument_id: &InstrumentId) -> Option<&TradeTick> { + self.trades + .get(instrument_id) + .and_then(|trades| trades.front()) + } + + #[must_use] + pub fn bar(&self, bar_type: &BarType) -> Option<&Bar> { + self.bars.get(bar_type).and_then(|bars| bars.front()) + } + + #[must_use] + pub fn book_update_count(&self, instrument_id: &InstrumentId) -> u64 { + self.books.get(instrument_id).map_or(0, |book| book.count) + } + + #[must_use] + pub fn quote_tick_count(&self, instrument_id: &InstrumentId) -> u64 { + self.quotes + .get(instrument_id) + .map_or(0, std::collections::VecDeque::len) as u64 + } + + #[must_use] + pub fn trade_tick_count(&self, instrument_id: &InstrumentId) -> u64 { + self.trades + .get(instrument_id) + .map_or(0, std::collections::VecDeque::len) as u64 + } + + #[must_use] + pub fn bar_count(&self, bar_type: &BarType) -> u64 { + self.bars + .get(bar_type) + .map_or(0, std::collections::VecDeque::len) as u64 + } + + #[must_use] + pub fn has_order_book(&self, instrument_id: &InstrumentId) -> bool { + self.books.contains_key(instrument_id) + } + + #[must_use] + pub fn has_quote_ticks(&self, instrument_id: &InstrumentId) -> bool { + self.quote_tick_count(instrument_id) > 0 + } + + #[must_use] + pub fn has_trade_ticks(&self, instrument_id: &InstrumentId) -> bool { + self.trade_tick_count(instrument_id) > 0 + } + + #[must_use] + pub fn has_bars(&self, bar_type: &BarType) -> bool { + self.bar_count(bar_type) > 0 + } + + // -- INSTRUMENT QUERIES ---------------------------------------------------------------------- + + #[must_use] + pub fn instrument(&self, instrument_id: &InstrumentId) -> Option<&InstrumentAny> { + self.instruments.get(instrument_id) + } + + #[must_use] + pub fn instrument_ids(&self, venue: &Venue) -> Vec<&InstrumentId> { + self.instruments + .keys() + .filter(|i| &i.venue == venue) + .collect() + } + + #[must_use] + pub fn instruments(&self, venue: &Venue) -> Vec<&InstrumentAny> { + self.instruments + .values() + .filter(|i| &i.id().venue == venue) + .collect() + } + + #[must_use] + pub fn bar_types( + &self, + instrument_id: Option<&InstrumentId>, + price_type: Option<&PriceType>, + aggregation_source: AggregationSource, + ) -> Vec<&BarType> { + let mut bar_types = self + .bars + .keys() + .filter(|bar_type| bar_type.aggregation_source == aggregation_source) + .collect::>(); + + if let Some(instrument_id) = instrument_id { + bar_types.retain(|bar_type| &bar_type.instrument_id == instrument_id); + } + + if let Some(price_type) = price_type { + bar_types.retain(|bar_type| &bar_type.spec.price_type == price_type); + } + + bar_types + } + + // -- SYNTHETIC QUERIES ----------------------------------------------------------------------- + + #[must_use] + pub fn synthetic(&self, instrument_id: &InstrumentId) -> Option<&SyntheticInstrument> { + self.synthetics.get(instrument_id) + } + + #[must_use] + pub fn synthetic_ids(&self) -> Vec<&InstrumentId> { + self.synthetics.keys().collect() + } + + #[must_use] + pub fn synthetics(&self) -> Vec<&SyntheticInstrument> { + self.synthetics.values().collect() + } + + // -- ACCOUNT QUERIES ----------------------------------------------------------------------- + + #[must_use] + pub fn account(&self, account_id: &AccountId) -> Option<&dyn Account> { + self.accounts + .get(account_id) + .map(std::convert::AsRef::as_ref) + } + + #[must_use] + pub fn account_for_venue(&self, venue: &Venue) -> Option<&dyn Account> { + self.index + .venue_account + .get(venue) + .and_then(|account_id| self.accounts.get(account_id)) + .map(std::convert::AsRef::as_ref) + } + + #[must_use] + pub fn account_id(&self, venue: &Venue) -> Option<&AccountId> { + self.index.venue_account.get(venue) + } + + #[must_use] + pub fn accounts(&self, account_id: &AccountId) -> Vec<&dyn Account> { + self.accounts + .values() + .map(std::convert::AsRef::as_ref) + .collect() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + instruments::{ + currency_pair::CurrencyPair, stubs::*, synthetic::SyntheticInstrument, InstrumentAny, + }, + }; + use rstest::*; + + use super::Cache; + + #[fixture] + fn cache() -> Cache { + Cache::default() + } + + #[rstest] + fn test_build_index_when_empty(mut cache: Cache) { + cache.build_index(); + } + + #[rstest] + fn test_clear_index_when_empty(mut cache: Cache) { + cache.clear_index(); + } + + #[rstest] + fn test_reset_when_empty(mut cache: Cache) { + cache.reset(); + } + + #[rstest] + fn test_dispose_when_empty(cache: Cache) { + let result = cache.dispose(); + assert!(result.is_ok()); + } + + #[rstest] + fn test_flush_db_when_empty(cache: Cache) { + let result = cache.flush_db(); + assert!(result.is_ok()); + } + + #[rstest] + fn test_check_residuals_when_empty(cache: Cache) { + let result = cache.flush_db(); + assert!(result.is_ok()); + } + + #[rstest] + fn test_cache_general_load_when_no_database(mut cache: Cache) { + assert!(cache.cache_general().is_ok()); + } + + #[rstest] + fn test_cache_currencies_load_when_no_database(mut cache: Cache) { + assert!(cache.cache_currencies().is_ok()); + } + + #[rstest] + fn test_cache_instruments_load_when_no_database(mut cache: Cache) { + assert!(cache.cache_instruments().is_ok()); + } + + #[rstest] + fn test_cache_synthetics_when_no_database(mut cache: Cache) { + assert!(cache.cache_synthetics().is_ok()); + } + + #[rstest] + fn test_cache_orders_when_no_database(mut cache: Cache) { + assert!(cache.cache_orders().is_ok()); + } + + #[rstest] + fn test_cache_positions_when_no_database(mut cache: Cache) { + assert!(cache.cache_positions().is_ok()); + } + + #[rstest] + fn test_get_general_when_empty(cache: Cache) { + let result = cache.get("A").unwrap(); + assert_eq!(result, None); + } + + #[rstest] + fn test_add_general_when_value(mut cache: Cache) { + let key = "A"; + let value = vec![0_u8]; + cache.add(key, value.clone()).unwrap(); + let result = cache.get(key).unwrap(); + assert_eq!(result, Some(&value.as_slice()).copied()); + } + + #[rstest] + fn test_instrument_when_empty(cache: Cache, audusd_sim: CurrencyPair) { + let result = cache.instrument(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_instrument_when_some(mut cache: Cache, audusd_sim: CurrencyPair) { + cache + .add_instrument(InstrumentAny::CurrencyPair(audusd_sim)) + .unwrap(); + + let result = cache.instrument(&audusd_sim.id); + assert_eq!( + result, + Some(InstrumentAny::CurrencyPair(audusd_sim)).as_ref() + ); + } + + #[rstest] + fn test_synthetic_when_empty(cache: Cache) { + let synth = SyntheticInstrument::default(); + let result = cache.synthetic(&synth.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_synthetic_when_some(cache: Cache) { + let mut cache = Cache::default(); + let synth = SyntheticInstrument::default(); + cache.add_synthetic(synth.clone()).unwrap(); + let result = cache.synthetic(&synth.id); + assert_eq!(result, Some(synth).as_ref()); + } + + #[rstest] + fn test_quote_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { + let result = cache.quote_tick(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_quote_tick_when_some(mut cache: Cache) { + let quote = QuoteTick::default(); + cache.add_quote(quote).unwrap(); + let result = cache.quote_tick("e.instrument_id); + assert_eq!(result, Some("e)); + } + + #[rstest] + fn test_quote_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { + let result = cache.quote_ticks(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_quote_ticks_when_some(mut cache: Cache) { + let quotes = vec![ + QuoteTick::default(), + QuoteTick::default(), + QuoteTick::default(), + ]; + cache.add_quotes("es).unwrap(); + let result = cache.quote_ticks("es[0].instrument_id); + assert_eq!(result, Some(quotes)); + } + + #[rstest] + fn test_trade_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { + let result = cache.trade_tick(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_trade_tick_when_some(mut cache: Cache) { + let trade = TradeTick::default(); + cache.add_trade(trade).unwrap(); + let result = cache.trade_tick(&trade.instrument_id); + assert_eq!(result, Some(&trade)); + } + + #[rstest] + fn test_trade_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { + let result = cache.trade_ticks(&audusd_sim.id); + assert_eq!(result, None); + } + + #[rstest] + fn test_trade_ticks_when_some(mut cache: Cache) { + let trades = vec![ + TradeTick::default(), + TradeTick::default(), + TradeTick::default(), + ]; + cache.add_trades(&trades).unwrap(); + let result = cache.trade_ticks(&trades[0].instrument_id); + assert_eq!(result, Some(trades)); + } + + #[rstest] + fn test_bar_when_empty(cache: Cache) { + let bar = Bar::default(); + let result = cache.bar(&bar.bar_type); + assert_eq!(result, None); + } + + #[rstest] + fn test_bar_when_some(mut cache: Cache) { + let bar = Bar::default(); + cache.add_bar(bar).unwrap(); + let result = cache.bar(&bar.bar_type); + assert_eq!(result, Some(bar).as_ref()); + } + + #[rstest] + fn test_bars_when_empty(cache: Cache) { + let bar = Bar::default(); + let result = cache.bars(&bar.bar_type); + assert_eq!(result, None); + } + + #[rstest] + fn test_bars_when_some(mut cache: Cache) { + let bars = vec![Bar::default(), Bar::default(), Bar::default()]; + cache.add_bars(&bars).unwrap(); + let result = cache.bars(&bars[0].bar_type); + assert_eq!(result, Some(bars)); + } +} diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 0f6cf272d413..298bb0af0046 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -19,2598 +19,7 @@ #![allow(dead_code)] #![allow(unused_variables)] +pub mod core; pub mod database; -use std::{ - collections::{HashMap, HashSet, VecDeque}, - time::{SystemTime, UNIX_EPOCH}, -}; - -use log::{debug, error, info, warn}; -use nautilus_core::correctness::{check_key_not_in_map, check_slice_not_empty, check_valid_string}; -use nautilus_model::{ - data::{ - bar::{Bar, BarType}, - quote::QuoteTick, - trade::TradeTick, - }, - enums::{AggregationSource, OmsType, OrderSide, PositionSide, PriceType, TriggerType}, - identifiers::{ - account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, - component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, - order_list_id::OrderListId, position_id::PositionId, strategy_id::StrategyId, venue::Venue, - venue_order_id::VenueOrderId, - }, - instruments::{synthetic::SyntheticInstrument, InstrumentAny}, - orderbook::book::OrderBook, - orders::{base::OrderAny, list::OrderList}, - polymorphism::{ - GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, - GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetPositionId, - GetStrategyId, GetVenueOrderId, IsClosed, IsInflight, IsOpen, - }, - position::Position, - types::{currency::Currency, price::Price, quantity::Quantity}, -}; -use ustr::Ustr; - -use self::database::CacheDatabaseAdapter; -use crate::{enums::SerializationEncoding, interface::account::Account}; - -/// The configuration for `Cache` instances. -pub struct CacheConfig { - pub encoding: SerializationEncoding, - pub timestamps_as_iso8601: bool, - pub use_trader_prefix: bool, - pub use_instance_id: bool, - pub flush_on_start: bool, - pub drop_instruments_on_reset: bool, - pub tick_capacity: usize, - pub bar_capacity: usize, -} - -impl CacheConfig { - #[allow(clippy::too_many_arguments)] - #[must_use] - pub fn new( - encoding: SerializationEncoding, - timestamps_as_iso8601: bool, - use_trader_prefix: bool, - use_instance_id: bool, - flush_on_start: bool, - drop_instruments_on_reset: bool, - tick_capacity: usize, - bar_capacity: usize, - ) -> Self { - Self { - encoding, - timestamps_as_iso8601, - use_trader_prefix, - use_instance_id, - flush_on_start, - drop_instruments_on_reset, - tick_capacity, - bar_capacity, - } - } -} - -impl Default for CacheConfig { - fn default() -> Self { - Self::new( - SerializationEncoding::MsgPack, - false, - true, - false, - false, - true, - 10_000, - 10_000, - ) - } -} - -/// A key-value lookup index for a `Cache`. -pub struct CacheIndex { - venue_account: HashMap, - venue_orders: HashMap>, - venue_positions: HashMap>, - order_ids: HashMap, - order_position: HashMap, - order_strategy: HashMap, - order_client: HashMap, - position_strategy: HashMap, - position_orders: HashMap>, - instrument_orders: HashMap>, - instrument_positions: HashMap>, - strategy_orders: HashMap>, - strategy_positions: HashMap>, - exec_algorithm_orders: HashMap>, - exec_spawn_orders: HashMap>, - orders: HashSet, - orders_open: HashSet, - orders_closed: HashSet, - orders_emulated: HashSet, - orders_inflight: HashSet, - orders_pending_cancel: HashSet, - positions: HashSet, - positions_open: HashSet, - positions_closed: HashSet, - actors: HashSet, - strategies: HashSet, - exec_algorithms: HashSet, -} - -impl CacheIndex { - /// Clear the index which will clear/reset all internal state. - pub fn clear(&mut self) { - self.venue_account.clear(); - self.venue_orders.clear(); - self.venue_positions.clear(); - self.order_ids.clear(); - self.order_position.clear(); - self.order_strategy.clear(); - self.order_client.clear(); - self.position_strategy.clear(); - self.position_orders.clear(); - self.instrument_orders.clear(); - self.instrument_positions.clear(); - self.strategy_orders.clear(); - self.strategy_positions.clear(); - self.exec_algorithm_orders.clear(); - self.exec_spawn_orders.clear(); - self.orders.clear(); - self.orders_open.clear(); - self.orders_closed.clear(); - self.orders_emulated.clear(); - self.orders_inflight.clear(); - self.orders_pending_cancel.clear(); - self.positions.clear(); - self.positions_open.clear(); - self.positions_closed.clear(); - self.actors.clear(); - self.strategies.clear(); - self.exec_algorithms.clear(); - } -} - -/// A common in-memory `Cache` for market and execution related data. -pub struct Cache { - config: CacheConfig, - index: CacheIndex, - database: Option, - general: HashMap>, - quotes: HashMap>, - trades: HashMap>, - books: HashMap, - bars: HashMap>, - currencies: HashMap, - instruments: HashMap, - synthetics: HashMap, - accounts: HashMap>, - orders: HashMap, - order_lists: HashMap, - positions: HashMap, - position_snapshots: HashMap>, -} - -impl Default for Cache { - fn default() -> Self { - Self::new(CacheConfig::default(), None) - } -} - -impl Cache { - #[must_use] - pub fn new(config: CacheConfig, database: Option) -> Self { - let index = CacheIndex { - venue_account: HashMap::new(), - venue_orders: HashMap::new(), - venue_positions: HashMap::new(), - order_ids: HashMap::new(), - order_position: HashMap::new(), - order_strategy: HashMap::new(), - order_client: HashMap::new(), - position_strategy: HashMap::new(), - position_orders: HashMap::new(), - instrument_orders: HashMap::new(), - instrument_positions: HashMap::new(), - strategy_orders: HashMap::new(), - strategy_positions: HashMap::new(), - exec_algorithm_orders: HashMap::new(), - exec_spawn_orders: HashMap::new(), - orders: HashSet::new(), - orders_open: HashSet::new(), - orders_closed: HashSet::new(), - orders_emulated: HashSet::new(), - orders_inflight: HashSet::new(), - orders_pending_cancel: HashSet::new(), - positions: HashSet::new(), - positions_open: HashSet::new(), - positions_closed: HashSet::new(), - actors: HashSet::new(), - strategies: HashSet::new(), - exec_algorithms: HashSet::new(), - }; - - Self { - config, - index, - database, - general: HashMap::new(), - quotes: HashMap::new(), - trades: HashMap::new(), - books: HashMap::new(), - bars: HashMap::new(), - currencies: HashMap::new(), - instruments: HashMap::new(), - synthetics: HashMap::new(), - accounts: HashMap::new(), - orders: HashMap::new(), - order_lists: HashMap::new(), - positions: HashMap::new(), - position_snapshots: HashMap::new(), - } - } - - // -- COMMANDS -------------------------------------------------------------------------------- - - /// Clear the current general cache and load the general objects from the cache database. - pub fn cache_general(&mut self) -> anyhow::Result<()> { - self.general = match &self.database { - Some(db) => db.load()?, - None => HashMap::new(), - }; - - info!( - "Cached {} general object(s) from database", - self.general.len() - ); - Ok(()) - } - - /// Clear the current currencies cache and load currencies from the cache database. - pub fn cache_currencies(&mut self) -> anyhow::Result<()> { - self.currencies = match &self.database { - Some(db) => db.load_currencies()?, - None => HashMap::new(), - }; - - info!("Cached {} currencies from database", self.general.len()); - Ok(()) - } - - /// Clear the current instruments cache and load instruments from the cache database. - pub fn cache_instruments(&mut self) -> anyhow::Result<()> { - self.instruments = match &self.database { - Some(db) => db.load_instruments()?, - None => HashMap::new(), - }; - - info!("Cached {} instruments from database", self.general.len()); - Ok(()) - } - - /// Clear the current synthetic instruments cache and load synthetic instruments from the cache - /// database. - pub fn cache_synthetics(&mut self) -> anyhow::Result<()> { - self.synthetics = match &self.database { - Some(db) => db.load_synthetics()?, - None => HashMap::new(), - }; - - info!( - "Cached {} synthetic instruments from database", - self.general.len() - ); - Ok(()) - } - - /// Clear the current accounts cache and load accounts from the cache database. - pub fn cache_accounts(&mut self) -> anyhow::Result<()> { - self.accounts = match &self.database { - Some(db) => db.load_accounts()?, - None => HashMap::new(), - }; - - info!( - "Cached {} synthetic instruments from database", - self.general.len() - ); - Ok(()) - } - - /// Clear the current orders cache and load orders from the cache database. - pub fn cache_orders(&mut self) -> anyhow::Result<()> { - self.orders = match &self.database { - Some(db) => db.load_orders()?, - None => HashMap::new(), - }; - - info!("Cached {} orders from database", self.general.len()); - Ok(()) - } - - /// Clear the current positions cache and load positions from the cache database. - pub fn cache_positions(&mut self) -> anyhow::Result<()> { - self.positions = match &self.database { - Some(db) => db.load_positions()?, - None => HashMap::new(), - }; - - info!("Cached {} positions from database", self.general.len()); - Ok(()) - } - - /// Clear the current cache index and re-build. - pub fn build_index(&mut self) { - self.index.clear(); - debug!("Building index"); - - // Index accounts - for account_id in self.accounts.keys() { - self.index - .venue_account - .insert(account_id.get_issuer(), *account_id); - } - - // Index orders - for (client_order_id, order) in &self.orders { - let instrument_id = order.instrument_id(); - let venue = instrument_id.venue; - let strategy_id = order.strategy_id(); - - // 1: Build index.venue_orders -> {Venue, {ClientOrderId}} - self.index - .venue_orders - .entry(venue) - .or_default() - .insert(*client_order_id); - - // 2: Build index.order_ids -> {VenueOrderId, ClientOrderId} - if let Some(venue_order_id) = order.venue_order_id() { - self.index - .order_ids - .insert(venue_order_id, *client_order_id); - } - - // 3: Build index.order_position -> {ClientOrderId, PositionId} - if let Some(position_id) = order.position_id() { - self.index - .order_position - .insert(*client_order_id, position_id); - } - - // 4: Build index.order_strategy -> {ClientOrderId, StrategyId} - self.index - .order_strategy - .insert(*client_order_id, order.strategy_id()); - - // 5: Build index.instrument_orders -> {InstrumentId, {ClientOrderId}} - self.index - .instrument_orders - .entry(instrument_id) - .or_default() - .insert(*client_order_id); - - // 6: Build index.strategy_orders -> {StrategyId, {ClientOrderId}} - self.index - .strategy_orders - .entry(strategy_id) - .or_default() - .insert(*client_order_id); - - // 7: Build index.exec_algorithm_orders -> {ExecAlgorithmId, {ClientOrderId}} - if let Some(exec_algorithm_id) = order.exec_algorithm_id() { - self.index - .exec_algorithm_orders - .entry(exec_algorithm_id) - .or_default() - .insert(*client_order_id); - } - - // 8: Build index.exec_spawn_orders -> {ClientOrderId, {ClientOrderId}} - if let Some(exec_spawn_id) = order.exec_spawn_id() { - self.index - .exec_spawn_orders - .entry(exec_spawn_id) - .or_default() - .insert(*client_order_id); - } - - // 9: Build index.orders -> {ClientOrderId} - self.index.orders.insert(*client_order_id); - - // 10: Build index.orders_open -> {ClientOrderId} - if order.is_open() { - self.index.orders_open.insert(*client_order_id); - } - - // 11: Build index.orders_closed -> {ClientOrderId} - if order.is_closed() { - self.index.orders_closed.insert(*client_order_id); - } - - // 12: Build index.orders_emulated -> {ClientOrderId} - if let Some(emulation_trigger) = order.emulation_trigger() { - if emulation_trigger != TriggerType::NoTrigger && !order.is_closed() { - self.index.orders_emulated.insert(*client_order_id); - } - } - - // 13: Build index.orders_inflight -> {ClientOrderId} - if order.is_inflight() { - self.index.orders_inflight.insert(*client_order_id); - } - - // 14: Build index.strategies -> {StrategyId} - self.index.strategies.insert(strategy_id); - - // 15: Build index.strategies -> {ExecAlgorithmId} - if let Some(exec_algorithm_id) = order.exec_algorithm_id() { - self.index.exec_algorithms.insert(exec_algorithm_id); - } - } - - // Index positions - for (position_id, position) in &self.positions { - let instrument_id = position.instrument_id; - let venue = instrument_id.venue; - let strategy_id = position.strategy_id; - - // 1: Build index.venue_positions -> {Venue, {PositionId}} - self.index - .venue_positions - .entry(venue) - .or_default() - .insert(*position_id); - - // 2: Build index.position_strategy -> {PositionId, StrategyId} - self.index - .position_strategy - .insert(*position_id, position.strategy_id); - - // 3: Build index.position_orders -> {PositionId, {ClientOrderId}} - self.index - .position_orders - .entry(*position_id) - .or_default() - .extend(position.client_order_ids().into_iter()); - - // 4: Build index.instrument_positions -> {InstrumentId, {PositionId}} - self.index - .instrument_positions - .entry(instrument_id) - .or_default() - .insert(*position_id); - - // 5: Build index.strategy_positions -> {StrategyId, {PositionId}} - self.index - .strategy_positions - .entry(strategy_id) - .or_default() - .insert(*position_id); - - // 6: Build index.positions -> {PositionId} - self.index.positions.insert(*position_id); - - // 7: Build index.positions_open -> {PositionId} - if position.is_open() { - self.index.positions_open.insert(*position_id); - } - - // 8: Build index.positions_closed -> {PositionId} - if position.is_closed() { - self.index.positions_closed.insert(*position_id); - } - - // 9: Build index.strategies -> {StrategyId} - self.index.strategies.insert(strategy_id); - } - } - - /// Check integrity of data within the cache. - /// - /// All data should be loaded from the database prior to this call. - /// If an error is found then a log error message will also be produced. - #[must_use] - fn check_integrity(&mut self) -> bool { - let mut error_count = 0; - let failure = "Integrity failure"; - - // Get current timestamp in microseconds - let timestamp_us = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros(); - - info!("Checking data integrity"); - - // Check object caches - for account_id in self.accounts.keys() { - if !self - .index - .venue_account - .contains_key(&account_id.get_issuer()) - { - error!( - "{} in accounts: {} not found in `self.index.venue_account`", - failure, account_id - ); - error_count += 1; - } - } - - for (client_order_id, order) in &self.orders { - if !self.index.order_strategy.contains_key(client_order_id) { - error!( - "{} in orders: {} not found in `self.index.order_strategy`", - failure, client_order_id - ); - error_count += 1; - } - if !self.index.orders.contains(client_order_id) { - error!( - "{} in orders: {} not found in `self.index.orders`", - failure, client_order_id - ); - error_count += 1; - } - if order.is_inflight() && !self.index.orders_inflight.contains(client_order_id) { - error!( - "{} in orders: {} not found in `self.index.orders_inflight`", - failure, client_order_id - ); - error_count += 1; - } - if order.is_open() && !self.index.orders_open.contains(client_order_id) { - error!( - "{} in orders: {} not found in `self.index.orders_open`", - failure, client_order_id - ); - error_count += 1; - } - if order.is_closed() && !self.index.orders_closed.contains(client_order_id) { - error!( - "{} in orders: {} not found in `self.index.orders_closed`", - failure, client_order_id - ); - error_count += 1; - } - if let Some(exec_algorithm_id) = order.exec_algorithm_id() { - if !self - .index - .exec_algorithm_orders - .contains_key(&exec_algorithm_id) - { - error!( - "{} in orders: {} not found in `self.index.exec_algorithm_orders`", - failure, exec_algorithm_id - ); - error_count += 1; - } - if order.exec_spawn_id().is_none() - && !self.index.exec_spawn_orders.contains_key(client_order_id) - { - error!( - "{} in orders: {} not found in `self.index.exec_spawn_orders`", - failure, exec_algorithm_id - ); - error_count += 1; - } - } - } - - for (position_id, position) in &self.positions { - if !self.index.position_strategy.contains_key(position_id) { - error!( - "{} in positions: {} not found in `self.index.position_strategy`", - failure, position_id - ); - error_count += 1; - } - if !self.index.position_orders.contains_key(position_id) { - error!( - "{} in positions: {} not found in `self.index.position_orders`", - failure, position_id - ); - error_count += 1; - } - if !self.index.positions.contains(position_id) { - error!( - "{} in positions: {} not found in `self.index.positions`", - failure, position_id - ); - error_count += 1; - } - if position.is_open() && !self.index.positions_open.contains(position_id) { - error!( - "{} in positions: {} not found in `self.index.positions_open`", - failure, position_id - ); - error_count += 1; - } - if position.is_closed() && !self.index.positions_closed.contains(position_id) { - error!( - "{} in positions: {} not found in `self.index.positions_closed`", - failure, position_id - ); - error_count += 1; - } - } - - // Check indexes - for account_id in self.index.venue_account.values() { - if !self.accounts.contains_key(account_id) { - error!( - "{} in `index.venue_account`: {} not found in `self.accounts`", - failure, account_id - ); - error_count += 1; - } - } - - for client_order_id in self.index.order_ids.values() { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.order_ids`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - for client_order_id in self.index.order_position.keys() { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.order_position`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - // Check indexes - for client_order_id in self.index.order_strategy.keys() { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.order_strategy`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - for position_id in self.index.position_strategy.keys() { - if !self.positions.contains_key(position_id) { - error!( - "{} in `index.position_strategy`: {} not found in `self.positions`", - failure, position_id - ); - error_count += 1; - } - } - - for position_id in self.index.position_orders.keys() { - if !self.positions.contains_key(position_id) { - error!( - "{} in `index.position_orders`: {} not found in `self.positions`", - failure, position_id - ); - error_count += 1; - } - } - - for (instrument_id, client_order_ids) in &self.index.instrument_orders { - for client_order_id in client_order_ids { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.instrument_orders`: {} not found in `self.orders`", - failure, instrument_id - ); - error_count += 1; - } - } - } - - for instrument_id in self.index.instrument_positions.keys() { - if !self.index.instrument_orders.contains_key(instrument_id) { - error!( - "{} in `index.instrument_positions`: {} not found in `index.instrument_orders`", - failure, instrument_id - ); - error_count += 1; - } - } - - for client_order_ids in self.index.strategy_orders.values() { - for client_order_id in client_order_ids { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.strategy_orders`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - } - - for position_ids in self.index.strategy_positions.values() { - for position_id in position_ids { - if !self.positions.contains_key(position_id) { - error!( - "{} in `index.strategy_positions`: {} not found in `self.positions`", - failure, position_id - ); - error_count += 1; - } - } - } - - for client_order_id in &self.index.orders { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.orders`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - for client_order_id in &self.index.orders_emulated { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.orders_emulated`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - for client_order_id in &self.index.orders_inflight { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.orders_inflight`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - for client_order_id in &self.index.orders_open { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.orders_open`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - for client_order_id in &self.index.orders_closed { - if !self.orders.contains_key(client_order_id) { - error!( - "{} in `index.orders_closed`: {} not found in `self.orders`", - failure, client_order_id - ); - error_count += 1; - } - } - - for position_id in &self.index.positions { - if !self.positions.contains_key(position_id) { - error!( - "{} in `index.positions`: {} not found in `self.positions`", - failure, position_id - ); - error_count += 1; - } - } - - for position_id in &self.index.positions_open { - if !self.positions.contains_key(position_id) { - error!( - "{} in `index.positions_open`: {} not found in `self.positions`", - failure, position_id - ); - error_count += 1; - } - } - - for position_id in &self.index.positions_closed { - if !self.positions.contains_key(position_id) { - error!( - "{} in `index.positions_closed`: {} not found in `self.positions`", - failure, position_id - ); - error_count += 1; - } - } - - for strategy_id in &self.index.strategies { - if !self.index.strategy_orders.contains_key(strategy_id) { - error!( - "{} in `index.strategies`: {} not found in `index.strategy_orders`", - failure, strategy_id - ); - error_count += 1; - } - } - - for exec_algorithm_id in &self.index.exec_algorithms { - if !self - .index - .exec_algorithm_orders - .contains_key(exec_algorithm_id) - { - error!( - "{} in `index.exec_algorithms`: {} not found in `index.exec_algorithm_orders`", - failure, exec_algorithm_id - ); - error_count += 1; - } - } - - // Finally - let total_us = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros() - - timestamp_us; - - if error_count == 0 { - info!("Integrity check passed in {}μs", total_us); - true - } else { - error!( - "Integrity check failed with {} error{} in {}μs", - error_count, - if error_count == 1 { "" } else { "s" }, - total_us - ); - false - } - } - - /// Check for any residual open state and log warnings if any are found. - /// - ///'Open state' is considered to be open orders and open positions. - #[must_use] - pub fn check_residuals(&self) -> bool { - debug!("Checking residuals"); - - let mut residuals = false; - - // Check for any open orders - for order in self.orders_open(None, None, None, None) { - residuals = true; - warn!("Residual {:?}", order); - } - - // Check for any open positions - for position in self.positions_open(None, None, None, None) { - residuals = true; - warn!("Residual {}", position); - } - - residuals - } - - /// Clear the caches index. - pub fn clear_index(&mut self) { - self.index.clear(); - debug!("Cleared index"); - } - - /// Reset the cache. - /// - /// All stateful fields are reset to their initial value. - pub fn reset(&mut self) { - debug!("Resetting cache"); - - self.general.clear(); - self.quotes.clear(); - self.trades.clear(); - self.books.clear(); - self.bars.clear(); - self.instruments.clear(); - self.synthetics.clear(); - self.accounts.clear(); - self.orders.clear(); - // self.order_lists.clear(); // TODO - self.positions.clear(); - self.position_snapshots.clear(); - - self.clear_index(); - - info!("Reset cache"); - } - - /// Dispose of the cache which will close any underlying database adapter. - pub fn dispose(&self) -> anyhow::Result<()> { - if let Some(database) = &self.database { - // TODO: Log operations in database adapter - database.close()?; - } - Ok(()) - } - - /// Flush the caches database which permanently removes all persisted data. - pub fn flush_db(&self) -> anyhow::Result<()> { - if let Some(database) = &self.database { - // TODO: Log operations in database adapter - database.flush()?; - } - Ok(()) - } - - /// Add the given general object to the cache. - /// - /// The cache is agnostic to what the object actually is (and how it may be serialized), - /// offering maximum flexibility. - pub fn add(&mut self, key: &str, value: Vec) -> anyhow::Result<()> { - check_valid_string(key, stringify!(key))?; - check_slice_not_empty(value.as_slice(), stringify!(value))?; - - debug!("Add general {key}"); - self.general.insert(key.to_string(), value.clone()); - - if let Some(database) = &self.database { - database.add(key.to_string(), value)?; - } - Ok(()) - } - - /// Add the given order `book` to the cache. - pub fn add_order_book(&mut self, book: OrderBook) -> anyhow::Result<()> { - debug!("Add `OrderBook` {}", book.instrument_id); - self.books.insert(book.instrument_id, book); - Ok(()) - } - - /// Add the given `quote` tick to the cache. - pub fn add_quote(&mut self, quote: QuoteTick) -> anyhow::Result<()> { - debug!("Add `QuoteTick` {}", quote.instrument_id); - let quotes_deque = self - .quotes - .entry(quote.instrument_id) - .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - quotes_deque.push_front(quote); - Ok(()) - } - - /// Add the given `quotes` to the cache. - pub fn add_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> { - check_slice_not_empty(quotes, stringify!(quotes))?; - - let instrument_id = quotes[0].instrument_id; - debug!("Add `QuoteTick`[{}] {}", quotes.len(), instrument_id); - let quotes_deque = self - .quotes - .entry(instrument_id) - .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - - for quote in quotes { - quotes_deque.push_front(*quote); - } - Ok(()) - } - - /// Add the given `trade` tick to the cache. - pub fn add_trade(&mut self, trade: TradeTick) -> anyhow::Result<()> { - debug!("Add `TradeTick` {}", trade.instrument_id); - let trades_deque = self - .trades - .entry(trade.instrument_id) - .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - trades_deque.push_front(trade); - Ok(()) - } - - /// Add the give `trades` to the cache. - pub fn add_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> { - check_slice_not_empty(trades, stringify!(trades))?; - - let instrument_id = trades[0].instrument_id; - debug!("Add `TradeTick`[{}] {}", trades.len(), instrument_id); - let trades_deque = self - .trades - .entry(instrument_id) - .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - - for trade in trades { - trades_deque.push_front(*trade); - } - Ok(()) - } - - /// Add the given `bar` to the cache. - pub fn add_bar(&mut self, bar: Bar) -> anyhow::Result<()> { - debug!("Add `Bar` {}", bar.bar_type); - let bars = self - .bars - .entry(bar.bar_type) - .or_insert_with(|| VecDeque::with_capacity(self.config.bar_capacity)); - bars.push_front(bar); - Ok(()) - } - - /// Add the given `bars` to the cache. - pub fn add_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> { - check_slice_not_empty(bars, stringify!(bars))?; - - let bar_type = bars[0].bar_type; - debug!("Add `Bar`[{}] {}", bars.len(), bar_type); - let bars_deque = self - .bars - .entry(bar_type) - .or_insert_with(|| VecDeque::with_capacity(self.config.tick_capacity)); - - for bar in bars { - bars_deque.push_front(*bar); - } - Ok(()) - } - - /// Add the given `currency` to the cache. - pub fn add_currency(&mut self, currency: Currency) -> anyhow::Result<()> { - debug!("Add `Currency` {}", currency.code); - - if let Some(database) = &self.database { - database.add_currency(¤cy)?; - } - - self.currencies.insert(currency.code, currency); - Ok(()) - } - - /// Add the given `instrument` to the cache. - pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> { - debug!("Add `Instrument` {}", instrument.id()); - - if let Some(database) = &self.database { - database.add_instrument(&instrument)?; - } - - self.instruments.insert(instrument.id(), instrument); - Ok(()) - } - - /// Add the given `synthetic` instrument to the cache. - pub fn add_synthetic(&mut self, synthetic: SyntheticInstrument) -> anyhow::Result<()> { - debug!("Add `SyntheticInstrument` {}", synthetic.id); - - if let Some(database) = &self.database { - database.add_synthetic(&synthetic)?; - } - - self.synthetics.insert(synthetic.id, synthetic); - Ok(()) - } - - /// Add the given `account` to the cache. - pub fn add_account(&mut self, account: Box) -> anyhow::Result<()> { - debug!("Add `Account` {}", account.id()); - - if let Some(database) = &self.database { - database.add_account(account.as_ref())?; - } - - self.accounts.insert(account.id(), account); - Ok(()) - } - - /// Add the order to the cache indexed with any given identifiers. - /// - /// # Parameters - /// - /// `override_existing`: If the added order should 'override' any existing order and replace - /// it in the cache. This is currently used for emulated orders which are - /// being released and transformed into another type. - /// - /// # Errors - /// - /// If not `replace_existing` and the `order.client_order_id` is already contained in the cache. - pub fn add_order( - &mut self, - order: OrderAny, - _position_id: Option, - client_id: Option, - replace_existing: bool, - ) -> anyhow::Result<()> { - let instrument_id = order.instrument_id(); - let venue = instrument_id.venue; - let client_order_id = order.client_order_id(); - let strategy_id = order.strategy_id(); - let exec_algorithm_id = order.exec_algorithm_id(); - let exec_spawn_id = order.exec_spawn_id(); - - if !replace_existing { - check_key_not_in_map( - &client_order_id, - &self.orders, - stringify!(client_order_id), - stringify!(orders), - )?; - check_key_not_in_map( - &client_order_id, - &self.orders, - stringify!(client_order_id), - stringify!(orders), - )?; - check_key_not_in_map( - &client_order_id, - &self.orders, - stringify!(client_order_id), - stringify!(orders), - )?; - check_key_not_in_map( - &client_order_id, - &self.orders, - stringify!(client_order_id), - stringify!(orders), - )?; - }; - - debug!("Added {:?}", order); - - self.index.orders.insert(client_order_id); - self.index - .order_strategy - .insert(client_order_id, strategy_id); - self.index.strategies.insert(strategy_id); - - // Update venue -> orders index - self.index - .venue_orders - .entry(venue) - .or_default() - .insert(client_order_id); - - // Update instrument -> orders index - self.index - .instrument_orders - .entry(instrument_id) - .or_default() - .insert(client_order_id); - - // Update strategy -> orders index - self.index - .strategy_orders - .entry(strategy_id) - .or_default() - .insert(client_order_id); - - // Update exec_algorithm -> orders index - if let Some(exec_algorithm_id) = exec_algorithm_id { - self.index.exec_algorithms.insert(exec_algorithm_id); - - self.index - .exec_algorithm_orders - .entry(exec_algorithm_id) - .or_default() - .insert(client_order_id); - - // SAFETY: We can guarantee the `exec_spawn_id` is Some - self.index - .exec_spawn_orders - .entry(exec_spawn_id.unwrap()) - .or_default() - .insert(client_order_id); - } - - // TODO: Change emulation trigger setup - // Update emulation index - // match order.emulation_trigger() { - // TriggerType::NoTrigger => { - // self.index.orders_emulated.remove(&client_order_id); - // } - // _ => { - // self.index.orders_emulated.insert(client_order_id.clone()); - // } - // } - - // Index position ID if provided - if let Some(position_id) = order.position_id() { - self.add_position_id( - &position_id, - &order.instrument_id().venue, - &client_order_id, - &strategy_id, - )?; - } - - // Index client ID if provided - if let Some(client_id) = client_id { - self.index.order_client.insert(client_order_id, client_id); - log::debug!("Indexed {:?}", client_id); - } - - if let Some(database) = &mut self.database { - database.add_order(&order)?; - // TODO: Implement - // if self.config.snapshot_orders { - // database.snapshot_order_state(order)?; - // } - } - - self.orders.insert(client_order_id, order); - - Ok(()) - } - - /// Index the given `position_id` with the other given IDs. - pub fn add_position_id( - &mut self, - position_id: &PositionId, - venue: &Venue, - client_order_id: &ClientOrderId, - strategy_id: &StrategyId, - ) -> anyhow::Result<()> { - self.index - .order_position - .insert(*client_order_id, *position_id); - - // Index: ClientOrderId -> PositionId - if let Some(database) = &mut self.database { - database.index_order_position(*client_order_id, *position_id)?; - } - - // Index: PositionId -> StrategyId - self.index - .position_strategy - .insert(*position_id, *strategy_id); - - // Index: PositionId -> set[ClientOrderId] - self.index - .position_orders - .entry(*position_id) - .or_default() - .insert(*client_order_id); - - // Index: StrategyId -> set[PositionId] - self.index - .strategy_positions - .entry(*strategy_id) - .or_default() - .insert(*position_id); - - Ok(()) - } - - pub fn add_position(&mut self, position: Position, oms_type: OmsType) -> anyhow::Result<()> { - self.positions.insert(position.id, position.clone()); - self.index.positions.insert(position.id); - self.index.positions_open.insert(position.id); - - self.add_position_id( - &position.id, - &position.instrument_id.venue, - &position.opening_order_id, - &position.strategy_id, - )?; - - let venue = position.instrument_id.venue; - let venue_positions = self.index.venue_positions.entry(venue).or_default(); - venue_positions.insert(position.id); - - // Index: InstrumentId -> HashSet - let instrument_id = position.instrument_id; - let instrument_positions = self - .index - .instrument_positions - .entry(instrument_id) - .or_default(); - instrument_positions.insert(position.id); - - log::debug!( - "Added Position(id={}, strategy_id={})", - position.id, - position.strategy_id, - ); - - if let Some(database) = &mut self.database { - database.add_position(&position)?; - // TODO: Implement position snapshots - // if self.snapshot_positions { - // database.snapshot_position_state( - // position, - // position.ts_last, - // self.calculate_unrealized_pnl(&position), - // )?; - // } - } - - Ok(()) - } - - /// Update the given `account` in the cache. - pub fn update_account(&mut self, account: &dyn Account) -> anyhow::Result<()> { - if let Some(database) = &mut self.database { - database.update_account(account)?; - } - Ok(()) - } - - /// Update the given `order` in the cache. - pub fn update_order(&mut self, order: &OrderAny) -> anyhow::Result<()> { - let client_order_id = order.client_order_id(); - - // Update venue order ID - if let Some(venue_order_id) = order.venue_order_id() { - // Assumes order_id does not change - self.index.order_ids.insert(venue_order_id, client_order_id); - } - - // Update in-flight state - if order.is_inflight() { - self.index.orders_inflight.insert(client_order_id); - } else { - self.index.orders_inflight.remove(&client_order_id); - } - - // Update open/closed state - if order.is_open() { - self.index.orders_closed.remove(&client_order_id); - self.index.orders_open.insert(client_order_id); - } else if order.is_closed() { - self.index.orders_open.remove(&client_order_id); - self.index.orders_pending_cancel.remove(&client_order_id); - self.index.orders_closed.insert(client_order_id); - } - - // Update emulation - if let Some(emulation_trigger) = order.emulation_trigger() { - match emulation_trigger { - TriggerType::NoTrigger => self.index.orders_emulated.remove(&client_order_id), - _ => self.index.orders_emulated.insert(client_order_id), - }; - } - - if let Some(database) = &mut self.database { - database.update_order(order)?; - // TODO: Implement order snapshots - // if self.snapshot_orders { - // database.snapshot_order_state(order)?; - // } - } - - Ok(()) - } - - /// Update the given `order` as pending cancel locally. - pub fn update_order_pending_cancel_local(&mut self, order: &OrderAny) { - self.index - .orders_pending_cancel - .insert(order.client_order_id()); - } - - /// Update the given `position` in the cache. - pub fn update_position(&mut self, position: &Position) -> anyhow::Result<()> { - // Update open/closed state - if position.is_open() { - self.index.positions_open.insert(position.id); - self.index.positions_closed.remove(&position.id); - } else { - self.index.positions_closed.insert(position.id); - self.index.positions_open.remove(&position.id); - } - - if let Some(database) = &mut self.database { - database.update_position(position)?; - // TODO: Implement order snapshots - // if self.snapshot_orders { - // database.snapshot_order_state(order)?; - // } - } - Ok(()) - } - - // -- IDENTIFIER QUERIES ---------------------------------------------------------------------- - - fn build_order_query_filter_set( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> Option> { - let mut query: Option> = None; - - if let Some(venue) = venue { - query = Some( - self.index - .venue_orders - .get(venue) - .map_or(HashSet::new(), |o| o.iter().copied().collect()), - ); - }; - - if let Some(instrument_id) = instrument_id { - let instrument_orders = self - .index - .instrument_orders - .get(instrument_id) - .map_or(HashSet::new(), |o| o.iter().copied().collect()); - - if let Some(existing_query) = &mut query { - *existing_query = existing_query - .intersection(&instrument_orders) - .copied() - .collect(); - } else { - query = Some(instrument_orders); - }; - }; - - if let Some(strategy_id) = strategy_id { - let strategy_orders = self - .index - .strategy_orders - .get(strategy_id) - .map_or(HashSet::new(), |o| o.iter().copied().collect()); - - if let Some(existing_query) = &mut query { - *existing_query = existing_query - .intersection(&strategy_orders) - .copied() - .collect(); - } else { - query = Some(strategy_orders); - }; - }; - - query - } - - fn build_position_query_filter_set( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> Option> { - let mut query: Option> = None; - - if let Some(venue) = venue { - query = Some( - self.index - .venue_positions - .get(venue) - .map_or(HashSet::new(), |p| p.iter().copied().collect()), - ); - }; - - if let Some(instrument_id) = instrument_id { - let instrument_positions = self - .index - .instrument_positions - .get(instrument_id) - .map_or(HashSet::new(), |p| p.iter().copied().collect()); - - if let Some(existing_query) = query { - query = Some( - existing_query - .intersection(&instrument_positions) - .copied() - .collect(), - ); - } else { - query = Some(instrument_positions); - }; - }; - - if let Some(strategy_id) = strategy_id { - let strategy_positions = self - .index - .strategy_positions - .get(strategy_id) - .map_or(HashSet::new(), |p| p.iter().copied().collect()); - - if let Some(existing_query) = query { - query = Some( - existing_query - .intersection(&strategy_positions) - .copied() - .collect(), - ); - } else { - query = Some(strategy_positions); - }; - }; - - query - } - - fn get_orders_for_ids( - &self, - client_order_ids: &HashSet, - side: Option, - ) -> Vec<&OrderAny> { - let side = side.unwrap_or(OrderSide::NoOrderSide); - let mut orders = Vec::new(); - - for client_order_id in client_order_ids { - let order = self - .orders - .get(client_order_id) - .unwrap_or_else(|| panic!("Order {client_order_id} not found")); - if side == OrderSide::NoOrderSide || side == order.order_side() { - orders.push(order); - }; - } - - orders - } - - fn get_positions_for_ids( - &self, - position_ids: &HashSet, - side: Option, - ) -> Vec<&Position> { - let side = side.unwrap_or(PositionSide::NoPositionSide); - let mut positions = Vec::new(); - - for position_id in position_ids { - let position = self - .positions - .get(position_id) - .unwrap_or_else(|| panic!("Position {position_id} not found")); - if side == PositionSide::NoPositionSide || side == position.side { - positions.push(position); - }; - } - - positions - } - - #[must_use] - pub fn client_order_ids( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self.index.orders.intersection(&query).copied().collect(), - None => self.index.orders.clone(), - } - } - - #[must_use] - pub fn client_order_ids_open( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self - .index - .orders_open - .intersection(&query) - .copied() - .collect(), - None => self.index.orders_open.clone(), - } - } - - #[must_use] - pub fn client_order_ids_closed( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self - .index - .orders_closed - .intersection(&query) - .copied() - .collect(), - None => self.index.orders_closed.clone(), - } - } - - #[must_use] - pub fn client_order_ids_emulated( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self - .index - .orders_emulated - .intersection(&query) - .copied() - .collect(), - None => self.index.orders_emulated.clone(), - } - } - - #[must_use] - pub fn client_order_ids_inflight( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self - .index - .orders_inflight - .intersection(&query) - .copied() - .collect(), - None => self.index.orders_inflight.clone(), - } - } - - #[must_use] - pub fn position_ids( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self.index.positions.intersection(&query).copied().collect(), - None => self.index.positions.clone(), - } - } - - #[must_use] - pub fn position_open_ids( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self - .index - .positions_open - .intersection(&query) - .copied() - .collect(), - None => self.index.positions_open.clone(), - } - } - - #[must_use] - pub fn position_closed_ids( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> HashSet { - let query = self.build_position_query_filter_set(venue, instrument_id, strategy_id); - match query { - Some(query) => self - .index - .positions_closed - .intersection(&query) - .copied() - .collect(), - None => self.index.positions_closed.clone(), - } - } - - #[must_use] - pub fn actor_ids(&self) -> HashSet { - self.index.actors.clone() - } - - #[must_use] - pub fn strategy_ids(&self) -> HashSet { - self.index.strategies.clone() - } - - #[must_use] - pub fn exec_algorithm_ids(&self) -> HashSet { - self.index.exec_algorithms.clone() - } - - // -- ORDER QUERIES --------------------------------------------------------------------------- - - #[must_use] - pub fn order(&self, client_order_id: &ClientOrderId) -> Option<&OrderAny> { - self.orders.get(client_order_id) - } - - #[must_use] - pub fn client_order_id(&self, venue_order_id: &VenueOrderId) -> Option<&ClientOrderId> { - self.index.order_ids.get(venue_order_id) - } - - #[must_use] - pub fn venue_order_id(&self, client_order_id: &ClientOrderId) -> Option { - self.orders - .get(client_order_id) - .and_then(nautilus_model::polymorphism::GetVenueOrderId::venue_order_id) - } - - #[must_use] - pub fn client_id(&self, client_order_id: &ClientOrderId) -> Option<&ClientId> { - self.index.order_client.get(client_order_id) - } - - #[must_use] - pub fn orders( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&OrderAny> { - let client_order_ids = self.client_order_ids(venue, instrument_id, strategy_id); - self.get_orders_for_ids(&client_order_ids, side) - } - - #[must_use] - pub fn orders_open( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&OrderAny> { - let client_order_ids = self.client_order_ids_open(venue, instrument_id, strategy_id); - self.get_orders_for_ids(&client_order_ids, side) - } - - #[must_use] - pub fn orders_closed( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&OrderAny> { - let client_order_ids = self.client_order_ids_closed(venue, instrument_id, strategy_id); - self.get_orders_for_ids(&client_order_ids, side) - } - - #[must_use] - pub fn orders_emulated( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&OrderAny> { - let client_order_ids = self.client_order_ids_emulated(venue, instrument_id, strategy_id); - self.get_orders_for_ids(&client_order_ids, side) - } - - #[must_use] - pub fn orders_inflight( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&OrderAny> { - let client_order_ids = self.client_order_ids_inflight(venue, instrument_id, strategy_id); - self.get_orders_for_ids(&client_order_ids, side) - } - - #[must_use] - pub fn orders_for_position(&self, position_id: PositionId) -> Vec<&OrderAny> { - let client_order_ids = self.index.position_orders.get(&position_id); - match client_order_ids { - Some(client_order_ids) => { - self.get_orders_for_ids(&client_order_ids.iter().copied().collect(), None) - } - None => Vec::new(), - } - } - - #[must_use] - pub fn order_exists(&self, client_order_id: &ClientOrderId) -> bool { - self.index.orders.contains(client_order_id) - } - - #[must_use] - pub fn is_order_open(&self, client_order_id: &ClientOrderId) -> bool { - self.index.orders_open.contains(client_order_id) - } - - #[must_use] - pub fn is_order_closed(&self, client_order_id: &ClientOrderId) -> bool { - self.index.orders_closed.contains(client_order_id) - } - - #[must_use] - pub fn is_order_emulated(&self, client_order_id: &ClientOrderId) -> bool { - self.index.orders_emulated.contains(client_order_id) - } - - #[must_use] - pub fn is_order_inflight(&self, client_order_id: &ClientOrderId) -> bool { - self.index.orders_inflight.contains(client_order_id) - } - - #[must_use] - pub fn is_order_pending_cancel_local(&self, client_order_id: &ClientOrderId) -> bool { - self.index.orders_pending_cancel.contains(client_order_id) - } - - #[must_use] - pub fn orders_open_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> usize { - self.orders_open(venue, instrument_id, strategy_id, side) - .len() - } - - #[must_use] - pub fn orders_closed_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> usize { - self.orders_closed(venue, instrument_id, strategy_id, side) - .len() - } - - #[must_use] - pub fn orders_emulated_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> usize { - self.orders_emulated(venue, instrument_id, strategy_id, side) - .len() - } - - #[must_use] - pub fn orders_inflight_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> usize { - self.orders_inflight(venue, instrument_id, strategy_id, side) - .len() - } - - #[must_use] - pub fn orders_total_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> usize { - self.orders(venue, instrument_id, strategy_id, side).len() - } - - #[must_use] - pub fn order_list(&self, order_list_id: &OrderListId) -> Option<&OrderList> { - self.order_lists.get(order_list_id) - } - - #[must_use] - pub fn order_lists( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - ) -> Vec<&OrderList> { - let mut order_lists = self.order_lists.values().collect::>(); - - if let Some(venue) = venue { - order_lists.retain(|ol| &ol.instrument_id.venue == venue); - } - - if let Some(instrument_id) = instrument_id { - order_lists.retain(|ol| &ol.instrument_id == instrument_id); - } - - if let Some(strategy_id) = strategy_id { - order_lists.retain(|ol| &ol.strategy_id == strategy_id); - } - - order_lists - } - - #[must_use] - pub fn order_list_exists(&self, order_list_id: &OrderListId) -> bool { - self.order_lists.contains_key(order_list_id) - } - - // -- EXEC ALGORITHM QUERIES ------------------------------------------------------------------ - - #[must_use] - pub fn orders_for_exec_algorithm( - &self, - exec_algorithm_id: &ExecAlgorithmId, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&OrderAny> { - let query = self.build_order_query_filter_set(venue, instrument_id, strategy_id); - let exec_algorithm_order_ids = self.index.exec_algorithm_orders.get(exec_algorithm_id); - - if let Some(query) = query { - if let Some(exec_algorithm_order_ids) = exec_algorithm_order_ids { - let exec_algorithm_order_ids = exec_algorithm_order_ids.intersection(&query); - } - } - - if let Some(exec_algorithm_order_ids) = exec_algorithm_order_ids { - self.get_orders_for_ids(exec_algorithm_order_ids, side) - } else { - Vec::new() - } - } - - #[must_use] - pub fn orders_for_exec_spawn(&self, exec_spawn_id: &ClientOrderId) -> Vec<&OrderAny> { - self.get_orders_for_ids( - self.index - .exec_spawn_orders - .get(exec_spawn_id) - .unwrap_or(&HashSet::new()), - None, - ) - } - - #[must_use] - pub fn exec_spawn_total_quantity( - &self, - exec_spawn_id: &ClientOrderId, - active_only: bool, - ) -> Option { - let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); - - let mut total_quantity: Option = None; - - for spawn_order in exec_spawn_orders { - if !active_only || !spawn_order.is_closed() { - if let Some(mut total_quantity) = total_quantity { - total_quantity += spawn_order.quantity(); - } - } else { - total_quantity = Some(spawn_order.quantity()); - } - } - - total_quantity - } - - #[must_use] - pub fn exec_spawn_total_filled_qty( - &self, - exec_spawn_id: &ClientOrderId, - active_only: bool, - ) -> Option { - let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); - - let mut total_quantity: Option = None; - - for spawn_order in exec_spawn_orders { - if !active_only || !spawn_order.is_closed() { - if let Some(mut total_quantity) = total_quantity { - total_quantity += spawn_order.filled_qty(); - } - } else { - total_quantity = Some(spawn_order.filled_qty()); - } - } - - total_quantity - } - - #[must_use] - pub fn exec_spawn_total_leaves_qty( - &self, - exec_spawn_id: &ClientOrderId, - active_only: bool, - ) -> Option { - let exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id); - - let mut total_quantity: Option = None; - - for spawn_order in exec_spawn_orders { - if !active_only || !spawn_order.is_closed() { - if let Some(mut total_quantity) = total_quantity { - total_quantity += spawn_order.leaves_qty(); - } - } else { - total_quantity = Some(spawn_order.leaves_qty()); - } - } - - total_quantity - } - - // -- POSITION QUERIES ------------------------------------------------------------------------ - - #[must_use] - pub fn position(&self, position_id: &PositionId) -> Option<&Position> { - self.positions.get(position_id) - } - - #[must_use] - pub fn position_for_order(&self, client_order_id: &ClientOrderId) -> Option<&Position> { - self.index - .order_position - .get(client_order_id) - .and_then(|position_id| self.positions.get(position_id)) - } - - #[must_use] - pub fn position_id(&self, client_order_id: &ClientOrderId) -> Option<&PositionId> { - self.index.order_position.get(client_order_id) - } - - #[must_use] - pub fn positions( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&Position> { - let position_ids = self.position_ids(venue, instrument_id, strategy_id); - self.get_positions_for_ids(&position_ids, side) - } - - #[must_use] - pub fn positions_open( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&Position> { - let position_ids = self.position_open_ids(venue, instrument_id, strategy_id); - self.get_positions_for_ids(&position_ids, side) - } - - #[must_use] - pub fn positions_closed( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> Vec<&Position> { - let position_ids = self.position_closed_ids(venue, instrument_id, strategy_id); - self.get_positions_for_ids(&position_ids, side) - } - - #[must_use] - pub fn position_exists(&self, position_id: &PositionId) -> bool { - self.index.positions.contains(position_id) - } - - #[must_use] - pub fn is_position_open(&self, position_id: &PositionId) -> bool { - self.index.positions_open.contains(position_id) - } - - #[must_use] - pub fn is_position_closed(&self, position_id: &PositionId) -> bool { - self.index.positions_closed.contains(position_id) - } - - #[must_use] - pub fn positions_open_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> u64 { - self.positions_open(venue, instrument_id, strategy_id, side) - .len() as u64 - } - - #[must_use] - pub fn positions_closed_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> u64 { - self.positions_closed(venue, instrument_id, strategy_id, side) - .len() as u64 - } - - #[must_use] - pub fn positions_total_count( - &self, - venue: Option<&Venue>, - instrument_id: Option<&InstrumentId>, - strategy_id: Option<&StrategyId>, - side: Option, - ) -> u64 { - self.positions(venue, instrument_id, strategy_id, side) - .len() as u64 - } - - // -- STRATEGY QUERIES ------------------------------------------------------------------------ - - #[must_use] - pub fn strategy_id_for_order(&self, client_order_id: &ClientOrderId) -> Option<&StrategyId> { - self.index.order_strategy.get(client_order_id) - } - - #[must_use] - pub fn strategy_id_for_position(&self, position_id: &PositionId) -> Option<&StrategyId> { - self.index.position_strategy.get(position_id) - } - - // -- GENERAL --------------------------------------------------------------------------------- - - pub fn get(&self, key: &str) -> anyhow::Result> { - check_valid_string(key, stringify!(key))?; - - Ok(self.general.get(key).map(std::vec::Vec::as_slice)) - } - - // -- DATA QUERIES ---------------------------------------------------------------------------- - - #[must_use] - pub fn price(&self, instrument_id: &InstrumentId, price_type: PriceType) -> Option { - match price_type { - PriceType::Bid => self - .quotes - .get(instrument_id) - .and_then(|quotes| quotes.front().map(|quote| quote.bid_price)), - PriceType::Ask => self - .quotes - .get(instrument_id) - .and_then(|quotes| quotes.front().map(|quote| quote.ask_price)), - PriceType::Mid => self.quotes.get(instrument_id).and_then(|quotes| { - quotes.front().map(|quote| { - Price::new( - (quote.ask_price.as_f64() + quote.bid_price.as_f64()) / 2.0, - quote.bid_price.precision + 1, - ) - .expect("Error calculating mid price") - }) - }), - PriceType::Last => self - .trades - .get(instrument_id) - .and_then(|trades| trades.front().map(|trade| trade.price)), - } - } - - #[must_use] - pub fn quote_ticks(&self, instrument_id: &InstrumentId) -> Option> { - self.quotes - .get(instrument_id) - .map(|quotes| quotes.iter().copied().collect()) - } - - #[must_use] - pub fn trade_ticks(&self, instrument_id: &InstrumentId) -> Option> { - self.trades - .get(instrument_id) - .map(|trades| trades.iter().copied().collect()) - } - - #[must_use] - pub fn bars(&self, bar_type: &BarType) -> Option> { - self.bars - .get(bar_type) - .map(|bars| bars.iter().copied().collect()) - } - - #[must_use] - pub fn order_book(&self, instrument_id: &InstrumentId) -> Option<&OrderBook> { - self.books.get(instrument_id) - } - - #[must_use] - pub fn quote_tick(&self, instrument_id: &InstrumentId) -> Option<&QuoteTick> { - self.quotes - .get(instrument_id) - .and_then(|quotes| quotes.front()) - } - - #[must_use] - pub fn trade_tick(&self, instrument_id: &InstrumentId) -> Option<&TradeTick> { - self.trades - .get(instrument_id) - .and_then(|trades| trades.front()) - } - - #[must_use] - pub fn bar(&self, bar_type: &BarType) -> Option<&Bar> { - self.bars.get(bar_type).and_then(|bars| bars.front()) - } - - #[must_use] - pub fn book_update_count(&self, instrument_id: &InstrumentId) -> u64 { - self.books.get(instrument_id).map_or(0, |book| book.count) - } - - #[must_use] - pub fn quote_tick_count(&self, instrument_id: &InstrumentId) -> u64 { - self.quotes - .get(instrument_id) - .map_or(0, std::collections::VecDeque::len) as u64 - } - - #[must_use] - pub fn trade_tick_count(&self, instrument_id: &InstrumentId) -> u64 { - self.trades - .get(instrument_id) - .map_or(0, std::collections::VecDeque::len) as u64 - } - - #[must_use] - pub fn bar_count(&self, bar_type: &BarType) -> u64 { - self.bars - .get(bar_type) - .map_or(0, std::collections::VecDeque::len) as u64 - } - - #[must_use] - pub fn has_order_book(&self, instrument_id: &InstrumentId) -> bool { - self.books.contains_key(instrument_id) - } - - #[must_use] - pub fn has_quote_ticks(&self, instrument_id: &InstrumentId) -> bool { - self.quote_tick_count(instrument_id) > 0 - } - - #[must_use] - pub fn has_trade_ticks(&self, instrument_id: &InstrumentId) -> bool { - self.trade_tick_count(instrument_id) > 0 - } - - #[must_use] - pub fn has_bars(&self, bar_type: &BarType) -> bool { - self.bar_count(bar_type) > 0 - } - - // -- INSTRUMENT QUERIES ---------------------------------------------------------------------- - - #[must_use] - pub fn instrument(&self, instrument_id: &InstrumentId) -> Option<&InstrumentAny> { - self.instruments.get(instrument_id) - } - - #[must_use] - pub fn instrument_ids(&self, venue: &Venue) -> Vec<&InstrumentId> { - self.instruments - .keys() - .filter(|i| &i.venue == venue) - .collect() - } - - #[must_use] - pub fn instruments(&self, venue: &Venue) -> Vec<&InstrumentAny> { - self.instruments - .values() - .filter(|i| &i.id().venue == venue) - .collect() - } - - #[must_use] - pub fn bar_types( - &self, - instrument_id: Option<&InstrumentId>, - price_type: Option<&PriceType>, - aggregation_source: AggregationSource, - ) -> Vec<&BarType> { - let mut bar_types = self - .bars - .keys() - .filter(|bar_type| bar_type.aggregation_source == aggregation_source) - .collect::>(); - - if let Some(instrument_id) = instrument_id { - bar_types.retain(|bar_type| &bar_type.instrument_id == instrument_id); - } - - if let Some(price_type) = price_type { - bar_types.retain(|bar_type| &bar_type.spec.price_type == price_type); - } - - bar_types - } - - // -- SYNTHETIC QUERIES ----------------------------------------------------------------------- - - #[must_use] - pub fn synthetic(&self, instrument_id: &InstrumentId) -> Option<&SyntheticInstrument> { - self.synthetics.get(instrument_id) - } - - #[must_use] - pub fn synthetic_ids(&self) -> Vec<&InstrumentId> { - self.synthetics.keys().collect() - } - - #[must_use] - pub fn synthetics(&self) -> Vec<&SyntheticInstrument> { - self.synthetics.values().collect() - } - - // -- ACCOUNT QUERIES ----------------------------------------------------------------------- - - #[must_use] - pub fn account(&self, account_id: &AccountId) -> Option<&dyn Account> { - self.accounts - .get(account_id) - .map(std::convert::AsRef::as_ref) - } - - #[must_use] - pub fn account_for_venue(&self, venue: &Venue) -> Option<&dyn Account> { - self.index - .venue_account - .get(venue) - .and_then(|account_id| self.accounts.get(account_id)) - .map(std::convert::AsRef::as_ref) - } - - #[must_use] - pub fn account_id(&self, venue: &Venue) -> Option<&AccountId> { - self.index.venue_account.get(venue) - } - - #[must_use] - pub fn accounts(&self, account_id: &AccountId) -> Vec<&dyn Account> { - self.accounts - .values() - .map(std::convert::AsRef::as_ref) - .collect() - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Tests -//////////////////////////////////////////////////////////////////////////////// -#[cfg(test)] -mod tests { - use nautilus_model::{ - data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, - instruments::{ - currency_pair::CurrencyPair, stubs::*, synthetic::SyntheticInstrument, InstrumentAny, - }, - }; - use rstest::*; - - use super::Cache; - - #[fixture] - fn cache() -> Cache { - Cache::default() - } - - #[rstest] - fn test_build_index_when_empty(mut cache: Cache) { - cache.build_index(); - } - - #[rstest] - fn test_clear_index_when_empty(mut cache: Cache) { - cache.clear_index(); - } - - #[rstest] - fn test_reset_when_empty(mut cache: Cache) { - cache.reset(); - } - - #[rstest] - fn test_dispose_when_empty(cache: Cache) { - let result = cache.dispose(); - assert!(result.is_ok()); - } - - #[rstest] - fn test_flush_db_when_empty(cache: Cache) { - let result = cache.flush_db(); - assert!(result.is_ok()); - } - - #[rstest] - fn test_check_residuals_when_empty(cache: Cache) { - let result = cache.flush_db(); - assert!(result.is_ok()); - } - - #[rstest] - fn test_cache_general_load_when_no_database(mut cache: Cache) { - assert!(cache.cache_general().is_ok()); - } - - #[rstest] - fn test_cache_currencies_load_when_no_database(mut cache: Cache) { - assert!(cache.cache_currencies().is_ok()); - } - - #[rstest] - fn test_cache_instruments_load_when_no_database(mut cache: Cache) { - assert!(cache.cache_instruments().is_ok()); - } - - #[rstest] - fn test_cache_synthetics_when_no_database(mut cache: Cache) { - assert!(cache.cache_synthetics().is_ok()); - } - - #[rstest] - fn test_cache_orders_when_no_database(mut cache: Cache) { - assert!(cache.cache_orders().is_ok()); - } - - #[rstest] - fn test_cache_positions_when_no_database(mut cache: Cache) { - assert!(cache.cache_positions().is_ok()); - } - - #[rstest] - fn test_get_general_when_empty(cache: Cache) { - let result = cache.get("A").unwrap(); - assert_eq!(result, None); - } - - #[rstest] - fn test_add_general_when_value(mut cache: Cache) { - let key = "A"; - let value = vec![0_u8]; - cache.add(key, value.clone()).unwrap(); - let result = cache.get(key).unwrap(); - assert_eq!(result, Some(&value.as_slice()).copied()); - } - - #[rstest] - fn test_instrument_when_empty(cache: Cache, audusd_sim: CurrencyPair) { - let result = cache.instrument(&audusd_sim.id); - assert_eq!(result, None); - } - - #[rstest] - fn test_instrument_when_some(mut cache: Cache, audusd_sim: CurrencyPair) { - cache - .add_instrument(InstrumentAny::CurrencyPair(audusd_sim)) - .unwrap(); - - let result = cache.instrument(&audusd_sim.id); - assert_eq!( - result, - Some(InstrumentAny::CurrencyPair(audusd_sim)).as_ref() - ); - } - - #[rstest] - fn test_synthetic_when_empty(cache: Cache) { - let synth = SyntheticInstrument::default(); - let result = cache.synthetic(&synth.id); - assert_eq!(result, None); - } - - #[rstest] - fn test_synthetic_when_some(cache: Cache) { - let mut cache = Cache::default(); - let synth = SyntheticInstrument::default(); - cache.add_synthetic(synth.clone()).unwrap(); - let result = cache.synthetic(&synth.id); - assert_eq!(result, Some(synth).as_ref()); - } - - #[rstest] - fn test_quote_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { - let result = cache.quote_tick(&audusd_sim.id); - assert_eq!(result, None); - } - - #[rstest] - fn test_quote_tick_when_some(mut cache: Cache) { - let quote = QuoteTick::default(); - cache.add_quote(quote).unwrap(); - let result = cache.quote_tick("e.instrument_id); - assert_eq!(result, Some("e)); - } - - #[rstest] - fn test_quote_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { - let result = cache.quote_ticks(&audusd_sim.id); - assert_eq!(result, None); - } - - #[rstest] - fn test_quote_ticks_when_some(mut cache: Cache) { - let quotes = vec![ - QuoteTick::default(), - QuoteTick::default(), - QuoteTick::default(), - ]; - cache.add_quotes("es).unwrap(); - let result = cache.quote_ticks("es[0].instrument_id); - assert_eq!(result, Some(quotes)); - } - - #[rstest] - fn test_trade_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { - let result = cache.trade_tick(&audusd_sim.id); - assert_eq!(result, None); - } - - #[rstest] - fn test_trade_tick_when_some(mut cache: Cache) { - let trade = TradeTick::default(); - cache.add_trade(trade).unwrap(); - let result = cache.trade_tick(&trade.instrument_id); - assert_eq!(result, Some(&trade)); - } - - #[rstest] - fn test_trade_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { - let result = cache.trade_ticks(&audusd_sim.id); - assert_eq!(result, None); - } - - #[rstest] - fn test_trade_ticks_when_some(mut cache: Cache) { - let trades = vec![ - TradeTick::default(), - TradeTick::default(), - TradeTick::default(), - ]; - cache.add_trades(&trades).unwrap(); - let result = cache.trade_ticks(&trades[0].instrument_id); - assert_eq!(result, Some(trades)); - } - - #[rstest] - fn test_bar_when_empty(cache: Cache) { - let bar = Bar::default(); - let result = cache.bar(&bar.bar_type); - assert_eq!(result, None); - } - - #[rstest] - fn test_bar_when_some(mut cache: Cache) { - let bar = Bar::default(); - cache.add_bar(bar).unwrap(); - let result = cache.bar(&bar.bar_type); - assert_eq!(result, Some(bar).as_ref()); - } - - #[rstest] - fn test_bars_when_empty(cache: Cache) { - let bar = Bar::default(); - let result = cache.bars(&bar.bar_type); - assert_eq!(result, None); - } - - #[rstest] - fn test_bars_when_some(mut cache: Cache) { - let bars = vec![Bar::default(), Bar::default(), Bar::default()]; - cache.add_bars(&bars).unwrap(); - let result = cache.bars(&bars[0].bar_type); - assert_eq!(result, Some(bars)); - } -} +pub use self::core::Cache; diff --git a/nautilus_core/common/src/msgbus/core.rs b/nautilus_core/common/src/msgbus/core.rs new file mode 100644 index 000000000000..441a27123de2 --- /dev/null +++ b/nautilus_core/common/src/msgbus/core.rs @@ -0,0 +1,638 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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::{ + collections::HashMap, + fmt, + hash::{Hash, Hasher}, +}; + +use indexmap::IndexMap; +use log::error; +use nautilus_core::uuid::UUID4; +use nautilus_model::identifiers::trader_id::TraderId; +use serde::{Deserialize, Serialize}; +use ustr::Ustr; + +use crate::handlers::MessageHandler; + +pub const CLOSE_TOPIC: &str = "CLOSE"; + +// Represents a subscription to a particular topic. +// +// This is an internal class intended to be used by the message bus to organize +// topics and their subscribers. +#[derive(Clone, Debug)] +pub struct Subscription { + pub handler: MessageHandler, + pub topic: Ustr, + pub sequence: usize, + pub priority: u8, +} + +impl Subscription { + #[must_use] + pub fn new( + topic: Ustr, + handler: MessageHandler, + sequence: usize, + priority: Option, + ) -> Self { + Self { + topic, + handler, + sequence, + priority: priority.unwrap_or(0), + } + } +} + +impl PartialEq for Subscription { + fn eq(&self, other: &Self) -> bool { + self.topic == other.topic && self.handler.handler_id == other.handler.handler_id + } +} + +impl Eq for Subscription {} + +impl PartialOrd for Subscription { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Subscription { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match other.priority.cmp(&self.priority) { + std::cmp::Ordering::Equal => self.sequence.cmp(&other.sequence), + other => other, + } + } +} + +impl Hash for Subscription { + fn hash(&self, state: &mut H) { + self.topic.hash(state); + self.handler.handler_id.hash(state); + } +} + +/// Represents a bus message including a topic and payload. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BusMessage { + /// The topic to publish on. + pub topic: String, + /// The serialized payload for the message. + pub payload: Vec, +} + +impl fmt::Display for BusMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[{}] {}", + self.topic, + String::from_utf8_lossy(&self.payload) + ) + } +} + +/// Provides a generic message bus to facilitate various messaging patterns. +/// +/// The bus provides both a producer and consumer API for Pub/Sub, Req/Rep, as +/// well as direct point-to-point messaging to registered endpoints. +/// +/// Pub/Sub wildcard patterns for hierarchical topics are possible: +/// - `*` asterisk represents one or more characters in a pattern. +/// - `?` question mark represents a single character in a pattern. +/// +/// Given a topic and pattern potentially containing wildcard characters, i.e. +/// `*` and `?`, where `?` can match any single character in the topic, and `*` +/// can match any number of characters including zero characters. +/// +/// The asterisk in a wildcard matches any character zero or more times. For +/// example, `comp*` matches anything beginning with `comp` which means `comp`, +/// `complete`, and `computer` are all matched. +/// +/// A question mark matches a single character once. For example, `c?mp` matches +/// `camp` and `comp`. The question mark can also be used more than once. +/// For example, `c??p` would match both of the above examples and `coop`. +#[derive(Clone)] +#[allow(clippy::type_complexity)] // Complexity will reduce when Cython eliminated +pub struct MessageBus { + /// The trader ID associated with the message bus. + pub trader_id: TraderId, + /// The instance ID associated with the message bus. + pub instance_id: UUID4, + /// The name for the message bus. + pub name: String, + // The count of messages sent through the bus. + pub sent_count: u64, + // The count of requests processed by the bus. + pub req_count: u64, + // The count of responses processed by the bus. + pub res_count: u64, + /// The count of messages published by the bus. + pub pub_count: u64, + /// If the message bus is backed by a database. + pub has_backing: bool, + /// mapping from topic to the corresponding handler + /// a topic can be a string with wildcards + /// * '?' - any character + /// * '*' - any number of any characters + subscriptions: IndexMap>, + /// maps a pattern to all the handlers registered for it + /// this is updated whenever a new subscription is created. + patterns: IndexMap>, + /// handles a message or a request destined for a specific endpoint. + endpoints: IndexMap, + /// Relates a request with a response + /// a request maps it's id to a handler so that a response + /// with the same id can later be handled. + correlation_index: IndexMap, +} + +impl MessageBus { + /// Creates a new `MessageBus` instance. + pub fn new( + trader_id: TraderId, + instance_id: UUID4, + name: Option, + _config: Option>, + ) -> anyhow::Result { + Ok(Self { + trader_id, + instance_id, + name: name.unwrap_or(stringify!(MessageBus).to_owned()), + sent_count: 0, + req_count: 0, + res_count: 0, + pub_count: 0, + subscriptions: IndexMap::new(), + patterns: IndexMap::new(), + endpoints: IndexMap::new(), + correlation_index: IndexMap::new(), + has_backing: false, + }) + } + + /// Returns the registered endpoint addresses. + #[must_use] + pub fn endpoints(&self) -> Vec<&str> { + self.endpoints.keys().map(Ustr::as_str).collect() + } + + /// Returns the topics for active subscriptions. + #[must_use] + pub fn topics(&self) -> Vec<&str> { + self.subscriptions + .keys() + .map(|s| s.topic.as_str()) + .collect() + } + + /// Returns the active correlation IDs. + #[must_use] + pub fn correlation_ids(&self) -> Vec<&UUID4> { + self.correlation_index.keys().collect() + } + + /// Returns whether there are subscribers for the given `pattern`. + #[must_use] + pub fn has_subscribers(&self, pattern: &str) -> bool { + self.matching_handlers(&Ustr::from(pattern)) + .next() + .is_some() + } + + /// Returns whether there are subscribers for the given `pattern`. + #[must_use] + pub fn subscriptions(&self) -> Vec<&Subscription> { + self.subscriptions.keys().collect() + } + + /// Returns whether there are subscribers for the given `pattern`. + #[must_use] + pub fn subscription_handler_ids(&self) -> Vec<&str> { + self.subscriptions + .keys() + .map(|s| s.handler.handler_id.as_str()) + .collect() + } + + /// Returns whether there are subscribers for the given `pattern`. + #[must_use] + pub fn is_registered(&self, endpoint: &str) -> bool { + self.endpoints.contains_key(&Ustr::from(endpoint)) + } + + /// Returns whether there are subscribers for the given `pattern`. + #[must_use] + pub fn is_subscribed(&self, topic: &str, handler: MessageHandler) -> bool { + let sub = Subscription::new(Ustr::from(topic), handler, self.subscriptions.len(), None); + self.subscriptions.contains_key(&sub) + } + + /// Returns whether there is a pending request for the given `request_id`. + #[must_use] + pub fn is_pending_response(&self, request_id: &UUID4) -> bool { + self.correlation_index.contains_key(request_id) + } + + /// Close the message bus which will close the sender channel and join the thread. + pub fn close(&self) -> anyhow::Result<()> { + // TODO: Integrate the backing database + Ok(()) + } + + /// Registers the given `handler` for the `endpoint` address. + pub fn register(&mut self, endpoint: &str, handler: MessageHandler) { + // Updates value if key already exists + self.endpoints.insert(Ustr::from(endpoint), handler); + } + + /// Deregisters the given `handler` for the `endpoint` address. + pub fn deregister(&mut self, endpoint: &str) { + // Removes entry if it exists for endpoint + self.endpoints.shift_remove(&Ustr::from(endpoint)); + } + + /// Subscribes the given `handler` to the `topic`. + pub fn subscribe(&mut self, topic: &str, handler: MessageHandler, priority: Option) { + let topic = Ustr::from(topic); + let sub = Subscription::new(topic, handler, self.subscriptions.len(), priority); + + if self.subscriptions.contains_key(&sub) { + error!("{sub:?} already exists."); + return; + } + + // Find existing patterns which match this topic + let mut matches = Vec::new(); + for (pattern, subs) in &mut self.patterns { + if is_matching(&topic, pattern) { + subs.push(sub.clone()); + subs.sort(); + // subs.sort_by(|a, b| a.priority.cmp(&b.priority).then_with(|| a.cmp(b))); + matches.push(*pattern); + } + } + + matches.sort(); + + self.subscriptions.insert(sub, matches); + } + + /// Unsubscribes the given `handler` from the `topic`. + pub fn unsubscribe(&mut self, topic: &str, handler: MessageHandler) { + let sub = Subscription::new(Ustr::from(topic), handler, self.subscriptions.len(), None); + self.subscriptions.shift_remove(&sub); + } + + /// Returns the handler for the given `endpoint`. + #[must_use] + pub fn get_endpoint(&self, endpoint: &Ustr) -> Option<&MessageHandler> { + self.endpoints.get(&Ustr::from(endpoint)) + } + + /// Returns the handler for the request `endpoint` and adds the request ID to the internal + /// correlation index to match with the expected response. + #[must_use] + pub fn request_handler( + &mut self, + endpoint: &Ustr, + request_id: UUID4, + response_handler: MessageHandler, + ) -> Option<&MessageHandler> { + if let Some(handler) = self.endpoints.get(endpoint) { + self.correlation_index.insert(request_id, response_handler); + Some(handler) + } else { + None + } + } + + /// Returns the handler for the matching correlation ID (if found). + #[must_use] + pub fn correlation_id_handler(&mut self, correlation_id: &UUID4) -> Option<&MessageHandler> { + self.correlation_index.get(correlation_id) + } + + /// Returns the handler for the matching response `endpoint` based on the internal correlation + /// index. + #[must_use] + pub fn response_handler(&mut self, correlation_id: &UUID4) -> Option { + self.correlation_index.shift_remove(correlation_id) + } + + #[must_use] + pub fn matching_subscriptions<'a>(&'a self, pattern: &'a Ustr) -> Vec<&'a Subscription> { + let mut matching_subs: Vec<&'a Subscription> = Vec::new(); + + // Collect matching subscriptions from direct subscriptions + matching_subs.extend(self.subscriptions.iter().filter_map(|(sub, _)| { + if is_matching(&sub.topic, pattern) { + Some(sub) + } else { + None + } + })); + + // Collect matching subscriptions from pattern-based subscriptions + // TODO: Improve efficiency of this + for subs in self.patterns.values() { + let filtered_subs: Vec<&Subscription> = subs + .iter() + // .filter(|sub| is_matching(&sub.topic, pattern)) + // .filter(|sub| !matching_subs.contains(sub) && is_matching(&sub.topic, pattern)) + .collect(); + + matching_subs.extend(filtered_subs); + } + + // Sort into priority order + matching_subs.sort(); + matching_subs + } + + fn matching_handlers<'a>( + &'a self, + pattern: &'a Ustr, + ) -> impl Iterator { + self.subscriptions.iter().filter_map(move |(sub, _)| { + if is_matching(&sub.topic, pattern) { + Some(&sub.handler) + } else { + None + } + }) + } +} + +/// Match a topic and a string pattern +/// pattern can contains - +/// '*' - match 0 or more characters after this +/// '?' - match any character once +/// 'a-z' - match the specific character +#[must_use] +pub fn is_matching(topic: &Ustr, pattern: &Ustr) -> bool { + let mut table = [[false; 256]; 256]; + table[0][0] = true; + + let m = pattern.len(); + let n = topic.len(); + + pattern.chars().enumerate().for_each(|(j, c)| { + if c == '*' { + table[0][j + 1] = table[0][j]; + } + }); + + topic.chars().enumerate().for_each(|(i, tc)| { + pattern.chars().enumerate().for_each(|(j, pc)| { + if pc == '*' { + table[i + 1][j + 1] = table[i][j + 1] || table[i + 1][j]; + } else if pc == '?' || tc == pc { + table[i + 1][j + 1] = table[i][j]; + } + }); + }); + + table[n][m] +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(not(feature = "python"))] +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use nautilus_core::{message::Message, uuid::UUID4}; + use rstest::*; + + use super::*; + use crate::handlers::{MessageHandler, SafeMessageCallback}; + + fn stub_msgbus() -> MessageBus { + MessageBus::new(TraderId::from("trader-001"), UUID4::new(), None, None) + } + + fn stub_rust_callback() -> SafeMessageCallback { + SafeMessageCallback { + callback: Arc::new(|m: Message| { + format!("{m:?}"); + }), + } + } + + #[rstest] + fn test_new() { + let trader_id = TraderId::from("trader-001"); + let msgbus = MessageBus::new(trader_id, UUID4::new(), None, None); + + assert_eq!(msgbus.trader_id, trader_id); + assert_eq!(msgbus.name, stringify!(MessageBus)); + } + + #[rstest] + fn test_endpoints_when_no_endpoints() { + let msgbus = stub_msgbus(); + + assert!(msgbus.endpoints().is_empty()); + } + + #[rstest] + fn test_topics_when_no_subscriptions() { + let msgbus = stub_msgbus(); + + assert!(msgbus.topics().is_empty()); + assert!(!msgbus.has_subscribers("my-topic")); + } + + #[rstest] + fn test_is_subscribed_when_no_subscriptions() { + let msgbus = stub_msgbus(); + + let callback = stub_rust_callback(); + let handler_id = Ustr::from("1"); + let handler = MessageHandler::new(handler_id, Some(callback)); + + assert!(!msgbus.is_subscribed("my-topic", handler)); + } + + #[rstest] + fn test_is_registered_when_no_registrations() { + let msgbus = stub_msgbus(); + + assert!(!msgbus.is_registered("MyEndpoint")); + } + + #[rstest] + fn test_is_pending_response_when_no_requests() { + let msgbus = stub_msgbus(); + + assert!(!msgbus.is_pending_response(&UUID4::default())); + } + + #[rstest] + fn test_regsiter_endpoint() { + let mut msgbus = stub_msgbus(); + let endpoint = "MyEndpoint"; + + let callback = stub_rust_callback(); + let handler_id = Ustr::from("1"); + let handler = MessageHandler::new(handler_id, Some(callback)); + + msgbus.register(endpoint, handler); + + assert_eq!(msgbus.endpoints(), vec!["MyEndpoint".to_string()]); + assert!(msgbus.get_endpoint(&Ustr::from(endpoint)).is_some()); + } + + #[rstest] + fn test_deregsiter_endpoint() { + let mut msgbus = stub_msgbus(); + let endpoint = "MyEndpoint"; + + let callback = stub_rust_callback(); + let handler_id = Ustr::from("1"); + let handler = MessageHandler::new(handler_id, Some(callback)); + + msgbus.register(endpoint, handler); + msgbus.deregister(endpoint); + + assert!(msgbus.endpoints().is_empty()); + } + + #[rstest] + fn test_subscribe() { + let mut msgbus = stub_msgbus(); + let topic = "my-topic"; + + let callback = stub_rust_callback(); + let handler_id = Ustr::from("1"); + let handler = MessageHandler::new(handler_id, Some(callback)); + + msgbus.subscribe(topic, handler, Some(1)); + + assert!(msgbus.has_subscribers(topic)); + assert_eq!(msgbus.topics(), vec![topic]); + } + + #[rstest] + fn test_unsubscribe() { + let mut msgbus = stub_msgbus(); + let topic = "my-topic"; + + let callback = stub_rust_callback(); + let handler_id = Ustr::from("1"); + let handler = MessageHandler::new(handler_id, Some(callback)); + + msgbus.subscribe(topic, handler.clone(), None); + msgbus.unsubscribe(topic, handler); + + assert!(!msgbus.has_subscribers(topic)); + assert!(msgbus.topics().is_empty()); + } + + #[rstest] + fn test_request_handler() { + let mut msgbus = stub_msgbus(); + let endpoint = "MyEndpoint"; + let request_id = UUID4::new(); + + let callback = stub_rust_callback(); + let handler_id1 = Ustr::from("1"); + let handler1 = MessageHandler::new(handler_id1, Some(callback)); + + msgbus.register(endpoint, handler1.clone()); + + let callback = stub_rust_callback(); + let handler_id2 = Ustr::from("1"); + let handler2 = MessageHandler::new(handler_id2, Some(callback)); + + assert_eq!( + msgbus.request_handler(&Ustr::from(endpoint), request_id, handler2), + Some(&handler1) + ); + } + + #[rstest] + fn test_response_handler() { + let mut msgbus = stub_msgbus(); + let correlation_id = UUID4::new(); + + let callback = stub_rust_callback(); + let handler_id = Ustr::from("1"); + let handler = MessageHandler::new(handler_id, Some(callback)); + + msgbus + .correlation_index + .insert(correlation_id, handler.clone()); + + assert_eq!(msgbus.response_handler(&correlation_id), Some(handler)); + } + + #[rstest] + fn test_matching_subscriptions() { + let mut msgbus = stub_msgbus(); + let topic = "my-topic"; + + let callback = stub_rust_callback(); + let handler_id1 = Ustr::from("1"); + let handler1 = MessageHandler::new(handler_id1, Some(callback.clone())); + + let handler_id2 = Ustr::from("2"); + let handler2 = MessageHandler::new(handler_id2, Some(callback.clone())); + + let handler_id3 = Ustr::from("3"); + let handler3 = MessageHandler::new(handler_id3, Some(callback.clone())); + + let handler_id4 = Ustr::from("4"); + let handler4 = MessageHandler::new(handler_id4, Some(callback)); + + msgbus.subscribe(topic, handler1, None); + msgbus.subscribe(topic, handler2, None); + msgbus.subscribe(topic, handler3, Some(1)); + msgbus.subscribe(topic, handler4, Some(2)); + let topic_ustr = Ustr::from(topic); + let subs = msgbus.matching_subscriptions(&topic_ustr); + + assert_eq!(subs.len(), 4); + assert_eq!(subs[0].handler.handler_id, handler_id4); + assert_eq!(subs[1].handler.handler_id, handler_id3); + assert_eq!(subs[2].handler.handler_id, handler_id1); + assert_eq!(subs[3].handler.handler_id, handler_id2); + } + + #[rstest] + #[case("*", "*", true)] + #[case("a", "*", true)] + #[case("a", "a", true)] + #[case("a", "b", false)] + #[case("data.quotes.BINANCE", "data.*", true)] + #[case("data.quotes.BINANCE", "data.quotes*", true)] + #[case("data.quotes.BINANCE", "data.*.BINANCE", true)] + #[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.*", true)] + #[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.ETH*", true)] + fn test_is_matching(#[case] topic: &str, #[case] pattern: &str, #[case] expected: bool) { + assert_eq!( + is_matching(&Ustr::from(topic), &Ustr::from(pattern)), + expected + ); + } +} diff --git a/nautilus_core/common/src/msgbus/mod.rs b/nautilus_core/common/src/msgbus/mod.rs index 63ec48f78fb3..12d2419c3639 100644 --- a/nautilus_core/common/src/msgbus/mod.rs +++ b/nautilus_core/common/src/msgbus/mod.rs @@ -15,628 +15,7 @@ //! A common in-memory `MessageBus` for loosely coupled message passing patterns. +pub mod core; pub mod database; -use std::{ - collections::HashMap, - fmt, - hash::{Hash, Hasher}, -}; - -use indexmap::IndexMap; -use log::error; -use nautilus_core::uuid::UUID4; -use nautilus_model::identifiers::trader_id::TraderId; -use serde::{Deserialize, Serialize}; -use ustr::Ustr; - -use crate::handlers::MessageHandler; - -pub const CLOSE_TOPIC: &str = "CLOSE"; - -// Represents a subscription to a particular topic. -// -// This is an internal class intended to be used by the message bus to organize -// topics and their subscribers. -#[derive(Clone, Debug)] -pub struct Subscription { - pub handler: MessageHandler, - pub topic: Ustr, - pub sequence: usize, - pub priority: u8, -} - -impl Subscription { - #[must_use] - pub fn new( - topic: Ustr, - handler: MessageHandler, - sequence: usize, - priority: Option, - ) -> Self { - Self { - topic, - handler, - sequence, - priority: priority.unwrap_or(0), - } - } -} - -impl PartialEq for Subscription { - fn eq(&self, other: &Self) -> bool { - self.topic == other.topic && self.handler.handler_id == other.handler.handler_id - } -} - -impl Eq for Subscription {} - -impl PartialOrd for Subscription { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Subscription { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match other.priority.cmp(&self.priority) { - std::cmp::Ordering::Equal => self.sequence.cmp(&other.sequence), - other => other, - } - } -} - -impl Hash for Subscription { - fn hash(&self, state: &mut H) { - self.topic.hash(state); - self.handler.handler_id.hash(state); - } -} - -/// Represents a bus message including a topic and payload. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BusMessage { - /// The topic to publish on. - pub topic: String, - /// The serialized payload for the message. - pub payload: Vec, -} - -impl fmt::Display for BusMessage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "[{}] {}", - self.topic, - String::from_utf8_lossy(&self.payload) - ) - } -} - -/// Provides a generic message bus to facilitate various messaging patterns. -/// -/// The bus provides both a producer and consumer API for Pub/Sub, Req/Rep, as -/// well as direct point-to-point messaging to registered endpoints. -/// -/// Pub/Sub wildcard patterns for hierarchical topics are possible: -/// - `*` asterisk represents one or more characters in a pattern. -/// - `?` question mark represents a single character in a pattern. -/// -/// Given a topic and pattern potentially containing wildcard characters, i.e. -/// `*` and `?`, where `?` can match any single character in the topic, and `*` -/// can match any number of characters including zero characters. -/// -/// The asterisk in a wildcard matches any character zero or more times. For -/// example, `comp*` matches anything beginning with `comp` which means `comp`, -/// `complete`, and `computer` are all matched. -/// -/// A question mark matches a single character once. For example, `c?mp` matches -/// `camp` and `comp`. The question mark can also be used more than once. -/// For example, `c??p` would match both of the above examples and `coop`. -#[derive(Clone)] -#[allow(clippy::type_complexity)] // Complexity will reduce when Cython eliminated -pub struct MessageBus { - /// The trader ID associated with the message bus. - pub trader_id: TraderId, - /// The instance ID associated with the message bus. - pub instance_id: UUID4, - /// The name for the message bus. - pub name: String, - // The count of messages sent through the bus. - pub sent_count: u64, - // The count of requests processed by the bus. - pub req_count: u64, - // The count of responses processed by the bus. - pub res_count: u64, - /// The count of messages published by the bus. - pub pub_count: u64, - /// If the message bus is backed by a database. - pub has_backing: bool, - /// mapping from topic to the corresponding handler - /// a topic can be a string with wildcards - /// * '?' - any character - /// * '*' - any number of any characters - subscriptions: IndexMap>, - /// maps a pattern to all the handlers registered for it - /// this is updated whenever a new subscription is created. - patterns: IndexMap>, - /// handles a message or a request destined for a specific endpoint. - endpoints: IndexMap, - /// Relates a request with a response - /// a request maps it's id to a handler so that a response - /// with the same id can later be handled. - correlation_index: IndexMap, -} - -impl MessageBus { - /// Creates a new `MessageBus` instance. - pub fn new( - trader_id: TraderId, - instance_id: UUID4, - name: Option, - _config: Option>, - ) -> anyhow::Result { - Ok(Self { - trader_id, - instance_id, - name: name.unwrap_or(stringify!(MessageBus).to_owned()), - sent_count: 0, - req_count: 0, - res_count: 0, - pub_count: 0, - subscriptions: IndexMap::new(), - patterns: IndexMap::new(), - endpoints: IndexMap::new(), - correlation_index: IndexMap::new(), - has_backing: false, - }) - } - - /// Returns the registered endpoint addresses. - #[must_use] - pub fn endpoints(&self) -> Vec<&str> { - self.endpoints.keys().map(Ustr::as_str).collect() - } - - /// Returns the topics for active subscriptions. - #[must_use] - pub fn topics(&self) -> Vec<&str> { - self.subscriptions - .keys() - .map(|s| s.topic.as_str()) - .collect() - } - - /// Returns the active correlation IDs. - #[must_use] - pub fn correlation_ids(&self) -> Vec<&UUID4> { - self.correlation_index.keys().collect() - } - - /// Returns whether there are subscribers for the given `pattern`. - #[must_use] - pub fn has_subscribers(&self, pattern: &str) -> bool { - self.matching_handlers(&Ustr::from(pattern)) - .next() - .is_some() - } - - /// Returns whether there are subscribers for the given `pattern`. - #[must_use] - pub fn subscriptions(&self) -> Vec<&Subscription> { - self.subscriptions.keys().collect() - } - - /// Returns whether there are subscribers for the given `pattern`. - #[must_use] - pub fn subscription_handler_ids(&self) -> Vec<&str> { - self.subscriptions - .keys() - .map(|s| s.handler.handler_id.as_str()) - .collect() - } - - /// Returns whether there are subscribers for the given `pattern`. - #[must_use] - pub fn is_registered(&self, endpoint: &str) -> bool { - self.endpoints.contains_key(&Ustr::from(endpoint)) - } - - /// Returns whether there are subscribers for the given `pattern`. - #[must_use] - pub fn is_subscribed(&self, topic: &str, handler: MessageHandler) -> bool { - let sub = Subscription::new(Ustr::from(topic), handler, self.subscriptions.len(), None); - self.subscriptions.contains_key(&sub) - } - - /// Returns whether there is a pending request for the given `request_id`. - #[must_use] - pub fn is_pending_response(&self, request_id: &UUID4) -> bool { - self.correlation_index.contains_key(request_id) - } - - /// Close the message bus which will close the sender channel and join the thread. - pub fn close(&self) -> anyhow::Result<()> { - // TODO: Integrate the backing database - Ok(()) - } - - /// Registers the given `handler` for the `endpoint` address. - pub fn register(&mut self, endpoint: &str, handler: MessageHandler) { - // Updates value if key already exists - self.endpoints.insert(Ustr::from(endpoint), handler); - } - - /// Deregisters the given `handler` for the `endpoint` address. - pub fn deregister(&mut self, endpoint: &str) { - // Removes entry if it exists for endpoint - self.endpoints.shift_remove(&Ustr::from(endpoint)); - } - - /// Subscribes the given `handler` to the `topic`. - pub fn subscribe(&mut self, topic: &str, handler: MessageHandler, priority: Option) { - let topic = Ustr::from(topic); - let sub = Subscription::new(topic, handler, self.subscriptions.len(), priority); - - if self.subscriptions.contains_key(&sub) { - error!("{sub:?} already exists."); - return; - } - - // Find existing patterns which match this topic - let mut matches = Vec::new(); - for (pattern, subs) in &mut self.patterns { - if is_matching(&topic, pattern) { - subs.push(sub.clone()); - subs.sort(); - // subs.sort_by(|a, b| a.priority.cmp(&b.priority).then_with(|| a.cmp(b))); - matches.push(*pattern); - } - } - - matches.sort(); - - self.subscriptions.insert(sub, matches); - } - - /// Unsubscribes the given `handler` from the `topic`. - pub fn unsubscribe(&mut self, topic: &str, handler: MessageHandler) { - let sub = Subscription::new(Ustr::from(topic), handler, self.subscriptions.len(), None); - self.subscriptions.shift_remove(&sub); - } - - /// Returns the handler for the given `endpoint`. - #[must_use] - pub fn get_endpoint(&self, endpoint: &Ustr) -> Option<&MessageHandler> { - self.endpoints.get(&Ustr::from(endpoint)) - } - - /// Returns the handler for the request `endpoint` and adds the request ID to the internal - /// correlation index to match with the expected response. - #[must_use] - pub fn request_handler( - &mut self, - endpoint: &Ustr, - request_id: UUID4, - response_handler: MessageHandler, - ) -> Option<&MessageHandler> { - if let Some(handler) = self.endpoints.get(endpoint) { - self.correlation_index.insert(request_id, response_handler); - Some(handler) - } else { - None - } - } - - /// Returns the handler for the matching correlation ID (if found). - #[must_use] - pub fn correlation_id_handler(&mut self, correlation_id: &UUID4) -> Option<&MessageHandler> { - self.correlation_index.get(correlation_id) - } - - /// Returns the handler for the matching response `endpoint` based on the internal correlation - /// index. - #[must_use] - pub fn response_handler(&mut self, correlation_id: &UUID4) -> Option { - self.correlation_index.shift_remove(correlation_id) - } - - #[must_use] - pub fn matching_subscriptions<'a>(&'a self, pattern: &'a Ustr) -> Vec<&'a Subscription> { - let mut matching_subs: Vec<&'a Subscription> = Vec::new(); - - // Collect matching subscriptions from direct subscriptions - matching_subs.extend(self.subscriptions.iter().filter_map(|(sub, _)| { - if is_matching(&sub.topic, pattern) { - Some(sub) - } else { - None - } - })); - - // Collect matching subscriptions from pattern-based subscriptions - // TODO: Improve efficiency of this - for subs in self.patterns.values() { - let filtered_subs: Vec<&Subscription> = subs - .iter() - // .filter(|sub| is_matching(&sub.topic, pattern)) - // .filter(|sub| !matching_subs.contains(sub) && is_matching(&sub.topic, pattern)) - .collect(); - - matching_subs.extend(filtered_subs); - } - - // Sort into priority order - matching_subs.sort(); - matching_subs - } - - fn matching_handlers<'a>( - &'a self, - pattern: &'a Ustr, - ) -> impl Iterator { - self.subscriptions.iter().filter_map(move |(sub, _)| { - if is_matching(&sub.topic, pattern) { - Some(&sub.handler) - } else { - None - } - }) - } -} - -/// Match a topic and a string pattern -/// pattern can contains - -/// '*' - match 0 or more characters after this -/// '?' - match any character once -/// 'a-z' - match the specific character -#[must_use] -pub fn is_matching(topic: &Ustr, pattern: &Ustr) -> bool { - let mut table = [[false; 256]; 256]; - table[0][0] = true; - - let m = pattern.len(); - let n = topic.len(); - - pattern.chars().enumerate().for_each(|(j, c)| { - if c == '*' { - table[0][j + 1] = table[0][j]; - } - }); - - topic.chars().enumerate().for_each(|(i, tc)| { - pattern.chars().enumerate().for_each(|(j, pc)| { - if pc == '*' { - table[i + 1][j + 1] = table[i][j + 1] || table[i + 1][j]; - } else if pc == '?' || tc == pc { - table[i + 1][j + 1] = table[i][j]; - } - }); - }); - - table[n][m] -} - -//////////////////////////////////////////////////////////////////////////////// -// Tests -//////////////////////////////////////////////////////////////////////////////// -#[cfg(not(feature = "python"))] -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use nautilus_core::{message::Message, uuid::UUID4}; - use rstest::*; - - use super::*; - use crate::handlers::{MessageHandler, SafeMessageCallback}; - - fn stub_msgbus() -> MessageBus { - MessageBus::new(TraderId::from("trader-001"), UUID4::new(), None, None) - } - - fn stub_rust_callback() -> SafeMessageCallback { - SafeMessageCallback { - callback: Arc::new(|m: Message| { - format!("{m:?}"); - }), - } - } - - #[rstest] - fn test_new() { - let trader_id = TraderId::from("trader-001"); - let msgbus = MessageBus::new(trader_id, UUID4::new(), None, None); - - assert_eq!(msgbus.trader_id, trader_id); - assert_eq!(msgbus.name, stringify!(MessageBus)); - } - - #[rstest] - fn test_endpoints_when_no_endpoints() { - let msgbus = stub_msgbus(); - - assert!(msgbus.endpoints().is_empty()); - } - - #[rstest] - fn test_topics_when_no_subscriptions() { - let msgbus = stub_msgbus(); - - assert!(msgbus.topics().is_empty()); - assert!(!msgbus.has_subscribers("my-topic")); - } - - #[rstest] - fn test_is_subscribed_when_no_subscriptions() { - let msgbus = stub_msgbus(); - - let callback = stub_rust_callback(); - let handler_id = Ustr::from("1"); - let handler = MessageHandler::new(handler_id, Some(callback)); - - assert!(!msgbus.is_subscribed("my-topic", handler)); - } - - #[rstest] - fn test_is_registered_when_no_registrations() { - let msgbus = stub_msgbus(); - - assert!(!msgbus.is_registered("MyEndpoint")); - } - - #[rstest] - fn test_is_pending_response_when_no_requests() { - let msgbus = stub_msgbus(); - - assert!(!msgbus.is_pending_response(&UUID4::default())); - } - - #[rstest] - fn test_regsiter_endpoint() { - let mut msgbus = stub_msgbus(); - let endpoint = "MyEndpoint"; - - let callback = stub_rust_callback(); - let handler_id = Ustr::from("1"); - let handler = MessageHandler::new(handler_id, Some(callback)); - - msgbus.register(endpoint, handler); - - assert_eq!(msgbus.endpoints(), vec!["MyEndpoint".to_string()]); - assert!(msgbus.get_endpoint(&Ustr::from(endpoint)).is_some()); - } - - #[rstest] - fn test_deregsiter_endpoint() { - let mut msgbus = stub_msgbus(); - let endpoint = "MyEndpoint"; - - let callback = stub_rust_callback(); - let handler_id = Ustr::from("1"); - let handler = MessageHandler::new(handler_id, Some(callback)); - - msgbus.register(endpoint, handler); - msgbus.deregister(endpoint); - - assert!(msgbus.endpoints().is_empty()); - } - - #[rstest] - fn test_subscribe() { - let mut msgbus = stub_msgbus(); - let topic = "my-topic"; - - let callback = stub_rust_callback(); - let handler_id = Ustr::from("1"); - let handler = MessageHandler::new(handler_id, Some(callback)); - - msgbus.subscribe(topic, handler, Some(1)); - - assert!(msgbus.has_subscribers(topic)); - assert_eq!(msgbus.topics(), vec![topic]); - } - - #[rstest] - fn test_unsubscribe() { - let mut msgbus = stub_msgbus(); - let topic = "my-topic"; - - let callback = stub_rust_callback(); - let handler_id = Ustr::from("1"); - let handler = MessageHandler::new(handler_id, Some(callback)); - - msgbus.subscribe(topic, handler.clone(), None); - msgbus.unsubscribe(topic, handler); - - assert!(!msgbus.has_subscribers(topic)); - assert!(msgbus.topics().is_empty()); - } - - #[rstest] - fn test_request_handler() { - let mut msgbus = stub_msgbus(); - let endpoint = "MyEndpoint"; - let request_id = UUID4::new(); - - let callback = stub_rust_callback(); - let handler_id1 = Ustr::from("1"); - let handler1 = MessageHandler::new(handler_id1, Some(callback)); - - msgbus.register(endpoint, handler1.clone()); - - let callback = stub_rust_callback(); - let handler_id2 = Ustr::from("1"); - let handler2 = MessageHandler::new(handler_id2, Some(callback)); - - assert_eq!( - msgbus.request_handler(&Ustr::from(endpoint), request_id, handler2), - Some(&handler1) - ); - } - - #[rstest] - fn test_response_handler() { - let mut msgbus = stub_msgbus(); - let correlation_id = UUID4::new(); - - let callback = stub_rust_callback(); - let handler_id = Ustr::from("1"); - let handler = MessageHandler::new(handler_id, Some(callback)); - - msgbus - .correlation_index - .insert(correlation_id, handler.clone()); - - assert_eq!(msgbus.response_handler(&correlation_id), Some(handler)); - } - - #[rstest] - fn test_matching_subscriptions() { - let mut msgbus = stub_msgbus(); - let topic = "my-topic"; - - let callback = stub_rust_callback(); - let handler_id1 = Ustr::from("1"); - let handler1 = MessageHandler::new(handler_id1, Some(callback.clone())); - - let handler_id2 = Ustr::from("2"); - let handler2 = MessageHandler::new(handler_id2, Some(callback.clone())); - - let handler_id3 = Ustr::from("3"); - let handler3 = MessageHandler::new(handler_id3, Some(callback.clone())); - - let handler_id4 = Ustr::from("4"); - let handler4 = MessageHandler::new(handler_id4, Some(callback)); - - msgbus.subscribe(topic, handler1, None); - msgbus.subscribe(topic, handler2, None); - msgbus.subscribe(topic, handler3, Some(1)); - msgbus.subscribe(topic, handler4, Some(2)); - let topic_ustr = Ustr::from(topic); - let subs = msgbus.matching_subscriptions(&topic_ustr); - - assert_eq!(subs.len(), 4); - assert_eq!(subs[0].handler.handler_id, handler_id4); - assert_eq!(subs[1].handler.handler_id, handler_id3); - assert_eq!(subs[2].handler.handler_id, handler_id1); - assert_eq!(subs[3].handler.handler_id, handler_id2); - } - - #[rstest] - #[case("*", "*", true)] - #[case("a", "*", true)] - #[case("a", "a", true)] - #[case("a", "b", false)] - #[case("data.quotes.BINANCE", "data.*", true)] - #[case("data.quotes.BINANCE", "data.quotes*", true)] - #[case("data.quotes.BINANCE", "data.*.BINANCE", true)] - #[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.*", true)] - #[case("data.trades.BINANCE.ETHUSDT", "data.*.BINANCE.ETH*", true)] - fn test_is_matching(#[case] topic: &str, #[case] pattern: &str, #[case] expected: bool) { - assert_eq!( - is_matching(&Ustr::from(topic), &Ustr::from(pattern)), - expected - ); - } -} +pub use self::core::{BusMessage, MessageBus}; diff --git a/nautilus_core/infrastructure/src/redis/msgbus.rs b/nautilus_core/infrastructure/src/redis/msgbus.rs index 7a0c7f5945dc..a3590116d064 100644 --- a/nautilus_core/infrastructure/src/redis/msgbus.rs +++ b/nautilus_core/infrastructure/src/redis/msgbus.rs @@ -20,7 +20,7 @@ use std::{ time::{Duration, Instant}, }; -use nautilus_common::msgbus::{database::MessageBusDatabaseAdapter, BusMessage, CLOSE_TOPIC}; +use nautilus_common::msgbus::{core::CLOSE_TOPIC, database::MessageBusDatabaseAdapter, BusMessage}; use nautilus_core::{time::duration_since_unix_epoch, uuid::UUID4}; use nautilus_model::identifiers::trader_id::TraderId; use redis::*; From d209dee6ac943c97a231350ba6eb78ef14b4f9e7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 May 2024 21:18:12 +1000 Subject: [PATCH 102/193] Reorganize data stubs in Rust --- nautilus_core/common/src/python/clock.rs | 21 +- nautilus_core/model/src/data/bar.rs | 69 +--- nautilus_core/model/src/data/delta.rs | 40 --- nautilus_core/model/src/data/deltas.rs | 120 +------ nautilus_core/model/src/data/depth.rs | 87 +---- nautilus_core/model/src/data/order.rs | 21 -- nautilus_core/model/src/data/quote.rs | 53 +--- nautilus_core/model/src/data/stubs.rs | 299 +++++++++++++++++- nautilus_core/model/src/data/trade.rs | 54 +--- nautilus_core/model/src/instruments/stubs.rs | 21 +- .../model/src/instruments/synthetic.rs | 25 -- nautilus_core/model/src/orderbook/book.rs | 5 +- nautilus_core/model/src/python/data/bar.rs | 16 +- nautilus_core/model/src/python/data/order.rs | 2 +- nautilus_core/model/src/python/data/quote.rs | 2 +- nautilus_core/model/src/python/data/trade.rs | 2 +- nautilus_core/persistence/src/arrow/depth.rs | 2 +- 17 files changed, 351 insertions(+), 488 deletions(-) diff --git a/nautilus_core/common/src/python/clock.rs b/nautilus_core/common/src/python/clock.rs index 3572b64ad426..ba7e650ad0d9 100644 --- a/nautilus_core/common/src/python/clock.rs +++ b/nautilus_core/common/src/python/clock.rs @@ -13,21 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(test)] -pub mod stubs { - use rstest::fixture; - - use crate::clock::TestClock; - - #[fixture] - pub fn test_clock() -> TestClock { - TestClock::new() - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -36,7 +21,6 @@ mod tests { use nautilus_core::nanos::UnixNanos; use pyo3::{prelude::*, types::PyList}; use rstest::*; - use stubs::*; use super::*; use crate::{ @@ -44,6 +28,11 @@ mod tests { handlers::EventHandler, }; + #[fixture] + pub fn test_clock() -> TestClock { + TestClock::new() + } + #[rstest] fn test_set_timer_ns_py(mut test_clock: TestClock) { pyo3::prepare_freethreaded_python(); diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 14470b334751..7a9abb0e47d2 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -296,65 +296,6 @@ impl Display for Bar { } } -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -pub mod stubs { - use nautilus_core::nanos::UnixNanos; - use rstest::fixture; - - use crate::{ - data::bar::{Bar, BarSpecification, BarType}, - enums::{AggregationSource, BarAggregation, PriceType}, - identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, - types::{price::Price, quantity::Quantity}, - }; - - impl Default for Bar { - fn default() -> Self { - Self { - bar_type: BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL"), - open: Price::from("1.00010"), - high: Price::from("1.00020"), - low: Price::from("1.00000"), - close: Price::from("1.00010"), - volume: Quantity::from(100_000), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::default(), - } - } - } - - #[fixture] - pub fn stub_bar() -> Bar { - let instrument_id = InstrumentId { - symbol: Symbol::new("AUDUSD").unwrap(), - venue: Venue::new("SIM").unwrap(), - }; - let bar_spec = BarSpecification { - step: 1, - aggregation: BarAggregation::Minute, - price_type: PriceType::Bid, - }; - let bar_type = BarType { - instrument_id, - spec: bar_spec, - aggregation_source: AggregationSource::External, - }; - Bar { - bar_type, - open: Price::from("1.00001"), - high: Price::from("1.00004"), - low: Price::from("1.00002"), - close: Price::from("1.00003"), - volume: Quantity::from("100000"), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::from(1), - } - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -362,7 +303,7 @@ pub mod stubs { mod tests { use rstest::rstest; - use super::{stubs::*, *}; + use super::*; use crate::{ enums::BarAggregation, identifiers::{symbol::Symbol, venue::Venue}, @@ -579,16 +520,16 @@ mod tests { } #[rstest] - fn test_json_serialization(stub_bar: Bar) { - let bar = stub_bar; + fn test_json_serialization() { + let bar = Bar::default(); let serialized = bar.as_json_bytes().unwrap(); let deserialized = Bar::from_json_bytes(serialized).unwrap(); assert_eq!(deserialized, bar); } #[rstest] - fn test_msgpack_serialization(stub_bar: Bar) { - let bar = stub_bar; + fn test_msgpack_serialization() { + let bar = Bar::default(); let serialized = bar.as_msgpack_bytes().unwrap(); let deserialized = Bar::from_msgpack_bytes(serialized).unwrap(); assert_eq!(deserialized, bar); diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs index de521981e4ce..952d429d14cd 100644 --- a/nautilus_core/model/src/data/delta.rs +++ b/nautilus_core/model/src/data/delta.rs @@ -147,46 +147,6 @@ impl Display for OrderBookDelta { impl Serializable for OrderBookDelta {} -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -pub mod stubs { - use rstest::fixture; - - use super::{BookAction, BookOrder, OrderBookDelta}; - use crate::{ - enums::OrderSide, - identifiers::instrument_id::InstrumentId, - types::{price::Price, quantity::Quantity}, - }; - - #[fixture] - pub fn stub_delta() -> OrderBookDelta { - let instrument_id = InstrumentId::from("AAPL.XNAS"); - let action = BookAction::Add; - let price = Price::from("100.00"); - let size = Quantity::from("10"); - let side = OrderSide::Buy; - let order_id = 123_456; - let flags = 0; - let sequence = 1; - let ts_event = 1; - let ts_init = 2; - - let order = BookOrder::new(side, price, size, order_id); - OrderBookDelta::new( - instrument_id, - action, - order, - flags, - sequence, - ts_event.into(), - ts_init.into(), - ) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/deltas.rs b/nautilus_core/model/src/data/deltas.rs index c5b0e02d7697..7b9ebc7752ba 100644 --- a/nautilus_core/model/src/data/deltas.rs +++ b/nautilus_core/model/src/data/deltas.rs @@ -139,122 +139,6 @@ impl DerefMut for OrderBookDeltas_API { } } -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -pub mod stubs { - use rstest::fixture; - - use super::OrderBookDeltas; - use crate::{ - data::{delta::OrderBookDelta, order::BookOrder}, - enums::{BookAction, OrderSide}, - identifiers::instrument_id::InstrumentId, - types::{price::Price, quantity::Quantity}, - }; - - #[fixture] - pub fn stub_deltas() -> OrderBookDeltas { - let instrument_id = InstrumentId::from("AAPL.XNAS"); - let flags = 32; // Snapshot flag - let sequence = 0; - let ts_event = 1; - let ts_init = 2; - - let delta0 = - OrderBookDelta::clear(instrument_id, sequence, ts_event.into(), ts_init.into()); - let delta1 = OrderBookDelta::new( - instrument_id, - BookAction::Add, - BookOrder::new( - OrderSide::Sell, - Price::from("102.00"), - Quantity::from("300"), - 1, - ), - flags, - sequence, - ts_event.into(), - ts_init.into(), - ); - let delta2 = OrderBookDelta::new( - instrument_id, - BookAction::Add, - BookOrder::new( - OrderSide::Sell, - Price::from("101.00"), - Quantity::from("200"), - 2, - ), - flags, - sequence, - ts_event.into(), - ts_init.into(), - ); - let delta3 = OrderBookDelta::new( - instrument_id, - BookAction::Add, - BookOrder::new( - OrderSide::Sell, - Price::from("100.00"), - Quantity::from("100"), - 3, - ), - flags, - sequence, - ts_event.into(), - ts_init.into(), - ); - let delta4 = OrderBookDelta::new( - instrument_id, - BookAction::Add, - BookOrder::new( - OrderSide::Buy, - Price::from("99.00"), - Quantity::from("100"), - 4, - ), - flags, - sequence, - ts_event.into(), - ts_init.into(), - ); - let delta5 = OrderBookDelta::new( - instrument_id, - BookAction::Add, - BookOrder::new( - OrderSide::Buy, - Price::from("98.00"), - Quantity::from("200"), - 5, - ), - flags, - sequence, - ts_event.into(), - ts_init.into(), - ); - let delta6 = OrderBookDelta::new( - instrument_id, - BookAction::Add, - BookOrder::new( - OrderSide::Buy, - Price::from("97.00"), - Quantity::from("300"), - 6, - ), - flags, - sequence, - ts_event.into(), - ts_init.into(), - ); - - let deltas = vec![delta0, delta1, delta2, delta3, delta4, delta5, delta6]; - - OrderBookDeltas::new(instrument_id, deltas) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -262,9 +146,9 @@ pub mod stubs { mod tests { use rstest::rstest; - use super::{stubs::*, *}; + use super::*; use crate::{ - data::order::BookOrder, + data::{order::BookOrder, stubs::stub_deltas}, enums::{BookAction, OrderSide}, types::{price::Price, quantity::Quantity}, }; diff --git a/nautilus_core/model/src/data/depth.rs b/nautilus_core/model/src/data/depth.rs index b88796de9539..bf41c338b8bc 100644 --- a/nautilus_core/model/src/data/depth.rs +++ b/nautilus_core/model/src/data/depth.rs @@ -192,90 +192,6 @@ impl Display for OrderBookDepth10 { impl Serializable for OrderBookDepth10 {} -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -#[allow(clippy::needless_range_loop)] // False positive? -pub mod stubs { - use rstest::fixture; - - use super::{OrderBookDepth10, DEPTH10_LEN}; - use crate::{ - data::order::BookOrder, - enums::OrderSide, - identifiers::instrument_id::InstrumentId, - types::{price::Price, quantity::Quantity}, - }; - - #[fixture] - pub fn stub_depth10() -> OrderBookDepth10 { - let instrument_id = InstrumentId::from("AAPL.XNAS"); - let flags = 0; - let sequence = 0; - let ts_event = 1; - let ts_init = 2; - - let mut bids: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN]; - let mut asks: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN]; - - // Create bids - let mut price = 99.00; - let mut quantity = 100.0; - let mut order_id = 1; - - for i in 0..DEPTH10_LEN { - let order = BookOrder::new( - OrderSide::Buy, - Price::new(price, 2).unwrap(), - Quantity::new(quantity, 0).unwrap(), - order_id, - ); - - bids[i] = order; - - price -= 1.0; - quantity += 100.0; - order_id += 1; - } - - // Create asks - let mut price = 100.00; - let mut quantity = 100.0; - let mut order_id = 11; - - for i in 0..DEPTH10_LEN { - let order = BookOrder::new( - OrderSide::Sell, - Price::new(price, 2).unwrap(), - Quantity::new(quantity, 0).unwrap(), - order_id, - ); - - asks[i] = order; - - price += 1.0; - quantity += 100.0; - order_id += 1; - } - - let bid_counts: [u32; DEPTH10_LEN] = [1; DEPTH10_LEN]; - let ask_counts: [u32; DEPTH10_LEN] = [1; DEPTH10_LEN]; - - OrderBookDepth10::new( - instrument_id, - bids, - asks, - bid_counts, - ask_counts, - flags, - sequence, - ts_event.into(), - ts_init.into(), - ) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -283,7 +199,8 @@ pub mod stubs { mod tests { use rstest::rstest; - use super::{stubs::*, *}; + use super::*; + use crate::data::stubs::*; #[rstest] fn test_new(stub_depth10: OrderBookDepth10) { diff --git a/nautilus_core/model/src/data/order.rs b/nautilus_core/model/src/data/order.rs index 37d426840319..e00578737d66 100644 --- a/nautilus_core/model/src/data/order.rs +++ b/nautilus_core/model/src/data/order.rs @@ -124,27 +124,6 @@ impl Display for BookOrder { impl Serializable for BookOrder {} -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -pub mod stubs { - use rstest::fixture; - - use super::{BookOrder, OrderSide}; - use crate::types::{price::Price, quantity::Quantity}; - - #[fixture] - pub fn stub_book_order() -> BookOrder { - let price = Price::from("100.00"); - let size = Quantity::from("10"); - let side = OrderSide::Buy; - let order_id = 123_456; - - BookOrder::new(side, price, size, order_id) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 26560d83d460..7139a497a038 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -165,53 +165,6 @@ impl Display for QuoteTick { impl Serializable for QuoteTick {} -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -pub mod stubs { - use nautilus_core::nanos::UnixNanos; - use rstest::fixture; - - use crate::{ - data::quote::QuoteTick, - identifiers::instrument_id::InstrumentId, - types::{price::Price, quantity::Quantity}, - }; - - impl Default for QuoteTick { - fn default() -> Self { - Self { - instrument_id: InstrumentId::from("AUDUSD.SIM"), - bid_price: Price::from("1.00000"), - ask_price: Price::from("1.00000"), - bid_size: Quantity::from(100_000), - ask_size: Quantity::from(100_000), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::default(), - } - } - } - - #[fixture] - pub fn quote_tick_audusd_sim() -> QuoteTick { - QuoteTick::default() - } - - #[fixture] - pub fn quote_tick_ethusdt_binance() -> QuoteTick { - QuoteTick { - instrument_id: InstrumentId::from("ETHUSDT-PERP.BINANCE"), - bid_price: Price::from("10000.0000"), - ask_price: Price::from("10001.0000"), - bid_size: Quantity::from("1.00000000"), - ask_size: Quantity::from("1.00000000"), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::from(1), - } - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -221,8 +174,10 @@ mod tests { use pyo3::{IntoPy, Python}; use rstest::rstest; - use super::stubs::*; - use crate::{data::quote::QuoteTick, enums::PriceType}; + use crate::{ + data::{quote::QuoteTick, stubs::quote_tick_ethusdt_binance}, + enums::PriceType, + }; #[rstest] fn test_to_string(quote_tick_ethusdt_binance: QuoteTick) { diff --git a/nautilus_core/model/src/data/stubs.rs b/nautilus_core/model/src/data/stubs.rs index 7dc2ebec20f1..d38b62f925d1 100644 --- a/nautilus_core/model/src/data/stubs.rs +++ b/nautilus_core/model/src/data/stubs.rs @@ -15,16 +15,67 @@ //! Type stubs to facilitate testing. +use nautilus_core::nanos::UnixNanos; use rstest::fixture; -use super::OrderBookDelta; +use super::{ + bar::{Bar, BarSpecification, BarType}, + deltas::OrderBookDeltas, + depth::DEPTH10_LEN, + quote::QuoteTick, + trade::TradeTick, + OrderBookDelta, OrderBookDepth10, +}; use crate::{ data::order::BookOrder, - enums::{BookAction, OrderSide}, - identifiers::instrument_id::InstrumentId, + enums::{AggregationSource, AggressorSide, BarAggregation, BookAction, OrderSide, PriceType}, + identifiers::{instrument_id::InstrumentId, symbol::Symbol, trade_id::TradeId, venue::Venue}, types::{price::Price, quantity::Quantity}, }; +impl Default for QuoteTick { + fn default() -> Self { + Self { + instrument_id: InstrumentId::from("AUDUSD.SIM"), + bid_price: Price::from("1.00000"), + ask_price: Price::from("1.00000"), + bid_size: Quantity::from(100_000), + ask_size: Quantity::from(100_000), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } +} + +impl Default for TradeTick { + fn default() -> Self { + TradeTick { + instrument_id: InstrumentId::from("AUDUSD.SIM"), + price: Price::from("1.00000"), + size: Quantity::from(100_000), + aggressor_side: AggressorSide::Buyer, + trade_id: TradeId::new("123456789").unwrap(), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } +} + +impl Default for Bar { + fn default() -> Self { + Self { + bar_type: BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL"), + open: Price::from("1.00010"), + high: Price::from("1.00020"), + low: Price::from("1.00000"), + close: Price::from("1.00010"), + volume: Quantity::from(100_000), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::default(), + } + } +} + #[fixture] pub fn stub_delta() -> OrderBookDelta { let instrument_id = InstrumentId::from("AAPL.XNAS"); @@ -49,3 +100,245 @@ pub fn stub_delta() -> OrderBookDelta { ts_init.into(), ) } + +#[fixture] +pub fn stub_deltas() -> OrderBookDeltas { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let flags = 32; // Snapshot flag + let sequence = 0; + let ts_event = 1; + let ts_init = 2; + + let delta0 = OrderBookDelta::clear(instrument_id, sequence, ts_event.into(), ts_init.into()); + let delta1 = OrderBookDelta::new( + instrument_id, + BookAction::Add, + BookOrder::new( + OrderSide::Sell, + Price::from("102.00"), + Quantity::from("300"), + 1, + ), + flags, + sequence, + ts_event.into(), + ts_init.into(), + ); + let delta2 = OrderBookDelta::new( + instrument_id, + BookAction::Add, + BookOrder::new( + OrderSide::Sell, + Price::from("101.00"), + Quantity::from("200"), + 2, + ), + flags, + sequence, + ts_event.into(), + ts_init.into(), + ); + let delta3 = OrderBookDelta::new( + instrument_id, + BookAction::Add, + BookOrder::new( + OrderSide::Sell, + Price::from("100.00"), + Quantity::from("100"), + 3, + ), + flags, + sequence, + ts_event.into(), + ts_init.into(), + ); + let delta4 = OrderBookDelta::new( + instrument_id, + BookAction::Add, + BookOrder::new( + OrderSide::Buy, + Price::from("99.00"), + Quantity::from("100"), + 4, + ), + flags, + sequence, + ts_event.into(), + ts_init.into(), + ); + let delta5 = OrderBookDelta::new( + instrument_id, + BookAction::Add, + BookOrder::new( + OrderSide::Buy, + Price::from("98.00"), + Quantity::from("200"), + 5, + ), + flags, + sequence, + ts_event.into(), + ts_init.into(), + ); + let delta6 = OrderBookDelta::new( + instrument_id, + BookAction::Add, + BookOrder::new( + OrderSide::Buy, + Price::from("97.00"), + Quantity::from("300"), + 6, + ), + flags, + sequence, + ts_event.into(), + ts_init.into(), + ); + + let deltas = vec![delta0, delta1, delta2, delta3, delta4, delta5, delta6]; + + OrderBookDeltas::new(instrument_id, deltas) +} + +#[fixture] +pub fn stub_depth10() -> OrderBookDepth10 { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let flags = 0; + let sequence = 0; + let ts_event = 1; + let ts_init = 2; + + let mut bids: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN]; + let mut asks: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN]; + + // Create bids + let mut price = 99.00; + let mut quantity = 100.0; + let mut order_id = 1; + + #[allow(clippy::needless_range_loop)] + for i in 0..DEPTH10_LEN { + let order = BookOrder::new( + OrderSide::Buy, + Price::new(price, 2).unwrap(), + Quantity::new(quantity, 0).unwrap(), + order_id, + ); + + bids[i] = order; + + price -= 1.0; + quantity += 100.0; + order_id += 1; + } + + // Create asks + let mut price = 100.00; + let mut quantity = 100.0; + let mut order_id = 11; + + #[allow(clippy::needless_range_loop)] + for i in 0..DEPTH10_LEN { + let order = BookOrder::new( + OrderSide::Sell, + Price::new(price, 2).unwrap(), + Quantity::new(quantity, 0).unwrap(), + order_id, + ); + + asks[i] = order; + + price += 1.0; + quantity += 100.0; + order_id += 1; + } + + let bid_counts: [u32; DEPTH10_LEN] = [1; DEPTH10_LEN]; + let ask_counts: [u32; DEPTH10_LEN] = [1; DEPTH10_LEN]; + + OrderBookDepth10::new( + instrument_id, + bids, + asks, + bid_counts, + ask_counts, + flags, + sequence, + ts_event.into(), + ts_init.into(), + ) +} + +#[fixture] +pub fn stub_book_order() -> BookOrder { + let price = Price::from("100.00"); + let size = Quantity::from("10"); + let side = OrderSide::Buy; + let order_id = 123_456; + + BookOrder::new(side, price, size, order_id) +} + +#[fixture] +pub fn quote_tick_audusd_sim() -> QuoteTick { + QuoteTick::default() +} + +#[fixture] +pub fn quote_tick_ethusdt_binance() -> QuoteTick { + QuoteTick { + instrument_id: InstrumentId::from("ETHUSDT-PERP.BINANCE"), + bid_price: Price::from("10000.0000"), + ask_price: Price::from("10001.0000"), + bid_size: Quantity::from("1.00000000"), + ask_size: Quantity::from("1.00000000"), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::from(1), + } +} + +#[fixture] +pub fn trade_tick_audusd_sim() -> TradeTick { + TradeTick::default() +} + +#[fixture] +pub fn stub_trade_tick_ethusdt_buyer() -> TradeTick { + TradeTick { + instrument_id: InstrumentId::from("ETHUSDT-PERP.BINANCE"), + price: Price::from("10000.0000"), + size: Quantity::from("1.00000000"), + aggressor_side: AggressorSide::Buyer, + trade_id: TradeId::new("123456789").unwrap(), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::from(1), + } +} + +#[fixture] +pub fn stub_bar() -> Bar { + let instrument_id = InstrumentId { + symbol: Symbol::new("AUDUSD").unwrap(), + venue: Venue::new("SIM").unwrap(), + }; + let bar_spec = BarSpecification { + step: 1, + aggregation: BarAggregation::Minute, + price_type: PriceType::Bid, + }; + let bar_type = BarType { + instrument_id, + spec: bar_spec, + aggregation_source: AggregationSource::External, + }; + Bar { + bar_type, + open: Price::from("1.00001"), + high: Price::from("1.00004"), + low: Price::from("1.00002"), + close: Price::from("1.00003"), + volume: Quantity::from("100000"), + ts_event: UnixNanos::default(), + ts_init: UnixNanos::from(1), + } +} diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index 0b5034f80f5b..7f9130d089ac 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -125,54 +125,6 @@ impl Display for TradeTick { impl Serializable for TradeTick {} -//////////////////////////////////////////////////////////////////////////////// -// Stubs -//////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -pub mod stubs { - use nautilus_core::nanos::UnixNanos; - use rstest::fixture; - - use crate::{ - data::trade::TradeTick, - enums::AggressorSide, - identifiers::{instrument_id::InstrumentId, trade_id::TradeId}, - types::{price::Price, quantity::Quantity}, - }; - - impl Default for TradeTick { - fn default() -> Self { - TradeTick { - instrument_id: InstrumentId::from("AUDUSD.SIM"), - price: Price::from("1.00000"), - size: Quantity::from(100_000), - aggressor_side: AggressorSide::Buyer, - trade_id: TradeId::new("123456789").unwrap(), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::default(), - } - } - } - - #[fixture] - pub fn trade_tick_audusd_sim() -> TradeTick { - TradeTick::default() - } - - #[fixture] - pub fn stub_trade_tick_ethusdt_buyer() -> TradeTick { - TradeTick { - instrument_id: InstrumentId::from("ETHUSDT-PERP.BINANCE"), - price: Price::from("10000.0000"), - size: Quantity::from("1.00000000"), - aggressor_side: AggressorSide::Buyer, - trade_id: TradeId::new("123456789").unwrap(), - ts_event: UnixNanos::default(), - ts_init: UnixNanos::from(1), - } - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -182,8 +134,10 @@ mod tests { use pyo3::{IntoPy, Python}; use rstest::rstest; - use super::stubs::*; - use crate::{data::trade::TradeTick, enums::AggressorSide}; + use crate::{ + data::{stubs::stub_trade_tick_ethusdt_buyer, trade::TradeTick}, + enums::AggressorSide, + }; #[rstest] fn test_to_string(stub_trade_tick_ethusdt_buyer: TradeTick) { diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index eb05beaacef8..8bdb5ae42e71 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -19,7 +19,9 @@ use rstest::*; use rust_decimal_macros::dec; use ustr::Ustr; -use super::{futures_spread::FuturesSpread, options_spread::OptionsSpread}; +use super::{ + futures_spread::FuturesSpread, options_spread::OptionsSpread, synthetic::SyntheticInstrument, +}; use crate::{ enums::{AssetClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, @@ -31,6 +33,23 @@ use crate::{ types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; +impl Default for SyntheticInstrument { + fn default() -> Self { + let btc_binance = InstrumentId::from("BTC.BINANCE"); + let ltc_binance = InstrumentId::from("LTC.BINANCE"); + let formula = "(BTC.BINANCE + LTC.BINANCE) / 2.0".to_string(); + SyntheticInstrument::new( + Symbol::new("BTC-LTC").unwrap(), + 2, + vec![btc_binance, ltc_binance], + formula.clone(), + 0.into(), + 0.into(), + ) + .unwrap() + } +} + //////////////////////////////////////////////////////////////////////////////// // CryptoFuture //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/instruments/synthetic.rs b/nautilus_core/model/src/instruments/synthetic.rs index 0f6b4363b5de..139ec8945891 100644 --- a/nautilus_core/model/src/instruments/synthetic.rs +++ b/nautilus_core/model/src/instruments/synthetic.rs @@ -148,31 +148,6 @@ impl Hash for SyntheticInstrument { } } -//////////////////////////////////////////////////////////////////////////////// -// Stubs -/////////////////////////////////////////////////////////////////////////////// -#[cfg(feature = "stubs")] -pub mod stubs { - use super::*; - - impl Default for SyntheticInstrument { - fn default() -> Self { - let btc_binance = InstrumentId::from("BTC.BINANCE"); - let ltc_binance = InstrumentId::from("LTC.BINANCE"); - let formula = "(BTC.BINANCE + LTC.BINANCE) / 2.0".to_string(); - SyntheticInstrument::new( - Symbol::new("BTC-LTC").unwrap(), - 2, - vec![btc_binance, ltc_binance], - formula.clone(), - 0.into(), - 0.into(), - ) - .unwrap() - } - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests /////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/orderbook/book.rs b/nautilus_core/model/src/orderbook/book.rs index 9e41db59f7a5..b7056d0bcc3d 100644 --- a/nautilus_core/model/src/orderbook/book.rs +++ b/nautilus_core/model/src/orderbook/book.rs @@ -263,10 +263,7 @@ mod tests { use crate::{ data::{ - depth::{stubs::stub_depth10, OrderBookDepth10}, - order::BookOrder, - quote::QuoteTick, - trade::TradeTick, + depth::OrderBookDepth10, order::BookOrder, quote::QuoteTick, stubs::*, trade::TradeTick, }, enums::{AggressorSide, BookType, OrderSide}, identifiers::{instrument_id::InstrumentId, trade_id::TradeId}, diff --git a/nautilus_core/model/src/python/data/bar.rs b/nautilus_core/model/src/python/data/bar.rs index 5ad073b7f201..5b03ea125a60 100644 --- a/nautilus_core/model/src/python/data/bar.rs +++ b/nautilus_core/model/src/python/data/bar.rs @@ -376,24 +376,24 @@ mod tests { use pyo3::{IntoPy, Python}; use rstest::rstest; - use crate::data::bar::{stubs::stub_bar, Bar}; + use crate::data::bar::Bar; #[rstest] - fn test_as_dict(stub_bar: Bar) { + fn test_as_dict() { pyo3::prepare_freethreaded_python(); - let bar = stub_bar; + let bar = Bar::default(); Python::with_gil(|py| { let dict_string = bar.py_as_dict(py).unwrap().to_string(); - let expected_string = r"{'type': 'Bar', 'bar_type': 'AUDUSD.SIM-1-MINUTE-BID-EXTERNAL', 'open': '1.00001', 'high': '1.00004', 'low': '1.00002', 'close': '1.00003', 'volume': '100000', 'ts_event': 0, 'ts_init': 1}"; + let expected_string = r"{'type': 'Bar', 'bar_type': 'AUDUSD.SIM-1-MINUTE-LAST-INTERNAL', 'open': '1.00010', 'high': '1.00020', 'low': '1.00000', 'close': '1.00010', 'volume': '100000', 'ts_event': 0, 'ts_init': 0}"; assert_eq!(dict_string, expected_string); }); } #[rstest] - fn test_as_from_dict(stub_bar: Bar) { + fn test_as_from_dict() { pyo3::prepare_freethreaded_python(); - let bar = stub_bar; + let bar = Bar::default(); Python::with_gil(|py| { let dict = bar.py_as_dict(py).unwrap(); @@ -403,9 +403,9 @@ mod tests { } #[rstest] - fn test_from_pyobject(stub_bar: Bar) { + fn test_from_pyobject() { pyo3::prepare_freethreaded_python(); - let bar = stub_bar; + let bar = Bar::default(); Python::with_gil(|py| { let bar_pyobject = bar.into_py(py); diff --git a/nautilus_core/model/src/python/data/order.rs b/nautilus_core/model/src/python/data/order.rs index 1778ec8b2902..8344ad9f2069 100644 --- a/nautilus_core/model/src/python/data/order.rs +++ b/nautilus_core/model/src/python/data/order.rs @@ -154,7 +154,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::data::order::stubs::stub_book_order; + use crate::data::stubs::stub_book_order; #[rstest] fn test_as_dict(stub_book_order: BookOrder) { diff --git a/nautilus_core/model/src/python/data/quote.rs b/nautilus_core/model/src/python/data/quote.rs index 03335d8610e5..b910cc3a47d4 100644 --- a/nautilus_core/model/src/python/data/quote.rs +++ b/nautilus_core/model/src/python/data/quote.rs @@ -390,7 +390,7 @@ mod tests { use pyo3::{IntoPy, Python}; use rstest::rstest; - use crate::data::quote::{stubs::*, QuoteTick}; + use crate::data::{quote::QuoteTick, stubs::quote_tick_ethusdt_binance}; #[rstest] fn test_as_dict(quote_tick_ethusdt_binance: QuoteTick) { diff --git a/nautilus_core/model/src/python/data/trade.rs b/nautilus_core/model/src/python/data/trade.rs index e702aef61ee3..c000827016d7 100644 --- a/nautilus_core/model/src/python/data/trade.rs +++ b/nautilus_core/model/src/python/data/trade.rs @@ -339,7 +339,7 @@ mod tests { use pyo3::{IntoPy, Python}; use rstest::rstest; - use crate::data::trade::{stubs::*, TradeTick}; + use crate::data::{stubs::stub_trade_tick_ethusdt_buyer, trade::TradeTick}; #[rstest] fn test_as_dict(stub_trade_tick_ethusdt_buyer: TradeTick) { diff --git a/nautilus_core/persistence/src/arrow/depth.rs b/nautilus_core/persistence/src/arrow/depth.rs index dc27b10129e3..976c77ca594e 100644 --- a/nautilus_core/persistence/src/arrow/depth.rs +++ b/nautilus_core/persistence/src/arrow/depth.rs @@ -426,7 +426,7 @@ impl DecodeDataFromRecordBatch for OrderBookDepth10 { mod tests { use datafusion::arrow::datatypes::{DataType, Field, Schema}; - use nautilus_model::data::depth::stubs::stub_depth10; + use nautilus_model::data::stubs::stub_depth10; use rstest::rstest; use super::*; From a00c7d3fc0c210724c3fde1c9d956192cb8645b0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 07:49:32 +1000 Subject: [PATCH 103/193] Fix typo --- nautilus_trader/common/actor.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/common/actor.pyx b/nautilus_trader/common/actor.pyx index 862c785901b3..4f4a80fa7e2b 100644 --- a/nautilus_trader/common/actor.pyx +++ b/nautilus_trader/common/actor.pyx @@ -1410,7 +1410,7 @@ cdef class Actor(Component): If ``None`` then will be inferred from the venue in the instrument ID. await_partial : bool, default False If the bar aggregator should await the arrival of a historical partial bar prior - to activaely aggregating new bars. + to actively aggregating new bars. """ Condition.not_none(bar_type, "bar_type") From 8b552c0b18b94c6d1483fc1233a1be26e3d0a81f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 08:00:04 +1000 Subject: [PATCH 104/193] Move data GetTsInit impls --- nautilus_core/model/src/data/bar.rs | 11 +++++-- nautilus_core/model/src/data/delta.rs | 7 ++++ nautilus_core/model/src/data/deltas.rs | 8 ++++- nautilus_core/model/src/data/depth.rs | 8 ++++- nautilus_core/model/src/data/mod.rs | 44 ++------------------------ nautilus_core/model/src/data/quote.rs | 7 ++++ nautilus_core/model/src/data/trade.rs | 7 ++++ 7 files changed, 46 insertions(+), 46 deletions(-) diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 7a9abb0e47d2..44c8d4d997f0 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -30,6 +30,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{ enums::{AggregationSource, BarAggregation, PriceType}, identifiers::instrument_id::InstrumentId, + polymorphism::GetTsInit, types::{price::Price, quantity::Quantity}, }; @@ -284,8 +285,6 @@ impl Bar { } } -impl Serializable for Bar {} - impl Display for Bar { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( @@ -296,6 +295,14 @@ impl Display for Bar { } } +impl Serializable for Bar {} + +impl GetTsInit for Bar { + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs index 952d429d14cd..63e87196a922 100644 --- a/nautilus_core/model/src/data/delta.rs +++ b/nautilus_core/model/src/data/delta.rs @@ -29,6 +29,7 @@ use super::order::{BookOrder, NULL_ORDER}; use crate::{ enums::{BookAction, RecordFlag}, identifiers::instrument_id::InstrumentId, + polymorphism::GetTsInit, }; /// Represents a single change/delta in an order book. @@ -147,6 +148,12 @@ impl Display for OrderBookDelta { impl Serializable for OrderBookDelta {} +impl GetTsInit for OrderBookDelta { + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/deltas.rs b/nautilus_core/model/src/data/deltas.rs index 7b9ebc7752ba..7f7d478e5317 100644 --- a/nautilus_core/model/src/data/deltas.rs +++ b/nautilus_core/model/src/data/deltas.rs @@ -24,7 +24,7 @@ use std::{ use nautilus_core::nanos::UnixNanos; use super::delta::OrderBookDelta; -use crate::identifiers::instrument_id::InstrumentId; +use crate::{identifiers::instrument_id::InstrumentId, polymorphism::GetTsInit}; /// Represents a grouped batch of `OrderBookDelta` updates for an `OrderBook`. /// @@ -105,6 +105,12 @@ impl Display for OrderBookDeltas { } } +impl GetTsInit for OrderBookDeltas { + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + /// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`]. /// /// This struct wraps `OrderBookDeltas` in a way that makes it compatible with C function diff --git a/nautilus_core/model/src/data/depth.rs b/nautilus_core/model/src/data/depth.rs index bf41c338b8bc..fbc85ced4f5a 100644 --- a/nautilus_core/model/src/data/depth.rs +++ b/nautilus_core/model/src/data/depth.rs @@ -25,7 +25,7 @@ use nautilus_core::{nanos::UnixNanos, serialization::Serializable}; use serde::{Deserialize, Serialize}; use super::order::BookOrder; -use crate::identifiers::instrument_id::InstrumentId; +use crate::{identifiers::instrument_id::InstrumentId, polymorphism::GetTsInit}; pub const DEPTH10_LEN: usize = 10; @@ -192,6 +192,12 @@ impl Display for OrderBookDepth10 { impl Serializable for OrderBookDepth10 {} +impl GetTsInit for OrderBookDepth10 { + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/mod.rs b/nautilus_core/model/src/data/mod.rs index c6abae3d3250..451abf6aae71 100644 --- a/nautilus_core/model/src/data/mod.rs +++ b/nautilus_core/model/src/data/mod.rs @@ -28,12 +28,8 @@ pub mod trade; use nautilus_core::nanos::UnixNanos; use self::{ - bar::Bar, - delta::OrderBookDelta, - deltas::{OrderBookDeltas, OrderBookDeltas_API}, - depth::OrderBookDepth10, - quote::QuoteTick, - trade::TradeTick, + bar::Bar, delta::OrderBookDelta, deltas::OrderBookDeltas_API, depth::OrderBookDepth10, + quote::QuoteTick, trade::TradeTick, }; use crate::polymorphism::GetTsInit; @@ -66,42 +62,6 @@ impl GetTsInit for Data { } } -impl GetTsInit for OrderBookDelta { - fn ts_init(&self) -> UnixNanos { - self.ts_init - } -} - -impl GetTsInit for OrderBookDeltas { - fn ts_init(&self) -> UnixNanos { - self.ts_init - } -} - -impl GetTsInit for OrderBookDepth10 { - fn ts_init(&self) -> UnixNanos { - self.ts_init - } -} - -impl GetTsInit for QuoteTick { - fn ts_init(&self) -> UnixNanos { - self.ts_init - } -} - -impl GetTsInit for TradeTick { - fn ts_init(&self) -> UnixNanos { - self.ts_init - } -} - -impl GetTsInit for Bar { - fn ts_init(&self) -> UnixNanos { - self.ts_init - } -} - pub fn is_monotonically_increasing_by_init(data: &[T]) -> bool { data.windows(2) .all(|window| window[0].ts_init() <= window[1].ts_init()) diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 7139a497a038..64a283bcb7c6 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -30,6 +30,7 @@ use serde::{Deserialize, Serialize}; use crate::{ enums::PriceType, identifiers::instrument_id::InstrumentId, + polymorphism::GetTsInit, types::{fixed::FIXED_PRECISION, price::Price, quantity::Quantity}, }; @@ -165,6 +166,12 @@ impl Display for QuoteTick { impl Serializable for QuoteTick {} +impl GetTsInit for QuoteTick { + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index 7f9130d089ac..f7f419d995b2 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -29,6 +29,7 @@ use serde::{Deserialize, Serialize}; use crate::{ enums::AggressorSide, identifiers::{instrument_id::InstrumentId, trade_id::TradeId}, + polymorphism::GetTsInit, types::{price::Price, quantity::Quantity}, }; @@ -125,6 +126,12 @@ impl Display for TradeTick { impl Serializable for TradeTick {} +impl GetTsInit for TradeTick { + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// From 88dfc3b3535ea9990aa325b810b653a92434517b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 08:10:00 +1000 Subject: [PATCH 105/193] Move default identifiers to stubs --- .../model/src/identifiers/account_id.rs | 7 --- .../model/src/identifiers/client_id.rs | 7 --- .../model/src/identifiers/client_order_id.rs | 7 --- .../model/src/identifiers/position_id.rs | 7 --- .../model/src/identifiers/strategy_id.rs | 7 --- nautilus_core/model/src/identifiers/stubs.rs | 60 +++++++++++++++++++ nautilus_core/model/src/identifiers/symbol.rs | 7 --- .../model/src/identifiers/trade_id.rs | 6 -- .../model/src/identifiers/trader_id.rs | 7 --- nautilus_core/model/src/identifiers/venue.rs | 7 --- .../model/src/identifiers/venue_order_id.rs | 7 --- 11 files changed, 60 insertions(+), 69 deletions(-) diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index fd3bcbe332a0..c1586d0930f7 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -83,13 +83,6 @@ impl AccountId { } } -impl Default for AccountId { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("SIM-001").unwrap() - } -} - impl Debug for AccountId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/client_id.rs b/nautilus_core/model/src/identifiers/client_id.rs index d120cd72554b..a0c680e08d6c 100644 --- a/nautilus_core/model/src/identifiers/client_id.rs +++ b/nautilus_core/model/src/identifiers/client_id.rs @@ -60,13 +60,6 @@ impl ClientId { } } -impl Default for ClientId { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("SIM").unwrap() - } -} - impl Debug for ClientId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index ffc94a89cec7..fb2d1e1abb2a 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -60,13 +60,6 @@ impl ClientOrderId { } } -impl Default for ClientOrderId { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("O-123456789").unwrap() - } -} - impl Debug for ClientOrderId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index 5d55e983b242..618b0ad7a14a 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -60,13 +60,6 @@ impl PositionId { } } -impl Default for PositionId { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("P-001").unwrap() - } -} - impl Debug for PositionId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index 9be8c61b7da7..137a8063e3c9 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -89,13 +89,6 @@ impl StrategyId { } } -impl Default for StrategyId { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("S-001").unwrap() - } -} - impl Debug for StrategyId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/stubs.rs b/nautilus_core/model/src/identifiers/stubs.rs index 785e56cd4402..3241cf15d88a 100644 --- a/nautilus_core/model/src/identifiers/stubs.rs +++ b/nautilus_core/model/src/identifiers/stubs.rs @@ -23,6 +23,66 @@ use crate::identifiers::{ trade_id::TradeId, trader_id::TraderId, venue::Venue, venue_order_id::VenueOrderId, }; +impl Default for AccountId { + fn default() -> Self { + Self::from("SIM-001") + } +} + +impl Default for ClientId { + fn default() -> Self { + Self::from("SIM") + } +} + +impl Default for ClientOrderId { + fn default() -> Self { + Self::from("O-123456789") + } +} + +impl Default for PositionId { + fn default() -> Self { + Self::from("P-001") + } +} + +impl Default for StrategyId { + fn default() -> Self { + Self::from("S-001") + } +} + +impl Default for Symbol { + fn default() -> Self { + Self::from("AUD/USD") + } +} + +impl Default for TradeId { + fn default() -> Self { + Self::from("1") + } +} + +impl Default for TraderId { + fn default() -> Self { + Self::from("TRADER-000") + } +} + +impl Default for Venue { + fn default() -> Self { + Self::from("SIM") + } +} + +impl Default for VenueOrderId { + fn default() -> Self { + Self::from("001") + } +} + // ---- AccountId ---- #[fixture] diff --git a/nautilus_core/model/src/identifiers/symbol.rs b/nautilus_core/model/src/identifiers/symbol.rs index 6178c05ffcce..dacbb5d1051a 100644 --- a/nautilus_core/model/src/identifiers/symbol.rs +++ b/nautilus_core/model/src/identifiers/symbol.rs @@ -65,13 +65,6 @@ impl Symbol { } } -impl Default for Symbol { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("AUD/USD").unwrap() - } -} - impl Debug for Symbol { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index addc1a7e8d37..8ab6adbac517 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -74,12 +74,6 @@ impl TradeId { } } -impl Default for TradeId { - fn default() -> Self { - Self::from("1") - } -} - impl Display for TradeId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_cstr().to_str().unwrap()) diff --git a/nautilus_core/model/src/identifiers/trader_id.rs b/nautilus_core/model/src/identifiers/trader_id.rs index 7bf551b53d48..2b1de86aa54b 100644 --- a/nautilus_core/model/src/identifiers/trader_id.rs +++ b/nautilus_core/model/src/identifiers/trader_id.rs @@ -73,13 +73,6 @@ impl TraderId { } } -impl Default for TraderId { - fn default() -> Self { - // SAFETY: Default value is safe - Self(Ustr::from("TRADER-000")) - } -} - impl Debug for TraderId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/venue.rs b/nautilus_core/model/src/identifiers/venue.rs index ad86d70296c5..347a2763cc6a 100644 --- a/nautilus_core/model/src/identifiers/venue.rs +++ b/nautilus_core/model/src/identifiers/venue.rs @@ -90,13 +90,6 @@ impl Venue { } } -impl Default for Venue { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("SIM").unwrap() - } -} - impl Debug for Venue { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/nautilus_core/model/src/identifiers/venue_order_id.rs b/nautilus_core/model/src/identifiers/venue_order_id.rs index 2efb5585c07c..f1ce60d71524 100644 --- a/nautilus_core/model/src/identifiers/venue_order_id.rs +++ b/nautilus_core/model/src/identifiers/venue_order_id.rs @@ -60,13 +60,6 @@ impl VenueOrderId { } } -impl Default for VenueOrderId { - fn default() -> Self { - // SAFETY: Default value is safe - Self::new("001").unwrap() - } -} - impl Debug for VenueOrderId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) From a44b87f3c3490bed7d5ea1e079949e73c38500f8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 08:29:04 +1000 Subject: [PATCH 106/193] Consolidate OrderAny in separate module --- nautilus_core/backtest/src/matching_engine.rs | 2 +- nautilus_core/common/src/cache/core.rs | 2 +- nautilus_core/common/src/cache/database.rs | 3 +- nautilus_core/execution/src/engine.rs | 2 +- nautilus_core/execution/src/matching_core.rs | 3 +- nautilus_core/model/src/orders/any.rs | 568 ++++++++++++++++++ nautilus_core/model/src/orders/base.rs | 546 +---------------- nautilus_core/model/src/orders/limit.rs | 5 +- .../model/src/orders/limit_if_touched.rs | 5 +- nautilus_core/model/src/orders/list.rs | 4 +- nautilus_core/model/src/orders/market.rs | 5 +- .../model/src/orders/market_if_touched.rs | 5 +- .../model/src/orders/market_to_limit.rs | 5 +- nautilus_core/model/src/orders/mod.rs | 1 + nautilus_core/model/src/orders/stop_limit.rs | 5 +- nautilus_core/model/src/orders/stop_market.rs | 5 +- .../model/src/orders/trailing_stop_limit.rs | 5 +- .../model/src/orders/trailing_stop_market.rs | 5 +- 18 files changed, 617 insertions(+), 559 deletions(-) create mode 100644 nautilus_core/model/src/orders/any.rs diff --git a/nautilus_core/backtest/src/matching_engine.rs b/nautilus_core/backtest/src/matching_engine.rs index 46a8c7caa688..744a7ab800fa 100644 --- a/nautilus_core/backtest/src/matching_engine.rs +++ b/nautilus_core/backtest/src/matching_engine.rs @@ -38,7 +38,7 @@ use nautilus_model::{ instruments::Instrument, orderbook::book::OrderBook, orders::{ - base::{PassiveOrderAny, StopOrderAny}, + any::{PassiveOrderAny, StopOrderAny}, trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder, }, diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs index 0519b8440cbd..3ad33d40a102 100644 --- a/nautilus_core/common/src/cache/core.rs +++ b/nautilus_core/common/src/cache/core.rs @@ -35,7 +35,7 @@ use nautilus_model::{ }, instruments::{synthetic::SyntheticInstrument, InstrumentAny}, orderbook::book::OrderBook, - orders::{base::OrderAny, list::OrderList}, + orders::{any::OrderAny, list::OrderList}, polymorphism::{ GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetPositionId, diff --git a/nautilus_core/common/src/cache/database.rs b/nautilus_core/common/src/cache/database.rs index 50025b35f19c..e685a26aeb04 100644 --- a/nautilus_core/common/src/cache/database.rs +++ b/nautilus_core/common/src/cache/database.rs @@ -29,7 +29,8 @@ use nautilus_model::{ strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, }, instruments::{synthetic::SyntheticInstrument, InstrumentAny}, - orders::base::{Order, OrderAny}, + orders::any::OrderAny, + orders::base::Order, position::Position, types::currency::Currency, }; diff --git a/nautilus_core/execution/src/engine.rs b/nautilus_core/execution/src/engine.rs index aa3a87d2d756..3a51a6aba69b 100644 --- a/nautilus_core/execution/src/engine.rs +++ b/nautilus_core/execution/src/engine.rs @@ -30,7 +30,7 @@ use nautilus_model::{ client_id::ClientId, instrument_id::InstrumentId, strategy_id::StrategyId, venue::Venue, }, instruments::InstrumentAny, - orders::base::OrderAny, + orders::any::OrderAny, position::Position, types::quantity::Quantity, }; diff --git a/nautilus_core/execution/src/matching_core.rs b/nautilus_core/execution/src/matching_core.rs index fb96f99087c3..deac0486b23b 100644 --- a/nautilus_core/execution/src/matching_core.rs +++ b/nautilus_core/execution/src/matching_core.rs @@ -23,7 +23,8 @@ use nautilus_model::{ enums::OrderSideSpecified, identifiers::{client_order_id::ClientOrderId, instrument_id::InstrumentId}, orders::{ - base::{LimitOrderAny, OrderError, PassiveOrderAny, StopOrderAny}, + any::{LimitOrderAny, PassiveOrderAny, StopOrderAny}, + base::OrderError, market::MarketOrder, }, polymorphism::{GetClientOrderId, GetLimitPrice, GetOrderSideSpecified, GetStopPrice}, diff --git a/nautilus_core/model/src/orders/any.rs b/nautilus_core/model/src/orders/any.rs new file mode 100644 index 000000000000..5cc67e3721bc --- /dev/null +++ b/nautilus_core/model/src/orders/any.rs @@ -0,0 +1,568 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 nautilus_core::nanos::UnixNanos; +use serde::{Deserialize, Serialize}; + +use super::{ + base::Order, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder, + market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder, + stop_limit::StopLimitOrder, stop_market::StopMarketOrder, + trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder, +}; +use crate::{ + enums::{OrderSide, OrderSideSpecified, TriggerType}, + identifiers::{ + client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, + venue_order_id::VenueOrderId, + }, + polymorphism::{ + GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, + GetLimitPrice, GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, + GetOrderSideSpecified, GetPositionId, GetStopPrice, GetStrategyId, GetVenueOrderId, + IsClosed, IsInflight, IsOpen, + }, + types::{price::Price, quantity::Quantity}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum OrderAny { + Limit(LimitOrder), + LimitIfTouched(LimitIfTouchedOrder), + Market(MarketOrder), + MarketIfTouched(MarketIfTouchedOrder), + MarketToLimit(MarketToLimitOrder), + StopLimit(StopLimitOrder), + StopMarket(StopMarketOrder), + TrailingStopLimit(TrailingStopLimitOrder), + TrailingStopMarket(TrailingStopMarketOrder), +} + +impl OrderAny { + #[must_use] + pub fn from_limit(order: LimitOrder) -> Self { + Self::Limit(order) + } + + #[must_use] + pub fn from_limit_if_touched(order: LimitIfTouchedOrder) -> Self { + Self::LimitIfTouched(order) + } + + #[must_use] + pub fn from_market(order: MarketOrder) -> Self { + Self::Market(order) + } + + #[must_use] + pub fn from_market_if_touched(order: MarketIfTouchedOrder) -> Self { + Self::MarketIfTouched(order) + } + + #[must_use] + pub fn from_market_to_limit(order: MarketToLimitOrder) -> Self { + Self::MarketToLimit(order) + } + + #[must_use] + pub fn from_stop_limit(order: StopLimitOrder) -> Self { + Self::StopLimit(order) + } + + #[must_use] + pub fn from_stop_market(order: StopMarketOrder) -> Self { + Self::StopMarket(order) + } + + #[must_use] + pub fn from_trailing_stop_limit(order: StopLimitOrder) -> Self { + Self::StopLimit(order) + } + + #[must_use] + pub fn from_trailing_stop_market(order: StopMarketOrder) -> Self { + Self::StopMarket(order) + } +} + +impl GetInstrumentId for OrderAny { + fn instrument_id(&self) -> InstrumentId { + match self { + Self::Limit(order) => order.instrument_id, + Self::LimitIfTouched(order) => order.instrument_id, + Self::Market(order) => order.instrument_id, + Self::MarketIfTouched(order) => order.instrument_id, + Self::MarketToLimit(order) => order.instrument_id, + Self::StopLimit(order) => order.instrument_id, + Self::StopMarket(order) => order.instrument_id, + Self::TrailingStopLimit(order) => order.instrument_id, + Self::TrailingStopMarket(order) => order.instrument_id, + } + } +} + +impl GetClientOrderId for OrderAny { + fn client_order_id(&self) -> ClientOrderId { + match self { + Self::Limit(order) => order.client_order_id, + Self::LimitIfTouched(order) => order.client_order_id, + Self::Market(order) => order.client_order_id, + Self::MarketIfTouched(order) => order.client_order_id, + Self::MarketToLimit(order) => order.client_order_id, + Self::StopLimit(order) => order.client_order_id, + Self::StopMarket(order) => order.client_order_id, + Self::TrailingStopLimit(order) => order.client_order_id, + Self::TrailingStopMarket(order) => order.client_order_id, + } + } +} + +impl GetVenueOrderId for OrderAny { + fn venue_order_id(&self) -> Option { + match self { + Self::Limit(order) => order.venue_order_id, + Self::LimitIfTouched(order) => order.venue_order_id, + Self::Market(order) => order.venue_order_id, + Self::MarketIfTouched(order) => order.venue_order_id, + Self::MarketToLimit(order) => order.venue_order_id, + Self::StopLimit(order) => order.venue_order_id, + Self::StopMarket(order) => order.venue_order_id, + Self::TrailingStopLimit(order) => order.venue_order_id, + Self::TrailingStopMarket(order) => order.venue_order_id, + } + } +} + +impl GetStrategyId for OrderAny { + fn strategy_id(&self) -> StrategyId { + match self { + Self::Limit(order) => order.strategy_id, + Self::LimitIfTouched(order) => order.strategy_id, + Self::Market(order) => order.strategy_id, + Self::MarketIfTouched(order) => order.strategy_id, + Self::MarketToLimit(order) => order.strategy_id, + Self::StopLimit(order) => order.strategy_id, + Self::StopMarket(order) => order.strategy_id, + Self::TrailingStopLimit(order) => order.strategy_id, + Self::TrailingStopMarket(order) => order.strategy_id, + } + } +} + +impl GetPositionId for OrderAny { + fn position_id(&self) -> Option { + match self { + Self::Limit(order) => order.position_id, + Self::LimitIfTouched(order) => order.position_id, + Self::Market(order) => order.position_id, + Self::MarketIfTouched(order) => order.position_id, + Self::MarketToLimit(order) => order.position_id, + Self::StopLimit(order) => order.position_id, + Self::StopMarket(order) => order.position_id, + Self::TrailingStopLimit(order) => order.position_id, + Self::TrailingStopMarket(order) => order.position_id, + } + } +} + +impl GetExecAlgorithmId for OrderAny { + fn exec_algorithm_id(&self) -> Option { + match self { + Self::Limit(order) => order.exec_algorithm_id, + Self::LimitIfTouched(order) => order.exec_algorithm_id, + Self::Market(order) => order.exec_algorithm_id, + Self::MarketIfTouched(order) => order.exec_algorithm_id, + Self::MarketToLimit(order) => order.exec_algorithm_id, + Self::StopLimit(order) => order.exec_algorithm_id, + Self::StopMarket(order) => order.exec_algorithm_id, + Self::TrailingStopLimit(order) => order.exec_algorithm_id, + Self::TrailingStopMarket(order) => order.exec_algorithm_id, + } + } +} + +impl GetExecSpawnId for OrderAny { + fn exec_spawn_id(&self) -> Option { + match self { + Self::Limit(order) => order.exec_spawn_id, + Self::LimitIfTouched(order) => order.exec_spawn_id, + Self::Market(order) => order.exec_spawn_id, + Self::MarketIfTouched(order) => order.exec_spawn_id, + Self::MarketToLimit(order) => order.exec_spawn_id, + Self::StopLimit(order) => order.exec_spawn_id, + Self::StopMarket(order) => order.exec_spawn_id, + Self::TrailingStopLimit(order) => order.exec_spawn_id, + Self::TrailingStopMarket(order) => order.exec_spawn_id, + } + } +} + +impl GetOrderSide for OrderAny { + fn order_side(&self) -> OrderSide { + match self { + Self::Limit(order) => order.side, + Self::LimitIfTouched(order) => order.side, + Self::Market(order) => order.side, + Self::MarketIfTouched(order) => order.side, + Self::MarketToLimit(order) => order.side, + Self::StopLimit(order) => order.side, + Self::StopMarket(order) => order.side, + Self::TrailingStopLimit(order) => order.side, + Self::TrailingStopMarket(order) => order.side, + } + } +} + +impl GetOrderQuantity for OrderAny { + fn quantity(&self) -> Quantity { + match self { + Self::Limit(order) => order.quantity, + Self::LimitIfTouched(order) => order.quantity, + Self::Market(order) => order.quantity, + Self::MarketIfTouched(order) => order.quantity, + Self::MarketToLimit(order) => order.quantity, + Self::StopLimit(order) => order.quantity, + Self::StopMarket(order) => order.quantity, + Self::TrailingStopLimit(order) => order.quantity, + Self::TrailingStopMarket(order) => order.quantity, + } + } +} + +impl GetOrderFilledQty for OrderAny { + fn filled_qty(&self) -> Quantity { + match self { + Self::Limit(order) => order.filled_qty(), + Self::LimitIfTouched(order) => order.filled_qty(), + Self::Market(order) => order.filled_qty(), + Self::MarketIfTouched(order) => order.filled_qty(), + Self::MarketToLimit(order) => order.filled_qty(), + Self::StopLimit(order) => order.filled_qty(), + Self::StopMarket(order) => order.filled_qty(), + Self::TrailingStopLimit(order) => order.filled_qty(), + Self::TrailingStopMarket(order) => order.filled_qty(), + } + } +} + +impl GetOrderLeavesQty for OrderAny { + fn leaves_qty(&self) -> Quantity { + match self { + Self::Limit(order) => order.leaves_qty(), + Self::LimitIfTouched(order) => order.leaves_qty(), + Self::Market(order) => order.leaves_qty(), + Self::MarketIfTouched(order) => order.leaves_qty(), + Self::MarketToLimit(order) => order.leaves_qty(), + Self::StopLimit(order) => order.leaves_qty(), + Self::StopMarket(order) => order.leaves_qty(), + Self::TrailingStopLimit(order) => order.leaves_qty(), + Self::TrailingStopMarket(order) => order.leaves_qty(), + } + } +} + +impl GetOrderSideSpecified for OrderAny { + fn order_side_specified(&self) -> OrderSideSpecified { + match self { + Self::Limit(order) => order.side.as_specified(), + Self::LimitIfTouched(order) => order.side.as_specified(), + Self::Market(order) => order.side.as_specified(), + Self::MarketIfTouched(order) => order.side.as_specified(), + Self::MarketToLimit(order) => order.side.as_specified(), + Self::StopLimit(order) => order.side.as_specified(), + Self::StopMarket(order) => order.side.as_specified(), + Self::TrailingStopLimit(order) => order.side.as_specified(), + Self::TrailingStopMarket(order) => order.side.as_specified(), + } + } +} + +impl GetEmulationTrigger for OrderAny { + fn emulation_trigger(&self) -> Option { + match self { + Self::Limit(order) => order.emulation_trigger, + Self::LimitIfTouched(order) => order.emulation_trigger, + Self::Market(order) => order.emulation_trigger, + Self::MarketIfTouched(order) => order.emulation_trigger, + Self::MarketToLimit(order) => order.emulation_trigger, + Self::StopLimit(order) => order.emulation_trigger, + Self::StopMarket(order) => order.emulation_trigger, + Self::TrailingStopLimit(order) => order.emulation_trigger, + Self::TrailingStopMarket(order) => order.emulation_trigger, + } + } +} + +impl IsOpen for OrderAny { + fn is_open(&self) -> bool { + match self { + Self::Limit(order) => order.is_open(), + Self::LimitIfTouched(order) => order.is_open(), + Self::Market(order) => order.is_open(), + Self::MarketIfTouched(order) => order.is_open(), + Self::MarketToLimit(order) => order.is_open(), + Self::StopLimit(order) => order.is_open(), + Self::StopMarket(order) => order.is_open(), + Self::TrailingStopLimit(order) => order.is_open(), + Self::TrailingStopMarket(order) => order.is_open(), + } + } +} + +impl IsClosed for OrderAny { + fn is_closed(&self) -> bool { + match self { + Self::Limit(order) => order.is_closed(), + Self::LimitIfTouched(order) => order.is_closed(), + Self::Market(order) => order.is_closed(), + Self::MarketIfTouched(order) => order.is_closed(), + Self::MarketToLimit(order) => order.is_closed(), + Self::StopLimit(order) => order.is_closed(), + Self::StopMarket(order) => order.is_closed(), + Self::TrailingStopLimit(order) => order.is_closed(), + Self::TrailingStopMarket(order) => order.is_closed(), + } + } +} + +impl IsInflight for OrderAny { + fn is_inflight(&self) -> bool { + match self { + Self::Limit(order) => order.is_inflight(), + Self::LimitIfTouched(order) => order.is_inflight(), + Self::Market(order) => order.is_inflight(), + Self::MarketIfTouched(order) => order.is_inflight(), + Self::MarketToLimit(order) => order.is_inflight(), + Self::StopLimit(order) => order.is_inflight(), + Self::StopMarket(order) => order.is_inflight(), + Self::TrailingStopLimit(order) => order.is_inflight(), + Self::TrailingStopMarket(order) => order.is_inflight(), + } + } +} + +#[derive(Clone, Debug)] +pub enum PassiveOrderAny { + Limit(LimitOrderAny), + Stop(StopOrderAny), +} + +impl PassiveOrderAny { + #[must_use] + pub fn is_closed(&self) -> bool { + match self { + Self::Limit(order) => order.is_closed(), + Self::Stop(order) => order.is_closed(), + } + } + + #[must_use] + pub fn expire_time(&self) -> Option { + match self { + Self::Limit(order) => order.expire_time(), + Self::Stop(order) => order.expire_time(), + } + } +} + +impl PartialEq for PassiveOrderAny { + fn eq(&self, rhs: &Self) -> bool { + match self { + Self::Limit(order) => order.client_order_id() == rhs.client_order_id(), + Self::Stop(order) => order.client_order_id() == rhs.client_order_id(), + } + } +} + +#[derive(Clone, Debug)] +pub enum LimitOrderAny { + Limit(LimitOrder), + MarketToLimit(MarketToLimitOrder), + StopLimit(StopLimitOrder), + TrailingStopLimit(TrailingStopLimitOrder), +} + +impl LimitOrderAny { + #[must_use] + pub fn is_closed(&self) -> bool { + match self { + Self::Limit(order) => order.is_closed(), + Self::MarketToLimit(order) => order.is_closed(), + Self::StopLimit(order) => order.is_closed(), + Self::TrailingStopLimit(order) => order.is_closed(), + } + } + + #[must_use] + pub fn expire_time(&self) -> Option { + match self { + Self::Limit(order) => order.expire_time, + Self::MarketToLimit(order) => order.expire_time, + Self::StopLimit(order) => order.expire_time, + Self::TrailingStopLimit(order) => order.expire_time, + } + } +} + +impl PartialEq for LimitOrderAny { + fn eq(&self, rhs: &Self) -> bool { + match self { + Self::Limit(order) => order.client_order_id == rhs.client_order_id(), + Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(), + Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(), + Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(), + } + } +} + +#[derive(Clone, Debug)] +pub enum StopOrderAny { + LimitIfTouched(LimitIfTouchedOrder), + MarketIfTouched(MarketIfTouchedOrder), + StopLimit(StopLimitOrder), + StopMarket(StopMarketOrder), + TrailingStopLimit(TrailingStopLimitOrder), + TrailingStopMarket(TrailingStopMarketOrder), +} + +impl StopOrderAny { + #[must_use] + pub fn is_closed(&self) -> bool { + match self { + Self::LimitIfTouched(order) => order.is_closed(), + Self::MarketIfTouched(order) => order.is_closed(), + Self::StopLimit(order) => order.is_closed(), + Self::StopMarket(order) => order.is_closed(), + Self::TrailingStopLimit(order) => order.is_closed(), + Self::TrailingStopMarket(order) => order.is_closed(), + } + } + + #[must_use] + pub fn expire_time(&self) -> Option { + match self { + Self::LimitIfTouched(order) => order.expire_time, + Self::MarketIfTouched(order) => order.expire_time, + Self::StopLimit(order) => order.expire_time, + Self::StopMarket(order) => order.expire_time, + Self::TrailingStopLimit(order) => order.expire_time, + Self::TrailingStopMarket(order) => order.expire_time, + } + } +} + +impl PartialEq for StopOrderAny { + fn eq(&self, rhs: &Self) -> bool { + match self { + Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(), + Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(), + Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(), + Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(), + Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(), + Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(), + } + } +} + +impl GetClientOrderId for PassiveOrderAny { + fn client_order_id(&self) -> ClientOrderId { + match self { + Self::Limit(order) => order.client_order_id(), + Self::Stop(order) => order.client_order_id(), + } + } +} + +impl GetOrderSideSpecified for PassiveOrderAny { + fn order_side_specified(&self) -> OrderSideSpecified { + match self { + Self::Limit(order) => order.order_side_specified(), + Self::Stop(order) => order.order_side_specified(), + } + } +} + +impl GetClientOrderId for LimitOrderAny { + fn client_order_id(&self) -> ClientOrderId { + match self { + Self::Limit(order) => order.client_order_id, + Self::MarketToLimit(order) => order.client_order_id, + Self::StopLimit(order) => order.client_order_id, + Self::TrailingStopLimit(order) => order.client_order_id, + } + } +} + +impl GetOrderSideSpecified for LimitOrderAny { + fn order_side_specified(&self) -> OrderSideSpecified { + match self { + Self::Limit(order) => order.side.as_specified(), + Self::MarketToLimit(order) => order.side.as_specified(), + Self::StopLimit(order) => order.side.as_specified(), + Self::TrailingStopLimit(order) => order.side.as_specified(), + } + } +} + +impl GetLimitPrice for LimitOrderAny { + fn limit_px(&self) -> Price { + match self { + Self::Limit(order) => order.price, + Self::MarketToLimit(order) => order.price.expect("No price for order"), // TBD + Self::StopLimit(order) => order.price, + Self::TrailingStopLimit(order) => order.price, + } + } +} + +impl GetClientOrderId for StopOrderAny { + fn client_order_id(&self) -> ClientOrderId { + match self { + Self::LimitIfTouched(order) => order.client_order_id, + Self::MarketIfTouched(order) => order.client_order_id, + Self::StopLimit(order) => order.client_order_id, + Self::StopMarket(order) => order.client_order_id, + Self::TrailingStopLimit(order) => order.client_order_id, + Self::TrailingStopMarket(order) => order.client_order_id, + } + } +} + +impl GetOrderSideSpecified for StopOrderAny { + fn order_side_specified(&self) -> OrderSideSpecified { + match self { + Self::LimitIfTouched(order) => order.side.as_specified(), + Self::MarketIfTouched(order) => order.side.as_specified(), + Self::StopLimit(order) => order.side.as_specified(), + Self::StopMarket(order) => order.side.as_specified(), + Self::TrailingStopLimit(order) => order.side.as_specified(), + Self::TrailingStopMarket(order) => order.side.as_specified(), + } + } +} + +impl GetStopPrice for StopOrderAny { + fn stop_px(&self) -> Price { + match self { + Self::LimitIfTouched(order) => order.trigger_price, + Self::MarketIfTouched(order) => order.trigger_price, + Self::StopLimit(order) => order.trigger_price, + Self::StopMarket(order) => order.trigger_price, + Self::TrailingStopLimit(order) => order.trigger_price, + Self::TrailingStopMarket(order) => order.trigger_price, + } + } +} diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 63aa0770f139..e16f84ddf1ab 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -20,16 +20,11 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{ - limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder, - market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder, - stop_limit::StopLimitOrder, stop_market::StopMarketOrder, - trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder, -}; +use super::any::OrderAny; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderSideSpecified, OrderStatus, OrderType, - PositionSide, TimeInForce, TrailingOffsetType, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, + TimeInForce, TrailingOffsetType, TriggerType, }, events::order::{ accepted::OrderAccepted, cancel_rejected::OrderCancelRejected, canceled::OrderCanceled, @@ -45,12 +40,6 @@ use crate::{ strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, venue::Venue, venue_order_id::VenueOrderId, }, - polymorphism::{ - GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, - GetLimitPrice, GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, - GetOrderSideSpecified, GetPositionId, GetStopPrice, GetStrategyId, GetVenueOrderId, - IsClosed, IsInflight, IsOpen, - }, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; @@ -90,535 +79,6 @@ pub enum OrderError { NoPreviousState, } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum OrderAny { - Limit(LimitOrder), - LimitIfTouched(LimitIfTouchedOrder), - Market(MarketOrder), - MarketIfTouched(MarketIfTouchedOrder), - MarketToLimit(MarketToLimitOrder), - StopLimit(StopLimitOrder), - StopMarket(StopMarketOrder), - TrailingStopLimit(TrailingStopLimitOrder), - TrailingStopMarket(TrailingStopMarketOrder), -} - -impl OrderAny { - #[must_use] - pub fn from_limit(order: LimitOrder) -> Self { - Self::Limit(order) - } - - #[must_use] - pub fn from_limit_if_touched(order: LimitIfTouchedOrder) -> Self { - Self::LimitIfTouched(order) - } - - #[must_use] - pub fn from_market(order: MarketOrder) -> Self { - Self::Market(order) - } - - #[must_use] - pub fn from_market_if_touched(order: MarketIfTouchedOrder) -> Self { - Self::MarketIfTouched(order) - } - - #[must_use] - pub fn from_market_to_limit(order: MarketToLimitOrder) -> Self { - Self::MarketToLimit(order) - } - - #[must_use] - pub fn from_stop_limit(order: StopLimitOrder) -> Self { - Self::StopLimit(order) - } - - #[must_use] - pub fn from_stop_market(order: StopMarketOrder) -> Self { - Self::StopMarket(order) - } - - #[must_use] - pub fn from_trailing_stop_limit(order: StopLimitOrder) -> Self { - Self::StopLimit(order) - } - - #[must_use] - pub fn from_trailing_stop_market(order: StopMarketOrder) -> Self { - Self::StopMarket(order) - } -} - -impl GetInstrumentId for OrderAny { - fn instrument_id(&self) -> InstrumentId { - match self { - Self::Limit(order) => order.instrument_id, - Self::LimitIfTouched(order) => order.instrument_id, - Self::Market(order) => order.instrument_id, - Self::MarketIfTouched(order) => order.instrument_id, - Self::MarketToLimit(order) => order.instrument_id, - Self::StopLimit(order) => order.instrument_id, - Self::StopMarket(order) => order.instrument_id, - Self::TrailingStopLimit(order) => order.instrument_id, - Self::TrailingStopMarket(order) => order.instrument_id, - } - } -} - -impl GetClientOrderId for OrderAny { - fn client_order_id(&self) -> ClientOrderId { - match self { - Self::Limit(order) => order.client_order_id, - Self::LimitIfTouched(order) => order.client_order_id, - Self::Market(order) => order.client_order_id, - Self::MarketIfTouched(order) => order.client_order_id, - Self::MarketToLimit(order) => order.client_order_id, - Self::StopLimit(order) => order.client_order_id, - Self::StopMarket(order) => order.client_order_id, - Self::TrailingStopLimit(order) => order.client_order_id, - Self::TrailingStopMarket(order) => order.client_order_id, - } - } -} - -impl GetVenueOrderId for OrderAny { - fn venue_order_id(&self) -> Option { - match self { - Self::Limit(order) => order.venue_order_id, - Self::LimitIfTouched(order) => order.venue_order_id, - Self::Market(order) => order.venue_order_id, - Self::MarketIfTouched(order) => order.venue_order_id, - Self::MarketToLimit(order) => order.venue_order_id, - Self::StopLimit(order) => order.venue_order_id, - Self::StopMarket(order) => order.venue_order_id, - Self::TrailingStopLimit(order) => order.venue_order_id, - Self::TrailingStopMarket(order) => order.venue_order_id, - } - } -} - -impl GetStrategyId for OrderAny { - fn strategy_id(&self) -> StrategyId { - match self { - Self::Limit(order) => order.strategy_id, - Self::LimitIfTouched(order) => order.strategy_id, - Self::Market(order) => order.strategy_id, - Self::MarketIfTouched(order) => order.strategy_id, - Self::MarketToLimit(order) => order.strategy_id, - Self::StopLimit(order) => order.strategy_id, - Self::StopMarket(order) => order.strategy_id, - Self::TrailingStopLimit(order) => order.strategy_id, - Self::TrailingStopMarket(order) => order.strategy_id, - } - } -} - -impl GetPositionId for OrderAny { - fn position_id(&self) -> Option { - match self { - Self::Limit(order) => order.position_id, - Self::LimitIfTouched(order) => order.position_id, - Self::Market(order) => order.position_id, - Self::MarketIfTouched(order) => order.position_id, - Self::MarketToLimit(order) => order.position_id, - Self::StopLimit(order) => order.position_id, - Self::StopMarket(order) => order.position_id, - Self::TrailingStopLimit(order) => order.position_id, - Self::TrailingStopMarket(order) => order.position_id, - } - } -} - -impl GetExecAlgorithmId for OrderAny { - fn exec_algorithm_id(&self) -> Option { - match self { - Self::Limit(order) => order.exec_algorithm_id, - Self::LimitIfTouched(order) => order.exec_algorithm_id, - Self::Market(order) => order.exec_algorithm_id, - Self::MarketIfTouched(order) => order.exec_algorithm_id, - Self::MarketToLimit(order) => order.exec_algorithm_id, - Self::StopLimit(order) => order.exec_algorithm_id, - Self::StopMarket(order) => order.exec_algorithm_id, - Self::TrailingStopLimit(order) => order.exec_algorithm_id, - Self::TrailingStopMarket(order) => order.exec_algorithm_id, - } - } -} - -impl GetExecSpawnId for OrderAny { - fn exec_spawn_id(&self) -> Option { - match self { - Self::Limit(order) => order.exec_spawn_id, - Self::LimitIfTouched(order) => order.exec_spawn_id, - Self::Market(order) => order.exec_spawn_id, - Self::MarketIfTouched(order) => order.exec_spawn_id, - Self::MarketToLimit(order) => order.exec_spawn_id, - Self::StopLimit(order) => order.exec_spawn_id, - Self::StopMarket(order) => order.exec_spawn_id, - Self::TrailingStopLimit(order) => order.exec_spawn_id, - Self::TrailingStopMarket(order) => order.exec_spawn_id, - } - } -} - -impl GetOrderSide for OrderAny { - fn order_side(&self) -> OrderSide { - match self { - Self::Limit(order) => order.side, - Self::LimitIfTouched(order) => order.side, - Self::Market(order) => order.side, - Self::MarketIfTouched(order) => order.side, - Self::MarketToLimit(order) => order.side, - Self::StopLimit(order) => order.side, - Self::StopMarket(order) => order.side, - Self::TrailingStopLimit(order) => order.side, - Self::TrailingStopMarket(order) => order.side, - } - } -} - -impl GetOrderQuantity for OrderAny { - fn quantity(&self) -> Quantity { - match self { - Self::Limit(order) => order.quantity, - Self::LimitIfTouched(order) => order.quantity, - Self::Market(order) => order.quantity, - Self::MarketIfTouched(order) => order.quantity, - Self::MarketToLimit(order) => order.quantity, - Self::StopLimit(order) => order.quantity, - Self::StopMarket(order) => order.quantity, - Self::TrailingStopLimit(order) => order.quantity, - Self::TrailingStopMarket(order) => order.quantity, - } - } -} - -impl GetOrderFilledQty for OrderAny { - fn filled_qty(&self) -> Quantity { - match self { - Self::Limit(order) => order.filled_qty(), - Self::LimitIfTouched(order) => order.filled_qty(), - Self::Market(order) => order.filled_qty(), - Self::MarketIfTouched(order) => order.filled_qty(), - Self::MarketToLimit(order) => order.filled_qty(), - Self::StopLimit(order) => order.filled_qty(), - Self::StopMarket(order) => order.filled_qty(), - Self::TrailingStopLimit(order) => order.filled_qty(), - Self::TrailingStopMarket(order) => order.filled_qty(), - } - } -} - -impl GetOrderLeavesQty for OrderAny { - fn leaves_qty(&self) -> Quantity { - match self { - Self::Limit(order) => order.leaves_qty(), - Self::LimitIfTouched(order) => order.leaves_qty(), - Self::Market(order) => order.leaves_qty(), - Self::MarketIfTouched(order) => order.leaves_qty(), - Self::MarketToLimit(order) => order.leaves_qty(), - Self::StopLimit(order) => order.leaves_qty(), - Self::StopMarket(order) => order.leaves_qty(), - Self::TrailingStopLimit(order) => order.leaves_qty(), - Self::TrailingStopMarket(order) => order.leaves_qty(), - } - } -} - -impl GetOrderSideSpecified for OrderAny { - fn order_side_specified(&self) -> OrderSideSpecified { - match self { - Self::Limit(order) => order.side.as_specified(), - Self::LimitIfTouched(order) => order.side.as_specified(), - Self::Market(order) => order.side.as_specified(), - Self::MarketIfTouched(order) => order.side.as_specified(), - Self::MarketToLimit(order) => order.side.as_specified(), - Self::StopLimit(order) => order.side.as_specified(), - Self::StopMarket(order) => order.side.as_specified(), - Self::TrailingStopLimit(order) => order.side.as_specified(), - Self::TrailingStopMarket(order) => order.side.as_specified(), - } - } -} - -impl GetEmulationTrigger for OrderAny { - fn emulation_trigger(&self) -> Option { - match self { - Self::Limit(order) => order.emulation_trigger, - Self::LimitIfTouched(order) => order.emulation_trigger, - Self::Market(order) => order.emulation_trigger, - Self::MarketIfTouched(order) => order.emulation_trigger, - Self::MarketToLimit(order) => order.emulation_trigger, - Self::StopLimit(order) => order.emulation_trigger, - Self::StopMarket(order) => order.emulation_trigger, - Self::TrailingStopLimit(order) => order.emulation_trigger, - Self::TrailingStopMarket(order) => order.emulation_trigger, - } - } -} - -impl IsOpen for OrderAny { - fn is_open(&self) -> bool { - match self { - Self::Limit(order) => order.is_open(), - Self::LimitIfTouched(order) => order.is_open(), - Self::Market(order) => order.is_open(), - Self::MarketIfTouched(order) => order.is_open(), - Self::MarketToLimit(order) => order.is_open(), - Self::StopLimit(order) => order.is_open(), - Self::StopMarket(order) => order.is_open(), - Self::TrailingStopLimit(order) => order.is_open(), - Self::TrailingStopMarket(order) => order.is_open(), - } - } -} - -impl IsClosed for OrderAny { - fn is_closed(&self) -> bool { - match self { - Self::Limit(order) => order.is_closed(), - Self::LimitIfTouched(order) => order.is_closed(), - Self::Market(order) => order.is_closed(), - Self::MarketIfTouched(order) => order.is_closed(), - Self::MarketToLimit(order) => order.is_closed(), - Self::StopLimit(order) => order.is_closed(), - Self::StopMarket(order) => order.is_closed(), - Self::TrailingStopLimit(order) => order.is_closed(), - Self::TrailingStopMarket(order) => order.is_closed(), - } - } -} - -impl IsInflight for OrderAny { - fn is_inflight(&self) -> bool { - match self { - Self::Limit(order) => order.is_inflight(), - Self::LimitIfTouched(order) => order.is_inflight(), - Self::Market(order) => order.is_inflight(), - Self::MarketIfTouched(order) => order.is_inflight(), - Self::MarketToLimit(order) => order.is_inflight(), - Self::StopLimit(order) => order.is_inflight(), - Self::StopMarket(order) => order.is_inflight(), - Self::TrailingStopLimit(order) => order.is_inflight(), - Self::TrailingStopMarket(order) => order.is_inflight(), - } - } -} - -#[derive(Clone, Debug)] -pub enum PassiveOrderAny { - Limit(LimitOrderAny), - Stop(StopOrderAny), -} - -impl PassiveOrderAny { - #[must_use] - pub fn is_closed(&self) -> bool { - match self { - Self::Limit(o) => o.is_closed(), - Self::Stop(o) => o.is_closed(), - } - } - - #[must_use] - pub fn expire_time(&self) -> Option { - match self { - Self::Limit(o) => o.expire_time(), - Self::Stop(o) => o.expire_time(), - } - } -} - -impl PartialEq for PassiveOrderAny { - fn eq(&self, rhs: &Self) -> bool { - match self { - Self::Limit(order) => order.client_order_id() == rhs.client_order_id(), - Self::Stop(order) => order.client_order_id() == rhs.client_order_id(), - } - } -} - -#[derive(Clone, Debug)] -pub enum LimitOrderAny { - Limit(LimitOrder), - MarketToLimit(MarketToLimitOrder), - StopLimit(StopLimitOrder), - TrailingStopLimit(TrailingStopLimitOrder), -} - -impl LimitOrderAny { - #[must_use] - pub fn is_closed(&self) -> bool { - match self { - Self::Limit(o) => o.is_closed(), - Self::MarketToLimit(o) => o.is_closed(), - Self::StopLimit(o) => o.is_closed(), - Self::TrailingStopLimit(o) => o.is_closed(), - } - } - - #[must_use] - pub fn expire_time(&self) -> Option { - match self { - Self::Limit(o) => o.expire_time, - Self::MarketToLimit(o) => o.expire_time, - Self::StopLimit(o) => o.expire_time, - Self::TrailingStopLimit(o) => o.expire_time, - } - } -} - -impl PartialEq for LimitOrderAny { - fn eq(&self, rhs: &Self) -> bool { - match self { - Self::Limit(order) => order.client_order_id == rhs.client_order_id(), - Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(), - Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(), - Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(), - } - } -} - -#[derive(Clone, Debug)] -pub enum StopOrderAny { - LimitIfTouched(LimitIfTouchedOrder), - MarketIfTouched(MarketIfTouchedOrder), - StopLimit(StopLimitOrder), - StopMarket(StopMarketOrder), - TrailingStopLimit(TrailingStopLimitOrder), - TrailingStopMarket(TrailingStopMarketOrder), -} - -impl StopOrderAny { - #[must_use] - pub fn is_closed(&self) -> bool { - match self { - Self::LimitIfTouched(o) => o.is_closed(), - Self::MarketIfTouched(o) => o.is_closed(), - Self::StopLimit(o) => o.is_closed(), - Self::StopMarket(o) => o.is_closed(), - Self::TrailingStopLimit(o) => o.is_closed(), - Self::TrailingStopMarket(o) => o.is_closed(), - } - } - - #[must_use] - pub fn expire_time(&self) -> Option { - match self { - Self::LimitIfTouched(o) => o.expire_time, - Self::MarketIfTouched(o) => o.expire_time, - Self::StopLimit(o) => o.expire_time, - Self::StopMarket(o) => o.expire_time, - Self::TrailingStopLimit(o) => o.expire_time, - Self::TrailingStopMarket(o) => o.expire_time, - } - } -} - -impl PartialEq for StopOrderAny { - fn eq(&self, rhs: &Self) -> bool { - match self { - Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(), - Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(), - Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(), - Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(), - Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(), - Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(), - } - } -} - -impl GetClientOrderId for PassiveOrderAny { - fn client_order_id(&self) -> ClientOrderId { - match self { - Self::Limit(order) => order.client_order_id(), - Self::Stop(order) => order.client_order_id(), - } - } -} - -impl GetOrderSideSpecified for PassiveOrderAny { - fn order_side_specified(&self) -> OrderSideSpecified { - match self { - Self::Limit(order) => order.order_side_specified(), - Self::Stop(order) => order.order_side_specified(), - } - } -} - -impl GetClientOrderId for LimitOrderAny { - fn client_order_id(&self) -> ClientOrderId { - match self { - Self::Limit(order) => order.client_order_id, - Self::MarketToLimit(order) => order.client_order_id, - Self::StopLimit(order) => order.client_order_id, - Self::TrailingStopLimit(order) => order.client_order_id, - } - } -} - -impl GetOrderSideSpecified for LimitOrderAny { - fn order_side_specified(&self) -> OrderSideSpecified { - match self { - Self::Limit(order) => order.side.as_specified(), - Self::MarketToLimit(order) => order.side.as_specified(), - Self::StopLimit(order) => order.side.as_specified(), - Self::TrailingStopLimit(order) => order.side.as_specified(), - } - } -} - -impl GetLimitPrice for LimitOrderAny { - fn limit_px(&self) -> Price { - match self { - Self::Limit(order) => order.price, - Self::MarketToLimit(order) => order.price.expect("No price for order"), // TBD - Self::StopLimit(order) => order.price, - Self::TrailingStopLimit(order) => order.price, - } - } -} - -impl GetClientOrderId for StopOrderAny { - fn client_order_id(&self) -> ClientOrderId { - match self { - Self::LimitIfTouched(order) => order.client_order_id, - Self::MarketIfTouched(order) => order.client_order_id, - Self::StopLimit(order) => order.client_order_id, - Self::StopMarket(order) => order.client_order_id, - Self::TrailingStopLimit(order) => order.client_order_id, - Self::TrailingStopMarket(order) => order.client_order_id, - } - } -} - -impl GetOrderSideSpecified for StopOrderAny { - fn order_side_specified(&self) -> OrderSideSpecified { - match self { - Self::LimitIfTouched(order) => order.side.as_specified(), - Self::MarketIfTouched(order) => order.side.as_specified(), - Self::StopLimit(order) => order.side.as_specified(), - Self::StopMarket(order) => order.side.as_specified(), - Self::TrailingStopLimit(order) => order.side.as_specified(), - Self::TrailingStopMarket(order) => order.side.as_specified(), - } - } -} - -impl GetStopPrice for StopOrderAny { - fn stop_px(&self) -> Price { - match self { - Self::LimitIfTouched(o) => o.trigger_price, - Self::MarketIfTouched(o) => o.trigger_price, - Self::StopLimit(o) => o.trigger_price, - Self::StopMarket(o) => o.trigger_price, - Self::TrailingStopLimit(o) => o.trigger_price, - Self::TrailingStopMarket(o) => o.trigger_price, - } - } -} - #[must_use] pub fn ustr_hashmap_to_str(h: HashMap) -> HashMap { h.into_iter() diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index c1b34780f8b8..c8515b999479 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -23,7 +23,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore}; +use super::{ + any::OrderAny, + base::{Order, OrderCore}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index 735aff6ed204..17052c92d3e0 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -22,7 +22,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore, OrderError}; +use super::{ + any::OrderAny, + base::{Order, OrderCore, OrderError}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/list.rs b/nautilus_core/model/src/orders/list.rs index bebb27cebdb9..b98722b5a0ac 100644 --- a/nautilus_core/model/src/orders/list.rs +++ b/nautilus_core/model/src/orders/list.rs @@ -18,7 +18,7 @@ use std::fmt::Display; use nautilus_core::{correctness::check_slice_not_empty, nanos::UnixNanos}; use serde::{Deserialize, Serialize}; -use super::base::OrderAny; +use super::any::OrderAny; use crate::{ identifiers::{ instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, @@ -97,7 +97,7 @@ mod tests { enums::OrderSide, identifiers::{order_list_id::OrderListId, strategy_id::StrategyId}, instruments::{currency_pair::CurrencyPair, stubs::*}, - orders::stubs::TestOrderStubs, + orders::{any::OrderAny, stubs::TestOrderStubs}, types::{price::Price, quantity::Quantity}, }; diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index a4640a10e670..19bf96ce2c36 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -23,7 +23,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore}; +use super::{ + any::OrderAny, + base::{Order, OrderCore}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index 8b0a013a6fce..3f4a19c311eb 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -22,7 +22,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore, OrderError}; +use super::{ + any::OrderAny, + base::{Order, OrderCore, OrderError}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index a4a78d8017cb..ba920c4a12ad 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -22,7 +22,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore}; +use super::{ + any::OrderAny, + base::{Order, OrderCore}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/mod.rs b/nautilus_core/model/src/orders/mod.rs index 59027c636043..b0b48809ff43 100644 --- a/nautilus_core/model/src/orders/mod.rs +++ b/nautilus_core/model/src/orders/mod.rs @@ -17,6 +17,7 @@ #![allow(dead_code)] +pub mod any; pub mod base; pub mod default; pub mod limit; diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index 8095cf983bda..f9920037b0d0 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -23,7 +23,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore, OrderError}; +use super::{ + any::OrderAny, + base::{Order, OrderCore, OrderError}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index ed0b93c9e968..de3caed2b61c 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -22,7 +22,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore}; +use super::{ + any::OrderAny, + base::{Order, OrderCore}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index 29b3a457eb33..f522477d2e2d 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -22,7 +22,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore, OrderError}; +use super::{ + any::OrderAny, + base::{Order, OrderCore, OrderError}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index 4dc23855ba1e..f3a28d7b8bd9 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -22,7 +22,10 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::base::{Order, OrderAny, OrderCore}; +use super::{ + any::OrderAny, + base::{Order, OrderCore}, +}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, From 29f00bf073e4d101250a388b0040b929b19c1caf Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 08:42:45 +1000 Subject: [PATCH 107/193] Consolidate InstrumentAny in separate module --- nautilus_core/accounting/src/account/base.rs | 2 +- nautilus_core/accounting/src/account/cash.rs | 2 +- .../accounting/src/account/margin.rs | 2 +- nautilus_core/accounting/src/account/stubs.rs | 2 +- nautilus_core/accounting/src/python/margin.rs | 2 +- .../adapters/src/databento/decode.rs | 5 +- nautilus_core/adapters/src/databento/live.rs | 2 +- .../adapters/src/databento/loader.rs | 2 +- nautilus_core/common/src/cache/core.rs | 5 +- nautilus_core/common/src/cache/database.rs | 5 +- nautilus_core/common/src/interface/account.rs | 2 +- nautilus_core/execution/src/engine.rs | 2 +- .../infrastructure/src/sql/cache_database.rs | 3 +- .../src/sql/models/instruments.rs | 4 +- .../infrastructure/src/sql/queries.rs | 2 +- .../tests/test_cache_database_postgres.rs | 3 +- nautilus_core/model/src/instruments/any.rs | 263 ++++++++++++++++++ .../model/src/instruments/crypto_future.rs | 2 +- .../model/src/instruments/crypto_perpetual.rs | 2 +- .../model/src/instruments/currency_pair.rs | 2 +- nautilus_core/model/src/instruments/equity.rs | 2 +- .../model/src/instruments/futures_contract.rs | 2 +- .../model/src/instruments/futures_spread.rs | 2 +- nautilus_core/model/src/instruments/mod.rs | 244 +--------------- .../model/src/instruments/options_contract.rs | 2 +- .../model/src/instruments/options_spread.rs | 2 +- .../model/src/python/instruments/mod.rs | 6 +- nautilus_core/model/src/python/position.rs | 2 +- 28 files changed, 301 insertions(+), 275 deletions(-) create mode 100644 nautilus_core/model/src/instruments/any.rs diff --git a/nautilus_core/accounting/src/account/base.rs b/nautilus_core/accounting/src/account/base.rs index 9d0ad9f5c339..5c80cca7959d 100644 --- a/nautilus_core/accounting/src/account/base.rs +++ b/nautilus_core/accounting/src/account/base.rs @@ -19,7 +19,7 @@ use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, identifiers::account_id::AccountId, - instruments::InstrumentAny, + instruments::any::InstrumentAny, position::Position, types::{ balance::AccountBalance, currency::Currency, money::Money, price::Price, quantity::Quantity, diff --git a/nautilus_core/accounting/src/account/cash.rs b/nautilus_core/accounting/src/account/cash.rs index 8fc9d713ef44..efd9b2265d94 100644 --- a/nautilus_core/accounting/src/account/cash.rs +++ b/nautilus_core/accounting/src/account/cash.rs @@ -24,7 +24,7 @@ use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, identifiers::account_id::AccountId, - instruments::InstrumentAny, + instruments::any::InstrumentAny, position::Position, types::{ balance::AccountBalance, currency::Currency, money::Money, price::Price, quantity::Quantity, diff --git a/nautilus_core/accounting/src/account/margin.rs b/nautilus_core/accounting/src/account/margin.rs index fb0ea8f1f3b6..4c016b43dc9b 100644 --- a/nautilus_core/accounting/src/account/margin.rs +++ b/nautilus_core/accounting/src/account/margin.rs @@ -27,7 +27,7 @@ use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, identifiers::{account_id::AccountId, instrument_id::InstrumentId}, - instruments::{Instrument, InstrumentAny}, + instruments::{any::InstrumentAny, Instrument}, position::Position, types::{ balance::{AccountBalance, MarginBalance}, diff --git a/nautilus_core/accounting/src/account/stubs.rs b/nautilus_core/accounting/src/account/stubs.rs index 14afb03504af..0b66b733db75 100644 --- a/nautilus_core/accounting/src/account/stubs.rs +++ b/nautilus_core/accounting/src/account/stubs.rs @@ -17,7 +17,7 @@ use nautilus_common::interface::account::Account; use nautilus_model::{ enums::LiquiditySide, events::account::{state::AccountState, stubs::*}, - instruments::InstrumentAny, + instruments::any::InstrumentAny, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; use rstest::fixture; diff --git a/nautilus_core/accounting/src/python/margin.rs b/nautilus_core/accounting/src/python/margin.rs index 6a022ef658c6..ee4277948deb 100644 --- a/nautilus_core/accounting/src/python/margin.rs +++ b/nautilus_core/accounting/src/python/margin.rs @@ -17,7 +17,7 @@ use nautilus_core::python::to_pyvalue_err; use nautilus_model::{ events::account::state::AccountState, identifiers::{account_id::AccountId, instrument_id::InstrumentId}, - instruments::InstrumentAny, + instruments::any::InstrumentAny, python::instruments::convert_pyobject_to_instrument_any, types::{money::Money, price::Price, quantity::Quantity}, }; diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index ddb30cced6dc..ac9b81c0057e 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -38,8 +38,9 @@ use nautilus_model::{ }, identifiers::{instrument_id::InstrumentId, trade_id::TradeId}, instruments::{ - equity::Equity, futures_contract::FuturesContract, futures_spread::FuturesSpread, - options_contract::OptionsContract, options_spread::OptionsSpread, InstrumentAny, + any::InstrumentAny, equity::Equity, futures_contract::FuturesContract, + futures_spread::FuturesSpread, options_contract::OptionsContract, + options_spread::OptionsSpread, }, types::{currency::Currency, fixed::FIXED_SCALAR, price::Price, quantity::Quantity}, }; diff --git a/nautilus_core/adapters/src/databento/live.rs b/nautilus_core/adapters/src/databento/live.rs index 34563e86c3ec..6a8bf30cde13 100644 --- a/nautilus_core/adapters/src/databento/live.rs +++ b/nautilus_core/adapters/src/databento/live.rs @@ -33,7 +33,7 @@ use nautilus_model::{ }, enums::RecordFlag, identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, - instruments::InstrumentAny, + instruments::any::InstrumentAny, }; use tokio::{ sync::mpsc::{self, error::TryRecvError}, diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs index 5a4e679dca9c..b67428c726d5 100644 --- a/nautilus_core/adapters/src/databento/loader.rs +++ b/nautilus_core/adapters/src/databento/loader.rs @@ -24,7 +24,7 @@ use indexmap::IndexMap; use nautilus_model::{ data::Data, identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, - instruments::InstrumentAny, + instruments::any::InstrumentAny, types::currency::Currency, }; use streaming_iterator::StreamingIterator; diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs index 3ad33d40a102..f9066152786b 100644 --- a/nautilus_core/common/src/cache/core.rs +++ b/nautilus_core/common/src/cache/core.rs @@ -33,7 +33,7 @@ use nautilus_model::{ order_list_id::OrderListId, position_id::PositionId, strategy_id::StrategyId, venue::Venue, venue_order_id::VenueOrderId, }, - instruments::{synthetic::SyntheticInstrument, InstrumentAny}, + instruments::{any::InstrumentAny, synthetic::SyntheticInstrument}, orderbook::book::OrderBook, orders::{any::OrderAny, list::OrderList}, polymorphism::{ @@ -2387,7 +2387,8 @@ mod tests { use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, instruments::{ - currency_pair::CurrencyPair, stubs::*, synthetic::SyntheticInstrument, InstrumentAny, + any::InstrumentAny, currency_pair::CurrencyPair, stubs::*, + synthetic::SyntheticInstrument, }, }; use rstest::*; diff --git a/nautilus_core/common/src/cache/database.rs b/nautilus_core/common/src/cache/database.rs index e685a26aeb04..8ee5905df020 100644 --- a/nautilus_core/common/src/cache/database.rs +++ b/nautilus_core/common/src/cache/database.rs @@ -28,9 +28,8 @@ use nautilus_model::{ component_id::ComponentId, instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, }, - instruments::{synthetic::SyntheticInstrument, InstrumentAny}, - orders::any::OrderAny, - orders::base::Order, + instruments::{any::InstrumentAny, synthetic::SyntheticInstrument}, + orders::{any::OrderAny, base::Order}, position::Position, types::currency::Currency, }; diff --git a/nautilus_core/common/src/interface/account.rs b/nautilus_core/common/src/interface/account.rs index 510c43d65b22..afd2fe52989a 100644 --- a/nautilus_core/common/src/interface/account.rs +++ b/nautilus_core/common/src/interface/account.rs @@ -19,7 +19,7 @@ use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, identifiers::account_id::AccountId, - instruments::InstrumentAny, + instruments::any::InstrumentAny, position::Position, types::{ balance::AccountBalance, currency::Currency, money::Money, price::Price, quantity::Quantity, diff --git a/nautilus_core/execution/src/engine.rs b/nautilus_core/execution/src/engine.rs index 3a51a6aba69b..b918a982ef63 100644 --- a/nautilus_core/execution/src/engine.rs +++ b/nautilus_core/execution/src/engine.rs @@ -29,7 +29,7 @@ use nautilus_model::{ identifiers::{ client_id::ClientId, instrument_id::InstrumentId, strategy_id::StrategyId, venue::Venue, }, - instruments::InstrumentAny, + instruments::any::InstrumentAny, orders::any::OrderAny, position::Position, types::quantity::Quantity, diff --git a/nautilus_core/infrastructure/src/sql/cache_database.rs b/nautilus_core/infrastructure/src/sql/cache_database.rs index de99609a62a4..9999f1875f38 100644 --- a/nautilus_core/infrastructure/src/sql/cache_database.rs +++ b/nautilus_core/infrastructure/src/sql/cache_database.rs @@ -19,7 +19,8 @@ use std::{ }; use nautilus_model::{ - identifiers::instrument_id::InstrumentId, instruments::InstrumentAny, types::currency::Currency, + identifiers::instrument_id::InstrumentId, instruments::any::InstrumentAny, + types::currency::Currency, }; use sqlx::{postgres::PgConnectOptions, PgPool}; use tokio::{ diff --git a/nautilus_core/infrastructure/src/sql/models/instruments.rs b/nautilus_core/infrastructure/src/sql/models/instruments.rs index e78882a9d82e..c54c4ddff960 100644 --- a/nautilus_core/infrastructure/src/sql/models/instruments.rs +++ b/nautilus_core/infrastructure/src/sql/models/instruments.rs @@ -24,10 +24,10 @@ use nautilus_model::{ enums::{AssetClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, instruments::{ - crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, + any::InstrumentAny, crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair, equity::Equity, futures_contract::FuturesContract, futures_spread::FuturesSpread, options_contract::OptionsContract, - options_spread::OptionsSpread, InstrumentAny, + options_spread::OptionsSpread, }, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; diff --git a/nautilus_core/infrastructure/src/sql/queries.rs b/nautilus_core/infrastructure/src/sql/queries.rs index 751a73b84067..449326f4aadc 100644 --- a/nautilus_core/infrastructure/src/sql/queries.rs +++ b/nautilus_core/infrastructure/src/sql/queries.rs @@ -17,7 +17,7 @@ use std::collections::HashMap; use nautilus_model::{ identifiers::instrument_id::InstrumentId, - instruments::{Instrument, InstrumentAny}, + instruments::{any::InstrumentAny, Instrument}, types::currency::Currency, }; use sqlx::PgPool; diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs index 38ad3c35dcf7..b2928547e085 100644 --- a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -65,11 +65,12 @@ mod tests { enums::CurrencyType, identifiers::instrument_id::InstrumentId, instruments::{ + any::InstrumentAny, stubs::{ crypto_future_btcusdt, crypto_perpetual_ethusdt, currency_pair_ethusdt, equity_aapl, futures_contract_es, options_contract_appl, }, - Instrument, InstrumentAny, + Instrument, }, types::{currency::Currency, price::Price, quantity::Quantity}, }; diff --git a/nautilus_core/model/src/instruments/any.rs b/nautilus_core/model/src/instruments/any.rs new file mode 100644 index 000000000000..fefba9a3ad03 --- /dev/null +++ b/nautilus_core/model/src/instruments/any.rs @@ -0,0 +1,263 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 rust_decimal::Decimal; + +use super::{ + crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair, + equity::Equity, futures_contract::FuturesContract, futures_spread::FuturesSpread, + options_contract::OptionsContract, options_spread::OptionsSpread, Instrument, +}; +use crate::{ + identifiers::instrument_id::InstrumentId, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, +}; + +#[derive(Clone, Debug)] +pub enum InstrumentAny { + CryptoFuture(CryptoFuture), + CryptoPerpetual(CryptoPerpetual), + CurrencyPair(CurrencyPair), + Equity(Equity), + FuturesContract(FuturesContract), + FuturesSpread(FuturesSpread), + OptionsContract(OptionsContract), + OptionsSpread(OptionsSpread), +} + +impl InstrumentAny { + #[must_use] + pub fn id(&self) -> InstrumentId { + match self { + Self::CryptoFuture(inst) => inst.id, + Self::CryptoPerpetual(inst) => inst.id, + Self::CurrencyPair(inst) => inst.id, + Self::Equity(inst) => inst.id, + Self::FuturesContract(inst) => inst.id, + Self::FuturesSpread(inst) => inst.id, + Self::OptionsContract(inst) => inst.id, + Self::OptionsSpread(inst) => inst.id, + } + } + + #[must_use] + pub fn base_currency(&self) -> Option { + match self { + Self::CryptoFuture(inst) => inst.base_currency(), + Self::CryptoPerpetual(inst) => inst.base_currency(), + Self::CurrencyPair(inst) => inst.base_currency(), + Self::Equity(inst) => inst.base_currency(), + Self::FuturesContract(inst) => inst.base_currency(), + Self::FuturesSpread(inst) => inst.base_currency(), + Self::OptionsContract(inst) => inst.base_currency(), + Self::OptionsSpread(inst) => inst.base_currency(), + } + } + + #[must_use] + pub fn quote_currency(&self) -> Currency { + match self { + Self::CryptoFuture(inst) => inst.quote_currency(), + Self::CryptoPerpetual(inst) => inst.quote_currency(), + Self::CurrencyPair(inst) => inst.quote_currency(), + Self::Equity(inst) => inst.quote_currency(), + Self::FuturesContract(inst) => inst.quote_currency(), + Self::FuturesSpread(inst) => inst.quote_currency(), + Self::OptionsContract(inst) => inst.quote_currency(), + Self::OptionsSpread(inst) => inst.quote_currency(), + } + } + + #[must_use] + pub fn settlement_currency(&self) -> Currency { + match self { + Self::CryptoFuture(inst) => inst.settlement_currency(), + Self::CryptoPerpetual(inst) => inst.settlement_currency(), + Self::CurrencyPair(inst) => inst.settlement_currency(), + Self::Equity(inst) => inst.settlement_currency(), + Self::FuturesContract(inst) => inst.settlement_currency(), + Self::FuturesSpread(inst) => inst.settlement_currency(), + Self::OptionsContract(inst) => inst.settlement_currency(), + Self::OptionsSpread(inst) => inst.settlement_currency(), + } + } + + #[must_use] + pub fn is_inverse(&self) -> bool { + match self { + Self::CryptoFuture(inst) => inst.is_inverse(), + Self::CryptoPerpetual(inst) => inst.is_inverse(), + Self::CurrencyPair(inst) => inst.is_inverse(), + Self::Equity(inst) => inst.is_inverse(), + Self::FuturesContract(inst) => inst.is_inverse(), + Self::FuturesSpread(inst) => inst.is_inverse(), + Self::OptionsContract(inst) => inst.is_inverse(), + Self::OptionsSpread(inst) => inst.is_inverse(), + } + } + + #[must_use] + pub fn price_precision(&self) -> u8 { + match self { + Self::CryptoFuture(inst) => inst.price_precision(), + Self::CryptoPerpetual(inst) => inst.price_precision(), + Self::CurrencyPair(inst) => inst.price_precision(), + Self::Equity(inst) => inst.price_precision(), + Self::FuturesContract(inst) => inst.price_precision(), + Self::FuturesSpread(inst) => inst.price_precision(), + Self::OptionsContract(inst) => inst.price_precision(), + Self::OptionsSpread(inst) => inst.price_precision(), + } + } + + #[must_use] + pub fn size_precision(&self) -> u8 { + match self { + Self::CryptoFuture(inst) => inst.size_precision(), + Self::CryptoPerpetual(inst) => inst.size_precision(), + Self::CurrencyPair(inst) => inst.size_precision(), + Self::Equity(inst) => inst.size_precision(), + Self::FuturesContract(inst) => inst.size_precision(), + Self::FuturesSpread(inst) => inst.size_precision(), + Self::OptionsContract(inst) => inst.size_precision(), + Self::OptionsSpread(inst) => inst.size_precision(), + } + } + + #[must_use] + pub fn price_increment(&self) -> Price { + match self { + Self::CryptoFuture(inst) => inst.price_increment(), + Self::CryptoPerpetual(inst) => inst.price_increment(), + Self::CurrencyPair(inst) => inst.price_increment(), + Self::Equity(inst) => inst.price_increment(), + Self::FuturesContract(inst) => inst.price_increment(), + Self::FuturesSpread(inst) => inst.price_increment(), + Self::OptionsContract(inst) => inst.price_increment(), + Self::OptionsSpread(inst) => inst.price_increment(), + } + } + + #[must_use] + pub fn size_increment(&self) -> Quantity { + match self { + Self::CryptoFuture(inst) => inst.size_increment(), + Self::CryptoPerpetual(inst) => inst.size_increment(), + Self::CurrencyPair(inst) => inst.size_increment(), + Self::Equity(inst) => inst.size_increment(), + Self::FuturesContract(inst) => inst.size_increment(), + Self::FuturesSpread(inst) => inst.size_increment(), + Self::OptionsContract(inst) => inst.size_increment(), + Self::OptionsSpread(inst) => inst.size_increment(), + } + } + + pub fn make_price(&self, value: f64) -> anyhow::Result { + match self { + Self::CryptoFuture(inst) => inst.make_price(value), + Self::CryptoPerpetual(inst) => inst.make_price(value), + Self::CurrencyPair(inst) => inst.make_price(value), + Self::Equity(inst) => inst.make_price(value), + Self::FuturesContract(inst) => inst.make_price(value), + Self::FuturesSpread(inst) => inst.make_price(value), + Self::OptionsContract(inst) => inst.make_price(value), + Self::OptionsSpread(inst) => inst.make_price(value), + } + } + + pub fn make_qty(&self, value: f64) -> anyhow::Result { + match self { + Self::CryptoFuture(inst) => inst.make_qty(value), + Self::CryptoPerpetual(inst) => inst.make_qty(value), + Self::CurrencyPair(inst) => inst.make_qty(value), + Self::Equity(inst) => inst.make_qty(value), + Self::FuturesContract(inst) => inst.make_qty(value), + Self::FuturesSpread(inst) => inst.make_qty(value), + Self::OptionsContract(inst) => inst.make_qty(value), + Self::OptionsSpread(inst) => inst.make_qty(value), + } + } + + #[must_use] + pub fn calculate_notional_value( + &self, + quantity: Quantity, + price: Price, + use_quote_for_inverse: Option, + ) -> Money { + match self { + Self::CryptoFuture(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + Self::CryptoPerpetual(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + Self::CurrencyPair(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + Self::Equity(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + Self::FuturesContract(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + Self::FuturesSpread(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + Self::OptionsContract(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + Self::OptionsSpread(inst) => { + inst.calculate_notional_value(quantity, price, use_quote_for_inverse) + } + } + } + + // #[deprecated(since = "0.21.0", note = "Will be removed in a future version")] + #[must_use] + pub fn maker_fee(&self) -> Decimal { + match self { + Self::CryptoFuture(inst) => inst.maker_fee(), + Self::CryptoPerpetual(inst) => inst.maker_fee(), + Self::CurrencyPair(inst) => inst.maker_fee(), + Self::Equity(inst) => inst.maker_fee(), + Self::FuturesContract(inst) => inst.maker_fee(), + Self::FuturesSpread(inst) => inst.maker_fee(), + Self::OptionsContract(inst) => inst.maker_fee(), + Self::OptionsSpread(inst) => inst.maker_fee(), + } + } + + // #[deprecated(since = "0.21.0", note = "Will be removed in a future version")] + #[must_use] + pub fn taker_fee(&self) -> Decimal { + match self { + Self::CryptoFuture(inst) => inst.taker_fee(), + Self::CryptoPerpetual(inst) => inst.taker_fee(), + Self::CurrencyPair(inst) => inst.taker_fee(), + Self::Equity(inst) => inst.taker_fee(), + Self::FuturesContract(inst) => inst.taker_fee(), + Self::FuturesSpread(inst) => inst.taker_fee(), + Self::OptionsContract(inst) => inst.taker_fee(), + Self::OptionsSpread(inst) => inst.taker_fee(), + } + } +} + +impl PartialEq for InstrumentAny { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index 95f14b17386b..1759275c315f 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -23,7 +23,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{Instrument, InstrumentAny}; +use super::{any::InstrumentAny, Instrument}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index b4c0925aeb65..14996cfc8134 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -23,7 +23,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::InstrumentAny; +use super::any::InstrumentAny; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 9d285ed64478..46dec3a40668 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -23,7 +23,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{Instrument, InstrumentAny}; +use super::{any::InstrumentAny, Instrument}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index 911829b0b70e..f3365f6e0157 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -23,7 +23,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{Instrument, InstrumentAny}; +use super::{any::InstrumentAny, Instrument}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 82a2b01feea4..c4204fede104 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -25,7 +25,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{Instrument, InstrumentAny}; +use super::{any::InstrumentAny, Instrument}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index 72ed78588da2..f8f8940c9f3f 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -25,7 +25,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{Instrument, InstrumentAny}; +use super::{any::InstrumentAny, Instrument}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index 52e38ab063f5..1e0dd6340097 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -15,6 +15,7 @@ //! Defines instrument definitions for the trading domain models. +pub mod any; pub mod crypto_future; pub mod crypto_perpetual; pub mod currency_pair; @@ -33,254 +34,13 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; use ustr::Ustr; -use self::{ - crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair, - equity::Equity, futures_contract::FuturesContract, futures_spread::FuturesSpread, - options_contract::OptionsContract, options_spread::OptionsSpread, -}; +use self::any::InstrumentAny; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; -#[derive(Clone, Debug)] -pub enum InstrumentAny { - CryptoFuture(CryptoFuture), - CryptoPerpetual(CryptoPerpetual), - CurrencyPair(CurrencyPair), - Equity(Equity), - FuturesContract(FuturesContract), - FuturesSpread(FuturesSpread), - OptionsContract(OptionsContract), - OptionsSpread(OptionsSpread), -} - -impl InstrumentAny { - #[must_use] - pub fn id(&self) -> InstrumentId { - match self { - Self::CryptoFuture(inst) => inst.id, - Self::CryptoPerpetual(inst) => inst.id, - Self::CurrencyPair(inst) => inst.id, - Self::Equity(inst) => inst.id, - Self::FuturesContract(inst) => inst.id, - Self::FuturesSpread(inst) => inst.id, - Self::OptionsContract(inst) => inst.id, - Self::OptionsSpread(inst) => inst.id, - } - } - - #[must_use] - pub fn base_currency(&self) -> Option { - match self { - Self::CryptoFuture(inst) => inst.base_currency(), - Self::CryptoPerpetual(inst) => inst.base_currency(), - Self::CurrencyPair(inst) => inst.base_currency(), - Self::Equity(inst) => inst.base_currency(), - Self::FuturesContract(inst) => inst.base_currency(), - Self::FuturesSpread(inst) => inst.base_currency(), - Self::OptionsContract(inst) => inst.base_currency(), - Self::OptionsSpread(inst) => inst.base_currency(), - } - } - - #[must_use] - pub fn quote_currency(&self) -> Currency { - match self { - Self::CryptoFuture(inst) => inst.quote_currency(), - Self::CryptoPerpetual(inst) => inst.quote_currency(), - Self::CurrencyPair(inst) => inst.quote_currency(), - Self::Equity(inst) => inst.quote_currency(), - Self::FuturesContract(inst) => inst.quote_currency(), - Self::FuturesSpread(inst) => inst.quote_currency(), - Self::OptionsContract(inst) => inst.quote_currency(), - Self::OptionsSpread(inst) => inst.quote_currency(), - } - } - - #[must_use] - pub fn settlement_currency(&self) -> Currency { - match self { - Self::CryptoFuture(inst) => inst.settlement_currency(), - Self::CryptoPerpetual(inst) => inst.settlement_currency(), - Self::CurrencyPair(inst) => inst.settlement_currency(), - Self::Equity(inst) => inst.settlement_currency(), - Self::FuturesContract(inst) => inst.settlement_currency(), - Self::FuturesSpread(inst) => inst.settlement_currency(), - Self::OptionsContract(inst) => inst.settlement_currency(), - Self::OptionsSpread(inst) => inst.settlement_currency(), - } - } - - #[must_use] - pub fn is_inverse(&self) -> bool { - match self { - Self::CryptoFuture(inst) => inst.is_inverse(), - Self::CryptoPerpetual(inst) => inst.is_inverse(), - Self::CurrencyPair(inst) => inst.is_inverse(), - Self::Equity(inst) => inst.is_inverse(), - Self::FuturesContract(inst) => inst.is_inverse(), - Self::FuturesSpread(inst) => inst.is_inverse(), - Self::OptionsContract(inst) => inst.is_inverse(), - Self::OptionsSpread(inst) => inst.is_inverse(), - } - } - - #[must_use] - pub fn price_precision(&self) -> u8 { - match self { - Self::CryptoFuture(inst) => inst.price_precision(), - Self::CryptoPerpetual(inst) => inst.price_precision(), - Self::CurrencyPair(inst) => inst.price_precision(), - Self::Equity(inst) => inst.price_precision(), - Self::FuturesContract(inst) => inst.price_precision(), - Self::FuturesSpread(inst) => inst.price_precision(), - Self::OptionsContract(inst) => inst.price_precision(), - Self::OptionsSpread(inst) => inst.price_precision(), - } - } - - #[must_use] - pub fn size_precision(&self) -> u8 { - match self { - Self::CryptoFuture(inst) => inst.size_precision(), - Self::CryptoPerpetual(inst) => inst.size_precision(), - Self::CurrencyPair(inst) => inst.size_precision(), - Self::Equity(inst) => inst.size_precision(), - Self::FuturesContract(inst) => inst.size_precision(), - Self::FuturesSpread(inst) => inst.size_precision(), - Self::OptionsContract(inst) => inst.size_precision(), - Self::OptionsSpread(inst) => inst.size_precision(), - } - } - - #[must_use] - pub fn price_increment(&self) -> Price { - match self { - Self::CryptoFuture(inst) => inst.price_increment(), - Self::CryptoPerpetual(inst) => inst.price_increment(), - Self::CurrencyPair(inst) => inst.price_increment(), - Self::Equity(inst) => inst.price_increment(), - Self::FuturesContract(inst) => inst.price_increment(), - Self::FuturesSpread(inst) => inst.price_increment(), - Self::OptionsContract(inst) => inst.price_increment(), - Self::OptionsSpread(inst) => inst.price_increment(), - } - } - - #[must_use] - pub fn size_increment(&self) -> Quantity { - match self { - Self::CryptoFuture(inst) => inst.size_increment(), - Self::CryptoPerpetual(inst) => inst.size_increment(), - Self::CurrencyPair(inst) => inst.size_increment(), - Self::Equity(inst) => inst.size_increment(), - Self::FuturesContract(inst) => inst.size_increment(), - Self::FuturesSpread(inst) => inst.size_increment(), - Self::OptionsContract(inst) => inst.size_increment(), - Self::OptionsSpread(inst) => inst.size_increment(), - } - } - - pub fn make_price(&self, value: f64) -> anyhow::Result { - match self { - Self::CryptoFuture(inst) => inst.make_price(value), - Self::CryptoPerpetual(inst) => inst.make_price(value), - Self::CurrencyPair(inst) => inst.make_price(value), - Self::Equity(inst) => inst.make_price(value), - Self::FuturesContract(inst) => inst.make_price(value), - Self::FuturesSpread(inst) => inst.make_price(value), - Self::OptionsContract(inst) => inst.make_price(value), - Self::OptionsSpread(inst) => inst.make_price(value), - } - } - - pub fn make_qty(&self, value: f64) -> anyhow::Result { - match self { - Self::CryptoFuture(inst) => inst.make_qty(value), - Self::CryptoPerpetual(inst) => inst.make_qty(value), - Self::CurrencyPair(inst) => inst.make_qty(value), - Self::Equity(inst) => inst.make_qty(value), - Self::FuturesContract(inst) => inst.make_qty(value), - Self::FuturesSpread(inst) => inst.make_qty(value), - Self::OptionsContract(inst) => inst.make_qty(value), - Self::OptionsSpread(inst) => inst.make_qty(value), - } - } - - #[must_use] - pub fn calculate_notional_value( - &self, - quantity: Quantity, - price: Price, - use_quote_for_inverse: Option, - ) -> Money { - match self { - Self::CryptoFuture(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - Self::CryptoPerpetual(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - Self::CurrencyPair(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - Self::Equity(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - Self::FuturesContract(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - Self::FuturesSpread(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - Self::OptionsContract(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - Self::OptionsSpread(inst) => { - inst.calculate_notional_value(quantity, price, use_quote_for_inverse) - } - } - } - - // #[deprecated(since = "0.21.0", note = "Will be removed in a future version")] - #[must_use] - pub fn maker_fee(&self) -> Decimal { - match self { - Self::CryptoFuture(inst) => inst.maker_fee(), - Self::CryptoPerpetual(inst) => inst.maker_fee(), - Self::CurrencyPair(inst) => inst.maker_fee(), - Self::Equity(inst) => inst.maker_fee(), - Self::FuturesContract(inst) => inst.maker_fee(), - Self::FuturesSpread(inst) => inst.maker_fee(), - Self::OptionsContract(inst) => inst.maker_fee(), - Self::OptionsSpread(inst) => inst.maker_fee(), - } - } - - // #[deprecated(since = "0.21.0", note = "Will be removed in a future version")] - #[must_use] - pub fn taker_fee(&self) -> Decimal { - match self { - Self::CryptoFuture(inst) => inst.taker_fee(), - Self::CryptoPerpetual(inst) => inst.taker_fee(), - Self::CurrencyPair(inst) => inst.taker_fee(), - Self::Equity(inst) => inst.taker_fee(), - Self::FuturesContract(inst) => inst.taker_fee(), - Self::FuturesSpread(inst) => inst.taker_fee(), - Self::OptionsContract(inst) => inst.taker_fee(), - Self::OptionsSpread(inst) => inst.taker_fee(), - } - } -} - -impl PartialEq for InstrumentAny { - fn eq(&self, other: &Self) -> bool { - self.id() == other.id() - } -} - pub trait Instrument: 'static + Send { fn into_any(self) -> InstrumentAny; fn id(&self) -> InstrumentId; diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index 524a36c19aa0..8233fe5d2142 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -25,7 +25,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{Instrument, InstrumentAny}; +use super::{any::InstrumentAny, Instrument}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 89a8a1a42063..3db07bb87441 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -25,7 +25,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use super::{Instrument, InstrumentAny}; +use super::{any::InstrumentAny, Instrument}; use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{instrument_id::InstrumentId, symbol::Symbol}, diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index 902b9610b3fc..710abaa05561 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -19,9 +19,9 @@ use nautilus_core::python::to_pyvalue_err; use pyo3::{IntoPy, PyObject, PyResult, Python}; use crate::instruments::{ - crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair, - equity::Equity, futures_contract::FuturesContract, futures_spread::FuturesSpread, - options_contract::OptionsContract, InstrumentAny, + any::InstrumentAny, crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, + currency_pair::CurrencyPair, equity::Equity, futures_contract::FuturesContract, + futures_spread::FuturesSpread, options_contract::OptionsContract, }; pub fn convert_instrument_any_to_pyobject( diff --git a/nautilus_core/model/src/python/position.rs b/nautilus_core/model/src/python/position.rs index 9a2227b56fb6..52d94ac60b06 100644 --- a/nautilus_core/model/src/python/position.rs +++ b/nautilus_core/model/src/python/position.rs @@ -29,7 +29,7 @@ use crate::{ strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, venue::Venue, venue_order_id::VenueOrderId, }, - instruments::InstrumentAny, + instruments::any::InstrumentAny, position::Position, python::instruments::convert_pyobject_to_instrument_any, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, From e9e8959633eb2ec7f018e6971cd3457301347631 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 09:06:16 +1000 Subject: [PATCH 108/193] Update dependencies --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 12 ++++++------ poetry.lock | 36 ++++++++++++++++++------------------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a1c9594e300..aa67446929ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,7 +82,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.2 + rev: v0.4.3 hooks: - id: ruff args: ["--fix"] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index cb435c6cce3c..4da36f31a954 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -2962,9 +2962,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -3737,9 +3737,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "relative-path" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rend" diff --git a/poetry.lock b/poetry.lock index 0a4c54e932d3..c6bbb4ee9e0e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2020,28 +2020,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.2" +version = "0.4.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, - {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, - {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, - {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, - {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, + {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"}, + {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"}, + {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"}, + {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"}, + {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"}, + {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"}, ] [[package]] From c70ddbe3cce3b74acf92a4ba8ec5a999fa58b747 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 09:39:09 +1000 Subject: [PATCH 109/193] Refine stubs use and fix events display --- .../model/src/events/order/accepted.rs | 2 +- .../model/src/events/order/cancel_rejected.rs | 2 +- .../model/src/events/order/denied.rs | 4 ++-- .../model/src/events/order/emulated.rs | 2 +- .../model/src/events/order/expired.rs | 2 +- .../model/src/events/order/filled.rs | 2 +- .../model/src/events/order/initialized.rs | 2 +- .../model/src/events/order/modify_rejected.rs | 2 +- .../model/src/events/order/pending_cancel.rs | 2 +- .../model/src/events/order/pending_update.rs | 2 +- .../model/src/events/order/rejected.rs | 2 +- .../model/src/events/order/released.rs | 4 ++-- .../model/src/events/order/submitted.rs | 2 +- .../model/src/events/order/triggered.rs | 2 +- .../model/src/events/order/updated.rs | 4 ++-- .../model/src/identifiers/client_order_id.rs | 4 ++-- nautilus_core/model/src/identifiers/stubs.rs | 4 ++-- nautilus_core/model/src/orders/any.rs | 6 ++++++ nautilus_core/model/src/orders/stubs.rs | 19 +++++-------------- 19 files changed, 33 insertions(+), 36 deletions(-) diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index 933007a871f9..7738120c14f1 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -103,7 +103,7 @@ mod tests { let display = format!("{order_accepted}"); assert_eq!( display, - "OrderAccepted(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" + "OrderAccepted(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" ); } } diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index b6be2327a794..c5aa459c31a0 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -108,7 +108,7 @@ mod tests { 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)" + "OrderCancelRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-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/denied.rs b/nautilus_core/model/src/events/order/denied.rs index 014f4e9256ba..60890a7c1047 100644 --- a/nautilus_core/model/src/events/order/denied.rs +++ b/nautilus_core/model/src/events/order/denied.rs @@ -73,7 +73,7 @@ impl Display for OrderDenied { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderDenied(instrument_id={}, client_order_id={},reason={})", + "OrderDenied(instrument_id={}, client_order_id={}, reason={})", self.instrument_id, self.client_order_id, self.reason ) } @@ -92,6 +92,6 @@ mod tests { #[rstest] fn test_order_denied_display(order_denied_max_submitted_rate: OrderDenied) { let display = format!("{order_denied_max_submitted_rate}"); - assert_eq!(display, "OrderDenied(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1,reason=Exceeded MAX_ORDER_SUBMIT_RATE)"); + assert_eq!(display, "OrderDenied(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, reason=Exceeded MAX_ORDER_SUBMIT_RATE)"); } } diff --git a/nautilus_core/model/src/events/order/emulated.rs b/nautilus_core/model/src/events/order/emulated.rs index df15ca4bd7e6..c2fa5c152dab 100644 --- a/nautilus_core/model/src/events/order/emulated.rs +++ b/nautilus_core/model/src/events/order/emulated.rs @@ -89,7 +89,7 @@ mod tests { let display = format!("{order_emulated}"); assert_eq!( display, - "OrderEmulated(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1)" + "OrderEmulated(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1)" ); } } diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index 90832dfe320c..f95c26688ee0 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -103,7 +103,7 @@ mod tests { let display = format!("{order_expired}"); assert_eq!( display, - "OrderExpired(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" + "OrderExpired(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" ); } } diff --git a/nautilus_core/model/src/events/order/filled.rs b/nautilus_core/model/src/events/order/filled.rs index 8cfd39f27563..1fa3d234773f 100644 --- a/nautilus_core/model/src/events/order/filled.rs +++ b/nautilus_core/model/src/events/order/filled.rs @@ -191,7 +191,7 @@ mod tests { let display = format!("{order_filled}"); assert_eq!( display, - "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, \ + "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ venue_order_id=123456, account_id=SIM-001, trade_id=1, position_id=P-001, \ order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22000, \ commission=12.20000000 USDT ,liquidity_side=TAKER, ts_event=0)"); diff --git a/nautilus_core/model/src/events/order/initialized.rs b/nautilus_core/model/src/events/order/initialized.rs index a27f05b445d2..18ee7e39a6d3 100644 --- a/nautilus_core/model/src/events/order/initialized.rs +++ b/nautilus_core/model/src/events/order/initialized.rs @@ -288,7 +288,7 @@ mod test { let display = format!("{order_initialized_buy_limit}"); assert_eq!( display, - "OrderInitialized(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, \ + "OrderInitialized(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ side=BUY, type=LIMIT, quantity=0.561, time_in_force=DAY, post_only=true, reduce_only=true, \ quote_quantity=false, price=22000, emulation_trigger=BID_ASK, trigger_instrument_id=BTCUSDT.COINBASE, \ contingency_type=OTO, order_list_id=1, linked_order_ids=[O-2020872378424], parent_order_id=None, \ diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index 95859692ddc7..f9a157e8d4a0 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -107,7 +107,7 @@ mod tests { let display = format!("{order_modify_rejected}"); assert_eq!( display, - "OrderModifyRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, \ + "OrderModifyRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ venue_order_id=001, account_id=SIM-001,reason=ORDER_DOES_NOT_EXIST, ts_event=0)" ); } diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index 5c9d41f1b538..773a0409c902 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -102,7 +102,7 @@ mod tests { let display = format!("{order_pending_cancel}"); assert_eq!( display, - "OrderPendingCancel(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" + "OrderPendingCancel(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" ); } } diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index 5f64ec71ccd0..44fc08889039 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -102,7 +102,7 @@ mod test { let display = format!("{order_pending_update}"); assert_eq!( display, - "OrderPendingUpdate(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" + "OrderPendingUpdate(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, ts_event=0)" ); } } diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index 975da4b555d1..05a02699f324 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -98,7 +98,7 @@ mod tests { #[rstest] fn test_order_rejected_display(order_rejected_insufficient_margin: OrderRejected) { let display = format!("{order_rejected_insufficient_margin}"); - assert_eq!(display, "OrderRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, \ + assert_eq!(display, "OrderRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ reason=INSUFFICIENT_MARGIN, ts_event=0)"); } } diff --git a/nautilus_core/model/src/events/order/released.rs b/nautilus_core/model/src/events/order/released.rs index cd47918e16d7..e723811c929b 100644 --- a/nautilus_core/model/src/events/order/released.rs +++ b/nautilus_core/model/src/events/order/released.rs @@ -75,7 +75,7 @@ impl Display for OrderReleased { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderReleased({}, {}, {})", + "OrderReleased(instrument_id={}, client_order_id={}, released_price={})", self.instrument_id, self.client_order_id, self.released_price, ) } @@ -94,7 +94,7 @@ mod tests { let display = format!("{order_released}"); assert_eq!( display, - "OrderReleased(BTCUSDT.COINBASE, O-20200814-102234-001-001-1, 22000)" + "OrderReleased(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, released_price=22000)" ); } } diff --git a/nautilus_core/model/src/events/order/submitted.rs b/nautilus_core/model/src/events/order/submitted.rs index 62fa5e0b2e71..8cc8a4ade5da 100644 --- a/nautilus_core/model/src/events/order/submitted.rs +++ b/nautilus_core/model/src/events/order/submitted.rs @@ -94,7 +94,7 @@ mod tests { let display = format!("{order_submitted}"); assert_eq!( display, - "OrderSubmitted(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, account_id=SIM-001, ts_event=0)" + "OrderSubmitted(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, account_id=SIM-001, ts_event=0)" ); } } diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index bbb8c47387c9..8e7718a59310 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -104,7 +104,7 @@ mod tests { #[rstest] fn test_order_triggered_display(order_triggered: OrderTriggered) { let display = format!("{order_triggered}"); - assert_eq!(display, "OrderTriggered(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, \ + assert_eq!(display, "OrderTriggered(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ venue_order_id=001, account_id=SIM-001)"); } } diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index 730ecb031154..ea8e4f188f00 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -90,7 +90,7 @@ impl Display for OrderUpdated { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderUpdated(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={},quantity={}, price={}, trigger_price={}, ts_event={})", + "OrderUpdated(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, ts_event={})", self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), @@ -117,7 +117,7 @@ mod tests { let display = format!("{order_updated}"); assert_eq!( display, - "OrderUpdated(instrument_id=BTCUSDT.COINBASE, client_order_id=O-20200814-102234-001-001-1, venue_order_id=001, account_id=SIM-001,quantity=100, price=22000, trigger_price=None, ts_event=0)" + "OrderUpdated(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, quantity=100, price=22000, trigger_price=None, ts_event=0)" ); } } diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index fb2d1e1abb2a..d99e6f199b36 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -119,8 +119,8 @@ mod tests { #[rstest] fn test_string_reprs(client_order_id: ClientOrderId) { - assert_eq!(client_order_id.as_str(), "O-20200814-102234-001-001-1"); - assert_eq!(format!("{client_order_id}"), "O-20200814-102234-001-001-1"); + assert_eq!(client_order_id.as_str(), "O-19700101-0000-000-001-1"); + assert_eq!(format!("{client_order_id}"), "O-19700101-0000-000-001-1"); } #[rstest] diff --git a/nautilus_core/model/src/identifiers/stubs.rs b/nautilus_core/model/src/identifiers/stubs.rs index 3241cf15d88a..bc45115fa253 100644 --- a/nautilus_core/model/src/identifiers/stubs.rs +++ b/nautilus_core/model/src/identifiers/stubs.rs @@ -37,7 +37,7 @@ impl Default for ClientId { impl Default for ClientOrderId { fn default() -> Self { - Self::from("O-123456789") + Self::from("O-19700101-0000-000-001-1") } } @@ -111,7 +111,7 @@ pub fn client_id_dydx() -> ClientId { #[fixture] pub fn client_order_id() -> ClientOrderId { - ClientOrderId::from("O-20200814-102234-001-001-1") + ClientOrderId::from("O-19700101-0000-000-001-1") } // ---- ComponentId ---- diff --git a/nautilus_core/model/src/orders/any.rs b/nautilus_core/model/src/orders/any.rs index 5cc67e3721bc..a4dae269daf1 100644 --- a/nautilus_core/model/src/orders/any.rs +++ b/nautilus_core/model/src/orders/any.rs @@ -98,6 +98,12 @@ impl OrderAny { } } +impl PartialEq for OrderAny { + fn eq(&self, other: &Self) -> bool { + self.client_order_id() == other.client_order_id() + } +} + impl GetInstrumentId for OrderAny { fn instrument_id(&self) -> InstrumentId { match self { diff --git a/nautilus_core/model/src/orders/stubs.rs b/nautilus_core/model/src/orders/stubs.rs index 07f2bbe31f30..3a37a6e60be6 100644 --- a/nautilus_core/model/src/orders/stubs.rs +++ b/nautilus_core/model/src/orders/stubs.rs @@ -22,14 +22,12 @@ use crate::{ enums::{LiquiditySide, OrderSide, TimeInForce, TriggerType}, events::order::filled::OrderFilled, identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, stubs::{strategy_id_ema_cross, trader_id}, trade_id::TradeId, - venue_order_id::VenueOrderId, }, instruments::Instrument, orders::{base::Order, market::MarketOrder}, @@ -55,12 +53,8 @@ impl TestOrderEventStubs { let trader_id = trader_id(); let strategy_id = strategy_id.unwrap_or(order.strategy_id()); let instrument_id = order.instrument_id(); - let venue_order_id = order - .venue_order_id() - .unwrap_or(VenueOrderId::new("1").unwrap()); - let account_id = order - .account_id() - .unwrap_or(AccountId::new("SIM-001").unwrap()); + let venue_order_id = order.venue_order_id().unwrap_or_default(); + let account_id = order.account_id().unwrap_or_default(); let trade_id = trade_id.unwrap_or( TradeId::new(order.client_order_id().as_str().replace('O', "E").as_str()).unwrap(), ); @@ -110,8 +104,7 @@ impl TestOrderStubs { ) -> MarketOrder { let trader = trader_id(); let strategy = strategy_id_ema_cross(); - let client_order_id = - client_order_id.unwrap_or(ClientOrderId::from("O-20200814-102234-001-001-1")); + let client_order_id = client_order_id.unwrap_or_default(); let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); MarketOrder::new( trader, @@ -148,8 +141,7 @@ impl TestOrderStubs { ) -> LimitOrder { let trader = trader_id(); let strategy = strategy_id_ema_cross(); - let client_order_id = - client_order_id.unwrap_or(ClientOrderId::from("O-19700101-0000-000-001-1")); + let client_order_id = client_order_id.unwrap_or_default(); let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); LimitOrder::new( trader, @@ -193,8 +185,7 @@ impl TestOrderStubs { ) -> StopMarketOrder { let trader = trader_id(); let strategy = strategy_id_ema_cross(); - let client_order_id = - client_order_id.unwrap_or(ClientOrderId::from("O-19700101-010000-001-001-1")); + let client_order_id = client_order_id.unwrap_or_default(); let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); StopMarketOrder::new( trader, From fb9f9235458cbc69a46df87abaf9c034f353b0cf Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 10:34:01 +1000 Subject: [PATCH 110/193] Add order traits and rename event variants --- nautilus_core/model/src/events/order/event.rs | 136 +++++++------- nautilus_core/model/src/orders/any.rs | 107 ++++++++--- nautilus_core/model/src/orders/base.rs | 166 +++++++++--------- nautilus_core/model/src/orders/limit.rs | 4 +- .../model/src/orders/limit_if_touched.rs | 4 +- nautilus_core/model/src/orders/market.rs | 2 +- .../model/src/orders/market_if_touched.rs | 4 +- .../model/src/orders/market_to_limit.rs | 4 +- nautilus_core/model/src/orders/stop_limit.rs | 4 +- nautilus_core/model/src/orders/stop_market.rs | 4 +- .../model/src/orders/trailing_stop_limit.rs | 4 +- .../model/src/orders/trailing_stop_market.rs | 4 +- nautilus_core/model/src/polymorphism.rs | 26 ++- .../model/src/python/events/order/mod.rs | 66 +++---- 14 files changed, 302 insertions(+), 233 deletions(-) diff --git a/nautilus_core/model/src/events/order/event.rs b/nautilus_core/model/src/events/order/event.rs index 3cdc17723a8f..007d423c6f57 100644 --- a/nautilus_core/model/src/events/order/event.rs +++ b/nautilus_core/model/src/events/order/event.rs @@ -31,92 +31,92 @@ use crate::{ #[derive(Clone, PartialEq, Eq, Display, Debug, Serialize, Deserialize)] pub enum OrderEvent { - OrderInitialized(OrderInitialized), - OrderDenied(OrderDenied), - OrderEmulated(OrderEmulated), - OrderReleased(OrderReleased), - OrderSubmitted(OrderSubmitted), - OrderAccepted(OrderAccepted), - OrderRejected(OrderRejected), - OrderCanceled(OrderCanceled), - OrderExpired(OrderExpired), - OrderTriggered(OrderTriggered), - OrderPendingUpdate(OrderPendingUpdate), - OrderPendingCancel(OrderPendingCancel), - OrderModifyRejected(OrderModifyRejected), - OrderCancelRejected(OrderCancelRejected), - OrderUpdated(OrderUpdated), - OrderPartiallyFilled(OrderFilled), - OrderFilled(OrderFilled), + Initialized(OrderInitialized), + Denied(OrderDenied), + Emulated(OrderEmulated), + Released(OrderReleased), + Submitted(OrderSubmitted), + Accepted(OrderAccepted), + Rejected(OrderRejected), + Canceled(OrderCanceled), + Expired(OrderExpired), + Triggered(OrderTriggered), + PendingUpdate(OrderPendingUpdate), + PendingCancel(OrderPendingCancel), + ModifyRejected(OrderModifyRejected), + CancelRejected(OrderCancelRejected), + Updated(OrderUpdated), + PartiallyFilled(OrderFilled), + Filled(OrderFilled), } impl OrderEvent { #[must_use] pub fn client_order_id(&self) -> ClientOrderId { match self { - Self::OrderInitialized(e) => e.client_order_id, - Self::OrderDenied(e) => e.client_order_id, - Self::OrderEmulated(e) => e.client_order_id, - Self::OrderReleased(e) => e.client_order_id, - Self::OrderSubmitted(e) => e.client_order_id, - Self::OrderAccepted(e) => e.client_order_id, - Self::OrderRejected(e) => e.client_order_id, - Self::OrderCanceled(e) => e.client_order_id, - Self::OrderExpired(e) => e.client_order_id, - Self::OrderTriggered(e) => e.client_order_id, - Self::OrderPendingUpdate(e) => e.client_order_id, - Self::OrderPendingCancel(e) => e.client_order_id, - Self::OrderModifyRejected(e) => e.client_order_id, - Self::OrderCancelRejected(e) => e.client_order_id, - Self::OrderUpdated(e) => e.client_order_id, - Self::OrderPartiallyFilled(e) => e.client_order_id, - Self::OrderFilled(e) => e.client_order_id, + Self::Initialized(e) => e.client_order_id, + Self::Denied(e) => e.client_order_id, + Self::Emulated(e) => e.client_order_id, + Self::Released(e) => e.client_order_id, + Self::Submitted(e) => e.client_order_id, + Self::Accepted(e) => e.client_order_id, + Self::Rejected(e) => e.client_order_id, + Self::Canceled(e) => e.client_order_id, + Self::Expired(e) => e.client_order_id, + Self::Triggered(e) => e.client_order_id, + Self::PendingUpdate(e) => e.client_order_id, + Self::PendingCancel(e) => e.client_order_id, + Self::ModifyRejected(e) => e.client_order_id, + Self::CancelRejected(e) => e.client_order_id, + Self::Updated(e) => e.client_order_id, + Self::PartiallyFilled(e) => e.client_order_id, + Self::Filled(e) => e.client_order_id, } } #[must_use] pub fn strategy_id(&self) -> StrategyId { match self { - Self::OrderInitialized(e) => e.strategy_id, - Self::OrderDenied(e) => e.strategy_id, - Self::OrderEmulated(e) => e.strategy_id, - Self::OrderReleased(e) => e.strategy_id, - Self::OrderSubmitted(e) => e.strategy_id, - Self::OrderAccepted(e) => e.strategy_id, - Self::OrderRejected(e) => e.strategy_id, - Self::OrderCanceled(e) => e.strategy_id, - Self::OrderExpired(e) => e.strategy_id, - Self::OrderTriggered(e) => e.strategy_id, - Self::OrderPendingUpdate(e) => e.strategy_id, - Self::OrderPendingCancel(e) => e.strategy_id, - Self::OrderModifyRejected(e) => e.strategy_id, - Self::OrderCancelRejected(e) => e.strategy_id, - Self::OrderUpdated(e) => e.strategy_id, - Self::OrderPartiallyFilled(e) => e.strategy_id, - Self::OrderFilled(e) => e.strategy_id, + Self::Initialized(e) => e.strategy_id, + Self::Denied(e) => e.strategy_id, + Self::Emulated(e) => e.strategy_id, + Self::Released(e) => e.strategy_id, + Self::Submitted(e) => e.strategy_id, + Self::Accepted(e) => e.strategy_id, + Self::Rejected(e) => e.strategy_id, + Self::Canceled(e) => e.strategy_id, + Self::Expired(e) => e.strategy_id, + Self::Triggered(e) => e.strategy_id, + Self::PendingUpdate(e) => e.strategy_id, + Self::PendingCancel(e) => e.strategy_id, + Self::ModifyRejected(e) => e.strategy_id, + Self::CancelRejected(e) => e.strategy_id, + Self::Updated(e) => e.strategy_id, + Self::PartiallyFilled(e) => e.strategy_id, + Self::Filled(e) => e.strategy_id, } } #[must_use] pub fn ts_event(&self) -> UnixNanos { match self { - Self::OrderInitialized(e) => e.ts_event, - Self::OrderDenied(e) => e.ts_event, - Self::OrderEmulated(e) => e.ts_event, - Self::OrderReleased(e) => e.ts_event, - Self::OrderSubmitted(e) => e.ts_event, - Self::OrderAccepted(e) => e.ts_event, - Self::OrderRejected(e) => e.ts_event, - Self::OrderCanceled(e) => e.ts_event, - Self::OrderExpired(e) => e.ts_event, - Self::OrderTriggered(e) => e.ts_event, - Self::OrderPendingUpdate(e) => e.ts_event, - Self::OrderPendingCancel(e) => e.ts_event, - Self::OrderModifyRejected(e) => e.ts_event, - Self::OrderCancelRejected(e) => e.ts_event, - Self::OrderUpdated(e) => e.ts_event, - Self::OrderPartiallyFilled(e) => e.ts_event, - Self::OrderFilled(e) => e.ts_event, + Self::Initialized(e) => e.ts_event, + Self::Denied(e) => e.ts_event, + Self::Emulated(e) => e.ts_event, + Self::Released(e) => e.ts_event, + Self::Submitted(e) => e.ts_event, + Self::Accepted(e) => e.ts_event, + Self::Rejected(e) => e.ts_event, + Self::Canceled(e) => e.ts_event, + Self::Expired(e) => e.ts_event, + Self::Triggered(e) => e.ts_event, + Self::PendingUpdate(e) => e.ts_event, + Self::PendingCancel(e) => e.ts_event, + Self::ModifyRejected(e) => e.ts_event, + Self::CancelRejected(e) => e.ts_event, + Self::Updated(e) => e.ts_event, + Self::PartiallyFilled(e) => e.ts_event, + Self::Filled(e) => e.ts_event, } } } diff --git a/nautilus_core/model/src/orders/any.rs b/nautilus_core/model/src/orders/any.rs index a4dae269daf1..8ece2077e5fd 100644 --- a/nautilus_core/model/src/orders/any.rs +++ b/nautilus_core/model/src/orders/any.rs @@ -17,23 +17,30 @@ use nautilus_core::nanos::UnixNanos; use serde::{Deserialize, Serialize}; use super::{ - base::Order, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder, - market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder, - stop_limit::StopLimitOrder, stop_market::StopMarketOrder, - trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder, + base::{Order, OrderError}, + limit::LimitOrder, + limit_if_touched::LimitIfTouchedOrder, + market::MarketOrder, + market_if_touched::MarketIfTouchedOrder, + market_to_limit::MarketToLimitOrder, + stop_limit::StopLimitOrder, + stop_market::StopMarketOrder, + trailing_stop_limit::TrailingStopLimitOrder, + trailing_stop_market::TrailingStopMarketOrder, }; use crate::{ enums::{OrderSide, OrderSideSpecified, TriggerType}, + events::order::event::OrderEvent, identifiers::{ - client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, - venue_order_id::VenueOrderId, + trader_id::TraderId, venue_order_id::VenueOrderId, }, polymorphism::{ - GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, - GetLimitPrice, GetOrderFilledQty, GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, - GetOrderSideSpecified, GetPositionId, GetStopPrice, GetStrategyId, GetVenueOrderId, - IsClosed, IsInflight, IsOpen, + ApplyOrderEvent, GetAccountId, GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, + GetExecSpawnId, GetInstrumentId, GetLimitPrice, GetOrderFilledQty, GetOrderLeavesQty, + GetOrderQuantity, GetOrderSide, GetOrderSideSpecified, GetPositionId, GetStopPrice, + GetStrategyId, GetTraderId, GetVenueOrderId, IsClosed, IsInflight, IsOpen, }, types::{price::Price, quantity::Quantity}, }; @@ -104,6 +111,38 @@ impl PartialEq for OrderAny { } } +impl GetTraderId for OrderAny { + fn trader_id(&self) -> TraderId { + match self { + Self::Limit(order) => order.trader_id, + Self::LimitIfTouched(order) => order.trader_id, + Self::Market(order) => order.trader_id, + Self::MarketIfTouched(order) => order.trader_id, + Self::MarketToLimit(order) => order.trader_id, + Self::StopLimit(order) => order.trader_id, + Self::StopMarket(order) => order.trader_id, + Self::TrailingStopLimit(order) => order.trader_id, + Self::TrailingStopMarket(order) => order.trader_id, + } + } +} + +impl GetStrategyId for OrderAny { + fn strategy_id(&self) -> StrategyId { + match self { + Self::Limit(order) => order.strategy_id, + Self::LimitIfTouched(order) => order.strategy_id, + Self::Market(order) => order.strategy_id, + Self::MarketIfTouched(order) => order.strategy_id, + Self::MarketToLimit(order) => order.strategy_id, + Self::StopLimit(order) => order.strategy_id, + Self::StopMarket(order) => order.strategy_id, + Self::TrailingStopLimit(order) => order.strategy_id, + Self::TrailingStopMarket(order) => order.strategy_id, + } + } +} + impl GetInstrumentId for OrderAny { fn instrument_id(&self) -> InstrumentId { match self { @@ -120,6 +159,22 @@ impl GetInstrumentId for OrderAny { } } +impl GetAccountId for OrderAny { + fn account_id(&self) -> Option { + match self { + Self::Limit(order) => order.account_id, + Self::LimitIfTouched(order) => order.account_id, + Self::Market(order) => order.account_id, + Self::MarketIfTouched(order) => order.account_id, + Self::MarketToLimit(order) => order.account_id, + Self::StopLimit(order) => order.account_id, + Self::StopMarket(order) => order.account_id, + Self::TrailingStopLimit(order) => order.account_id, + Self::TrailingStopMarket(order) => order.account_id, + } + } +} + impl GetClientOrderId for OrderAny { fn client_order_id(&self) -> ClientOrderId { match self { @@ -152,22 +207,6 @@ impl GetVenueOrderId for OrderAny { } } -impl GetStrategyId for OrderAny { - fn strategy_id(&self) -> StrategyId { - match self { - Self::Limit(order) => order.strategy_id, - Self::LimitIfTouched(order) => order.strategy_id, - Self::Market(order) => order.strategy_id, - Self::MarketIfTouched(order) => order.strategy_id, - Self::MarketToLimit(order) => order.strategy_id, - Self::StopLimit(order) => order.strategy_id, - Self::StopMarket(order) => order.strategy_id, - Self::TrailingStopLimit(order) => order.strategy_id, - Self::TrailingStopMarket(order) => order.strategy_id, - } - } -} - impl GetPositionId for OrderAny { fn position_id(&self) -> Option { match self { @@ -360,6 +399,22 @@ impl IsInflight for OrderAny { } } +impl ApplyOrderEvent for OrderAny { + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + match self { + Self::Limit(order) => order.apply(event), + Self::LimitIfTouched(order) => order.apply(event), + Self::Market(order) => order.apply(event), + Self::MarketIfTouched(order) => order.apply(event), + Self::MarketToLimit(order) => order.apply(event), + Self::StopLimit(order) => order.apply(event), + Self::StopMarket(order) => order.apply(event), + Self::TrailingStopLimit(order) => order.apply(event), + Self::TrailingStopMarket(order) => order.apply(event), + } + } +} + #[derive(Clone, Debug)] pub enum PassiveOrderAny { Limit(LimitOrderAny), diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index e16f84ddf1ab..f1e69a2f1779 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -97,67 +97,67 @@ impl OrderStatus { #[rustfmt::skip] pub fn transition(&mut self, event: &OrderEvent) -> Result { let new_state = match (self, event) { - (Self::Initialized, OrderEvent::OrderDenied(_)) => Self::Denied, - (Self::Initialized, OrderEvent::OrderEmulated(_)) => Self::Emulated, // Emulated orders - (Self::Initialized, OrderEvent::OrderReleased(_)) => Self::Released, // Emulated orders - (Self::Initialized, OrderEvent::OrderSubmitted(_)) => Self::Submitted, - (Self::Initialized, OrderEvent::OrderRejected(_)) => Self::Rejected, // External orders - (Self::Initialized, OrderEvent::OrderAccepted(_)) => Self::Accepted, // External orders - (Self::Initialized, OrderEvent::OrderCanceled(_)) => Self::Canceled, // External orders - (Self::Initialized, OrderEvent::OrderExpired(_)) => Self::Expired, // External orders - (Self::Initialized, OrderEvent::OrderTriggered(_)) => Self::Triggered, // External orders - (Self::Emulated, OrderEvent::OrderCanceled(_)) => Self::Canceled, // Emulated orders - (Self::Emulated, OrderEvent::OrderExpired(_)) => Self::Expired, // Emulated orders - (Self::Emulated, OrderEvent::OrderReleased(_)) => Self::Released, // Emulated orders - (Self::Released, OrderEvent::OrderSubmitted(_)) => Self::Submitted, // Emulated orders - (Self::Released, OrderEvent::OrderDenied(_)) => Self::Denied, // Emulated orders - (Self::Released, OrderEvent::OrderCanceled(_)) => Self::Canceled, // Execution algo - (Self::Submitted, OrderEvent::OrderPendingUpdate(_)) => Self::PendingUpdate, - (Self::Submitted, OrderEvent::OrderPendingCancel(_)) => Self::PendingCancel, - (Self::Submitted, OrderEvent::OrderRejected(_)) => Self::Rejected, - (Self::Submitted, OrderEvent::OrderCanceled(_)) => Self::Canceled, // FOK and IOC cases - (Self::Submitted, OrderEvent::OrderAccepted(_)) => Self::Accepted, - (Self::Submitted, OrderEvent::OrderPartiallyFilled(_)) => Self::PartiallyFilled, - (Self::Submitted, OrderEvent::OrderFilled(_)) => Self::Filled, - (Self::Accepted, OrderEvent::OrderRejected(_)) => Self::Rejected, // StopLimit order - (Self::Accepted, OrderEvent::OrderPendingUpdate(_)) => Self::PendingUpdate, - (Self::Accepted, OrderEvent::OrderPendingCancel(_)) => Self::PendingCancel, - (Self::Accepted, OrderEvent::OrderCanceled(_)) => Self::Canceled, - (Self::Accepted, OrderEvent::OrderTriggered(_)) => Self::Triggered, - (Self::Accepted, OrderEvent::OrderExpired(_)) => Self::Expired, - (Self::Accepted, OrderEvent::OrderPartiallyFilled(_)) => Self::PartiallyFilled, - (Self::Accepted, OrderEvent::OrderFilled(_)) => Self::Filled, - (Self::Canceled, OrderEvent::OrderPartiallyFilled(_)) => Self::PartiallyFilled, // Real world possibility - (Self::Canceled, OrderEvent::OrderFilled(_)) => Self::Filled, // Real world possibility - (Self::PendingUpdate, OrderEvent::OrderRejected(_)) => Self::Rejected, - (Self::PendingUpdate, OrderEvent::OrderAccepted(_)) => Self::Accepted, - (Self::PendingUpdate, OrderEvent::OrderCanceled(_)) => Self::Canceled, - (Self::PendingUpdate, OrderEvent::OrderExpired(_)) => Self::Expired, - (Self::PendingUpdate, OrderEvent::OrderTriggered(_)) => Self::Triggered, - (Self::PendingUpdate, OrderEvent::OrderPendingUpdate(_)) => Self::PendingUpdate, // Allow multiple requests - (Self::PendingUpdate, OrderEvent::OrderPendingCancel(_)) => Self::PendingCancel, - (Self::PendingUpdate, OrderEvent::OrderPartiallyFilled(_)) => Self::PartiallyFilled, - (Self::PendingUpdate, OrderEvent::OrderFilled(_)) => Self::Filled, - (Self::PendingCancel, OrderEvent::OrderRejected(_)) => Self::Rejected, - (Self::PendingCancel, OrderEvent::OrderPendingCancel(_)) => Self::PendingCancel, // Allow multiple requests - (Self::PendingCancel, OrderEvent::OrderCanceled(_)) => Self::Canceled, - (Self::PendingCancel, OrderEvent::OrderExpired(_)) => Self::Expired, - (Self::PendingCancel, OrderEvent::OrderAccepted(_)) => Self::Accepted, // Allow failed cancel requests - (Self::PendingCancel, OrderEvent::OrderPartiallyFilled(_)) => Self::PartiallyFilled, - (Self::PendingCancel, OrderEvent::OrderFilled(_)) => Self::Filled, - (Self::Triggered, OrderEvent::OrderRejected(_)) => Self::Rejected, - (Self::Triggered, OrderEvent::OrderPendingUpdate(_)) => Self::PendingUpdate, - (Self::Triggered, OrderEvent::OrderPendingCancel(_)) => Self::PendingCancel, - (Self::Triggered, OrderEvent::OrderCanceled(_)) => Self::Canceled, - (Self::Triggered, OrderEvent::OrderExpired(_)) => Self::Expired, - (Self::Triggered, OrderEvent::OrderPartiallyFilled(_)) => Self::PartiallyFilled, - (Self::Triggered, OrderEvent::OrderFilled(_)) => Self::Filled, - (Self::PartiallyFilled, OrderEvent::OrderPendingUpdate(_)) => Self::PendingUpdate, - (Self::PartiallyFilled, OrderEvent::OrderPendingCancel(_)) => Self::PendingCancel, - (Self::PartiallyFilled, OrderEvent::OrderCanceled(_)) => Self::Canceled, - (Self::PartiallyFilled, OrderEvent::OrderExpired(_)) => Self::Expired, - (Self::PartiallyFilled, OrderEvent::OrderPartiallyFilled(_)) => Self::PartiallyFilled, - (Self::PartiallyFilled, OrderEvent::OrderFilled(_)) => Self::Filled, + (Self::Initialized, OrderEvent::Denied(_)) => Self::Denied, + (Self::Initialized, OrderEvent::Emulated(_)) => Self::Emulated, // Emulated orders + (Self::Initialized, OrderEvent::Released(_)) => Self::Released, // Emulated orders + (Self::Initialized, OrderEvent::Submitted(_)) => Self::Submitted, + (Self::Initialized, OrderEvent::Rejected(_)) => Self::Rejected, // External orders + (Self::Initialized, OrderEvent::Accepted(_)) => Self::Accepted, // External orders + (Self::Initialized, OrderEvent::Canceled(_)) => Self::Canceled, // External orders + (Self::Initialized, OrderEvent::Expired(_)) => Self::Expired, // External orders + (Self::Initialized, OrderEvent::Triggered(_)) => Self::Triggered, // External orders + (Self::Emulated, OrderEvent::Canceled(_)) => Self::Canceled, // Emulated orders + (Self::Emulated, OrderEvent::Expired(_)) => Self::Expired, // Emulated orders + (Self::Emulated, OrderEvent::Released(_)) => Self::Released, // Emulated orders + (Self::Released, OrderEvent::Submitted(_)) => Self::Submitted, // Emulated orders + (Self::Released, OrderEvent::Denied(_)) => Self::Denied, // Emulated orders + (Self::Released, OrderEvent::Canceled(_)) => Self::Canceled, // Execution algo + (Self::Submitted, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, + (Self::Submitted, OrderEvent::PendingCancel(_)) => Self::PendingCancel, + (Self::Submitted, OrderEvent::Rejected(_)) => Self::Rejected, + (Self::Submitted, OrderEvent::Canceled(_)) => Self::Canceled, // FOK and IOC cases + (Self::Submitted, OrderEvent::Accepted(_)) => Self::Accepted, + (Self::Submitted, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::Submitted, OrderEvent::Filled(_)) => Self::Filled, + (Self::Accepted, OrderEvent::Rejected(_)) => Self::Rejected, // StopLimit order + (Self::Accepted, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, + (Self::Accepted, OrderEvent::PendingCancel(_)) => Self::PendingCancel, + (Self::Accepted, OrderEvent::Canceled(_)) => Self::Canceled, + (Self::Accepted, OrderEvent::Triggered(_)) => Self::Triggered, + (Self::Accepted, OrderEvent::Expired(_)) => Self::Expired, + (Self::Accepted, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::Accepted, OrderEvent::Filled(_)) => Self::Filled, + (Self::Canceled, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, // Real world possibility + (Self::Canceled, OrderEvent::Filled(_)) => Self::Filled, // Real world possibility + (Self::PendingUpdate, OrderEvent::Rejected(_)) => Self::Rejected, + (Self::PendingUpdate, OrderEvent::Accepted(_)) => Self::Accepted, + (Self::PendingUpdate, OrderEvent::Canceled(_)) => Self::Canceled, + (Self::PendingUpdate, OrderEvent::Expired(_)) => Self::Expired, + (Self::PendingUpdate, OrderEvent::Triggered(_)) => Self::Triggered, + (Self::PendingUpdate, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, // Allow multiple requests + (Self::PendingUpdate, OrderEvent::PendingCancel(_)) => Self::PendingCancel, + (Self::PendingUpdate, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::PendingUpdate, OrderEvent::Filled(_)) => Self::Filled, + (Self::PendingCancel, OrderEvent::Rejected(_)) => Self::Rejected, + (Self::PendingCancel, OrderEvent::PendingCancel(_)) => Self::PendingCancel, // Allow multiple requests + (Self::PendingCancel, OrderEvent::Canceled(_)) => Self::Canceled, + (Self::PendingCancel, OrderEvent::Expired(_)) => Self::Expired, + (Self::PendingCancel, OrderEvent::Accepted(_)) => Self::Accepted, // Allow failed cancel requests + (Self::PendingCancel, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::PendingCancel, OrderEvent::Filled(_)) => Self::Filled, + (Self::Triggered, OrderEvent::Rejected(_)) => Self::Rejected, + (Self::Triggered, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, + (Self::Triggered, OrderEvent::PendingCancel(_)) => Self::PendingCancel, + (Self::Triggered, OrderEvent::Canceled(_)) => Self::Canceled, + (Self::Triggered, OrderEvent::Expired(_)) => Self::Expired, + (Self::Triggered, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::Triggered, OrderEvent::Filled(_)) => Self::Filled, + (Self::PartiallyFilled, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, + (Self::PartiallyFilled, OrderEvent::PendingCancel(_)) => Self::PendingCancel, + (Self::PartiallyFilled, OrderEvent::Canceled(_)) => Self::Canceled, + (Self::PartiallyFilled, OrderEvent::Expired(_)) => Self::Expired, + (Self::PartiallyFilled, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::PartiallyFilled, OrderEvent::Filled(_)) => Self::Filled, _ => return Err(OrderError::InvalidStateTransition), }; Ok(new_state) @@ -417,7 +417,7 @@ pub struct OrderCore { impl OrderCore { pub fn new(init: OrderInitialized) -> anyhow::Result { - let events: Vec = vec![OrderEvent::OrderInitialized(init.clone())]; + let events: Vec = vec![OrderEvent::Initialized(init.clone())]; Ok(Self { events, commissions: HashMap::new(), @@ -470,23 +470,23 @@ impl OrderCore { self.status = new_status; match &event { - OrderEvent::OrderInitialized(_) => return Err(OrderError::AlreadyInitialized), - OrderEvent::OrderDenied(event) => self.denied(event), - OrderEvent::OrderEmulated(event) => self.emulated(event), - OrderEvent::OrderReleased(event) => self.released(event), - OrderEvent::OrderSubmitted(event) => self.submitted(event), - OrderEvent::OrderRejected(event) => self.rejected(event), - OrderEvent::OrderAccepted(event) => self.accepted(event), - OrderEvent::OrderPendingUpdate(event) => self.pending_update(event), - OrderEvent::OrderPendingCancel(event) => self.pending_cancel(event), - OrderEvent::OrderModifyRejected(event) => self.modify_rejected(event), - OrderEvent::OrderCancelRejected(event) => self.cancel_rejected(event), - OrderEvent::OrderUpdated(event) => self.updated(event), - OrderEvent::OrderTriggered(event) => self.triggered(event), - OrderEvent::OrderCanceled(event) => self.canceled(event), - OrderEvent::OrderExpired(event) => self.expired(event), - OrderEvent::OrderPartiallyFilled(event) => self.filled(event), - OrderEvent::OrderFilled(event) => self.filled(event), + OrderEvent::Initialized(_) => return Err(OrderError::AlreadyInitialized), + OrderEvent::Denied(event) => self.denied(event), + OrderEvent::Emulated(event) => self.emulated(event), + OrderEvent::Released(event) => self.released(event), + OrderEvent::Submitted(event) => self.submitted(event), + OrderEvent::Rejected(event) => self.rejected(event), + OrderEvent::Accepted(event) => self.accepted(event), + OrderEvent::PendingUpdate(event) => self.pending_update(event), + OrderEvent::PendingCancel(event) => self.pending_cancel(event), + OrderEvent::ModifyRejected(event) => self.modify_rejected(event), + OrderEvent::CancelRejected(event) => self.cancel_rejected(event), + OrderEvent::Updated(event) => self.updated(event), + OrderEvent::Triggered(event) => self.triggered(event), + OrderEvent::Canceled(event) => self.canceled(event), + OrderEvent::Expired(event) => self.expired(event), + OrderEvent::PartiallyFilled(event) => self.filled(event), + OrderEvent::Filled(event) => self.filled(event), } self.ts_last = event.ts_event(); @@ -747,7 +747,7 @@ mod tests { fn test_order_state_transition_denied() { let mut order: MarketOrder = OrderInitializedBuilder::default().build().unwrap().into(); let denied = OrderDeniedBuilder::default().build().unwrap(); - let event = OrderEvent::OrderDenied(denied); + let event = OrderEvent::Denied(denied); order.apply(event.clone()).unwrap(); @@ -766,9 +766,9 @@ mod tests { let filled = OrderFilledBuilder::default().build().unwrap(); let mut order: MarketOrder = init.clone().into(); - order.apply(OrderEvent::OrderSubmitted(submitted)).unwrap(); - order.apply(OrderEvent::OrderAccepted(accepted)).unwrap(); - order.apply(OrderEvent::OrderFilled(filled)).unwrap(); + order.apply(OrderEvent::Submitted(submitted)).unwrap(); + order.apply(OrderEvent::Accepted(accepted)).unwrap(); + order.apply(OrderEvent::Filled(filled)).unwrap(); assert_eq!(order.client_order_id, init.client_order_id); assert_eq!(order.status(), OrderStatus::Filled); diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index c8515b999479..cf4c12c649e4 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -362,10 +362,10 @@ impl Order for LimitOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index 17052c92d3e0..6037a9319401 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -349,10 +349,10 @@ impl Order for LimitIfTouchedOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 19bf96ce2c36..a2574497524b 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -339,7 +339,7 @@ impl Order for MarketOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index 3f4a19c311eb..eafcd203e970 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -343,10 +343,10 @@ impl Order for MarketIfTouchedOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index ba920c4a12ad..3e6f3cf731d1 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -335,10 +335,10 @@ impl Order for MarketToLimitOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index f9920037b0d0..aef658a06b4d 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -356,10 +356,10 @@ impl Order for StopLimitOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index de3caed2b61c..9fc46b0fc044 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -344,10 +344,10 @@ impl Order for StopMarketOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index f522477d2e2d..f96e4d2cd559 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -358,10 +358,10 @@ impl Order for TrailingStopLimitOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index f3a28d7b8bd9..ef27a183fe97 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -350,10 +350,10 @@ impl Order for TrailingStopMarketOrder { } fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::OrderUpdated(ref event) = event { + if let OrderEvent::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + let is_order_filled = matches!(event, OrderEvent::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/polymorphism.rs b/nautilus_core/model/src/polymorphism.rs index 420d8bd81af0..2c4201f24d6b 100644 --- a/nautilus_core/model/src/polymorphism.rs +++ b/nautilus_core/model/src/polymorphism.rs @@ -19,11 +19,13 @@ use nautilus_core::nanos::UnixNanos; use crate::{ enums::{OrderSide, OrderSideSpecified, TriggerType}, + events::order::event::OrderEvent, identifiers::{ - client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, - venue_order_id::VenueOrderId, + trader_id::TraderId, venue_order_id::VenueOrderId, }, + orders::base::OrderError, types::{price::Price, quantity::Quantity}, }; @@ -31,6 +33,14 @@ pub trait GetTsInit { fn ts_init(&self) -> UnixNanos; } +pub trait GetTraderId { + fn trader_id(&self) -> TraderId; +} + +pub trait GetStrategyId { + fn strategy_id(&self) -> StrategyId; +} + pub trait GetInstrumentId { fn instrument_id(&self) -> InstrumentId; } @@ -39,12 +49,12 @@ pub trait GetClientOrderId { fn client_order_id(&self) -> ClientOrderId; } -pub trait GetVenueOrderId { - fn venue_order_id(&self) -> Option; +pub trait GetAccountId { + fn account_id(&self) -> Option; } -pub trait GetStrategyId { - fn strategy_id(&self) -> StrategyId; +pub trait GetVenueOrderId { + fn venue_order_id(&self) -> Option; } pub trait GetPositionId { @@ -102,3 +112,7 @@ pub trait IsClosed { pub trait IsInflight { fn is_inflight(&self) -> bool; } + +pub trait ApplyOrderEvent { + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError>; +} diff --git a/nautilus_core/model/src/python/events/order/mod.rs b/nautilus_core/model/src/python/events/order/mod.rs index f4e71018d39f..410cbc6b409c 100644 --- a/nautilus_core/model/src/python/events/order/mod.rs +++ b/nautilus_core/model/src/python/events/order/mod.rs @@ -27,23 +27,23 @@ use crate::events::order::{ pub fn convert_order_event_to_pyobject(py: Python, order_event: OrderEvent) -> PyResult { match order_event { - OrderEvent::OrderInitialized(event) => Ok(event.into_py(py)), - OrderEvent::OrderDenied(event) => Ok(event.into_py(py)), - OrderEvent::OrderEmulated(event) => Ok(event.into_py(py)), - OrderEvent::OrderReleased(event) => Ok(event.into_py(py)), - OrderEvent::OrderSubmitted(event) => Ok(event.into_py(py)), - OrderEvent::OrderAccepted(event) => Ok(event.into_py(py)), - OrderEvent::OrderRejected(event) => Ok(event.into_py(py)), - OrderEvent::OrderCanceled(event) => Ok(event.into_py(py)), - OrderEvent::OrderExpired(event) => Ok(event.into_py(py)), - OrderEvent::OrderTriggered(event) => Ok(event.into_py(py)), - OrderEvent::OrderPendingUpdate(event) => Ok(event.into_py(py)), - OrderEvent::OrderPendingCancel(event) => Ok(event.into_py(py)), - OrderEvent::OrderModifyRejected(event) => Ok(event.into_py(py)), - OrderEvent::OrderCancelRejected(event) => Ok(event.into_py(py)), - OrderEvent::OrderUpdated(event) => Ok(event.into_py(py)), - OrderEvent::OrderPartiallyFilled(event) => Ok(event.into_py(py)), - OrderEvent::OrderFilled(event) => Ok(event.into_py(py)), + OrderEvent::Initialized(event) => Ok(event.into_py(py)), + OrderEvent::Denied(event) => Ok(event.into_py(py)), + OrderEvent::Emulated(event) => Ok(event.into_py(py)), + OrderEvent::Released(event) => Ok(event.into_py(py)), + OrderEvent::Submitted(event) => Ok(event.into_py(py)), + OrderEvent::Accepted(event) => Ok(event.into_py(py)), + OrderEvent::Rejected(event) => Ok(event.into_py(py)), + OrderEvent::Canceled(event) => Ok(event.into_py(py)), + OrderEvent::Expired(event) => Ok(event.into_py(py)), + OrderEvent::Triggered(event) => Ok(event.into_py(py)), + OrderEvent::PendingUpdate(event) => Ok(event.into_py(py)), + OrderEvent::PendingCancel(event) => Ok(event.into_py(py)), + OrderEvent::ModifyRejected(event) => Ok(event.into_py(py)), + OrderEvent::CancelRejected(event) => Ok(event.into_py(py)), + OrderEvent::Updated(event) => Ok(event.into_py(py)), + OrderEvent::PartiallyFilled(event) => Ok(event.into_py(py)), + OrderEvent::Filled(event) => Ok(event.into_py(py)), } } @@ -53,52 +53,52 @@ pub fn convert_pyobject_to_order_event(py: Python, order_event: PyObject) -> PyR .extract::(py)?; if order_event_type == "OrderAccepted" { let order_accepted = order_event.extract::(py)?; - Ok(OrderEvent::OrderAccepted(order_accepted)) + Ok(OrderEvent::Accepted(order_accepted)) } else if order_event_type == "OrderCanceled" { let order_canceled = order_event.extract::(py)?; - Ok(OrderEvent::OrderCanceled(order_canceled)) + Ok(OrderEvent::Canceled(order_canceled)) } else if order_event_type == "OrderCancelRejected" { let order_cancel_rejected = order_event.extract::(py)?; - Ok(OrderEvent::OrderCancelRejected(order_cancel_rejected)) + Ok(OrderEvent::CancelRejected(order_cancel_rejected)) } else if order_event_type == "OrderDenied" { let order_denied = order_event.extract::(py)?; - Ok(OrderEvent::OrderDenied(order_denied)) + Ok(OrderEvent::Denied(order_denied)) } else if order_event_type == "OrderEmulated" { let order_emulated = order_event.extract::(py)?; - Ok(OrderEvent::OrderEmulated(order_emulated)) + Ok(OrderEvent::Emulated(order_emulated)) } else if order_event_type == "OrderExpired" { let order_expired = order_event.extract::(py)?; - Ok(OrderEvent::OrderExpired(order_expired)) + Ok(OrderEvent::Expired(order_expired)) } else if order_event_type == "OrderFilled" { let order_filled = order_event.extract::(py)?; - Ok(OrderEvent::OrderFilled(order_filled)) + Ok(OrderEvent::Filled(order_filled)) } else if order_event_type == "OrderInitialized" { let order_initialized = order_event.extract::(py)?; - Ok(OrderEvent::OrderInitialized(order_initialized)) + Ok(OrderEvent::Initialized(order_initialized)) } else if order_event_type == "OrderModifyRejected" { let order_modify_rejected = order_event.extract::(py)?; - Ok(OrderEvent::OrderModifyRejected(order_modify_rejected)) + Ok(OrderEvent::ModifyRejected(order_modify_rejected)) } else if order_event_type == "OrderPendingCancel" { let order_pending_cancel = order_event.extract::(py)?; - Ok(OrderEvent::OrderPendingCancel(order_pending_cancel)) + Ok(OrderEvent::PendingCancel(order_pending_cancel)) } else if order_event_type == "OrderPendingUpdate" { let order_pending_update = order_event.extract::(py)?; - Ok(OrderEvent::OrderPendingUpdate(order_pending_update)) + Ok(OrderEvent::PendingUpdate(order_pending_update)) } else if order_event_type == "OrderRejected" { let order_rejected = order_event.extract::(py)?; - Ok(OrderEvent::OrderRejected(order_rejected)) + Ok(OrderEvent::Rejected(order_rejected)) } else if order_event_type == "OrderReleased" { let order_released = order_event.extract::(py)?; - Ok(OrderEvent::OrderReleased(order_released)) + Ok(OrderEvent::Released(order_released)) } else if order_event_type == "OrderSubmitted" { let order_submitted = order_event.extract::(py)?; - Ok(OrderEvent::OrderSubmitted(order_submitted)) + Ok(OrderEvent::Submitted(order_submitted)) } else if order_event_type == "OrderTriggered" { let order_triggered = order_event.extract::(py)?; - Ok(OrderEvent::OrderTriggered(order_triggered)) + Ok(OrderEvent::Triggered(order_triggered)) } else if order_event_type == "OrderUpdated" { let order_updated = order_event.extract::(py)?; - Ok(OrderEvent::OrderUpdated(order_updated)) + Ok(OrderEvent::Updated(order_updated)) } else { Err(to_pyvalue_err( "Error in conversion from pyobject to order event", From 50e2b53c831e6965282667c8d7253c088ecfb071 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 10:42:14 +1000 Subject: [PATCH 111/193] Fix Order.is_open logic in Rust --- nautilus_core/model/src/orders/base.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index f1e69a2f1779..16c7149471db 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -283,15 +283,20 @@ pub trait Order { } fn is_open(&self) -> bool { - self.emulation_trigger().is_none() - && matches!( - self.status(), - OrderStatus::Accepted - | OrderStatus::Triggered - | OrderStatus::PendingCancel - | OrderStatus::PendingUpdate - | OrderStatus::PartiallyFilled - ) + if let Some(emulation_trigger) = self.emulation_trigger() { + if emulation_trigger != TriggerType::NoTrigger { + return false; + } + } + + matches!( + self.status(), + OrderStatus::Accepted + | OrderStatus::Triggered + | OrderStatus::PendingCancel + | OrderStatus::PendingUpdate + | OrderStatus::PartiallyFilled + ) } fn is_canceled(&self) -> bool { From 8e4739d8086195d48480742a45ea9d1e0e3ccc08 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 10:49:14 +1000 Subject: [PATCH 112/193] Add Cache tests in Rust --- nautilus_core/common/src/cache/core.rs | 229 ++++++++++++++++++++++--- 1 file changed, 207 insertions(+), 22 deletions(-) diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs index f9066152786b..ecb5548159a1 100644 --- a/nautilus_core/common/src/cache/core.rs +++ b/nautilus_core/common/src/cache/core.rs @@ -1106,7 +1106,7 @@ impl Cache { pub fn add_order( &mut self, order: OrderAny, - _position_id: Option, + position_id: Option, client_id: Option, replace_existing: bool, ) -> anyhow::Result<()> { @@ -1191,19 +1191,18 @@ impl Cache { .insert(client_order_id); } - // TODO: Change emulation trigger setup // Update emulation index - // match order.emulation_trigger() { - // TriggerType::NoTrigger => { - // self.index.orders_emulated.remove(&client_order_id); - // } - // _ => { - // self.index.orders_emulated.insert(client_order_id.clone()); - // } - // } + match order.emulation_trigger() { + Some(_) => { + self.index.orders_emulated.remove(&client_order_id); + } + None => { + self.index.orders_emulated.insert(client_order_id); + } + } // Index position ID if provided - if let Some(position_id) = order.position_id() { + if let Some(position_id) = position_id { self.add_position_id( &position_id, &order.instrument_id().venue, @@ -1796,8 +1795,8 @@ impl Cache { } #[must_use] - pub fn orders_for_position(&self, position_id: PositionId) -> Vec<&OrderAny> { - let client_order_ids = self.index.position_orders.get(&position_id); + pub fn orders_for_position(&self, position_id: &PositionId) -> Vec<&OrderAny> { + let client_order_ids = self.index.position_orders.get(position_id); match client_order_ids { Some(client_order_ids) => { self.get_orders_for_ids(&client_order_ids.iter().copied().collect(), None) @@ -2384,12 +2383,25 @@ impl Cache { //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { + use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::OrderSide, + events::order::{accepted::OrderAccepted, event::OrderEvent, submitted::OrderSubmitted}, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, position_id::PositionId, + venue_order_id::VenueOrderId, + }, instruments::{ any::InstrumentAny, currency_pair::CurrencyPair, stubs::*, synthetic::SyntheticInstrument, }, + orders::{any::OrderAny, stubs::TestOrderStubs}, + polymorphism::{ + ApplyOrderEvent, GetAccountId, GetClientOrderId, GetInstrumentId, GetStrategyId, + GetTraderId, IsOpen, + }, + types::{price::Price, quantity::Quantity}, }; use rstest::*; @@ -2466,7 +2478,7 @@ mod tests { #[rstest] fn test_get_general_when_empty(cache: Cache) { let result = cache.get("A").unwrap(); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2478,10 +2490,183 @@ mod tests { assert_eq!(result, Some(&value.as_slice()).copied()); } + #[rstest] + fn test_order_when_empty(cache: Cache) { + let client_order_id = ClientOrderId::default(); + let result = cache.order(&client_order_id); + assert!(result.is_none()); + } + + #[rstest] + fn test_order_when_initialized(mut cache: Cache, audusd_sim: CurrencyPair) { + let order = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + let order = OrderAny::Limit(order); + cache.add_order(order.clone(), None, None, false).unwrap(); + let result = cache.order(&order.client_order_id()).unwrap(); + + assert_eq!(result, &order); + assert_eq!(cache.orders(None, None, None, None), vec![&order]); + assert!(cache.orders_open(None, None, None, None).is_empty()); + assert!(cache.orders_closed(None, None, None, None).is_empty()); + assert!(cache.orders_emulated(None, None, None, None).is_empty()); + assert!(cache.orders_inflight(None, None, None, None).is_empty()); + assert!(cache.order_exists(&order.client_order_id())); + assert!(!cache.is_order_open(&order.client_order_id())); + assert!(!cache.is_order_closed(&order.client_order_id())); + assert!(!cache.is_order_emulated(&order.client_order_id())); + assert!(!cache.is_order_inflight(&order.client_order_id())); + assert!(!cache.is_order_pending_cancel_local(&order.client_order_id())); + assert_eq!(cache.orders_open_count(None, None, None, None), 0); + assert_eq!(cache.orders_closed_count(None, None, None, None), 0); + assert_eq!(cache.orders_emulated_count(None, None, None, None), 0); + assert_eq!(cache.orders_inflight_count(None, None, None, None), 0); + assert_eq!(cache.orders_total_count(None, None, None, None), 1); + } + + #[rstest] + fn test_order_when_submitted(mut cache: Cache, audusd_sim: CurrencyPair) { + let order = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + let mut order = OrderAny::Limit(order); + cache.add_order(order.clone(), None, None, false).unwrap(); + + let submitted = OrderSubmitted::new( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + AccountId::default(), + UUID4::new(), + UnixNanos::default(), + UnixNanos::default(), + ) + .unwrap(); // TODO: Should event generation be fallible? + order.apply(OrderEvent::Submitted(submitted)).unwrap(); + cache.update_order(&order).unwrap(); + + let result = cache.order(&order.client_order_id()).unwrap(); + + assert_eq!(result, &order); + assert_eq!(cache.orders(None, None, None, None), vec![&order]); + assert!(cache.orders_open(None, None, None, None).is_empty()); + assert!(cache.orders_closed(None, None, None, None).is_empty()); + assert!(cache.orders_emulated(None, None, None, None).is_empty()); + assert!(cache.orders_inflight(None, None, None, None).is_empty()); + assert!(cache.order_exists(&order.client_order_id())); + assert!(!cache.is_order_open(&order.client_order_id())); + assert!(!cache.is_order_closed(&order.client_order_id())); + assert!(!cache.is_order_emulated(&order.client_order_id())); + assert!(!cache.is_order_inflight(&order.client_order_id())); + assert!(!cache.is_order_pending_cancel_local(&order.client_order_id())); + assert_eq!(cache.orders_open_count(None, None, None, None), 0); + assert_eq!(cache.orders_closed_count(None, None, None, None), 0); + assert_eq!(cache.orders_emulated_count(None, None, None, None), 0); + assert_eq!(cache.orders_inflight_count(None, None, None, None), 0); + assert_eq!(cache.orders_total_count(None, None, None, None), 1); + } + + #[rstest] + fn test_order_when_accepted_open(mut cache: Cache, audusd_sim: CurrencyPair) { + let order = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + let mut order = OrderAny::Limit(order); + cache.add_order(order.clone(), None, None, false).unwrap(); + + let submitted = OrderSubmitted::new( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + AccountId::default(), + UUID4::new(), + UnixNanos::default(), + UnixNanos::default(), + ) + .unwrap(); // TODO: Should event generation be fallible? + order.apply(OrderEvent::Submitted(submitted)).unwrap(); + cache.update_order(&order).unwrap(); + + let accepted = OrderAccepted::new( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + VenueOrderId::default(), + order.account_id().unwrap(), + UUID4::new(), + UnixNanos::default(), + UnixNanos::default(), + false, + ) + .unwrap(); + order.apply(OrderEvent::Accepted(accepted)).unwrap(); + cache.update_order(&order).unwrap(); + + let result = cache.order(&order.client_order_id()).unwrap(); + + assert!(order.is_open()); + assert_eq!(result, &order); + assert_eq!(cache.orders(None, None, None, None), vec![&order]); + assert_eq!(cache.orders_open(None, None, None, None), vec![&order]); + assert!(cache.orders_closed(None, None, None, None).is_empty()); + assert!(cache.orders_emulated(None, None, None, None).is_empty()); + assert!(cache.orders_inflight(None, None, None, None).is_empty()); + assert!(cache.order_exists(&order.client_order_id())); + assert!(cache.is_order_open(&order.client_order_id())); + assert!(!cache.is_order_closed(&order.client_order_id())); + assert!(!cache.is_order_emulated(&order.client_order_id())); + assert!(!cache.is_order_inflight(&order.client_order_id())); + assert!(!cache.is_order_pending_cancel_local(&order.client_order_id())); + assert_eq!(cache.orders_open_count(None, None, None, None), 1); + assert_eq!(cache.orders_closed_count(None, None, None, None), 0); + assert_eq!(cache.orders_emulated_count(None, None, None, None), 0); + assert_eq!(cache.orders_inflight_count(None, None, None, None), 0); + assert_eq!(cache.orders_total_count(None, None, None, None), 1); + } + + #[rstest] + fn test_orders_for_position(mut cache: Cache, audusd_sim: CurrencyPair) { + let order = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + let order = OrderAny::Limit(order); + let position_id = PositionId::default(); + cache + .add_order(order.clone(), Some(position_id), None, false) + .unwrap(); + let result = cache.order(&order.client_order_id()).unwrap(); + assert_eq!(result, &order); + assert_eq!(cache.orders_for_position(&position_id), vec![&order]); + } + #[rstest] fn test_instrument_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.instrument(&audusd_sim.id); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2501,7 +2686,7 @@ mod tests { fn test_synthetic_when_empty(cache: Cache) { let synth = SyntheticInstrument::default(); let result = cache.synthetic(&synth.id); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2516,7 +2701,7 @@ mod tests { #[rstest] fn test_quote_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.quote_tick(&audusd_sim.id); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2530,7 +2715,7 @@ mod tests { #[rstest] fn test_quote_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.quote_ticks(&audusd_sim.id); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2548,7 +2733,7 @@ mod tests { #[rstest] fn test_trade_tick_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.trade_tick(&audusd_sim.id); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2562,7 +2747,7 @@ mod tests { #[rstest] fn test_trade_ticks_when_empty(cache: Cache, audusd_sim: CurrencyPair) { let result = cache.trade_ticks(&audusd_sim.id); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2581,7 +2766,7 @@ mod tests { fn test_bar_when_empty(cache: Cache) { let bar = Bar::default(); let result = cache.bar(&bar.bar_type); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] @@ -2596,7 +2781,7 @@ mod tests { fn test_bars_when_empty(cache: Cache) { let bar = Bar::default(); let result = cache.bars(&bar.bar_type); - assert_eq!(result, None); + assert!(result.is_none()); } #[rstest] From 3e65eb3e0b8cf82353030ae21a1468556c672ef1 Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Sat, 4 May 2024 03:58:02 +0300 Subject: [PATCH 113/193] Add Bar.from_raw_arrays_to_list (#1623) --- nautilus_trader/model/data.pxd | 14 ++++++++ nautilus_trader/model/data.pyx | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/nautilus_trader/model/data.pxd b/nautilus_trader/model/data.pxd index f2651e8912f2..611c0b8103f1 100644 --- a/nautilus_trader/model/data.pxd +++ b/nautilus_trader/model/data.pxd @@ -167,6 +167,20 @@ cdef class Bar(Data): uint64_t ts_init, ) + @staticmethod + cdef list[Bar] from_raw_arrays_to_list_c( + BarType bar_type, + uint8_t price_prec, + uint8_t size_prec, + int64_t[:] opens, + int64_t[:] highs, + int64_t[:] lows, + int64_t[:] closes, + uint64_t[:] volumes, + uint64_t[:] ts_events, + uint64_t[:] ts_inits, + ) + @staticmethod cdef Bar from_mem_c(Bar_t mem) diff --git a/nautilus_trader/model/data.pyx b/nautilus_trader/model/data.pyx index 1c405f158cdb..eb1cb7eef751 100644 --- a/nautilus_trader/model/data.pyx +++ b/nautilus_trader/model/data.pyx @@ -1048,6 +1048,72 @@ cdef class Bar(Data): ) return bar + @staticmethod + cdef list[Bar] from_raw_arrays_to_list_c( + BarType bar_type, + uint8_t price_prec, + uint8_t size_prec, + int64_t[:] opens, + int64_t[:] highs, + int64_t[:] lows, + int64_t[:] closes, + uint64_t[:] volumes, + uint64_t[:] ts_events, + uint64_t[:] ts_inits, + ): + Condition.true(len(opens) == len(highs) == len(lows) == len(lows) == len(closes) == len(volumes) + == len(ts_events) == len(ts_inits), "Array lengths must be equal") + + cdef int count = ts_events.shape[0] + cdef list[Bar] bars = [] + + cdef: + int i + Bar bar + for i in range(count): + bar = Bar.__new__(Bar) + bar._mem = bar_new_from_raw( + bar_type._mem, + opens[i], + highs[i], + lows[i], + closes[i], + price_prec, + volumes[i], + size_prec, + ts_events[i], + ts_inits[i], + ) + bars.append(bar) + + return bars + + @staticmethod + def from_raw_arrays_to_list( + BarType bar_type, + uint8_t price_prec, + uint8_t size_prec, + int64_t[:] opens, + int64_t[:] highs, + int64_t[:] lows, + int64_t[:] closes, + uint64_t[:] volumes, + uint64_t[:] ts_events, + uint64_t[:] ts_inits, + ) -> list[Bar]: + return Bar.from_raw_arrays_to_list_c( + bar_type, + price_prec, + size_prec, + opens, + highs, + lows, + closes, + volumes, + ts_events, + ts_inits, + ) + @staticmethod cdef Bar from_dict_c(dict values): Condition.not_none(values, "values") From fa1a3db2d21795db12070c81260c72109a2a90d6 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 11:03:03 +1000 Subject: [PATCH 114/193] Minor formatting and update release notes --- RELEASES.md | 1 + nautilus_trader/model/data.pyx | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index c9dafc661694..e95fae77b874 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,6 +8,7 @@ Released on TBD (UTC). - Added `OrderMatchingEngine` futures and options contract activation and expiration simulation - Added Sandbox example with Interactive Brokers (#1618), thanks @rsmb7z - Added `ParquetDataCatalog` S3 support (#1620), thanks benjaminsingleton +- Added `Bar.from_raw_arrays_to_list` (#1623), thanks rsmb7z ### Breaking Changes - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) diff --git a/nautilus_trader/model/data.pyx b/nautilus_trader/model/data.pyx index eb1cb7eef751..03a8b044d2aa 100644 --- a/nautilus_trader/model/data.pyx +++ b/nautilus_trader/model/data.pyx @@ -1061,8 +1061,11 @@ cdef class Bar(Data): uint64_t[:] ts_events, uint64_t[:] ts_inits, ): - Condition.true(len(opens) == len(highs) == len(lows) == len(lows) == len(closes) == len(volumes) - == len(ts_events) == len(ts_inits), "Array lengths must be equal") + Condition.true( + len(opens) == len(highs) == len(lows) == len(lows) == + len(closes) == len(volumes) == len(ts_events) == len(ts_inits), + "Array lengths must be equal", + ) cdef int count = ts_events.shape[0] cdef list[Bar] bars = [] From 0890280e54d85907266ff7465a01367a7c59204f Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Sat, 4 May 2024 09:08:32 +0300 Subject: [PATCH 115/193] Fix position closing on contract expiration (#1624) --- nautilus_trader/backtest/matching_engine.pyx | 2 +- .../unit_tests/backtest/test_exchange_glbx.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 8114289cd8ed..48a84eb27e46 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -1322,7 +1322,7 @@ cdef class OrderMatchingEngine: self.cancel_order(order) # Close all open positions - for position in self.cache.positions(None, self.instrument.id): + for position in self.cache.positions_open(None, self.instrument.id): order = MarketOrder( trader_id=position.trader_id, strategy_id=position.strategy_id, diff --git a/tests/unit_tests/backtest/test_exchange_glbx.py b/tests/unit_tests/backtest/test_exchange_glbx.py index 9241e898c97a..b9c0a4551cef 100644 --- a/tests/unit_tests/backtest/test_exchange_glbx.py +++ b/tests/unit_tests/backtest/test_exchange_glbx.py @@ -287,3 +287,38 @@ def test_process_exchange_past_instrument_expiration_closed_open_position(self) assert order.status == OrderStatus.FILLED position = self.cache.positions()[0] assert position.is_closed + + def test_process_exchange_after_expiration_not_raise_exception_when_no_open_position( + self, + ) -> None: + # Arrange: Prepare market + tick = TestDataStubs.quote_tick( + instrument=_ESH4_GLBX, + bid_price=4010.00, + ask_price=4011.00, + ts_init=_ESH4_GLBX.expiration_ns, + ) + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + + order = self.strategy.order_factory.market( + _ESH4_GLBX.id, + OrderSide.BUY, + Quantity.from_int(10), + ) + self.strategy.submit_order(order) + self.exchange.process(_ESH4_GLBX.expiration_ns) + self.strategy.close_all_positions(instrument_id=_ESH4_GLBX.id) # <- Close position for test + self.exchange.process(_ESH4_GLBX.expiration_ns) + + # Assert test prerequisite + assert self.cache.positions_open_count() == 0 + assert self.cache.positions_total_count() == 1 + + # Act + one_nano_past_expiration = _ESH4_GLBX.expiration_ns + 1 + self.exchange.process(one_nano_past_expiration) + self.exchange.get_matching_engine(_ESH4_GLBX.id).iterate(_ESH4_GLBX.expiration_ns) + + # Assert + assert self.clock.timestamp_ns() == _ESH4_GLBX.expiration_ns == 1_710_513_000_000_000_000 From e7b41a24a2a1a06bd2ccc3d00bc36ef43eeecb0f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 16:29:10 +1000 Subject: [PATCH 116/193] Remove panic setting from workspace test profile --- nautilus_core/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index b8d22e429c68..f83ab88f77cd 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -78,7 +78,6 @@ debug = true debug-assertions = true # Fails Cython build if true (OK for cargo test) overflow-checks = true lto = false -panic = "unwind" incremental = true codegen-units = 256 From a8a78acd0827c8e52febc5186069fe7c4d863c30 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 4 May 2024 16:55:32 +1000 Subject: [PATCH 117/193] Refine UUID4 --- nautilus_core/core/src/python/uuid.rs | 6 +++--- nautilus_core/core/src/uuid.rs | 20 ++++++++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/nautilus_core/core/src/python/uuid.rs b/nautilus_core/core/src/python/uuid.rs index 4aff83a0c1fc..2189ed9ee540 100644 --- a/nautilus_core/core/src/python/uuid.rs +++ b/nautilus_core/core/src/python/uuid.rs @@ -26,7 +26,7 @@ use pyo3::{ }; use super::to_pyvalue_err; -use crate::uuid::UUID4; +use crate::uuid::{UUID4, UUID4_LEN}; #[pymethods] impl UUID4 { @@ -42,7 +42,7 @@ impl UUID4 { let bytes: &PyBytes = state.extract(py)?; let slice = bytes.as_bytes(); - if slice.len() != 37 { + if slice.len() != UUID4_LEN { return Err(to_pyvalue_err( "Invalid state for deserialzing, incorrect bytes length", )); @@ -86,7 +86,7 @@ impl UUID4 { } fn __repr__(&self) -> String { - format!("{}('{}')", stringify!(UUID4), self) + format!("{:?}", self) } #[getter] diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 5e90fdbf0ddf..6167a4ad63fb 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -27,12 +27,12 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; /// The maximum length of ASCII characters for a `UUID4` string value (includes null terminator). -const UUID4_LEN: usize = 37; +pub(crate) const UUID4_LEN: usize = 37; /// Represents a pseudo-random UUID (universally unique identifier) /// version 4 based on a 128-bit label as specified in RFC 4122. #[repr(C)] -#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, Hash, PartialEq, Eq)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core") @@ -87,6 +87,12 @@ impl Default for UUID4 { } } +impl Debug for UUID4 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}('{}')", stringify!(UUID4), self) + } +} + impl Display for UUID4 { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_cstr().to_string_lossy()) @@ -158,11 +164,17 @@ mod tests { assert_ne!(uuid1, uuid2); } + #[rstest] + fn test_uuid4_debug() { + let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; + let uuid = UUID4::from(uuid_string); + assert_eq!(format!("{:?}", uuid), format!("UUID4('{uuid_string}')")); + } + #[rstest] fn test_uuid4_display() { let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let uuid = UUID4::from(uuid_string); - let result_string = format!("{uuid}"); - assert_eq!(result_string, uuid_string); + assert_eq!(format!("{uuid}"), uuid_string); } } From 67db125475c0a7846fa4d4b01862e077420fb22c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 09:02:26 +1000 Subject: [PATCH 118/193] Improve Currency display and debug --- nautilus_core/model/src/types/currency.rs | 41 ++++++++++++++++++++++- nautilus_core/model/src/types/money.rs | 2 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index 28d0576e3870..93f20354904d 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -14,6 +14,7 @@ // ------------------------------------------------------------------------------------------------- use std::{ + fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, str::FromStr, }; @@ -26,7 +27,7 @@ use super::fixed::check_fixed_precision; use crate::{currencies::CURRENCY_MAP, enums::CurrencyType}; #[repr(C)] -#[derive(Clone, Copy, Debug, Eq)] +#[derive(Clone, Copy, Eq)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -103,6 +104,27 @@ impl Hash for Currency { } } +impl Debug for Currency { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}(code=\"{}\", precision={}, iso4217={}, name=\"{}\", currency_type={})", + stringify!(Currency), + self.code, + self.precision, + self.iso4217, + self.name, + self.currency_type, + ) + } +} + +impl Display for Currency { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.code) + } +} + impl FromStr for Currency { type Err = anyhow::Error; @@ -151,6 +173,23 @@ mod tests { use crate::{enums::CurrencyType, types::currency::Currency}; + #[rstest] + fn test_uuid4_debug() { + let currency = Currency::AUD(); + assert_eq!( + format!("{:?}", currency), + format!( + r#"Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)"# + ) + ); + } + + #[rstest] + fn test_uuid4_display() { + let currency = Currency::AUD(); + assert_eq!(format!("{currency}"), "AUD"); + } + #[rstest] #[should_panic(expected = "code")] fn test_invalid_currency_code() { diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 1b669f339fe1..098f9b4c5eeb 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -259,7 +259,7 @@ impl Display for Money { "{:.*} {}", self.currency.precision as usize, self.as_f64(), - self.currency.code + self.currency ) } } From 16f5393cb6c572e6a3245e6189975a5228e5256d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 10:02:10 +1000 Subject: [PATCH 119/193] Remove redundant allow_cash_positions config --- RELEASES.md | 1 + nautilus_core/execution/src/engine.rs | 1 - nautilus_trader/execution/config.py | 3 - nautilus_trader/execution/engine.pxd | 2 - nautilus_trader/execution/engine.pyx | 5 -- nautilus_trader/test_kit/stubs/config.py | 5 +- tests/acceptance_tests/test_backtest.py | 76 ++------------------ tests/unit_tests/backtest/test_config.py | 4 +- tests/unit_tests/model/test_currency.py | 4 +- tests/unit_tests/model/test_currency_pyo3.py | 4 +- 10 files changed, 14 insertions(+), 91 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index e95fae77b874..431119889185 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,6 +12,7 @@ Released on TBD (UTC). ### Breaking Changes - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) +- Removed `allow_cash_positions` config (simplify to the most common use case, spot trading should track positions) ### Fixes - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) diff --git a/nautilus_core/execution/src/engine.rs b/nautilus_core/execution/src/engine.rs index b918a982ef63..59e911752e1a 100644 --- a/nautilus_core/execution/src/engine.rs +++ b/nautilus_core/execution/src/engine.rs @@ -46,7 +46,6 @@ use crate::{ pub struct ExecutionEngineConfig { pub debug: bool, - pub allow_cash_positions: bool, } pub struct ExecutionEngine { diff --git a/nautilus_trader/execution/config.py b/nautilus_trader/execution/config.py index 6ae72ce381fe..a5c1aa5bd8e3 100644 --- a/nautilus_trader/execution/config.py +++ b/nautilus_trader/execution/config.py @@ -35,15 +35,12 @@ class ExecEngineConfig(NautilusConfig, frozen=True): ---------- load_cache : bool, default True If the cache should be loaded on initialization. - allow_cash_positions : bool, default True - If unleveraged spot/cash assets should generate positions. debug : bool, default False If debug mode is active (will provide extra debug logging). """ load_cache: bool = True - allow_cash_positions: bool = True debug: bool = False diff --git a/nautilus_trader/execution/engine.pxd b/nautilus_trader/execution/engine.pxd index cb0d9e377e2b..2c03de8429c3 100644 --- a/nautilus_trader/execution/engine.pxd +++ b/nautilus_trader/execution/engine.pxd @@ -53,8 +53,6 @@ cdef class ExecutionEngine(Component): cdef readonly bint debug """If debug mode is active (will provide extra debug logging).\n\n:returns: `bool`""" - cdef readonly bint allow_cash_positions - """If unleveraged spot/cash assets should generate positions.\n\n:returns: `bool`""" cdef readonly int command_count """The total count of commands received by the engine.\n\n:returns: `int`""" cdef readonly int event_count diff --git a/nautilus_trader/execution/engine.pyx b/nautilus_trader/execution/engine.pyx index 5605c01a8cd7..9ba45201d4ef 100644 --- a/nautilus_trader/execution/engine.pyx +++ b/nautilus_trader/execution/engine.pyx @@ -145,7 +145,6 @@ cdef class ExecutionEngine(Component): # Settings self.debug: bool = config.debug - self.allow_cash_positions: bool = config.allow_cash_positions # Counters self.command_count: int = 0 @@ -1040,10 +1039,6 @@ cdef class ExecutionEngine(Component): ) return - if not self.allow_cash_positions and isinstance(instrument, CurrencyPair): - if account.is_unleveraged(instrument.id): - return # No spot cash positions - cdef Position position = self._cache.position(fill.position_id) if position is None or position.is_closed_c(): position = self._open_position(instrument, position, fill, oms_type) diff --git a/nautilus_trader/test_kit/stubs/config.py b/nautilus_trader/test_kit/stubs/config.py index bbd37f230fc1..65f23adbb748 100644 --- a/nautilus_trader/test_kit/stubs/config.py +++ b/nautilus_trader/test_kit/stubs/config.py @@ -69,7 +69,7 @@ def order_book_imbalance( @staticmethod def exec_engine_config() -> ExecEngineConfig: - return ExecEngineConfig(allow_cash_positions=True, debug=True) + return ExecEngineConfig(debug=True) @staticmethod def risk_engine_config() -> RiskEngineConfig: @@ -99,7 +99,6 @@ def backtest_engine_config( log_level="INFO", bypass_logging: bool = True, bypass_risk: bool = False, - allow_cash_position: bool = True, persist: bool = False, strategies: list[ImportableStrategyConfig] | None = None, ) -> BacktestEngineConfig: @@ -107,7 +106,7 @@ def backtest_engine_config( assert catalog is not None, "If `persist=True`, must pass `catalog`" return BacktestEngineConfig( logging=LoggingConfig(log_level=log_level, bypass_logging=bypass_logging), - exec_engine=ExecEngineConfig(allow_cash_positions=allow_cash_position), + exec_engine=ExecEngineConfig(), risk_engine=RiskEngineConfig(bypass=bypass_risk), streaming=TestConfigStubs.streaming_config(catalog=catalog) if persist else None, strategies=strategies or [], diff --git a/tests/acceptance_tests/test_backtest.py b/tests/acceptance_tests/test_backtest.py index 7d226bea0760..d9323eaef9c1 100644 --- a/tests/acceptance_tests/test_backtest.py +++ b/tests/acceptance_tests/test_backtest.py @@ -383,79 +383,13 @@ def test_run_ema_cross_with_minute_bar_spec(self): assert ending_balance == Money(1_088_115.65, USD) -class TestBacktestAcceptanceTestsBTCUSDTSpotNoCashPositions: - def setup(self): - # Fixture Setup - config = BacktestEngineConfig( - run_analysis=False, - logging=LoggingConfig(bypass_logging=True), - exec_engine=ExecEngineConfig(allow_cash_positions=False), # <-- Normally True - risk_engine=RiskEngineConfig(bypass=True), - ) - self.engine = BacktestEngine( - config=config, - ) - self.venue = Venue("BINANCE") - - self.engine.add_venue( - venue=self.venue, - oms_type=OmsType.NETTING, - account_type=AccountType.CASH, # <-- Spot exchange - starting_balances=[Money(10, BTC), Money(10_000_000, USDT)], - base_currency=None, - ) - - self.btcusdt = TestInstrumentProvider.btcusdt_binance() - self.engine.add_instrument(self.btcusdt) - - def teardown(self): - self.engine.dispose() - - def test_run_ema_cross_with_minute_trade_bars(self): - # Arrange - wrangler = BarDataWrangler( - bar_type=BarType.from_str("BTCUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL"), - instrument=self.btcusdt, - ) - - provider = TestDataProvider() - - # Build externally aggregated bars - bars = wrangler.process( - data=provider.read_csv_bars("btc-perp-20211231-20220201_1m.csv")[:10_000], - ) - - self.engine.add_data(bars) - - config = EMACrossConfig( - instrument_id=self.btcusdt.id, - bar_type=BarType.from_str("BTCUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL"), - trade_size=Decimal(0.001), - fast_ema_period=10, - slow_ema_period=20, - ) - strategy = EMACross(config=config) - self.engine.add_strategy(strategy) - - # Act - self.engine.run() - - # Assert - assert strategy.fast_ema.count == 10_000 - assert self.engine.iteration == 10_000 - btc_ending_balance = self.engine.portfolio.account(self.venue).balance_total(BTC) - usdt_ending_balance = self.engine.portfolio.account(self.venue).balance_total(USDT) - assert btc_ending_balance == Money(9.57200000, BTC) - assert usdt_ending_balance == Money(10_017_571.74970600, USDT) - - class TestBacktestAcceptanceTestsBTCUSDTEmaCrossTWAP: def setup(self): # Fixture Setup config = BacktestEngineConfig( run_analysis=False, logging=LoggingConfig(bypass_logging=True), - exec_engine=ExecEngineConfig(allow_cash_positions=False), # <-- Normally True + exec_engine=ExecEngineConfig(), risk_engine=RiskEngineConfig(bypass=True), ) self.engine = BacktestEngine( @@ -516,8 +450,8 @@ def test_run_ema_cross_with_minute_trade_bars(self): assert self.engine.iteration == 10_000 btc_ending_balance = self.engine.portfolio.account(self.venue).balance_total(BTC) usdt_ending_balance = self.engine.portfolio.account(self.venue).balance_total(USDT) - assert btc_ending_balance == Money(5.71250000, BTC) - assert usdt_ending_balance == Money(10_176_033.01433484, USDT) + assert btc_ending_balance == Money(10.00000000, BTC) + assert usdt_ending_balance == Money(9_999_138.27266000, USDT) def test_run_ema_cross_with_trade_ticks_from_bar_data(self): # Arrange @@ -552,8 +486,8 @@ def test_run_ema_cross_with_trade_ticks_from_bar_data(self): assert self.engine.iteration == 40_000 btc_ending_balance = self.engine.portfolio.account(self.venue).balance_total(BTC) usdt_ending_balance = self.engine.portfolio.account(self.venue).balance_total(USDT) - assert btc_ending_balance == Money(9.57200000, BTC) - assert usdt_ending_balance == Money(10_017_571.74970600, USDT) + assert btc_ending_balance == Money(10.00000000, BTC) + assert usdt_ending_balance == Money(9_999_913.82726600, USDT) class TestBacktestAcceptanceTestsAUDUSD: diff --git a/tests/unit_tests/backtest/test_config.py b/tests/unit_tests/backtest/test_config.py index 9995112004e5..ceeb4dfdf189 100644 --- a/tests/unit_tests/backtest/test_config.py +++ b/tests/unit_tests/backtest/test_config.py @@ -285,7 +285,7 @@ def test_backtest_run_config_id(self) -> None: TestConfigStubs.backtest_engine_config, ("catalog",), {"persist": True}, - ("fa93b3a2e7e7004b9d287227928371a90de574bf9e32c43d4dd60abbd7f292f9",), + ("ec9a8febb3af4a4b3f7155b9a2c6f9e86597d97a3d62e568a01cd40565a2a7a3",), ), ( TestConfigStubs.risk_engine_config, @@ -297,7 +297,7 @@ def test_backtest_run_config_id(self) -> None: TestConfigStubs.exec_engine_config, (), {}, - ("a6ca5c188b92707f81a9ba5d45700dcbc8aebe0443c1e7b13b10a86c045c6391",), + ("33901383a61bc99b14f5f02de3735bcf8b287243de55ce330d32c3ade274d8e0",), ), ( TestConfigStubs.streaming_config, diff --git a/tests/unit_tests/model/test_currency.py b/tests/unit_tests/model/test_currency.py index c3f9fe91350d..8682cbd5c8aa 100644 --- a/tests/unit_tests/model/test_currency.py +++ b/tests/unit_tests/model/test_currency.py @@ -132,7 +132,7 @@ def test_str_repr(self): assert currency.name == "Australian dollar" assert ( repr(currency) - == 'Currency { code: u!("AUD"), precision: 2, iso4217: 36, name: u!("Australian dollar"), currency_type: Fiat }' + == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' ) def test_currency_pickle(self): @@ -153,7 +153,7 @@ def test_currency_pickle(self): assert unpickled == currency assert ( repr(unpickled) - == 'Currency { code: u!("AUD"), precision: 2, iso4217: 36, name: u!("Australian dollar"), currency_type: Fiat }' + == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' ) def test_register_adds_currency_to_internal_currency_map(self): diff --git a/tests/unit_tests/model/test_currency_pyo3.py b/tests/unit_tests/model/test_currency_pyo3.py index 953aaeacb577..a4f3fd8e64a5 100644 --- a/tests/unit_tests/model/test_currency_pyo3.py +++ b/tests/unit_tests/model/test_currency_pyo3.py @@ -129,7 +129,7 @@ def test_str_repr(self): assert currency.name == "Australian dollar" assert ( repr(currency) - == 'Currency { code: u!("AUD"), precision: 2, iso4217: 36, name: u!("Australian dollar"), currency_type: Fiat }' + == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' ) def test_currency_pickle(self): @@ -150,7 +150,7 @@ def test_currency_pickle(self): assert unpickled == currency assert ( repr(unpickled) - == 'Currency { code: u!("AUD"), precision: 2, iso4217: 36, name: u!("Australian dollar"), currency_type: Fiat }' + == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' ) def test_register_adds_currency_to_internal_currency_map(self): From 5e793cc580c246edee72e63750f19bf5858afc63 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 10:03:59 +1000 Subject: [PATCH 120/193] Standardize test naming --- nautilus_core/core/src/uuid.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 6167a4ad63fb..5e82b06bcea5 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -130,7 +130,7 @@ mod tests { use super::*; #[rstest] - fn test_uuid4_new() { + fn test_new() { let uuid = UUID4::new(); let uuid_string = uuid.to_string(); let uuid_parsed = Uuid::parse_str(&uuid_string).expect("Uuid::parse_str failed"); @@ -139,7 +139,7 @@ mod tests { } #[rstest] - fn test_uuid4_default() { + fn test_default() { let uuid: UUID4 = UUID4::default(); let uuid_string = uuid.to_string(); let uuid_parsed = Uuid::parse_str(&uuid_string).expect("Uuid::parse_str failed"); @@ -147,7 +147,7 @@ mod tests { } #[rstest] - fn test_uuid4_from_str() { + fn test_from_str() { let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let uuid = UUID4::from(uuid_string); let result_string = uuid.to_string(); @@ -165,14 +165,14 @@ mod tests { } #[rstest] - fn test_uuid4_debug() { + fn test_debug() { let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let uuid = UUID4::from(uuid_string); assert_eq!(format!("{:?}", uuid), format!("UUID4('{uuid_string}')")); } #[rstest] - fn test_uuid4_display() { + fn test_display() { let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let uuid = UUID4::from(uuid_string); assert_eq!(format!("{uuid}"), uuid_string); From c01066dc0c39b93e32629ed1067c24e7849a48de Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 10:10:28 +1000 Subject: [PATCH 121/193] Refine Currency display --- nautilus_core/model/src/types/currency.rs | 8 ++++---- tests/unit_tests/model/test_currency.py | 4 ++-- tests/unit_tests/model/test_currency_pyo3.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index 93f20354904d..257ae565204f 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -108,7 +108,7 @@ impl Debug for Currency { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{}(code=\"{}\", precision={}, iso4217={}, name=\"{}\", currency_type={})", + "{}(code='{}', precision={}, iso4217={}, name='{}', currency_type={})", stringify!(Currency), self.code, self.precision, @@ -174,18 +174,18 @@ mod tests { use crate::{enums::CurrencyType, types::currency::Currency}; #[rstest] - fn test_uuid4_debug() { + fn test_debug() { let currency = Currency::AUD(); assert_eq!( format!("{:?}", currency), format!( - r#"Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)"# + "Currency(code='AUD', precision=2, iso4217=36, name='Australian dollar', currency_type=FIAT)" ) ); } #[rstest] - fn test_uuid4_display() { + fn test_display() { let currency = Currency::AUD(); assert_eq!(format!("{currency}"), "AUD"); } diff --git a/tests/unit_tests/model/test_currency.py b/tests/unit_tests/model/test_currency.py index 8682cbd5c8aa..d08694e3ce9a 100644 --- a/tests/unit_tests/model/test_currency.py +++ b/tests/unit_tests/model/test_currency.py @@ -132,7 +132,7 @@ def test_str_repr(self): assert currency.name == "Australian dollar" assert ( repr(currency) - == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' + == "Currency(code='AUD', precision=2, iso4217=36, name='Australian dollar', currency_type=FIAT)" ) def test_currency_pickle(self): @@ -153,7 +153,7 @@ def test_currency_pickle(self): assert unpickled == currency assert ( repr(unpickled) - == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' + == "Currency(code='AUD', precision=2, iso4217=36, name='Australian dollar', currency_type=FIAT)" ) def test_register_adds_currency_to_internal_currency_map(self): diff --git a/tests/unit_tests/model/test_currency_pyo3.py b/tests/unit_tests/model/test_currency_pyo3.py index a4f3fd8e64a5..9c93f9e39a5c 100644 --- a/tests/unit_tests/model/test_currency_pyo3.py +++ b/tests/unit_tests/model/test_currency_pyo3.py @@ -129,7 +129,7 @@ def test_str_repr(self): assert currency.name == "Australian dollar" assert ( repr(currency) - == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' + == "Currency(code='AUD', precision=2, iso4217=36, name='Australian dollar', currency_type=FIAT)" ) def test_currency_pickle(self): @@ -150,7 +150,7 @@ def test_currency_pickle(self): assert unpickled == currency assert ( repr(unpickled) - == 'Currency(code="AUD", precision=2, iso4217=36, name="Australian dollar", currency_type=FIAT)' + == "Currency(code='AUD', precision=2, iso4217=36, name='Australian dollar', currency_type=FIAT)" ) def test_register_adds_currency_to_internal_currency_map(self): From 7e2459b6682ac53635301c48f5850be9ca8f5dea Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 10:38:04 +1000 Subject: [PATCH 122/193] Standardize order events display --- .../model/src/events/order/cancel_rejected.rs | 4 +-- .../model/src/events/order/denied.rs | 4 +-- .../model/src/events/order/modify_rejected.rs | 4 +-- .../model/src/events/order/rejected.rs | 4 +-- .../model/src/python/events/order/accepted.rs | 16 +++++----- .../python/events/order/cancel_rejected.rs | 16 +++++----- .../model/src/python/events/order/canceled.rs | 16 +++++----- .../model/src/python/events/order/denied.rs | 32 +++++++++---------- .../model/src/python/events/order/emulated.rs | 14 ++++---- .../model/src/python/events/order/expired.rs | 16 +++++----- .../model/src/python/events/order/filled.rs | 28 ++++++++-------- .../python/events/order/modify_rejected.rs | 18 +++++------ .../src/python/events/order/pending_cancel.rs | 16 +++++----- .../src/python/events/order/pending_update.rs | 16 +++++----- .../model/src/python/events/order/rejected.rs | 16 +++++----- .../src/python/events/order/submitted.rs | 16 +++++----- .../src/python/events/order/triggered.rs | 22 ++++++------- .../model/src/python/events/order/updated.rs | 18 +++++------ nautilus_trader/model/events/order.pyx | 12 +++---- tests/unit_tests/model/test_events.py | 20 ++++++------ tests/unit_tests/model/test_events_pyo3.py | 16 +++++----- 21 files changed, 162 insertions(+), 162 deletions(-) diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index c5aa459c31a0..808f010621c7 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -82,7 +82,7 @@ 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={})", + "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_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), @@ -108,7 +108,7 @@ mod tests { let display = format!("{order_cancel_rejected}"); assert_eq!( display, - "OrderCancelRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, reason=ORDER_DOES_NOT_EXISTS, ts_event=0)" + "OrderCancelRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-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/denied.rs b/nautilus_core/model/src/events/order/denied.rs index 60890a7c1047..fb97f09b5267 100644 --- a/nautilus_core/model/src/events/order/denied.rs +++ b/nautilus_core/model/src/events/order/denied.rs @@ -73,7 +73,7 @@ impl Display for OrderDenied { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderDenied(instrument_id={}, client_order_id={}, reason={})", + "OrderDenied(instrument_id={}, client_order_id={}, reason='{}')", self.instrument_id, self.client_order_id, self.reason ) } @@ -92,6 +92,6 @@ mod tests { #[rstest] fn test_order_denied_display(order_denied_max_submitted_rate: OrderDenied) { let display = format!("{order_denied_max_submitted_rate}"); - assert_eq!(display, "OrderDenied(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, reason=Exceeded MAX_ORDER_SUBMIT_RATE)"); + assert_eq!(display, "OrderDenied(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, reason='Exceeded MAX_ORDER_SUBMIT_RATE')"); } } diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index f9a157e8d4a0..0308958477ad 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -82,7 +82,7 @@ impl Display for OrderModifyRejected { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderModifyRejected(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={},reason={}, ts_event={})", + "OrderModifyRejected(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_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), @@ -108,7 +108,7 @@ mod tests { assert_eq!( display, "OrderModifyRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ - venue_order_id=001, account_id=SIM-001,reason=ORDER_DOES_NOT_EXIST, ts_event=0)" + venue_order_id=001, account_id=SIM-001, reason='ORDER_DOES_NOT_EXIST', ts_event=0)" ); } } diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index 05a02699f324..d0327e476d51 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -79,7 +79,7 @@ impl Display for OrderRejected { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderRejected(instrument_id={}, client_order_id={}, reason={}, ts_event={})", + "OrderRejected(instrument_id={}, client_order_id={}, reason='{}', ts_event={})", self.instrument_id, self.client_order_id, self.reason, self.ts_event ) } @@ -99,6 +99,6 @@ mod tests { fn test_order_rejected_display(order_rejected_insufficient_margin: OrderRejected) { let display = format!("{order_rejected_insufficient_margin}"); assert_eq!(display, "OrderRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ - reason=INSUFFICIENT_MARGIN, ts_event=0)"); + reason='INSUFFICIENT_MARGIN', ts_event=0)"); } } diff --git a/nautilus_core/model/src/python/events/order/accepted.rs b/nautilus_core/model/src/python/events/order/accepted.rs index 6d92889d35ca..9e4656637355 100644 --- a/nautilus_core/model/src/python/events/order/accepted.rs +++ b/nautilus_core/model/src/python/events/order/accepted.rs @@ -66,31 +66,31 @@ impl OrderAccepted { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", stringify!(OrderAccepted), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id, self.account_id, - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderAccepted), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id, self.account_id, + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/cancel_rejected.rs b/nautilus_core/model/src/python/events/order/cancel_rejected.rs index 9b9705fa2bdc..a4850aede4f7 100644 --- a/nautilus_core/model/src/python/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/python/events/order/cancel_rejected.rs @@ -72,33 +72,33 @@ impl OrderCancelRejected { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason='{}', ts_event={})", stringify!(OrderCancelRejected), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), self.reason, - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason={}, ts_event={})", + "{}(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_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), self.reason, + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/canceled.rs b/nautilus_core/model/src/python/events/order/canceled.rs index 6e4585b6ff45..c8553f91026b 100644 --- a/nautilus_core/model/src/python/events/order/canceled.rs +++ b/nautilus_core/model/src/python/events/order/canceled.rs @@ -66,31 +66,31 @@ impl OrderCanceled { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", stringify!(OrderCanceled), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderCanceled), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/denied.rs b/nautilus_core/model/src/python/events/order/denied.rs index 7f1fd8c1c4de..afefce8e7f11 100644 --- a/nautilus_core/model/src/python/events/order/denied.rs +++ b/nautilus_core/model/src/python/events/order/denied.rs @@ -58,38 +58,38 @@ impl OrderDenied { .map_err(to_pyvalue_err) } - fn __repr__(&self) -> String { + 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 __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, reason={}, event_id={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, reason='{}')", stringify!(OrderDenied), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.reason, - self.event_id, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, reason={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, reason='{}', event_id={}, ts_init={})", stringify!(OrderDenied), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.reason, + self.event_id, + self.ts_init ) } - 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(), - } - } - #[getter] #[pyo3(name = "order_event_type")] fn py_order_event_type(&self) -> &str { diff --git a/nautilus_core/model/src/python/events/order/emulated.rs b/nautilus_core/model/src/python/events/order/emulated.rs index 96d51fea33c0..7c0e3c747b06 100644 --- a/nautilus_core/model/src/python/events/order/emulated.rs +++ b/nautilus_core/model/src/python/events/order/emulated.rs @@ -60,6 +60,13 @@ impl OrderEmulated { } } + fn __str__(&self) -> String { + format!( + "OrderEmulated(instrument_id={}, client_order_id={})", + self.instrument_id, self.client_order_id, + ) + } + fn __repr__(&self) -> String { format!( "OrderEmulated(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, event_id={}, ts_init={})", @@ -72,13 +79,6 @@ impl OrderEmulated { ) } - fn __str__(&self) -> String { - format!( - "OrderEmulated(instrument_id={}, client_order_id={})", - self.instrument_id, self.client_order_id, - ) - } - #[getter] #[pyo3(name = "order_event_type")] fn py_order_event_type(&self) -> &str { diff --git a/nautilus_core/model/src/python/events/order/expired.rs b/nautilus_core/model/src/python/events/order/expired.rs index 543d51d370d3..5c529ef9bae4 100644 --- a/nautilus_core/model/src/python/events/order/expired.rs +++ b/nautilus_core/model/src/python/events/order/expired.rs @@ -66,31 +66,31 @@ impl OrderExpired { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", stringify!(OrderExpired), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderExpired), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/filled.rs b/nautilus_core/model/src/python/events/order/filled.rs index 2403b4ff964a..2fb39dfc6251 100644 --- a/nautilus_core/model/src/python/events/order/filled.rs +++ b/nautilus_core/model/src/python/events/order/filled.rs @@ -79,7 +79,7 @@ impl OrderFilled { .map_err(to_pyvalue_err) } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { let position_id_str = match self.position_id { Some(position_id) => position_id.to_string(), None => "None".to_string(), @@ -90,8 +90,6 @@ impl OrderFilled { }; format!( "{}(\ - trader_id={}, \ - strategy_id={}, \ instrument_id={}, \ client_order_id={}, \ venue_order_id={}, \ @@ -104,12 +102,8 @@ impl OrderFilled { last_px={} {}, \ commission={}, \ liquidity_side={}, \ - event_id={}, \ - ts_event={}, \ - ts_init={})", + ts_event={})", stringify!(OrderFilled), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id, @@ -123,13 +117,11 @@ impl OrderFilled { self.currency.code, commission_str, self.liquidity_side, - self.event_id, - self.ts_event, - self.ts_init + self.ts_event ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { let position_id_str = match self.position_id { Some(position_id) => position_id.to_string(), None => "None".to_string(), @@ -140,6 +132,8 @@ impl OrderFilled { }; format!( "{}(\ + trader_id={}, \ + strategy_id={}, \ instrument_id={}, \ client_order_id={}, \ venue_order_id={}, \ @@ -152,8 +146,12 @@ impl OrderFilled { last_px={} {}, \ commission={}, \ liquidity_side={}, \ - ts_event={})", + event_id={}, \ + ts_event={}, \ + ts_init={})", stringify!(OrderFilled), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id, @@ -167,7 +165,9 @@ impl OrderFilled { self.currency.code, commission_str, self.liquidity_side, - self.ts_event + self.event_id, + self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/modify_rejected.rs b/nautilus_core/model/src/python/events/order/modify_rejected.rs index c9724cb91cb7..6d8306d6dc56 100644 --- a/nautilus_core/model/src/python/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/python/events/order/modify_rejected.rs @@ -72,34 +72,34 @@ impl OrderModifyRejected { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason='{}', ts_event={})", stringify!(OrderModifyRejected), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), self.reason, - self.event_id, self.ts_event, - self.ts_init - ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason='{}', event_id={}, ts_event={}, ts_init={})", stringify!(OrderModifyRejected), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), self.reason, + self.event_id, self.ts_event, + self.ts_init + ) } diff --git a/nautilus_core/model/src/python/events/order/pending_cancel.rs b/nautilus_core/model/src/python/events/order/pending_cancel.rs index c046bc54217b..4db015e373ec 100644 --- a/nautilus_core/model/src/python/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/python/events/order/pending_cancel.rs @@ -66,31 +66,31 @@ impl OrderPendingCancel { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", stringify!(OrderPendingCancel), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id, - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderPendingCancel), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id, + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/pending_update.rs b/nautilus_core/model/src/python/events/order/pending_update.rs index f3d04f871748..d9f6b5b778ed 100644 --- a/nautilus_core/model/src/python/events/order/pending_update.rs +++ b/nautilus_core/model/src/python/events/order/pending_update.rs @@ -66,31 +66,31 @@ impl OrderPendingUpdate { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", stringify!(OrderPendingUpdate), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id, - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderPendingUpdate), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id, + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/rejected.rs b/nautilus_core/model/src/python/events/order/rejected.rs index 75d268c82671..48e03b759a9f 100644 --- a/nautilus_core/model/src/python/events/order/rejected.rs +++ b/nautilus_core/model/src/python/events/order/rejected.rs @@ -69,31 +69,31 @@ impl OrderRejected { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, reason={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, account_id={}, reason='{}', ts_event={})", stringify!(OrderRejected), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.account_id, self.reason, - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, account_id={}, reason={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, reason='{}', event_id={}, ts_event={}, ts_init={})", stringify!(OrderRejected), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.account_id, self.reason, + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/submitted.rs b/nautilus_core/model/src/python/events/order/submitted.rs index fdac5974bb58..56d0f434ac1e 100644 --- a/nautilus_core/model/src/python/events/order/submitted.rs +++ b/nautilus_core/model/src/python/events/order/submitted.rs @@ -62,29 +62,29 @@ impl OrderSubmitted { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, account_id={}, ts_event={})", stringify!(OrderSubmitted), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.account_id, - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, account_id={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderSubmitted), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.account_id, + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/triggered.rs b/nautilus_core/model/src/python/events/order/triggered.rs index bca16cbb7683..ad9d9acd2978 100644 --- a/nautilus_core/model/src/python/events/order/triggered.rs +++ b/nautilus_core/model/src/python/events/order/triggered.rs @@ -66,32 +66,32 @@ impl OrderTriggered { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", stringify!(OrderTriggered), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")) + , self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderTriggered), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")) - , + self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_core/model/src/python/events/order/updated.rs b/nautilus_core/model/src/python/events/order/updated.rs index 6d5ac683878a..55b5129be928 100644 --- a/nautilus_core/model/src/python/events/order/updated.rs +++ b/nautilus_core/model/src/python/events/order/updated.rs @@ -73,13 +73,10 @@ impl OrderUpdated { } } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, \ - venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, event_id={}, ts_event={}, ts_init={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, ts_event={})", stringify!(OrderUpdated), - self.trader_id, - self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), @@ -87,16 +84,17 @@ impl OrderUpdated { self.quantity, self.price.map_or("None".to_string(), |price| format!("{price}")), self.trigger_price.map_or("None".to_string(), |trigger_price| format!("{trigger_price}")), - self.event_id, self.ts_event, - self.ts_init ) } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, ts_event={})", + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, \ + venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, event_id={}, ts_event={}, ts_init={})", stringify!(OrderUpdated), + self.trader_id, + self.strategy_id, self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), @@ -104,7 +102,9 @@ impl OrderUpdated { self.quantity, self.price.map_or("None".to_string(), |price| format!("{price}")), self.trigger_price.map_or("None".to_string(), |trigger_price| format!("{trigger_price}")), + self.event_id, self.ts_event, + self.ts_init ) } diff --git a/nautilus_trader/model/events/order.pyx b/nautilus_trader/model/events/order.pyx index 94511c633f20..3e5147d432b1 100644 --- a/nautilus_trader/model/events/order.pyx +++ b/nautilus_trader/model/events/order.pyx @@ -681,7 +681,7 @@ cdef class OrderDenied(OrderEvent): f"{type(self).__name__}(" f"instrument_id={self.instrument_id}, " f"client_order_id={self.client_order_id}, " - f"reason={self.reason})" + f"reason='{self.reason}')" ) def __repr__(self) -> str: @@ -691,7 +691,7 @@ cdef class OrderDenied(OrderEvent): f"strategy_id={self.strategy_id}, " f"instrument_id={self.instrument_id}, " f"client_order_id={self.client_order_id}, " - f"reason={self.reason}, " + f"reason='{self.reason}', " f"event_id={self.id}, " f"ts_init={self.ts_init})" ) @@ -3648,7 +3648,7 @@ cdef class OrderModifyRejected(OrderEvent): f"client_order_id={self.client_order_id}, " f"venue_order_id={self.venue_order_id}, " f"account_id={self.account_id}, " - f"reason={self.reason}, " + f"reason='{self.reason}', " f"ts_event={self.ts_event})" ) @@ -3661,7 +3661,7 @@ cdef class OrderModifyRejected(OrderEvent): f"client_order_id={self.client_order_id}, " f"venue_order_id={self.venue_order_id}, " f"account_id={self.account_id}, " - f"reason={self.reason}, " + f"reason='{self.reason}', " f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" @@ -3946,7 +3946,7 @@ cdef class OrderCancelRejected(OrderEvent): f"client_order_id={self.client_order_id}, " f"venue_order_id={self.venue_order_id}, " f"account_id={self.account_id}, " - f"reason={self.reason}, " + f"reason='{self.reason}', " f"ts_event={self.ts_event})" ) @@ -3959,7 +3959,7 @@ cdef class OrderCancelRejected(OrderEvent): f"client_order_id={self.client_order_id}, " f"venue_order_id={self.venue_order_id}, " f"account_id={self.account_id}, " - f"reason={self.reason}, " + f"reason='{self.reason}', " f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" diff --git a/tests/unit_tests/model/test_events.py b/tests/unit_tests/model/test_events.py index 9621b02da487..d5d1b9cc019f 100644 --- a/tests/unit_tests/model/test_events.py +++ b/tests/unit_tests/model/test_events.py @@ -200,11 +200,11 @@ def test_order_denied_event_to_from_dict_and_str_repr(self): assert OrderDenied.from_dict(OrderDenied.to_dict(event)) == event assert ( str(event) - == "OrderDenied(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, reason=Exceeded MAX_ORDER_SUBMIT_RATE)" + == "OrderDenied(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, reason='Exceeded MAX_ORDER_SUBMIT_RATE')" ) assert ( repr(event) - == f"OrderDenied(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, reason=Exceeded MAX_ORDER_SUBMIT_RATE, event_id={uuid}, ts_init=0)" # noqa + == f"OrderDenied(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, reason='Exceeded MAX_ORDER_SUBMIT_RATE', event_id={uuid}, ts_init=0)" # noqa ) def test_order_emulated_event_to_from_dict_and_str_repr(self): @@ -533,11 +533,11 @@ def test_order_modify_rejected_event_to_from_dict_and_str_repr(self): assert OrderModifyRejected.from_dict(OrderModifyRejected.to_dict(event)) == event assert ( str(event) - == "OrderModifyRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, ts_event=0)" # noqa + == "OrderModifyRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', ts_event=0)" # noqa ) assert ( repr(event) - == f"OrderModifyRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, event_id={uuid}, ts_event=0, ts_init=0)" # noqa + == f"OrderModifyRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', event_id={uuid}, ts_event=0, ts_init=0)" # noqa ) def test_order_modify_rejected_event_with_none_venue_order_id_to_from_dict_and_str_repr(self): @@ -560,11 +560,11 @@ def test_order_modify_rejected_event_with_none_venue_order_id_to_from_dict_and_s assert OrderModifyRejected.from_dict(OrderModifyRejected.to_dict(event)) == event assert ( str(event) - == "OrderModifyRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, ts_event=0)" # noqa + == "OrderModifyRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', ts_event=0)" # noqa ) assert ( repr(event) - == f"OrderModifyRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, event_id={uuid}, ts_event=0, ts_init=0)" # noqa + == f"OrderModifyRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', event_id={uuid}, ts_event=0, ts_init=0)" # noqa ) def test_order_cancel_rejected_event_to_from_dict_and_str_repr(self): @@ -587,11 +587,11 @@ def test_order_cancel_rejected_event_to_from_dict_and_str_repr(self): assert OrderCancelRejected.from_dict(OrderCancelRejected.to_dict(event)) == event assert ( str(event) - == "OrderCancelRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, ts_event=0)" # noqa + == "OrderCancelRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', ts_event=0)" # noqa ) assert ( repr(event) - == f"OrderCancelRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, event_id={uuid}, ts_event=0, ts_init=0)" # noqa + == f"OrderCancelRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', event_id={uuid}, ts_event=0, ts_init=0)" # noqa ) def test_order_cancel_rejected_with_none_venue_order_id_event_to_from_dict_and_str_repr(self): @@ -614,11 +614,11 @@ def test_order_cancel_rejected_with_none_venue_order_id_event_to_from_dict_and_s assert OrderCancelRejected.from_dict(OrderCancelRejected.to_dict(event)) == event assert ( str(event) - == "OrderCancelRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, ts_event=0)" # noqa + == "OrderCancelRejected(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', ts_event=0)" # noqa ) assert ( repr(event) - == f"OrderCancelRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason=ORDER_DOES_NOT_EXIST, event_id={uuid}, ts_event=0, ts_init=0)" # noqa + == f"OrderCancelRejected(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=None, account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', event_id={uuid}, ts_event=0, ts_init=0)" # noqa ) def test_order_updated_event_to_from_dict_and_str_repr(self): diff --git a/tests/unit_tests/model/test_events_pyo3.py b/tests/unit_tests/model/test_events_pyo3.py index 76acb2d64a0c..691eda835656 100644 --- a/tests/unit_tests/model/test_events_pyo3.py +++ b/tests/unit_tests/model/test_events_pyo3.py @@ -40,13 +40,13 @@ def test_order_denied(): assert ( str(event) == "OrderDenied(instrument_id=AUD/USD.SIM, client_order_id=O-20210410-022422-001-001-1, " - + "reason=Exceeded MAX_ORDER_SUBMIT_RATE)" + + "reason='Exceeded MAX_ORDER_SUBMIT_RATE')" ) assert ( repr(event) == "OrderDenied(trader_id=TESTER-001, strategy_id=S-001, " + "instrument_id=AUD/USD.SIM, client_order_id=O-20210410-022422-001-001-1, " - + "reason=Exceeded MAX_ORDER_SUBMIT_RATE, event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_init=0)" + + "reason='Exceeded MAX_ORDER_SUBMIT_RATE', event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_init=0)" ) @@ -105,13 +105,13 @@ def test_order_rejected(): assert ( str(event) == "OrderRejected(instrument_id=AUD/USD.SIM, client_order_id=O-20210410-022422-001-001-1, " - + "account_id=SIM-000, reason=INSUFFICIENT_MARGIN, ts_event=0)" + + "account_id=SIM-000, reason='INSUFFICIENT_MARGIN', ts_event=0)" ) assert ( repr(event) == "OrderRejected(trader_id=TESTER-001, strategy_id=S-001, " + "instrument_id=AUD/USD.SIM, client_order_id=O-20210410-022422-001-001-1, account_id=SIM-000, " - + "reason=INSUFFICIENT_MARGIN, event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" + + "reason='INSUFFICIENT_MARGIN', event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" ) @@ -243,13 +243,13 @@ def test_order_modified_rejected(): assert ( str(event) == "OrderModifyRejected(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)" + + "account_id=SIM-000, reason='ORDER_DOES_NOT_EXIST', ts_event=0)" ) assert ( repr(event) == "OrderModifyRejected(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)" + + "reason='ORDER_DOES_NOT_EXIST', event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" ) @@ -278,13 +278,13 @@ def test_order_cancel_rejected(): 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)" + + "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)" + + "reason='ORDER_DOES_NOT_EXIST', event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" ) From 2722031094f6c312b8821b1a3e99bb7cb7930f75 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 10:41:24 +1000 Subject: [PATCH 123/193] Standardize order events display --- nautilus_core/model/src/events/order/cancel_rejected.rs | 2 +- nautilus_core/model/src/events/order/stubs.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index 808f010621c7..aeacfd56a5f5 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -108,7 +108,7 @@ mod tests { let display = format!("{order_cancel_rejected}"); assert_eq!( display, - "OrderCancelRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, reason='ORDER_DOES_NOT_EXISTS', ts_event=0)" + "OrderCancelRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, reason='ORDER_DOES_NOT_EXIST', ts_event=0)" ); } } diff --git a/nautilus_core/model/src/events/order/stubs.rs b/nautilus_core/model/src/events/order/stubs.rs index 37a560b8eb57..3040be5cf626 100644 --- a/nautilus_core/model/src/events/order/stubs.rs +++ b/nautilus_core/model/src/events/order/stubs.rs @@ -403,7 +403,7 @@ pub fn order_cancel_rejected( strategy_id_ema_cross, instrument_id_btc_usdt, client_order_id, - Ustr::from("ORDER_DOES_NOT_EXISTS"), + Ustr::from("ORDER_DOES_NOT_EXIST"), uuid4, UnixNanos::default(), UnixNanos::default(), From f5315a9d5cbd01aba73f3256f6c644662945d499 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 16:33:12 +1000 Subject: [PATCH 124/193] Fix typo --- nautilus_core/model/src/stubs.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nautilus_core/model/src/stubs.rs b/nautilus_core/model/src/stubs.rs index abe380c5889f..cd9db6785ae9 100644 --- a/nautilus_core/model/src/stubs.rs +++ b/nautilus_core/model/src/stubs.rs @@ -14,7 +14,6 @@ // ------------------------------------------------------------------------------------------------- //! Type stubs to facilitate testing. - use rstest::fixture; use rust_decimal::prelude::ToPrimitive; @@ -53,7 +52,7 @@ pub fn calculate_commission( } else if liquidity_side == LiquiditySide::Taker { notional * instrument.taker_fee().to_f64().unwrap() } else { - panic!("Invalid liquid side {liquidity_side}") + panic!("Invalid liquidity side {liquidity_side}") }; if instrument.is_inverse() && !use_quote_for_inverse.unwrap_or(false) { Ok(Money::new(commission, instrument.base_currency().unwrap()).unwrap()) From a75ed554cdc1947f0d1c33af5c70bc61ef30d305 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 16:37:16 +1000 Subject: [PATCH 125/193] Add RiskEngine test --- tests/unit_tests/risk/test_engine.py | 85 ++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/unit_tests/risk/test_engine.py b/tests/unit_tests/risk/test_engine.py index 5ecfa166b70b..6213ed75c32f 100644 --- a/tests/unit_tests/risk/test_engine.py +++ b/tests/unit_tests/risk/test_engine.py @@ -32,6 +32,7 @@ from nautilus_trader.execution.messages import SubmitOrderList from nautilus_trader.execution.messages import TradingCommand from nautilus_trader.model.currencies import ADA +from nautilus_trader.model.currencies import ETH from nautilus_trader.model.currencies import GBP from nautilus_trader.model.currencies import USD from nautilus_trader.model.currencies import USDT @@ -72,6 +73,7 @@ _GBPUSD_SIM = TestInstrumentProvider.default_fx_ccy("GBP/USD") _XBTUSD_BITMEX = TestInstrumentProvider.xbtusd_bitmex() _ADAUSDT_BINANCE = TestInstrumentProvider.adausdt_binance() +_ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() class TestRiskEngineWithCashAccount: @@ -2142,6 +2144,11 @@ def setup(self): Money(0, USDT), Money(268.84000000, USDT), ), + AccountBalance( + Money(0.00000000, ETH), + Money(0, ETH), + Money(0.00000000, ETH), + ), ] account_state = AccountState( @@ -2213,3 +2220,81 @@ def test_submit_order_for_less_than_max_cum_transaction_value_adausdt( # Assert assert order.status == OrderStatus.INITIALIZED assert self.exec_engine.command_count == 1 + + def test_partial_fill_and_full_fill_account_balance_correct(self): + # Arrange + self.cache.add_instrument(_ETHUSDT_BINANCE) + quote = TestDataStubs.quote_tick( + instrument=_ETHUSDT_BINANCE, + bid_price=10_000.00, + ask_price=10_000.10, + ) + self.cache.add_quote_tick(quote) + + strategy = Strategy() + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + + order1 = strategy.order_factory.market( + _ETHUSDT_BINANCE.id, + OrderSide.BUY, + _ETHUSDT_BINANCE.make_qty(0.02), + ) + + order2 = strategy.order_factory.market( + _ETHUSDT_BINANCE.id, + OrderSide.BUY, + _ETHUSDT_BINANCE.make_qty(0.02), + ) + + submit_order1 = SubmitOrder( + trader_id=self.trader_id, + strategy_id=strategy.id, + position_id=None, + order=order1, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.risk_engine.execute(submit_order1) + self.exec_engine.process(TestEventStubs.order_submitted(order1, account_id=self.account_id)) + self.exec_engine.process(TestEventStubs.order_accepted(order1)) + self.exec_engine.process( + TestEventStubs.order_filled( + order1, + _ETHUSDT_BINANCE, + account_id=self.account_id, + last_qty=_ETHUSDT_BINANCE.make_qty(0.0005), + ), + ) + + submit_order2 = SubmitOrder( + trader_id=self.trader_id, + strategy_id=strategy.id, + position_id=PositionId("P-19700101-0000-000-None-1"), + order=order2, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.risk_engine.execute(submit_order2) + self.exec_engine.process(TestEventStubs.order_submitted(order2, account_id=self.account_id)) + self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_filled( + order2, + _ETHUSDT_BINANCE, + account_id=self.account_id, + ), + ) + + # Assert + account = self.cache.account(self.account_id) + assert account.balance(_ETHUSDT_BINANCE.base_currency).total == Money(0.00000000, ETH) + assert self.portfolio.net_position(_ETHUSDT_BINANCE.id) == Decimal("0.02050") From 257e27913862043d921459033ad6dcbd4c136370 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 16:45:41 +1000 Subject: [PATCH 126/193] Update README --- README.md | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 2115a8dfb3da..a00a5eb3da6d 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ including FX, Equities, Futures, Options, CFDs, Crypto and Betting - across mult ## Features -- **Fast** - C-level speed through Rust and Cython. Asynchronous networking with [uvloop](https://github.com/MagicStack/uvloop) -- **Reliable** - Type safety through Rust and Cython. Redis backed performant state persistence +- **Fast** - Core written in Rust with asynchronous networking using [tokio](https://crates.io/crates/tokio) +- **Reliable** - Type-safety and thread-safety through Rust. Redis backed performant state persistence - **Portable** - OS independent, runs on Linux, macOS, Windows. Deploy using Docker - **Flexible** - Modular adapters mean any REST, WebSocket, or FIX API can be integrated - **Advanced** - Time in force `IOC`, `FOK`, `GTD`, `AT_THE_OPEN`, `AT_THE_CLOSE`, advanced order types and conditional triggers. Execution instructions `post-only`, `reduce-only`, and icebergs. Contingency order lists including `OCO`, `OTO` @@ -76,7 +76,8 @@ express the granular time and event dependent complexity of real-time trading, w proven to be more suitable due to their inherently higher performance, and type safety. One of the key advantages of NautilusTrader here, is that this reimplementation step is now circumvented - as the critical core components of the platform -have all been written entirely in Rust or Cython. This means we're using the right tools for the job, where systems programming languages compile performant binaries, +have all been written entirely in [Rust](https://www.rust-lang.org/) or [Cython](https://cython.org/). +This means we're using the right tools for the job, where systems programming languages compile performant binaries, with CPython C extension modules then able to offer a Python native environment, suitable for professional quantitative traders and trading firms. ## Why Python? @@ -91,16 +92,6 @@ implementing large performance-critical systems. Cython has addressed a lot of t of a statically typed language, embedded into Pythons rich ecosystem of software libraries and developer/user communities. -## What is Cython? - -[Cython](https://cython.org) is a compiled programming language which aims to be a superset of the Python programming -language, designed to give C-like performance with code that is written in Python - with -optional C-inspired syntax. - -The project heavily utilizes Cython to provide static type safety and increased performance -for Python through [C extension modules](https://docs.python.org/3/extending/extending.html). The vast majority of the production code is actually -written in Cython, however the libraries can be accessed from both Python and Cython. - ## What is Rust? [Rust](https://www.rust-lang.org/) is a multi-paradigm programming language designed for performance and safety, especially safe @@ -111,9 +102,8 @@ Rust’s rich type system and ownership model guarantees memory-safety and threa eliminating many classes of bugs at compile-time. The project increasingly utilizes Rust for core performance-critical components. Python language binding is handled through -Cython, with static libraries linked at compile-time before the wheel binaries are packaged, so a user -does not need to have Rust installed to run NautilusTrader. In the future as more Rust code is introduced, -[PyO3](https://pyo3.rs/latest) will be leveraged for easier Python bindings. +Cython and [PyO3](https://pyo3.rs/latest), with static libraries linked at compile-time before the wheel binaries are packaged, so a user +does not need to have Rust installed to run NautilusTrader. This project makes the [Soundness Pledge](https://raphlinus.github.io/rust/2020/01/18/soundness-pledge.html): @@ -124,15 +114,6 @@ This project makes the [Soundness Pledge](https://raphlinus.github.io/rust/2020/ ![Architecture](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/_images/architecture-overview.png?raw=true "architecture") -## Quality Attributes - -- Reliability -- Performance -- Modularity -- Testability -- Maintainability -- Deployability - ## Integrations NautilusTrader is designed in a modular way to work with _adapters_ which provide @@ -238,8 +219,8 @@ A `Makefile` is provided to automate most installation and build tasks for devel - `make install-debug` -- Same as `make install` but with `debug` build mode - `make install-just-deps` -- Installs just the `main`, `dev` and `test` dependencies (does not install package) - `make install-just-deps-all` -- Same as `make install-just-deps` and additionally installs `docs` dependencies -- `make build` -- Runs the Cython build script in `release` build mode (default) -- `make build-debug` -- Runs the Cython build script in `debug` build mode +- `make build` -- Runs the build script in `release` build mode (default) +- `make build-debug` -- Runs the build script in `debug` build mode - `make build-wheel` -- Runs the Poetry build with a wheel format in `release` mode - `make build-wheel-debug` -- Runs the Poetry build with a wheel format in `debug` mode - `make clean` -- **CAUTION** Cleans all non-source artifacts from the repository From f33803ad1c2dc6459085bd6995f08a5c9471f250 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 18:22:04 +1000 Subject: [PATCH 127/193] Standardize and refine events display in Rust --- .../model/src/events/account/state.rs | 3 +- .../model/src/events/order/accepted.rs | 25 ++++- .../model/src/events/order/cancel_rejected.rs | 27 ++++- .../model/src/events/order/canceled.rs | 25 ++++- .../model/src/events/order/denied.rs | 29 ++++- .../model/src/events/order/emulated.rs | 28 ++++- nautilus_core/model/src/events/order/event.rs | 102 ++++++++--------- .../model/src/events/order/expired.rs | 26 ++++- .../model/src/events/order/filled.rs | 69 ++++++++++-- .../model/src/events/order/initialized.rs | 101 ++++++++++++++++- .../model/src/events/order/modify_rejected.rs | 28 ++++- .../model/src/events/order/pending_cancel.rs | 27 ++++- .../model/src/events/order/pending_update.rs | 27 ++++- .../model/src/events/order/rejected.rs | 35 +++++- .../model/src/events/order/released.rs | 27 ++++- .../model/src/events/order/submitted.rs | 31 ++++- .../model/src/events/order/triggered.rs | 29 ++++- .../model/src/events/order/updated.rs | 31 ++++- .../model/src/python/events/account/state.rs | 22 +--- .../model/src/python/events/order/accepted.rs | 28 +---- .../python/events/order/cancel_rejected.rs | 30 +---- .../model/src/python/events/order/canceled.rs | 28 +---- .../model/src/python/events/order/denied.rs | 24 +--- .../model/src/python/events/order/emulated.rs | 19 +--- .../model/src/python/events/order/expired.rs | 28 +---- .../model/src/python/events/order/filled.rs | 106 ++---------------- .../src/python/events/order/initialized.rs | 89 +-------------- .../python/events/order/modify_rejected.rs | 31 +---- .../src/python/events/order/pending_cancel.rs | 28 +---- .../src/python/events/order/pending_update.rs | 28 +---- .../model/src/python/events/order/rejected.rs | 28 +---- .../model/src/python/events/order/released.rs | 20 +--- .../src/python/events/order/submitted.rs | 26 +---- .../src/python/events/order/triggered.rs | 29 +---- .../model/src/python/events/order/updated.rs | 35 +----- tests/unit_tests/risk/test_engine.py | 1 + 36 files changed, 620 insertions(+), 650 deletions(-) diff --git a/nautilus_core/model/src/events/account/state.rs b/nautilus_core/model/src/events/account/state.rs index 214f656c0ac4..2366b8f0f0f9 100644 --- a/nautilus_core/model/src/events/account/state.rs +++ b/nautilus_core/model/src/events/account/state.rs @@ -76,7 +76,8 @@ impl Display for AccountState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "AccountState(account_id={}, account_type={}, base_currency={}, is_reported={}, balances=[{}], margins=[{}], event_id={})", + "{}(account_id={}, account_type={}, base_currency={}, is_reported={}, balances=[{}], margins=[{}], event_id={})", + stringify!(AccountState), self.account_id, self.account_type, self.base_currency.map_or_else(|| "None".to_string(), |base_currency | format!("{}", base_currency.code)), diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index 7738120c14f1..704bddcc9b00 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::Display; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -74,11 +74,30 @@ impl OrderAccepted { } } +impl Debug for OrderAccepted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderAccepted), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id, + self.account_id, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderAccepted { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderAccepted(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + stringify!(OrderAccepted), self.instrument_id, self.client_order_id, self.venue_order_id, diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index aeacfd56a5f5..d4bd3ad2b51a 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::Display; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -26,7 +26,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -78,11 +78,32 @@ impl OrderCancelRejected { } } +impl Debug for OrderCancelRejected { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}(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_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), + self.reason, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + 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={})", + "{}(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_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), diff --git a/nautilus_core/model/src/events/order/canceled.rs b/nautilus_core/model/src/events/order/canceled.rs index f0edd04e66e7..e938c9f91d7d 100644 --- a/nautilus_core/model/src/events/order/canceled.rs +++ b/nautilus_core/model/src/events/order/canceled.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::Display; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -74,11 +74,30 @@ impl OrderCanceled { } } +impl Debug for OrderCanceled { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderCanceled), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderCanceled { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderCanceled(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + stringify!(OrderCanceled), self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), diff --git a/nautilus_core/model/src/events/order/denied.rs b/nautilus_core/model/src/events/order/denied.rs index fb97f09b5267..eab9fb4db251 100644 --- a/nautilus_core/model/src/events/order/denied.rs +++ b/nautilus_core/model/src/events/order/denied.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -26,7 +26,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -69,12 +69,31 @@ impl OrderDenied { } } +impl Debug for OrderDenied { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, reason='{}', event_id={}, ts_init={})", + stringify!(OrderDenied), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.reason, + self.event_id, + self.ts_init + ) + } +} + impl Display for OrderDenied { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderDenied(instrument_id={}, client_order_id={}, reason='{}')", - self.instrument_id, self.client_order_id, self.reason + "{}(instrument_id={}, client_order_id={}, reason='{}')", + stringify!(OrderDenied), + self.instrument_id, + self.client_order_id, + self.reason ) } } diff --git a/nautilus_core/model/src/events/order/emulated.rs b/nautilus_core/model/src/events/order/emulated.rs index c2fa5c152dab..d31176034573 100644 --- a/nautilus_core/model/src/events/order/emulated.rs +++ b/nautilus_core/model/src/events/order/emulated.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -65,12 +65,30 @@ impl OrderEmulated { } } +impl Debug for OrderEmulated { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, event_id={}, ts_init={})", + stringify!(OrderEmulated), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.event_id, + self.ts_init, + ) + } +} + impl Display for OrderEmulated { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderEmulated(instrument_id={}, client_order_id={})", - self.instrument_id, self.client_order_id + "{}(instrument_id={}, client_order_id={})", + stringify!(OrderEmulated), + self.instrument_id, + self.client_order_id, ) } } diff --git a/nautilus_core/model/src/events/order/event.rs b/nautilus_core/model/src/events/order/event.rs index 007d423c6f57..638ea13b1e40 100644 --- a/nautilus_core/model/src/events/order/event.rs +++ b/nautilus_core/model/src/events/order/event.rs @@ -54,69 +54,69 @@ impl OrderEvent { #[must_use] pub fn client_order_id(&self) -> ClientOrderId { match self { - Self::Initialized(e) => e.client_order_id, - Self::Denied(e) => e.client_order_id, - Self::Emulated(e) => e.client_order_id, - Self::Released(e) => e.client_order_id, - Self::Submitted(e) => e.client_order_id, - Self::Accepted(e) => e.client_order_id, - Self::Rejected(e) => e.client_order_id, - Self::Canceled(e) => e.client_order_id, - Self::Expired(e) => e.client_order_id, - Self::Triggered(e) => e.client_order_id, - Self::PendingUpdate(e) => e.client_order_id, - Self::PendingCancel(e) => e.client_order_id, - Self::ModifyRejected(e) => e.client_order_id, - Self::CancelRejected(e) => e.client_order_id, - Self::Updated(e) => e.client_order_id, - Self::PartiallyFilled(e) => e.client_order_id, - Self::Filled(e) => e.client_order_id, + Self::Initialized(event) => event.client_order_id, + Self::Denied(event) => event.client_order_id, + Self::Emulated(event) => event.client_order_id, + Self::Released(event) => event.client_order_id, + Self::Submitted(event) => event.client_order_id, + Self::Accepted(event) => event.client_order_id, + Self::Rejected(event) => event.client_order_id, + Self::Canceled(event) => event.client_order_id, + Self::Expired(event) => event.client_order_id, + Self::Triggered(event) => event.client_order_id, + Self::PendingUpdate(event) => event.client_order_id, + Self::PendingCancel(event) => event.client_order_id, + Self::ModifyRejected(event) => event.client_order_id, + Self::CancelRejected(event) => event.client_order_id, + Self::Updated(event) => event.client_order_id, + Self::PartiallyFilled(event) => event.client_order_id, + Self::Filled(event) => event.client_order_id, } } #[must_use] pub fn strategy_id(&self) -> StrategyId { match self { - Self::Initialized(e) => e.strategy_id, - Self::Denied(e) => e.strategy_id, - Self::Emulated(e) => e.strategy_id, - Self::Released(e) => e.strategy_id, - Self::Submitted(e) => e.strategy_id, - Self::Accepted(e) => e.strategy_id, - Self::Rejected(e) => e.strategy_id, - Self::Canceled(e) => e.strategy_id, - Self::Expired(e) => e.strategy_id, - Self::Triggered(e) => e.strategy_id, - Self::PendingUpdate(e) => e.strategy_id, - Self::PendingCancel(e) => e.strategy_id, - Self::ModifyRejected(e) => e.strategy_id, - Self::CancelRejected(e) => e.strategy_id, - Self::Updated(e) => e.strategy_id, - Self::PartiallyFilled(e) => e.strategy_id, - Self::Filled(e) => e.strategy_id, + Self::Initialized(event) => event.strategy_id, + Self::Denied(event) => event.strategy_id, + Self::Emulated(event) => event.strategy_id, + Self::Released(event) => event.strategy_id, + Self::Submitted(event) => event.strategy_id, + Self::Accepted(event) => event.strategy_id, + Self::Rejected(event) => event.strategy_id, + Self::Canceled(event) => event.strategy_id, + Self::Expired(event) => event.strategy_id, + Self::Triggered(event) => event.strategy_id, + Self::PendingUpdate(event) => event.strategy_id, + Self::PendingCancel(event) => event.strategy_id, + Self::ModifyRejected(event) => event.strategy_id, + Self::CancelRejected(event) => event.strategy_id, + Self::Updated(event) => event.strategy_id, + Self::PartiallyFilled(event) => event.strategy_id, + Self::Filled(event) => event.strategy_id, } } #[must_use] pub fn ts_event(&self) -> UnixNanos { match self { - Self::Initialized(e) => e.ts_event, - Self::Denied(e) => e.ts_event, - Self::Emulated(e) => e.ts_event, - Self::Released(e) => e.ts_event, - Self::Submitted(e) => e.ts_event, - Self::Accepted(e) => e.ts_event, - Self::Rejected(e) => e.ts_event, - Self::Canceled(e) => e.ts_event, - Self::Expired(e) => e.ts_event, - Self::Triggered(e) => e.ts_event, - Self::PendingUpdate(e) => e.ts_event, - Self::PendingCancel(e) => e.ts_event, - Self::ModifyRejected(e) => e.ts_event, - Self::CancelRejected(e) => e.ts_event, - Self::Updated(e) => e.ts_event, - Self::PartiallyFilled(e) => e.ts_event, - Self::Filled(e) => e.ts_event, + Self::Initialized(event) => event.ts_event, + Self::Denied(event) => event.ts_event, + Self::Emulated(event) => event.ts_event, + Self::Released(event) => event.ts_event, + Self::Submitted(event) => event.ts_event, + Self::Accepted(event) => event.ts_event, + Self::Rejected(event) => event.ts_event, + Self::Canceled(event) => event.ts_event, + Self::Expired(event) => event.ts_event, + Self::Triggered(event) => event.ts_event, + Self::PendingUpdate(event) => event.ts_event, + Self::PendingCancel(event) => event.ts_event, + Self::ModifyRejected(event) => event.ts_event, + Self::CancelRejected(event) => event.ts_event, + Self::Updated(event) => event.ts_event, + Self::PartiallyFilled(event) => event.ts_event, + Self::Filled(event) => event.ts_event, } } } diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index f95c26688ee0..f6b01b562de7 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::Display; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -74,11 +74,31 @@ impl OrderExpired { } } +impl Debug for OrderExpired { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderExpired), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderExpired { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderExpired(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + stringify!(OrderExpired), self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), diff --git a/nautilus_core/model/src/events/order/filled.rs b/nautilus_core/model/src/events/order/filled.rs index 1fa3d234773f..5a5789466179 100644 --- a/nautilus_core/model/src/events/order/filled.rs +++ b/nautilus_core/model/src/events/order/filled.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::Display; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -30,7 +30,7 @@ use crate::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -142,11 +142,64 @@ impl Default for OrderFilled { } } +impl Debug for OrderFilled { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let position_id_str = match self.position_id { + Some(position_id) => position_id.to_string(), + None => "None".to_string(), + }; + let commission_str = match self.commission { + Some(commission) => commission.to_string(), + None => "None".to_string(), + }; + write!( + f, + "{}(\ + trader_id={}, \ + strategy_id={}, \ + instrument_id={}, \ + client_order_id={}, \ + venue_order_id={}, \ + account_id={}, \ + trade_id={}, \ + position_id={}, \ + order_side={}, \ + order_type={}, \ + last_qty={}, \ + last_px={} {}, \ + commission={}, \ + liquidity_side={}, \ + event_id={}, \ + ts_event={}, \ + ts_init={})", + stringify!(OrderFilled), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id, + self.account_id, + self.trade_id, + position_id_str, + self.order_side, + self.order_type, + self.last_qty, + self.last_px, + self.currency, + commission_str, + self.liquidity_side, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderFilled { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderFilled(\ + "{}(\ instrument_id={}, \ client_order_id={}, \ venue_order_id={}, \ @@ -156,10 +209,11 @@ impl Display for OrderFilled { order_side={}, \ order_type={}, \ last_qty={}, \ - last_px={}, \ - commission={} ,\ + last_px={} {}, \ + commission={}, \ liquidity_side={}, \ ts_event={})", + stringify!(OrderFilled), self.instrument_id, self.client_order_id, self.venue_order_id, @@ -170,6 +224,7 @@ impl Display for OrderFilled { self.order_type, self.last_qty, self.last_px, + self.currency, self.commission.unwrap_or(Money::from("0.0 USD")), self.liquidity_side, self.ts_event @@ -193,8 +248,8 @@ mod tests { display, "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ venue_order_id=123456, account_id=SIM-001, trade_id=1, position_id=P-001, \ - order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22000, \ - commission=12.20000000 USDT ,liquidity_side=TAKER, ts_event=0)"); + order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22000 USDT, \ + commission=12.20000000 USDT, liquidity_side=TAKER, ts_event=0)"); } #[rstest] diff --git a/nautilus_core/model/src/events/order/initialized.rs b/nautilus_core/model/src/events/order/initialized.rs index 18ee7e39a6d3..74a9c789c9cb 100644 --- a/nautilus_core/model/src/events/order/initialized.rs +++ b/nautilus_core/model/src/events/order/initialized.rs @@ -15,7 +15,7 @@ use std::{ collections::HashMap, - fmt::{Display, Formatter}, + fmt::{Debug, Display}, }; use derive_builder::Builder; @@ -34,7 +34,7 @@ use crate::{ }; #[repr(C)] -#[derive(Clone, PartialEq, Eq, Debug, Builder, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -192,11 +192,103 @@ impl OrderInitialized { } } +impl Debug for OrderInitialized { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}(\ + trader_id={}, \ + strategy_id={}, \ + instrument_id={}, \ + client_order_id={}, \ + side={}, \ + type={}, \ + quantity={}, \ + time_in_force={}, \ + post_only={}, \ + reduce_only={}, \ + quote_quantity={}, \ + price={}, \ + emulation_trigger={}, \ + trigger_instrument_id={}, \ + contingency_type={}, \ + order_list_id={}, \ + linked_order_ids=[{}], \ + parent_order_id={}, \ + exec_algorithm_id={}, \ + exec_algorithm_params={}, \ + exec_spawn_id={}, \ + tags={}, \ + event_id={}, \ + ts_init={})", + stringify!(OrderInitialized), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.order_side, + self.order_type, + self.quantity, + self.time_in_force, + self.post_only, + self.reduce_only, + self.quote_quantity, + self.price + .map_or("None".to_string(), |price| format!("{price}")), + self.emulation_trigger + .map_or("None".to_string(), |trigger| format!("{trigger}")), + self.trigger_instrument_id + .map_or("None".to_string(), |instrument_id| format!( + "{instrument_id}" + )), + self.contingency_type + .map_or("None".to_string(), |contingency_type| format!( + "{contingency_type}" + )), + self.order_list_id + .map_or("None".to_string(), |order_list_id| format!( + "{order_list_id}" + )), + self.linked_order_ids + .as_ref() + .map_or("None".to_string(), |linked_order_ids| linked_order_ids + .iter() + .map(ToString::to_string) + .collect::>() + .join(", ")), + self.parent_order_id + .map_or("None".to_string(), |parent_order_id| format!( + "{parent_order_id}" + )), + self.exec_algorithm_id + .map_or("None".to_string(), |exec_algorithm_id| format!( + "{exec_algorithm_id}" + )), + self.exec_algorithm_params + .as_ref() + .map_or("None".to_string(), |exec_algorithm_params| format!( + "{exec_algorithm_params:?}" + )), + self.exec_spawn_id + .map_or("None".to_string(), |exec_spawn_id| format!( + "{exec_spawn_id}" + )), + self.tags.as_ref().map_or("None".to_string(), |tags| tags + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ")), + self.event_id, + self.ts_init + ) + } +} + impl Display for OrderInitialized { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderInitialized(\ + "{}(\ instrument_id={}, \ client_order_id={}, \ side={}, \ @@ -217,6 +309,7 @@ impl Display for OrderInitialized { exec_algorithm_params={}, \ exec_spawn_id={}, \ tags={})", + stringify!(OrderInitialized), self.instrument_id, self.client_order_id, self.order_side, diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index 0308958477ad..e8156ccd7862 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -26,7 +26,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -78,11 +78,31 @@ impl OrderModifyRejected { } } +impl Debug for OrderModifyRejected { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason='{}', event_id={}, ts_event={}, ts_init={})", + stringify!(OrderModifyRejected), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), + self.reason, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderModifyRejected { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderModifyRejected(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason='{}', ts_event={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason='{}', ts_event={})", + stringify!(OrderModifyRejected), self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index 773a0409c902..6f762ec87869 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -74,11 +74,30 @@ impl OrderPendingCancel { } } +impl Debug for OrderPendingCancel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderPendingCancel), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderPendingCancel { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderPendingCancel(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + stringify!(OrderPendingCancel), self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index 44fc08889039..d28fd95afcec 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -74,11 +74,30 @@ impl OrderPendingUpdate { } } +impl Debug for OrderPendingUpdate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderPendingUpdate), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderPendingUpdate { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderPendingUpdate(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", + stringify!(OrderPendingUpdate), self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index d0327e476d51..345ec07ec965 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -26,7 +26,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -75,12 +75,35 @@ impl OrderRejected { } } +impl Debug for OrderRejected { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, reason='{}', event_id={}, ts_event={}, ts_init={})", + stringify!(OrderRejected), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.account_id, + self.reason, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderRejected { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderRejected(instrument_id={}, client_order_id={}, reason='{}', ts_event={})", - self.instrument_id, self.client_order_id, self.reason, self.ts_event + "{}(instrument_id={}, client_order_id={}, account_id={}, reason='{}', ts_event={})", + stringify!(OrderRejected), + self.instrument_id, + self.client_order_id, + self.account_id, + self.reason, + self.ts_event ) } } @@ -99,6 +122,6 @@ mod tests { fn test_order_rejected_display(order_rejected_insufficient_margin: OrderRejected) { let display = format!("{order_rejected_insufficient_margin}"); assert_eq!(display, "OrderRejected(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ - reason='INSUFFICIENT_MARGIN', ts_event=0)"); + account_id=SIM-001, reason='INSUFFICIENT_MARGIN', ts_event=0)"); } } diff --git a/nautilus_core/model/src/events/order/released.rs b/nautilus_core/model/src/events/order/released.rs index e723811c929b..18e2c61ed363 100644 --- a/nautilus_core/model/src/events/order/released.rs +++ b/nautilus_core/model/src/events/order/released.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::Display; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -28,7 +28,7 @@ use crate::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -71,12 +71,31 @@ impl OrderReleased { } } +impl Debug for OrderReleased { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, released_price={}, event_id={}, ts_init={})", + stringify!(OrderReleased), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.released_price, + self.event_id, + self.ts_init + ) + } +} + impl Display for OrderReleased { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderReleased(instrument_id={}, client_order_id={}, released_price={})", - self.instrument_id, self.client_order_id, self.released_price, + "{}(instrument_id={}, client_order_id={}, released_price={})", + stringify!(OrderReleased), + self.instrument_id, + self.client_order_id, + self.released_price, ) } } diff --git a/nautilus_core/model/src/events/order/submitted.rs b/nautilus_core/model/src/events/order/submitted.rs index 8cc8a4ade5da..bbf80e317103 100644 --- a/nautilus_core/model/src/events/order/submitted.rs +++ b/nautilus_core/model/src/events/order/submitted.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -68,12 +68,33 @@ impl OrderSubmitted { } } +impl Debug for OrderSubmitted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderSubmitted), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.account_id, + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderSubmitted { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderSubmitted(instrument_id={}, client_order_id={}, account_id={}, ts_event={})", - self.instrument_id, self.client_order_id, self.account_id, self.ts_event + "{}(instrument_id={}, client_order_id={}, account_id={}, ts_event={})", + stringify!(OrderSubmitted), + self.instrument_id, + self.client_order_id, + self.account_id, + self.ts_event ) } } diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index 8e7718a59310..1299defb4c21 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::Display; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -25,7 +25,7 @@ use crate::identifiers::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -74,11 +74,29 @@ impl OrderTriggered { } } +impl Debug for OrderTriggered { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderTriggered), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderTriggered { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", stringify!(OrderTriggered), self.instrument_id, self.client_order_id, @@ -87,7 +105,8 @@ impl Display for OrderTriggered { "{venue_order_id}" )), self.account_id - .map_or("None".to_string(), |account_id| format!("{account_id}")) + .map_or("None".to_string(), |account_id| format!("{account_id}")), + self.ts_event, ) } } @@ -105,6 +124,6 @@ mod tests { fn test_order_triggered_display(order_triggered: OrderTriggered) { let display = format!("{order_triggered}"); assert_eq!(display, "OrderTriggered(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ - venue_order_id=001, account_id=SIM-001)"); + venue_order_id=001, account_id=SIM-001, ts_event=0)"); } } diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index ea8e4f188f00..6b6cfc623b91 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; @@ -28,7 +28,7 @@ use crate::{ }; #[repr(C)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] #[cfg_attr( @@ -86,11 +86,34 @@ impl OrderUpdated { } } +impl Debug for OrderUpdated { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, + "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, \ + venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, event_id={}, ts_event={}, ts_init={})", + stringify!(OrderUpdated), + self.trader_id, + self.strategy_id, + self.instrument_id, + self.client_order_id, + self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), + self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), + self.quantity, + self.price.map_or("None".to_string(), |price| format!("{price}")), + self.trigger_price.map_or("None".to_string(), |trigger_price| format!("{trigger_price}")), + self.event_id, + self.ts_event, + self.ts_init + ) + } +} + impl Display for OrderUpdated { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "OrderUpdated(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, ts_event={})", + "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, ts_event={})", + stringify!(OrderUpdated), self.instrument_id, self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), diff --git a/nautilus_core/model/src/python/events/account/state.rs b/nautilus_core/model/src/python/events/account/state.rs index dabf8282a070..05d5ec0fdfff 100644 --- a/nautilus_core/model/src/python/events/account/state.rs +++ b/nautilus_core/model/src/python/events/account/state.rs @@ -95,29 +95,11 @@ impl AccountState { } fn __repr__(&self) -> String { - format!( - "{}(account_id={},account_type={},base_currency={},balances={},margins={},is_reported={},event_id={})", - stringify!(AccountState), - self.account_id, - self.account_type, - self.base_currency.map_or_else(|| "None".to_string(), |base_currency | format!("{}", base_currency.code)), self.balances.iter().map(|b| format!("{b}")).collect::>().join(","), - self.margins.iter().map(|m| format!("{m}")).collect::>().join(","), - self.is_reported, - self.event_id, - ) + format!("{:?}", self) } fn __str__(&self) -> String { - format!( - "{}(account_id={},account_type={},base_currency={},balances={},margins={},is_reported={},event_id={})", - stringify!(AccountState), - self.account_id, - self.account_type, - self.base_currency.map_or_else(|| "None".to_string(), |base_currency | format!("{}", base_currency.code)), self.balances.iter().map(|b| format!("{b}")).collect::>().join(","), - self.margins.iter().map(|m| format!("{m}")).collect::>().join(","), - self.is_reported, - self.event_id, - ) + self.to_string() } #[staticmethod] diff --git a/nautilus_core/model/src/python/events/order/accepted.rs b/nautilus_core/model/src/python/events/order/accepted.rs index 9e4656637355..809f9a32b07a 100644 --- a/nautilus_core/model/src/python/events/order/accepted.rs +++ b/nautilus_core/model/src/python/events/order/accepted.rs @@ -66,32 +66,12 @@ impl OrderAccepted { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", - stringify!(OrderAccepted), - self.instrument_id, - self.client_order_id, - self.venue_order_id, - self.account_id, - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderAccepted), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id, - self.account_id, - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/cancel_rejected.rs b/nautilus_core/model/src/python/events/order/cancel_rejected.rs index a4850aede4f7..454253900089 100644 --- a/nautilus_core/model/src/python/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/python/events/order/cancel_rejected.rs @@ -72,34 +72,12 @@ impl OrderCancelRejected { } } - 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_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.reason, - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - 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_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.reason, - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/canceled.rs b/nautilus_core/model/src/python/events/order/canceled.rs index c8553f91026b..b244378b664d 100644 --- a/nautilus_core/model/src/python/events/order/canceled.rs +++ b/nautilus_core/model/src/python/events/order/canceled.rs @@ -66,32 +66,12 @@ impl OrderCanceled { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", - stringify!(OrderCanceled), - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderCanceled), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/denied.rs b/nautilus_core/model/src/python/events/order/denied.rs index afefce8e7f11..52c51b458bfd 100644 --- a/nautilus_core/model/src/python/events/order/denied.rs +++ b/nautilus_core/model/src/python/events/order/denied.rs @@ -66,28 +66,12 @@ impl OrderDenied { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, reason='{}')", - stringify!(OrderDenied), - self.instrument_id, - self.client_order_id, - self.reason, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, reason='{}', event_id={}, ts_init={})", - stringify!(OrderDenied), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.reason, - self.event_id, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/emulated.rs b/nautilus_core/model/src/python/events/order/emulated.rs index 7c0e3c747b06..e6af91fdc452 100644 --- a/nautilus_core/model/src/python/events/order/emulated.rs +++ b/nautilus_core/model/src/python/events/order/emulated.rs @@ -60,23 +60,12 @@ impl OrderEmulated { } } - fn __str__(&self) -> String { - format!( - "OrderEmulated(instrument_id={}, client_order_id={})", - self.instrument_id, self.client_order_id, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "OrderEmulated(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, event_id={}, ts_init={})", - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.event_id, - self.ts_init, - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/expired.rs b/nautilus_core/model/src/python/events/order/expired.rs index 5c529ef9bae4..b95040491f3d 100644 --- a/nautilus_core/model/src/python/events/order/expired.rs +++ b/nautilus_core/model/src/python/events/order/expired.rs @@ -66,32 +66,12 @@ impl OrderExpired { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", - stringify!(OrderExpired), - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderExpired), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or_else(|| "None".to_string(), |account_id| format!("{account_id}")), - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/filled.rs b/nautilus_core/model/src/python/events/order/filled.rs index 2fb39dfc6251..90321b948e91 100644 --- a/nautilus_core/model/src/python/events/order/filled.rs +++ b/nautilus_core/model/src/python/events/order/filled.rs @@ -79,96 +79,20 @@ impl OrderFilled { .map_err(to_pyvalue_err) } - fn __str__(&self) -> String { - let position_id_str = match self.position_id { - Some(position_id) => position_id.to_string(), - None => "None".to_string(), - }; - let commission_str = match self.commission { - Some(commission) => commission.to_string(), - None => "None".to_string(), - }; - format!( - "{}(\ - instrument_id={}, \ - client_order_id={}, \ - venue_order_id={}, \ - account_id={}, \ - trade_id={}, \ - position_id={}, \ - order_side={}, \ - order_type={}, \ - last_qty={}, \ - last_px={} {}, \ - commission={}, \ - liquidity_side={}, \ - ts_event={})", - stringify!(OrderFilled), - self.instrument_id, - self.client_order_id, - self.venue_order_id, - self.account_id, - self.trade_id, - position_id_str, - self.order_side, - self.order_type, - self.last_qty, - self.last_px, - self.currency.code, - commission_str, - self.liquidity_side, - self.ts_event - ) + 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 { - let position_id_str = match self.position_id { - Some(position_id) => position_id.to_string(), - None => "None".to_string(), - }; - let commission_str = match self.commission { - Some(commission) => commission.to_string(), - None => "None".to_string(), - }; - format!( - "{}(\ - trader_id={}, \ - strategy_id={}, \ - instrument_id={}, \ - client_order_id={}, \ - venue_order_id={}, \ - account_id={}, \ - trade_id={}, \ - position_id={}, \ - order_side={}, \ - order_type={}, \ - last_qty={}, \ - last_px={} {}, \ - commission={}, \ - liquidity_side={}, \ - event_id={}, \ - ts_event={}, \ - ts_init={})", - stringify!(OrderFilled), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id, - self.account_id, - self.trade_id, - position_id_str, - self.order_side, - self.order_type, - self.last_qty, - self.last_px, - self.currency.code, - commission_str, - self.liquidity_side, - self.event_id, - self.ts_event, - self.ts_init - ) + format!("{:?}", self) + } + + fn __str__(&self) -> String { + self.to_string() } #[getter] @@ -189,14 +113,6 @@ impl OrderFilled { self.is_sell() } - 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(), - } - } - #[getter] #[pyo3(name = "trader_id")] fn py_trader_id(&self) -> TraderId { diff --git a/nautilus_core/model/src/python/events/order/initialized.rs b/nautilus_core/model/src/python/events/order/initialized.rs index 9f6285b40c05..aa85b6189564 100644 --- a/nautilus_core/model/src/python/events/order/initialized.rs +++ b/nautilus_core/model/src/python/events/order/initialized.rs @@ -115,6 +115,7 @@ impl OrderInitialized { ) .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), @@ -124,95 +125,11 @@ impl OrderInitialized { } fn __repr__(&self) -> String { - format!( - "OrderInitialized(\ - trader_id={}, \ - strategy_id={}, \ - instrument_id={}, \ - client_order_id={}, \ - side={}, \ - type={}, \ - quantity={}, \ - time_in_force={}, \ - post_only={}, \ - reduce_only={}, \ - quote_quantity={}, \ - price={}, \ - emulation_trigger={}, \ - trigger_instrument_id={}, \ - contingency_type={}, \ - order_list_id={}, \ - linked_order_ids=[{}], \ - parent_order_id={}, \ - exec_algorithm_id={}, \ - exec_algorithm_params={}, \ - exec_spawn_id={}, \ - tags={}, \ - event_id={}, \ - ts_init={})", - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.order_side, - self.order_type, - self.quantity, - self.time_in_force, - self.post_only, - self.reduce_only, - self.quote_quantity, - self.price - .map_or("None".to_string(), |price| format!("{price}")), - self.emulation_trigger - .map_or("None".to_string(), |trigger| format!("{trigger}")), - self.trigger_instrument_id - .map_or("None".to_string(), |instrument_id| format!( - "{instrument_id}" - )), - self.contingency_type - .map_or("None".to_string(), |contingency_type| format!( - "{contingency_type}" - )), - self.order_list_id - .map_or("None".to_string(), |order_list_id| format!( - "{order_list_id}" - )), - self.linked_order_ids - .as_ref() - .map_or("None".to_string(), |linked_order_ids| linked_order_ids - .iter() - .map(ToString::to_string) - .collect::>() - .join(", ")), - self.parent_order_id - .map_or("None".to_string(), |parent_order_id| format!( - "{parent_order_id}" - )), - self.exec_algorithm_id - .map_or("None".to_string(), |exec_algorithm_id| format!( - "{exec_algorithm_id}" - )), - self.exec_algorithm_params - .as_ref() - .map_or("None".to_string(), |exec_algorithm_params| format!( - "{exec_algorithm_params:?}" - )), - self.exec_spawn_id - .map_or("None".to_string(), |exec_spawn_id| format!( - "{exec_spawn_id}" - )), - self.tags.as_ref().map_or("None".to_string(), |tags| tags - .iter() - .map(|x| x.to_string()) - .collect::>() - .join(", ")), - self.event_id, - self.ts_init - ) + format!("{:?}", self) } fn __str__(&self) -> String { - format!("{self}") + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/modify_rejected.rs b/nautilus_core/model/src/python/events/order/modify_rejected.rs index 6d8306d6dc56..1ca1d5bb9540 100644 --- a/nautilus_core/model/src/python/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/python/events/order/modify_rejected.rs @@ -72,35 +72,12 @@ impl OrderModifyRejected { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, reason='{}', ts_event={})", - stringify!(OrderModifyRejected), - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.reason, - self.ts_event, - ) - } - 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!(OrderModifyRejected), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.reason, - self.event_id, - self.ts_event, - self.ts_init + format!("{:?}", self) + } - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/pending_cancel.rs b/nautilus_core/model/src/python/events/order/pending_cancel.rs index 4db015e373ec..62da2f28de8f 100644 --- a/nautilus_core/model/src/python/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/python/events/order/pending_cancel.rs @@ -66,32 +66,12 @@ impl OrderPendingCancel { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", - stringify!(OrderPendingCancel), - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id, - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderPendingCancel), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id, - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/pending_update.rs b/nautilus_core/model/src/python/events/order/pending_update.rs index d9f6b5b778ed..80dd7229e585 100644 --- a/nautilus_core/model/src/python/events/order/pending_update.rs +++ b/nautilus_core/model/src/python/events/order/pending_update.rs @@ -66,32 +66,12 @@ impl OrderPendingUpdate { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", - stringify!(OrderPendingUpdate), - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id, - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderPendingUpdate), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or_else(|| "None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id, - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/rejected.rs b/nautilus_core/model/src/python/events/order/rejected.rs index 48e03b759a9f..1777c53f5e81 100644 --- a/nautilus_core/model/src/python/events/order/rejected.rs +++ b/nautilus_core/model/src/python/events/order/rejected.rs @@ -69,32 +69,12 @@ impl OrderRejected { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, account_id={}, reason='{}', ts_event={})", - stringify!(OrderRejected), - self.instrument_id, - self.client_order_id, - self.account_id, - self.reason, - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, reason='{}', event_id={}, ts_event={}, ts_init={})", - stringify!(OrderRejected), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.account_id, - self.reason, - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/released.rs b/nautilus_core/model/src/python/events/order/released.rs index 7382675237c4..d3c67a8cf223 100644 --- a/nautilus_core/model/src/python/events/order/released.rs +++ b/nautilus_core/model/src/python/events/order/released.rs @@ -64,27 +64,11 @@ impl OrderReleased { } fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, released_price={}, event_id={}, ts_init={})", - stringify!(OrderReleased), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.released_price, - self.event_id, - self.ts_init - ) + format!("{:?}", self) } fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, released_price={})", - stringify!(OrderReleased), - self.instrument_id, - self.client_order_id, - self.released_price - ) + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/submitted.rs b/nautilus_core/model/src/python/events/order/submitted.rs index 56d0f434ac1e..9aedaf9a0f76 100644 --- a/nautilus_core/model/src/python/events/order/submitted.rs +++ b/nautilus_core/model/src/python/events/order/submitted.rs @@ -62,30 +62,12 @@ impl OrderSubmitted { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, account_id={}, ts_event={})", - stringify!(OrderSubmitted), - self.instrument_id, - self.client_order_id, - self.account_id, - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderSubmitted), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.account_id, - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/triggered.rs b/nautilus_core/model/src/python/events/order/triggered.rs index ad9d9acd2978..8392475ce4ce 100644 --- a/nautilus_core/model/src/python/events/order/triggered.rs +++ b/nautilus_core/model/src/python/events/order/triggered.rs @@ -66,33 +66,12 @@ impl OrderTriggered { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, ts_event={})", - stringify!(OrderTriggered), - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")) - , - self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderTriggered), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/nautilus_core/model/src/python/events/order/updated.rs b/nautilus_core/model/src/python/events/order/updated.rs index 55b5129be928..d1ad97d1e04a 100644 --- a/nautilus_core/model/src/python/events/order/updated.rs +++ b/nautilus_core/model/src/python/events/order/updated.rs @@ -73,39 +73,12 @@ impl OrderUpdated { } } - fn __str__(&self) -> String { - format!( - "{}(instrument_id={}, client_order_id={}, venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, ts_event={})", - stringify!(OrderUpdated), - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.quantity, - self.price.map_or("None".to_string(), |price| format!("{price}")), - self.trigger_price.map_or("None".to_string(), |trigger_price| format!("{trigger_price}")), - self.ts_event, - ) + fn __repr__(&self) -> String { + format!("{:?}", self) } - fn __repr__(&self) -> String { - format!( - "{}(trader_id={}, strategy_id={}, instrument_id={}, client_order_id={}, \ - venue_order_id={}, account_id={}, quantity={}, price={}, trigger_price={}, event_id={}, ts_event={}, ts_init={})", - stringify!(OrderUpdated), - self.trader_id, - self.strategy_id, - self.instrument_id, - self.client_order_id, - self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), - self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.quantity, - self.price.map_or("None".to_string(), |price| format!("{price}")), - self.trigger_price.map_or("None".to_string(), |trigger_price| format!("{trigger_price}")), - self.event_id, - self.ts_event, - self.ts_init - ) + fn __str__(&self) -> String { + self.to_string() } #[getter] diff --git a/tests/unit_tests/risk/test_engine.py b/tests/unit_tests/risk/test_engine.py index 6213ed75c32f..ab51d4f5bda9 100644 --- a/tests/unit_tests/risk/test_engine.py +++ b/tests/unit_tests/risk/test_engine.py @@ -2221,6 +2221,7 @@ def test_submit_order_for_less_than_max_cum_transaction_value_adausdt( assert order.status == OrderStatus.INITIALIZED assert self.exec_engine.command_count == 1 + @pytest.mark.skip(reason="WIP") def test_partial_fill_and_full_fill_account_balance_correct(self): # Arrange self.cache.add_instrument(_ETHUSDT_BINANCE) From 5f719c99343bb2332fa26d06c77fcd8c3e5c698c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 5 May 2024 18:55:08 +1000 Subject: [PATCH 128/193] Standardize repr and str ordering --- nautilus_core/accounting/src/python/cash.rs | 13 -- nautilus_core/accounting/src/python/margin.rs | 13 -- .../adapters/src/databento/python/enums.rs | 16 +- .../adapters/src/databento/python/types.rs | 16 +- nautilus_core/common/src/python/enums.rs | 16 +- nautilus_core/common/src/python/timer.rs | 8 +- nautilus_core/core/src/python/uuid.rs | 8 +- nautilus_core/core/src/uuid.rs | 2 +- .../model/src/events/order/accepted.rs | 2 +- .../model/src/events/order/cancel_rejected.rs | 2 +- .../model/src/events/order/canceled.rs | 2 +- .../model/src/events/order/expired.rs | 2 +- .../model/src/events/order/modify_rejected.rs | 2 +- .../model/src/events/order/pending_cancel.rs | 2 +- .../model/src/events/order/pending_update.rs | 2 +- .../model/src/events/order/rejected.rs | 2 +- .../model/src/events/order/triggered.rs | 2 +- .../model/src/events/order/updated.rs | 2 +- nautilus_core/model/src/ffi/events/order.rs | 162 -------------- nautilus_core/model/src/python/data/bar.rs | 24 +-- nautilus_core/model/src/python/data/delta.rs | 8 +- nautilus_core/model/src/python/data/deltas.rs | 8 +- nautilus_core/model/src/python/data/depth.rs | 8 +- nautilus_core/model/src/python/data/order.rs | 8 +- nautilus_core/model/src/python/data/quote.rs | 8 +- nautilus_core/model/src/python/data/trade.rs | 8 +- nautilus_core/model/src/python/enums.rs | 200 +++++++++--------- .../src/python/identifiers/instrument_id.rs | 8 +- .../model/src/python/identifiers/trade_id.rs | 8 +- nautilus_core/model/src/python/macros.rs | 8 +- .../model/src/python/orderbook/book.rs | 6 +- .../model/src/python/orderbook/level.rs | 6 +- .../model/src/python/orders/limit.rs | 4 +- .../model/src/python/orders/market.rs | 4 +- .../model/src/python/orders/stop_limit.rs | 4 +- nautilus_core/model/src/python/position.rs | 4 +- .../model/src/python/types/currency.rs | 8 +- nautilus_core/model/src/python/types/money.rs | 8 +- nautilus_core/model/src/python/types/price.rs | 8 +- .../model/src/python/types/quantity.rs | 8 +- nautilus_trader/core/includes/model.h | 3 - nautilus_trader/core/rust/model.pxd | 3 - 42 files changed, 221 insertions(+), 415 deletions(-) diff --git a/nautilus_core/accounting/src/python/cash.rs b/nautilus_core/accounting/src/python/cash.rs index b0fef7f7d7d5..bb296d011e19 100644 --- a/nautilus_core/accounting/src/python/cash.rs +++ b/nautilus_core/accounting/src/python/cash.rs @@ -49,19 +49,6 @@ impl CashAccount { self.id } - fn __str__(&self) -> String { - format!( - "{}(id={}, type={}, base={})", - stringify!(CashAccount), - self.id, - self.account_type, - self.base_currency.map_or_else( - || "None".to_string(), - |base_currency| format!("{}", base_currency.code) - ), - ) - } - fn __repr__(&self) -> String { format!( "{}(id={}, type={}, base={})", diff --git a/nautilus_core/accounting/src/python/margin.rs b/nautilus_core/accounting/src/python/margin.rs index ee4277948deb..66dde3cd55dc 100644 --- a/nautilus_core/accounting/src/python/margin.rs +++ b/nautilus_core/accounting/src/python/margin.rs @@ -50,19 +50,6 @@ impl MarginAccount { self.default_leverage } - fn __str__(&self) -> String { - format!( - "{}(id={}, type={}, base={})", - stringify!(MarginAccount), - self.id, - self.account_type, - self.base_currency.map_or_else( - || "None".to_string(), - |base_currency| format!("{}", base_currency.code) - ) - ) - } - fn __repr__(&self) -> String { format!( "{}(id={}, type={}, base={})", diff --git a/nautilus_core/adapters/src/databento/python/enums.rs b/nautilus_core/adapters/src/databento/python/enums.rs index 40006e676489..2203a0273217 100644 --- a/nautilus_core/adapters/src/databento/python/enums.rs +++ b/nautilus_core/adapters/src/databento/python/enums.rs @@ -32,10 +32,6 @@ impl DatabentoStatisticType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -45,6 +41,10 @@ impl DatabentoStatisticType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -160,10 +160,6 @@ impl DatabentoStatisticUpdateAction { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -173,6 +169,10 @@ impl DatabentoStatisticUpdateAction { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { diff --git a/nautilus_core/adapters/src/databento/python/types.rs b/nautilus_core/adapters/src/databento/python/types.rs index 5cf3968c04ff..575533d400f5 100644 --- a/nautilus_core/adapters/src/databento/python/types.rs +++ b/nautilus_core/adapters/src/databento/python/types.rs @@ -47,10 +47,6 @@ impl DatabentoImbalance { hasher.finish() as isize } - fn __str__(&self) -> String { - self.__repr__() - } - fn __repr__(&self) -> String { format!( "{}(instrument_id={}, ref_price={}, cont_book_clr_price={}, auct_interest_clr_price={}, paired_qty={}, total_imbalance_qty={}, side={}, significant_imbalance={}, ts_event={}, ts_recv={}, ts_init={})", @@ -69,6 +65,10 @@ impl DatabentoImbalance { ) } + fn __str__(&self) -> String { + self.__repr__() + } + #[getter] #[pyo3(name = "instrument_id")] fn py_instrument_id(&self) -> InstrumentId { @@ -166,10 +166,6 @@ impl DatabentoStatistics { hasher.finish() as isize } - fn __str__(&self) -> String { - self.__repr__() - } - fn __repr__(&self) -> String { format!( "{}(instrument_id={}, stat_type={}, update_action={}, price={}, quantity={}, channel_id={}, stat_flags={}, sequence={}, ts_ref={}, ts_in_delta={}, ts_event={}, ts_recv={}, ts_init={})", @@ -190,6 +186,10 @@ impl DatabentoStatistics { ) } + fn __str__(&self) -> String { + self.__repr__() + } + #[getter] #[pyo3(name = "instrument_id")] fn py_instrument_id(&self) -> InstrumentId { diff --git a/nautilus_core/common/src/python/enums.rs b/nautilus_core/common/src/python/enums.rs index 61acc4630099..98e2c4469fba 100644 --- a/nautilus_core/common/src/python/enums.rs +++ b/nautilus_core/common/src/python/enums.rs @@ -33,10 +33,6 @@ impl LogLevel { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -46,6 +42,10 @@ impl LogLevel { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -114,10 +114,6 @@ impl LogColor { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -127,6 +123,10 @@ impl LogColor { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { diff --git a/nautilus_core/common/src/python/timer.rs b/nautilus_core/common/src/python/timer.rs index e295a714066d..5448540d44d8 100644 --- a/nautilus_core/common/src/python/timer.rs +++ b/nautilus_core/common/src/python/timer.rs @@ -79,14 +79,14 @@ impl TimeEvent { } } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{}('{}')", stringify!(TimeEvent), self) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "name")] fn py_name(&self) -> String { diff --git a/nautilus_core/core/src/python/uuid.rs b/nautilus_core/core/src/python/uuid.rs index 2189ed9ee540..6c5969630550 100644 --- a/nautilus_core/core/src/python/uuid.rs +++ b/nautilus_core/core/src/python/uuid.rs @@ -81,14 +81,14 @@ impl UUID4 { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{:?}", self) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "value")] fn py_value(&self) -> String { diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 5e82b06bcea5..e1bd3c5c526b 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -168,7 +168,7 @@ mod tests { fn test_debug() { let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let uuid = UUID4::from(uuid_string); - assert_eq!(format!("{:?}", uuid), format!("UUID4('{uuid_string}')")); + assert_eq!(format!("{uuid:?}"), format!("UUID4('{uuid_string}')")); } #[rstest] diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index 704bddcc9b00..1e456374a242 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -42,7 +42,7 @@ pub struct OrderAccepted { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed } impl OrderAccepted { diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index d4bd3ad2b51a..8d74c63e00a8 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -42,7 +42,7 @@ pub struct OrderCancelRejected { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, } diff --git a/nautilus_core/model/src/events/order/canceled.rs b/nautilus_core/model/src/events/order/canceled.rs index e938c9f91d7d..cd9944b23034 100644 --- a/nautilus_core/model/src/events/order/canceled.rs +++ b/nautilus_core/model/src/events/order/canceled.rs @@ -40,7 +40,7 @@ pub struct OrderCanceled { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, } diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index f6b01b562de7..0d91192717f8 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -40,7 +40,7 @@ pub struct OrderExpired { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, } diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index e8156ccd7862..6547afe3217c 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -42,7 +42,7 @@ pub struct OrderModifyRejected { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, } diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index 6f762ec87869..e0339f84f006 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -41,7 +41,7 @@ pub struct OrderPendingCancel { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, } diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index d28fd95afcec..90a4125c6860 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -41,7 +41,7 @@ pub struct OrderPendingUpdate { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, } diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index 345ec07ec965..e9e5c7c1c9ab 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -43,7 +43,7 @@ pub struct OrderRejected { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed } impl OrderRejected { diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index 1299defb4c21..5c76a1f83bec 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -40,7 +40,7 @@ pub struct OrderTriggered { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, } diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index 6b6cfc623b91..2d8a6353c906 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -48,7 +48,7 @@ pub struct OrderUpdated { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: u8, + pub reconciliation: u8, // TODO: Change to bool once Cython removed } impl OrderUpdated { diff --git a/nautilus_core/model/src/ffi/events/order.rs b/nautilus_core/model/src/ffi/events/order.rs index bfabcc03e5ae..b8fcb37a9f59 100644 --- a/nautilus_core/model/src/ffi/events/order.rs +++ b/nautilus_core/model/src/ffi/events/order.rs @@ -29,141 +29,6 @@ use crate::{ types::price::Price, }; -/// # Safety -/// -/// - Assumes valid C string pointers. -// #[no_mangle] -// #[allow(improper_ctypes_definitions)] -// pub unsafe extern "C" fn order_initialized_new( -// trader_id: TraderId, -// strategy_id: StrategyId, -// instrument_id: InstrumentId, -// client_order_id: ClientOrderId, -// order_side: OrderSide, -// order_type: OrderType, -// quantity: Quantity, -// price: *const Price, -// trigger_price: *const Price, -// trigger_type: TriggerType, -// limit_offset: *const Price, -// trailing_offset: *const Price, -// trailing_offset_type: TrailingOffsetType, -// time_in_force: TimeInForce, -// expire_time: *const UnixNanos, -// post_only: u8, -// reduce_only: u8, -// quote_quantity: u8, -// display_qty: *const Quantity, -// emulation_trigger: TriggerType, -// trigger_instrument_id: *const InstrumentId, -// contingency_type: ContingencyType, -// order_list_id: *const OrderListId, -// linked_order_ids: *const c_char, -// parent_order_id: *const ClientOrderId, -// exec_algorithm_id: *const ExecAlgorithmId, -// exec_algorithm_params: *const c_char, -// exec_spawn_id: *const ClientOrderId, -// tags: *const c_char, -// event_id: UUID4, -// ts_event: UnixNanos, -// ts_init: UnixNanos, -// reconciliation: u8, -// ) -> OrderInitialized { -// OrderInitialized { -// trader_id, -// strategy_id, -// instrument_id, -// client_order_id, -// order_side, -// order_type, -// quantity, -// price: if price.is_null() { None } else { Some(*price) }, -// trigger_price: if trigger_price.is_null() { -// None -// } else { -// Some(*trigger_price) -// }, -// trigger_type: if trigger_type == TriggerType::NoTrigger { -// None -// } else { -// Some(trigger_type) -// }, -// limit_offset: if limit_offset.is_null() { -// None -// } else { -// Some(*limit_offset) -// }, -// trailing_offset: if trailing_offset.is_null() { -// None -// } else { -// Some(*trailing_offset) -// }, -// trailing_offset_type: if trailing_offset_type == TrailingOffsetType::NoTrailingOffset { -// None -// } else { -// Some(trailing_offset_type) -// }, -// time_in_force, -// expire_time: if expire_time.is_null() { -// None -// } else { -// Some(*expire_time) -// }, -// post_only, -// reduce_only, -// quote_quantity, -// display_qty: if display_qty.is_null() { -// None -// } else { -// Some(*display_qty) -// }, -// emulation_trigger: if emulation_trigger == TriggerType::NoTrigger { -// None -// } else { -// Some(emulation_trigger) -// }, -// trigger_instrument_id: if trigger_instrument_id.is_null() { -// None -// } else { -// Some(*trigger_instrument_id) -// }, -// contingency_type: if contingency_type == ContingencyType::NoContingency { -// None -// } else { -// Some(contingency_type) -// }, -// order_list_id: if order_list_id.is_null() { -// None -// } else { -// Some(*order_list_id) -// }, -// linked_order_ids: optional_ustr_to_vec_client_order_ids(optional_cstr_to_ustr( -// linked_order_ids, -// )), -// parent_order_id: if parent_order_id.is_null() { -// None -// } else { -// Some(*parent_order_id) -// }, -// exec_algorithm_id: if exec_algorithm_id.is_null() { -// None -// } else { -// Some(*exec_algorithm_id) -// }, -// exec_algorithm_params: optional_bytes_to_str_map(exec_algorithm_params), -// exec_spawn_id: if exec_spawn_id.is_null() { -// None -// } else { -// Some(*exec_spawn_id) -// }, -// tags: optional_cstr_to_ustr(tags).into(), -// event_id, -// ts_event, -// ts_init, -// reconciliation, -// } -// } - /// # Safety /// /// - Assumes `reason_ptr` is a valid C string pointer. @@ -313,30 +178,3 @@ pub unsafe extern "C" fn order_rejected_new( reconciliation, } } - -// #[no_mangle] -// pub unsafe extern "C" fn order_canceled_new( -// trader_id: TraderId, -// strategy_id: StrategyId, -// instrument_id: InstrumentId, -// client_order_id: ClientOrderId, -// venue_order_id: VenueOrderId, -// account_id: AccountId, -// reconciliation: u8, -// event_id: UUID4, -// ts_event: UnixNanos, -// ts_init: UnixNanos, -// ) -> OrderCanceled { -// OrderCanceled { -// trader_id, -// strategy_id, -// instrument_id, -// client_order_id, -// venue_order_id, -// account_id, -// reconciliation, -// event_id, -// ts_event, -// ts_init, -// } -// } diff --git a/nautilus_core/model/src/python/data/bar.rs b/nautilus_core/model/src/python/data/bar.rs index 5b03ea125a60..b32f0a408091 100644 --- a/nautilus_core/model/src/python/data/bar.rs +++ b/nautilus_core/model/src/python/data/bar.rs @@ -62,14 +62,14 @@ impl BarSpecification { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> String { + self.to_string() + } + #[staticmethod] #[pyo3(name = "fully_qualified_name")] fn py_fully_qualified_name() -> String { @@ -107,14 +107,14 @@ impl BarType { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> String { + self.to_string() + } + #[staticmethod] #[pyo3(name = "fully_qualified_name")] fn py_fully_qualified_name() -> String { @@ -214,14 +214,14 @@ impl Bar { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "bar_type")] fn py_bar_type(&self) -> BarType { diff --git a/nautilus_core/model/src/python/data/delta.rs b/nautilus_core/model/src/python/data/delta.rs index a9e07bb1bd09..a0ea01c47680 100644 --- a/nautilus_core/model/src/python/data/delta.rs +++ b/nautilus_core/model/src/python/data/delta.rs @@ -132,14 +132,14 @@ impl OrderBookDelta { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "instrument_id")] fn py_instrument_id(&self) -> InstrumentId { diff --git a/nautilus_core/model/src/python/data/deltas.rs b/nautilus_core/model/src/python/data/deltas.rs index af5a26f5acd1..74a3f7f93d2e 100644 --- a/nautilus_core/model/src/python/data/deltas.rs +++ b/nautilus_core/model/src/python/data/deltas.rs @@ -53,14 +53,14 @@ impl OrderBookDeltas { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "instrument_id")] fn py_instrument_id(&self) -> InstrumentId { diff --git a/nautilus_core/model/src/python/data/depth.rs b/nautilus_core/model/src/python/data/depth.rs index 811b2edbe795..6678df91574c 100644 --- a/nautilus_core/model/src/python/data/depth.rs +++ b/nautilus_core/model/src/python/data/depth.rs @@ -79,14 +79,14 @@ impl OrderBookDepth10 { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "instrument_id")] fn py_instrument_id(&self) -> InstrumentId { diff --git a/nautilus_core/model/src/python/data/order.rs b/nautilus_core/model/src/python/data/order.rs index 8344ad9f2069..d95b18cd0527 100644 --- a/nautilus_core/model/src/python/data/order.rs +++ b/nautilus_core/model/src/python/data/order.rs @@ -52,14 +52,14 @@ impl BookOrder { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "side")] fn py_side(&self) -> OrderSide { diff --git a/nautilus_core/model/src/python/data/quote.rs b/nautilus_core/model/src/python/data/quote.rs index b910cc3a47d4..8ccf52f8ffc1 100644 --- a/nautilus_core/model/src/python/data/quote.rs +++ b/nautilus_core/model/src/python/data/quote.rs @@ -195,14 +195,14 @@ impl QuoteTick { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{}({})", stringify!(QuoteTick), self) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "instrument_id")] fn py_instrument_id(&self) -> InstrumentId { diff --git a/nautilus_core/model/src/python/data/trade.rs b/nautilus_core/model/src/python/data/trade.rs index c000827016d7..2c2d4f001e16 100644 --- a/nautilus_core/model/src/python/data/trade.rs +++ b/nautilus_core/model/src/python/data/trade.rs @@ -183,14 +183,14 @@ impl TradeTick { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{}({})", stringify!(TradeTick), self) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "instrument_id")] fn py_instrument_id(&self) -> InstrumentId { diff --git a/nautilus_core/model/src/python/enums.rs b/nautilus_core/model/src/python/enums.rs index 77d2398304da..0fbce073b174 100644 --- a/nautilus_core/model/src/python/enums.rs +++ b/nautilus_core/model/src/python/enums.rs @@ -43,10 +43,6 @@ impl AccountType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -56,6 +52,10 @@ impl AccountType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -112,10 +112,6 @@ impl AggregationSource { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -125,6 +121,10 @@ impl AggregationSource { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -175,10 +175,6 @@ impl AggressorSide { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -188,6 +184,10 @@ impl AggressorSide { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -244,10 +244,6 @@ impl AssetClass { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -257,6 +253,10 @@ impl AssetClass { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -337,10 +337,6 @@ impl InstrumentClass { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -350,6 +346,10 @@ impl InstrumentClass { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -442,10 +442,6 @@ impl BarAggregation { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -455,6 +451,10 @@ impl BarAggregation { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -588,10 +588,6 @@ impl BookAction { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -601,6 +597,10 @@ impl BookAction { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -663,10 +663,6 @@ impl ContingencyType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -676,6 +672,10 @@ impl ContingencyType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -738,10 +738,6 @@ impl CurrencyType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -751,6 +747,10 @@ impl CurrencyType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -807,10 +807,6 @@ impl InstrumentCloseType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -820,6 +816,10 @@ impl InstrumentCloseType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -870,10 +870,6 @@ impl LiquiditySide { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -883,6 +879,10 @@ impl LiquiditySide { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -939,10 +939,6 @@ impl MarketStatus { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -952,6 +948,10 @@ impl MarketStatus { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1032,10 +1032,6 @@ impl HaltReason { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1045,6 +1041,10 @@ impl HaltReason { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1101,10 +1101,6 @@ impl OmsType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1114,6 +1110,10 @@ impl OmsType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1170,10 +1170,6 @@ impl OptionKind { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1183,6 +1179,10 @@ impl OptionKind { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1233,10 +1233,6 @@ impl OrderSide { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1246,6 +1242,10 @@ impl OrderSide { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1302,10 +1302,6 @@ impl OrderStatus { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1315,6 +1311,10 @@ impl OrderStatus { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1437,10 +1437,6 @@ impl OrderType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1450,6 +1446,10 @@ impl OrderType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1542,10 +1542,6 @@ impl PositionSide { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1555,6 +1551,10 @@ impl PositionSide { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1644,10 +1644,6 @@ impl RecordFlag { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1657,6 +1653,10 @@ impl RecordFlag { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1724,10 +1724,6 @@ impl TimeInForce { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1737,6 +1733,10 @@ impl TimeInForce { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1817,10 +1817,6 @@ impl TrailingOffsetType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1830,6 +1826,10 @@ impl TrailingOffsetType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -1898,10 +1898,6 @@ impl TriggerType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -1911,6 +1907,10 @@ impl TriggerType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -2009,10 +2009,6 @@ impl BookType { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -2022,6 +2018,10 @@ impl BookType { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { @@ -2078,10 +2078,6 @@ impl TradingState { *self as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!( "<{}.{}: '{}'>", @@ -2091,6 +2087,10 @@ impl TradingState { ) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[must_use] pub fn name(&self) -> String { diff --git a/nautilus_core/model/src/python/identifiers/instrument_id.rs b/nautilus_core/model/src/python/identifiers/instrument_id.rs index aa5bafa8cd25..61f3af2f0023 100644 --- a/nautilus_core/model/src/python/identifiers/instrument_id.rs +++ b/nautilus_core/model/src/python/identifiers/instrument_id.rs @@ -75,14 +75,14 @@ impl InstrumentId { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{}('{}')", stringify!(InstrumentId), self) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] #[pyo3(name = "symbol")] fn py_symbol(&self) -> Symbol { diff --git a/nautilus_core/model/src/python/identifiers/trade_id.rs b/nautilus_core/model/src/python/identifiers/trade_id.rs index 30d63701ff33..7f78e605cf00 100644 --- a/nautilus_core/model/src/python/identifiers/trade_id.rs +++ b/nautilus_core/model/src/python/identifiers/trade_id.rs @@ -83,14 +83,14 @@ impl TradeId { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("{}('{}')", stringify!(TradeId), self) } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] fn value(&self) -> String { self.to_string() diff --git a/nautilus_core/model/src/python/macros.rs b/nautilus_core/model/src/python/macros.rs index dc02099a01f5..71f0520b0107 100644 --- a/nautilus_core/model/src/python/macros.rs +++ b/nautilus_core/model/src/python/macros.rs @@ -65,10 +65,6 @@ macro_rules! identifier_for_python { self.inner().precomputed_hash() as isize } - fn __str__(&self) -> &'static str { - self.inner().as_str() - } - fn __repr__(&self) -> String { format!( "{}('{}')", @@ -77,6 +73,10 @@ macro_rules! identifier_for_python { ) } + fn __str__(&self) -> &'static str { + self.inner().as_str() + } + #[getter] #[pyo3(name = "value")] fn py_value(&self) -> String { diff --git a/nautilus_core/model/src/python/orderbook/book.rs b/nautilus_core/model/src/python/orderbook/book.rs index bb3271265fc2..690ea9c74ea6 100644 --- a/nautilus_core/model/src/python/orderbook/book.rs +++ b/nautilus_core/model/src/python/orderbook/book.rs @@ -39,12 +39,12 @@ impl OrderBook { Self::new(book_type, instrument_id) } - fn __str__(&self) -> String { - // TODO: Return debug string for now + fn __repr__(&self) -> String { format!("{self:?}") } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { + // TODO: Return debug string for now format!("{self:?}") } diff --git a/nautilus_core/model/src/python/orderbook/level.rs b/nautilus_core/model/src/python/orderbook/level.rs index 101454a003f0..32ef5b39c663 100644 --- a/nautilus_core/model/src/python/orderbook/level.rs +++ b/nautilus_core/model/src/python/orderbook/level.rs @@ -19,12 +19,12 @@ use crate::{data::order::BookOrder, orderbook::level::Level, types::price::Price #[pymethods] impl Level { - fn __str__(&self) -> String { - // TODO: Return debug string for now + fn __repr__(&self) -> String { format!("{self:?}") } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { + // TODO: Return debug string for now format!("{self:?}") } diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index 78a8b4677fc1..3712a6f4f9e4 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -111,11 +111,11 @@ impl LimitOrder { } } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { self.to_string() } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { self.to_string() } diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index 388be7f2872f..fa20c897923a 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -98,11 +98,11 @@ impl MarketOrder { } } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { self.to_string() } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { self.to_string() } diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index f454fd2a3e7e..b3c6502fb04c 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -108,11 +108,11 @@ impl StopLimitOrder { } } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { self.to_string() } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { self.to_string() } diff --git a/nautilus_core/model/src/python/position.rs b/nautilus_core/model/src/python/position.rs index 52d94ac60b06..a16218265b47 100644 --- a/nautilus_core/model/src/python/position.rs +++ b/nautilus_core/model/src/python/position.rs @@ -60,11 +60,11 @@ impl Position { } } - fn __str__(&self) -> String { + fn __repr__(&self) -> String { self.to_string() } - fn __repr__(&self) -> String { + fn __str__(&self) -> String { self.to_string() } diff --git a/nautilus_core/model/src/python/types/currency.rs b/nautilus_core/model/src/python/types/currency.rs index b96ba3256e4f..0f76df810a75 100644 --- a/nautilus_core/model/src/python/types/currency.rs +++ b/nautilus_core/model/src/python/types/currency.rs @@ -83,14 +83,14 @@ impl Currency { self.code.precomputed_hash() as isize } - fn __str__(&self) -> &'static str { - self.code.as_str() - } - fn __repr__(&self) -> String { format!("{self:?}") } + fn __str__(&self) -> &'static str { + self.code.as_str() + } + #[getter] #[pyo3(name = "code")] fn py_code(&self) -> &'static str { diff --git a/nautilus_core/model/src/python/types/money.rs b/nautilus_core/model/src/python/types/money.rs index cbf9f7bf39a8..dd045a92c419 100644 --- a/nautilus_core/model/src/python/types/money.rs +++ b/nautilus_core/model/src/python/types/money.rs @@ -311,16 +311,16 @@ impl Money { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { let amount_str = format!("{:.*}", self.currency.precision as usize, self.as_f64()); let code = self.currency.code.as_str(); format!("Money('{amount_str}', {code})") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] fn raw(&self) -> i64 { self.raw diff --git a/nautilus_core/model/src/python/types/price.rs b/nautilus_core/model/src/python/types/price.rs index 83a8c14527b9..9819f7bac13c 100644 --- a/nautilus_core/model/src/python/types/price.rs +++ b/nautilus_core/model/src/python/types/price.rs @@ -311,14 +311,14 @@ impl Price { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("Price('{self:?}')") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] fn raw(&self) -> i64 { self.raw diff --git a/nautilus_core/model/src/python/types/quantity.rs b/nautilus_core/model/src/python/types/quantity.rs index fe3f6fc140de..6fca30393bf7 100644 --- a/nautilus_core/model/src/python/types/quantity.rs +++ b/nautilus_core/model/src/python/types/quantity.rs @@ -311,14 +311,14 @@ impl Quantity { h.finish() as isize } - fn __str__(&self) -> String { - self.to_string() - } - fn __repr__(&self) -> String { format!("Quantity('{self:?}')") } + fn __str__(&self) -> String { + self.to_string() + } + #[getter] fn raw(&self) -> u64 { self.raw diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index ac0bba909df2..dec7b94fcc23 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -1834,9 +1834,6 @@ const char *trigger_type_to_cstr(enum TriggerType value); enum TriggerType trigger_type_from_cstr(const char *ptr); /** - * # Safety - * - * - Assumes valid C string pointers. * # Safety * * - Assumes `reason_ptr` is a valid C string pointer. diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 7559d3c76604..1663a62ee6ab 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -1214,9 +1214,6 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is a valid C string pointer. TriggerType trigger_type_from_cstr(const char *ptr); - # # Safety - # - # - Assumes valid C string pointers. # # Safety # # - Assumes `reason_ptr` is a valid C string pointer. From dfb049f909a4d77d0df7255aa7817186af2c016f Mon Sep 17 00:00:00 2001 From: David Blom Date: Sun, 5 May 2024 12:12:35 +0200 Subject: [PATCH 129/193] Fix timer cancel when it is already expired (#1628) --- nautilus_core/common/src/timer.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 03159405b2d4..0fb6a441f1dc 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -342,9 +342,11 @@ impl LiveTimer { /// Cancels the timer (the timer will not generate an event). pub fn cancel(&mut self) -> anyhow::Result<()> { debug!("Cancel timer '{}'", self.name); - if let Some(sender) = self.canceler.take() { - // Send cancellation signal - sender.send(()).map_err(|e| anyhow::anyhow!("{:?}", e))?; + if !self.is_expired.load(atomic::Ordering::SeqCst) { + if let Some(sender) = self.canceler.take() { + // Send cancellation signal + sender.send(()).map_err(|e| anyhow::anyhow!("{:?}", e))?; + } } Ok(()) } From 648c3998fce157e512c716cf2f4d0e3542412695 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 6 May 2024 09:41:19 +1000 Subject: [PATCH 130/193] Update dependencies --- nautilus_core/Cargo.lock | 31 ++++---- nautilus_core/common/Cargo.toml | 2 +- poetry.lock | 125 ++++++++++++++++---------------- pyproject.toml | 2 +- 4 files changed, 79 insertions(+), 81 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 4da36f31a954..37a27ba7ac3d 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2939,9 +2939,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -4110,11 +4110,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -4123,9 +4123,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -4691,9 +4691,9 @@ checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "sysinfo" -version = "0.30.11" +version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" dependencies = [ "cfg-if", "core-foundation-sys", @@ -4994,16 +4994,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5712,18 +5711,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" dependencies = [ "proc-macro2", "quote", diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml index 5224d939f688..1c4be86600ac 100644 --- a/nautilus_core/common/Cargo.toml +++ b/nautilus_core/common/Cargo.toml @@ -26,7 +26,7 @@ rust_decimal_macros = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true } -sysinfo = "0.30.11" +sysinfo = "0.30.12" tokio = { workspace = true } # Disable default feature "tracing-log" since it interferes with custom logging tracing-subscriber = { version = "0.3.18", default-features = false, features = ["smallvec", "fmt", "ansi", "std", "env-filter"] } diff --git a/poetry.lock b/poetry.lock index c6bbb4ee9e0e..5292b3b95e5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -153,13 +153,13 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "babel" -version = "2.14.0" +version = "2.15.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, - {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, ] [package.extras] @@ -394,63 +394,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.0" +version = "7.5.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, - {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, - {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, - {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, - {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, - {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, - {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, - {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, - {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, - {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, - {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, - {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, - {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, - {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, + {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, + {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, + {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, + {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, + {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, + {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, + {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, + {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, + {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, + {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, + {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, + {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, ] [package.dependencies] @@ -1723,17 +1723,16 @@ numpy = ">=1.16.6" [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -2686,4 +2685,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "22271ae8e66964ff23d6975f761241e2707a7e1022126c2da800ff899a1134cf" +content-hash = "e249c472864a0664727d309d1c2bf39c22d3b050e55feb074b8e78b608647767" diff --git a/pyproject.toml b/pyproject.toml index 61d350331b9e..791d7e4e8ac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ types-toml = "^0.10.2" optional = true [tool.poetry.group.test.dependencies] -coverage = "^7.5.0" +coverage = "^7.5.1" pytest = "^7.4.4" pytest-aiohttp = "^1.0.5" pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_scoped_event_loop' attribute of immutable type From e69b53ec5dcbd83598e3642dbef6a9612df9ab23 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 6 May 2024 10:07:31 +1000 Subject: [PATCH 131/193] Standardize timer API in Rust --- nautilus_core/common/src/clock.rs | 10 ++++---- nautilus_core/common/src/timer.rs | 40 ++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index de5f37ac4b37..3051268f9616 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -108,7 +108,7 @@ impl TestClock { let mut timers: Vec = self .timers .iter_mut() - .filter(|(_, timer)| !timer.is_expired) + .filter(|(_, timer)| !timer.is_expired()) .flat_map(|(_, timer)| timer.advance(to_time_ns)) .collect(); @@ -166,7 +166,7 @@ impl Clock for TestClock { fn timer_names(&self) -> Vec<&str> { self.timers .iter() - .filter(|(_, timer)| !timer.is_expired) + .filter(|(_, timer)| !timer.is_expired()) .map(|(k, _)| k.as_str()) .collect() } @@ -174,7 +174,7 @@ impl Clock for TestClock { fn timer_count(&self) -> usize { self.timers .iter() - .filter(|(_, timer)| !timer.is_expired) + .filter(|(_, timer)| !timer.is_expired()) .count() } @@ -241,7 +241,7 @@ impl Clock for TestClock { let timer = self.timers.get(&Ustr::from(name)); match timer { None => 0.into(), - Some(timer) => timer.next_time_ns, + Some(timer) => timer.next_time_ns(), } } @@ -374,7 +374,7 @@ impl Clock for LiveClock { let timer = self.timers.get(&Ustr::from(name)); match timer { None => 0.into(), - Some(timer) => timer.next_time_ns, + Some(timer) => timer.next_time_ns(), } } diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 0fb6a441f1dc..3768a3ee569a 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -20,7 +20,7 @@ use std::{ ffi::c_char, fmt::{Display, Formatter}, sync::{ - atomic::{self, AtomicBool}, + atomic::{self, AtomicBool, AtomicU64}, Arc, }, }; @@ -139,8 +139,8 @@ pub struct TestTimer { pub interval_ns: u64, pub start_time_ns: UnixNanos, pub stop_time_ns: Option, - pub next_time_ns: UnixNanos, - pub is_expired: bool, + next_time_ns: UnixNanos, + is_expired: bool, } impl TestTimer { @@ -163,6 +163,16 @@ impl TestTimer { }) } + #[must_use] + pub fn next_time_ns(&self) -> UnixNanos { + self.next_time_ns + } + + #[must_use] + pub fn is_expired(&self) -> bool { + self.is_expired + } + #[must_use] pub fn pop_event(&self, event_id: UUID4, ts_init: UnixNanos) -> TimeEvent { TimeEvent { @@ -220,15 +230,12 @@ impl Iterator for TestTimer { } /// Provides a live timer for use with a `LiveClock`. -/// -/// Note: `next_time_ns` is only accurate when initially starting the timer -/// and will not incrementally update as the timer runs. pub struct LiveTimer { pub name: Ustr, pub interval_ns: u64, pub start_time_ns: UnixNanos, pub stop_time_ns: Option, - pub next_time_ns: UnixNanos, + next_time_ns: Arc, is_expired: Arc, callback: EventHandler, canceler: Option>, @@ -251,13 +258,18 @@ impl LiveTimer { interval_ns, start_time_ns, stop_time_ns, - next_time_ns: start_time_ns + interval_ns, + next_time_ns: Arc::new(AtomicU64::new(start_time_ns.as_u64() + interval_ns)), is_expired: Arc::new(AtomicBool::new(false)), callback, canceler: None, }) } + #[must_use] + pub fn next_time_ns(&self) -> UnixNanos { + UnixNanos::from(self.next_time_ns.load(atomic::Ordering::SeqCst)) + } + #[must_use] pub fn is_expired(&self) -> bool { self.is_expired.load(atomic::Ordering::SeqCst) @@ -267,13 +279,14 @@ impl LiveTimer { let event_name = self.name; let stop_time_ns = self.stop_time_ns; let mut start_time_ns = self.start_time_ns; - let next_time_ns = self.next_time_ns; + let next_time_ns = self.next_time_ns.load(atomic::Ordering::SeqCst); + let next_time_atomic = self.next_time_ns.clone(); let interval_ns = self.interval_ns; let is_expired = self.is_expired.clone(); let callback = self.callback.clone(); // Floor the next time to the nearest microsecond which is within the timers accuracy - let mut next_time_ns = UnixNanos::from(floor_to_nearest_microsecond(next_time_ns.into())); + let mut next_time_ns = UnixNanos::from(floor_to_nearest_microsecond(next_time_ns)); // Setup oneshot channel for cancelling timer task let (cancel_tx, mut cancel_rx) = oneshot::channel(); @@ -318,6 +331,7 @@ impl LiveTimer { // Prepare next time interval next_time_ns += interval_ns; + next_time_atomic.store(next_time_ns.as_u64(), atomic::Ordering::SeqCst); // Check if expired if let Some(stop_time_ns) = stop_time_ns { @@ -339,7 +353,7 @@ impl LiveTimer { }); } - /// Cancels the timer (the timer will not generate an event). + /// Cancels the timer (the timer will not generate a final event). pub fn cancel(&mut self) -> anyhow::Result<()> { debug!("Cancel timer '{}'", self.name); if !self.is_expired.load(atomic::Ordering::SeqCst) { @@ -486,6 +500,7 @@ mod tests { let interval_ns = 100 * NANOSECONDS_IN_MILLISECOND; let mut timer = LiveTimer::new("TEST_TIMER", interval_ns, start_time, None, handler).unwrap(); + let next_time_ns = timer.next_time_ns(); timer.start(); // Wait for timer to run @@ -493,6 +508,7 @@ mod tests { timer.cancel().unwrap(); wait_until(|| timer.is_expired(), Duration::from_secs(2)); + assert!(timer.next_time_ns() > next_time_ns); } #[tokio::test] @@ -517,11 +533,13 @@ mod tests { handler, ) .unwrap(); + let next_time_ns = timer.next_time_ns(); timer.start(); // Wait for a longer time than the stop time tokio::time::sleep(Duration::from_secs(1)).await; wait_until(|| timer.is_expired(), Duration::from_secs(2)); + assert!(timer.next_time_ns() > next_time_ns); } } From a91a4a8ee015fed5f6fe1347dfb82be212184cb2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 6 May 2024 10:11:10 +1000 Subject: [PATCH 132/193] Remove incorrect hyphens --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a00a5eb3da6d..6b847051e5cb 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ including FX, Equities, Futures, Options, CFDs, Crypto and Betting - across mult ## Features - **Fast** - Core written in Rust with asynchronous networking using [tokio](https://crates.io/crates/tokio) -- **Reliable** - Type-safety and thread-safety through Rust. Redis backed performant state persistence +- **Reliable** - Type safety and thread safety through Rust. Redis backed performant state persistence - **Portable** - OS independent, runs on Linux, macOS, Windows. Deploy using Docker - **Flexible** - Modular adapters mean any REST, WebSocket, or FIX API can be integrated - **Advanced** - Time in force `IOC`, `FOK`, `GTD`, `AT_THE_OPEN`, `AT_THE_CLOSE`, advanced order types and conditional triggers. Execution instructions `post-only`, `reduce-only`, and icebergs. Contingency order lists including `OCO`, `OTO` From f6c830ac93a2698f78f3fe09e6e9033dd27fa559 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 6 May 2024 17:13:30 +1000 Subject: [PATCH 133/193] Update example config and strategy docstrings --- nautilus_trader/examples/strategies/blank.py | 6 ------ nautilus_trader/examples/strategies/ema_cross.py | 11 +++-------- .../examples/strategies/ema_cross_bracket.py | 12 ++---------- .../examples/strategies/ema_cross_bracket_algo.py | 12 ++---------- .../examples/strategies/ema_cross_cython.pyx | 11 +++-------- .../examples/strategies/ema_cross_long_only.py | 10 ++-------- .../examples/strategies/ema_cross_stop_entry.py | 10 ++-------- .../examples/strategies/ema_cross_trailing_stop.py | 10 ++-------- .../examples/strategies/ema_cross_twap.py | 10 ++-------- .../examples/strategies/orderbook_imbalance.py | 8 +------- .../examples/strategies/orderbook_imbalance_rust.py | 10 ++-------- .../examples/strategies/signal_strategy.py | 6 ++++++ nautilus_trader/examples/strategies/subscribe.py | 6 ++++++ .../examples/strategies/volatility_market_maker.py | 3 --- nautilus_trader/trading/config.py | 2 +- 15 files changed, 34 insertions(+), 93 deletions(-) diff --git a/nautilus_trader/examples/strategies/blank.py b/nautilus_trader/examples/strategies/blank.py index a3159c07be52..0a9f3358df5d 100644 --- a/nautilus_trader/examples/strategies/blank.py +++ b/nautilus_trader/examples/strategies/blank.py @@ -32,12 +32,6 @@ class MyStrategyConfig(StrategyConfig, frozen=True): ---------- instrument_id : InstrumentId The instrument ID for the strategy. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/examples/strategies/ema_cross.py b/nautilus_trader/examples/strategies/ema_cross.py index d3972b6621cd..8eaade690412 100644 --- a/nautilus_trader/examples/strategies/ema_cross.py +++ b/nautilus_trader/examples/strategies/ema_cross.py @@ -51,8 +51,8 @@ class EMACrossConfig(StrategyConfig, frozen=True): The instrument ID for the strategy. bar_type : BarType The bar type for the strategy. - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. fast_ema_period : int, default 10 The fast EMA period. slow_ema_period : int, default 20 @@ -63,12 +63,6 @@ class EMACrossConfig(StrategyConfig, frozen=True): If quote ticks should be subscribed to. close_positions_on_stop : bool, default True If all open positions should be closed on strategy stop. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ @@ -141,6 +135,7 @@ def on_start(self) -> None: # Subscribe to live data self.subscribe_bars(self.bar_type) + if self.config.subscribe_quote_ticks: self.subscribe_quote_ticks(self.instrument_id) if self.config.subscribe_trade_ticks: diff --git a/nautilus_trader/examples/strategies/ema_cross_bracket.py b/nautilus_trader/examples/strategies/ema_cross_bracket.py index d0602bc3d36a..aacc9f93ec64 100644 --- a/nautilus_trader/examples/strategies/ema_cross_bracket.py +++ b/nautilus_trader/examples/strategies/ema_cross_bracket.py @@ -52,8 +52,8 @@ class EMACrossBracketConfig(StrategyConfig, frozen=True): The instrument ID for the strategy. bar_type : BarType The bar type for the strategy. - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. atr_period : PositiveInt, default 20 The period for the ATR indicator. fast_ema_period : PositiveInt, default 10 @@ -65,14 +65,6 @@ class EMACrossBracketConfig(StrategyConfig, frozen=True): emulation_trigger : str, default 'NO_TRIGGER' The emulation trigger for submitting emulated orders. If ``None`` then orders will not be emulated. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). - manage_gtd_expiry : bool, default True - If all order GTD time in force expirations should be managed by the strategy. """ diff --git a/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py b/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py index e289a63c61be..a770d3ac66e5 100644 --- a/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py +++ b/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py @@ -55,8 +55,8 @@ class EMACrossBracketAlgoConfig(StrategyConfig, frozen=True): The instrument ID for the strategy. bar_type : BarType The bar type for the strategy. - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. atr_period : PositiveInt, default 20 The period for the ATR indicator. fast_ema_period : PositiveInt, default 10 @@ -82,14 +82,6 @@ class EMACrossBracketAlgoConfig(StrategyConfig, frozen=True): The execution algorithm params for take-profit (TP) orders. close_positions_on_stop : bool, default True If all open positions should be closed on strategy stop. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). - manage_gtd_expiry : bool, default True - If all order GTD time in force expirations should be managed by the strategy. """ diff --git a/nautilus_trader/examples/strategies/ema_cross_cython.pyx b/nautilus_trader/examples/strategies/ema_cross_cython.pyx index 3441f52a7014..f204f1b11aa0 100644 --- a/nautilus_trader/examples/strategies/ema_cross_cython.pyx +++ b/nautilus_trader/examples/strategies/ema_cross_cython.pyx @@ -53,18 +53,13 @@ class EMACrossConfig(StrategyConfig, frozen=True): The instrument ID for the strategy. bar_type : BarType The bar type for the strategy. - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. fast_ema_period : int, default 10 The fast EMA period. Must be positive and less than `slow_ema_period`. slow_ema_period : int, default 20 The slow EMA period. Must be positive and less than `fast_ema_period`. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). + """ instrument_id: InstrumentId diff --git a/nautilus_trader/examples/strategies/ema_cross_long_only.py b/nautilus_trader/examples/strategies/ema_cross_long_only.py index 4f536bc76c58..9eb9ec6d3df4 100644 --- a/nautilus_trader/examples/strategies/ema_cross_long_only.py +++ b/nautilus_trader/examples/strategies/ema_cross_long_only.py @@ -52,8 +52,8 @@ class EMACrossLongOnlyConfig(StrategyConfig, frozen=True): The instrument ID for the strategy. bar_type : BarType The bar type for the strategy. - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. fast_ema_period : int, default 10 The fast EMA period. slow_ema_period : int, default 20 @@ -62,12 +62,6 @@ class EMACrossLongOnlyConfig(StrategyConfig, frozen=True): If historical bars should be requested on start. close_positions_on_stop : bool, default True If all open positions should be closed on strategy stop. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/examples/strategies/ema_cross_stop_entry.py b/nautilus_trader/examples/strategies/ema_cross_stop_entry.py index d0e1db5c89a3..fd6c443f2d8a 100644 --- a/nautilus_trader/examples/strategies/ema_cross_stop_entry.py +++ b/nautilus_trader/examples/strategies/ema_cross_stop_entry.py @@ -66,8 +66,8 @@ class EMACrossStopEntryConfig(StrategyConfig, frozen=True): The trailing offset amount. trigger_type : str The trailing stop trigger type (interpreted as `TriggerType`). - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. fast_ema_period : PositiveInt, default 10 The fast EMA period. slow_ema_period : PositiveInt, default 20 @@ -75,12 +75,6 @@ class EMACrossStopEntryConfig(StrategyConfig, frozen=True): emulation_trigger : str, default 'NO_TRIGGER' The emulation trigger for submitting emulated orders. If 'NONE' then orders will not be emulated. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py b/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py index a2f035cdfe31..f1c2144e00e1 100644 --- a/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py +++ b/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py @@ -65,8 +65,8 @@ class EMACrossTrailingStopConfig(StrategyConfig, frozen=True): The trailing offset type (interpreted as `TrailingOffsetType`). trigger_type : str The trailing stop trigger type (interpreted as `TriggerType`). - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. fast_ema_period : PositiveInt, default 10 The fast EMA period. slow_ema_period : PositiveInt, default 20 @@ -74,12 +74,6 @@ class EMACrossTrailingStopConfig(StrategyConfig, frozen=True): emulation_trigger : str, default 'NO_TRIGGER' The emulation trigger for submitting emulated orders. If 'NONE' then orders will not be emulated. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/examples/strategies/ema_cross_twap.py b/nautilus_trader/examples/strategies/ema_cross_twap.py index 3a08550e31f9..5656e56d25e3 100644 --- a/nautilus_trader/examples/strategies/ema_cross_twap.py +++ b/nautilus_trader/examples/strategies/ema_cross_twap.py @@ -53,8 +53,8 @@ class EMACrossTWAPConfig(StrategyConfig, frozen=True): The instrument ID for the strategy. bar_type : BarType The bar type for the strategy. - trade_size : str - The position size per trade (interpreted as Decimal). + trade_size : Decimal + The position size per trade. fast_ema_period : PositiveInt, default 10 The fast EMA period. slow_ema_period : PositiveInt, default 20 @@ -65,12 +65,6 @@ class EMACrossTWAPConfig(StrategyConfig, frozen=True): The TWAP interval (seconds) between orders. close_positions_on_stop : bool, default True If all open positions should be closed on strategy stop. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/examples/strategies/orderbook_imbalance.py b/nautilus_trader/examples/strategies/orderbook_imbalance.py index 07eabfa86419..7daf3eff3ed3 100644 --- a/nautilus_trader/examples/strategies/orderbook_imbalance.py +++ b/nautilus_trader/examples/strategies/orderbook_imbalance.py @@ -45,7 +45,7 @@ class OrderBookImbalanceConfig(StrategyConfig, frozen=True): ---------- instrument_id : InstrumentId The instrument ID for the strategy. - max_trade_size : str + max_trade_size : Decimal The max position size per trade (volume on the level can be less). trigger_min_size : PositiveFloat, default 100.0 The minimum size on the larger side to trigger an order. @@ -62,12 +62,6 @@ class OrderBookImbalanceConfig(StrategyConfig, frozen=True): If quote ticks should be used. subscribe_ticker : bool, default False If tickers should be subscribed to. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/examples/strategies/orderbook_imbalance_rust.py b/nautilus_trader/examples/strategies/orderbook_imbalance_rust.py index bd38103d029e..18288addad12 100644 --- a/nautilus_trader/examples/strategies/orderbook_imbalance_rust.py +++ b/nautilus_trader/examples/strategies/orderbook_imbalance_rust.py @@ -45,8 +45,8 @@ class OrderBookImbalanceConfig(StrategyConfig, frozen=True): ---------- instrument_id : InstrumentId The instrument ID for the strategy. - max_trade_size : str - The max position size per trade (volume on the level can be less). + max_trade_size : Decimal + The max position size per trade (size on the level can be less). trigger_min_size : PositiveFloat, default 100.0 The minimum size on the larger side to trigger an order. trigger_imbalance_ratio : PositiveFloat, default 0.20 @@ -62,12 +62,6 @@ class OrderBookImbalanceConfig(StrategyConfig, frozen=True): If quote ticks should be used. subscribe_ticker : bool, default False If tickers should be subscribed to. - order_id_tag : str - The unique order ID tag for the strategy. Must be unique - amongst all running strategies for a particular trader ID. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/examples/strategies/signal_strategy.py b/nautilus_trader/examples/strategies/signal_strategy.py index fc514b3bbaab..b0f73ba5840b 100644 --- a/nautilus_trader/examples/strategies/signal_strategy.py +++ b/nautilus_trader/examples/strategies/signal_strategy.py @@ -28,6 +28,12 @@ class SignalStrategyConfig(StrategyConfig, frozen=True): """ Configuration for ``SignalStrategy`` instances. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument ID for the strategy. + """ instrument_id: InstrumentId diff --git a/nautilus_trader/examples/strategies/subscribe.py b/nautilus_trader/examples/strategies/subscribe.py index 98930de5ff11..b01afbc59cc4 100644 --- a/nautilus_trader/examples/strategies/subscribe.py +++ b/nautilus_trader/examples/strategies/subscribe.py @@ -35,6 +35,12 @@ class SubscribeStrategyConfig(StrategyConfig, frozen=True): """ Configuration for ``SubscribeStrategy`` instances. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument ID for the strategy. + """ instrument_id: InstrumentId diff --git a/nautilus_trader/examples/strategies/volatility_market_maker.py b/nautilus_trader/examples/strategies/volatility_market_maker.py index a0293a1b412a..4262f45bf315 100644 --- a/nautilus_trader/examples/strategies/volatility_market_maker.py +++ b/nautilus_trader/examples/strategies/volatility_market_maker.py @@ -66,9 +66,6 @@ class VolatilityMarketMakerConfig(StrategyConfig, frozen=True): emulation_trigger : str, default 'NO_TRIGGER' The emulation trigger for submitting emulated orders. If ``None`` then orders will not be emulated. - oms_type : OmsType - The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). """ diff --git a/nautilus_trader/trading/config.py b/nautilus_trader/trading/config.py index c0af482a3ce4..c56ffa78e1d1 100644 --- a/nautilus_trader/trading/config.py +++ b/nautilus_trader/trading/config.py @@ -41,7 +41,7 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): amongst all running strategies for a particular trader ID. oms_type : OmsType, optional The order management system type for the strategy. This will determine - how the `ExecutionEngine` handles position IDs (see docs). + how the `ExecutionEngine` handles position IDs. external_order_claims : list[InstrumentId], optional The external order claim instrument IDs. External orders for matching instrument IDs will be associated with (claimed by) the strategy. From 21ecc7969a60d60163e3ef004c5c42fcfd1a45a1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 6 May 2024 17:52:58 +1000 Subject: [PATCH 134/193] Refine Money debug and display in Rust --- nautilus_core/model/src/types/money.rs | 34 +++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 098f9b4c5eeb..1ddcb7902af2 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -15,7 +15,7 @@ use std::{ cmp::Ordering, - fmt::{Display, Formatter, Result as FmtResult}, + fmt::{Debug, Display}, hash::{Hash, Hasher}, ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign}, str::FromStr, @@ -36,7 +36,7 @@ pub const MONEY_MAX: f64 = 9_223_372_036.0; pub const MONEY_MIN: f64 = -9_223_372_036.0; #[repr(C)] -#[derive(Clone, Copy, Debug, Eq)] +#[derive(Clone, Copy, Eq)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -252,8 +252,21 @@ impl Div for Money { } } +impl Debug for Money { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}({:.*}, currency={})", + stringify!(Money), + self.currency.precision as usize, + self.as_f64(), + self.currency, + ) + } +} + impl Display for Money { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{:.*} {}", @@ -295,6 +308,21 @@ mod tests { use super::*; + #[rstest] + fn test_debug() { + let money = Money::new(1010.12, Currency::USD()).unwrap(); + assert_eq!( + format!("{:?}", money), + format!("Money(1010.12, currency=USD)") + ); + } + + #[rstest] + fn test_display() { + let money = Money::new(1010.12, Currency::USD()).unwrap(); + assert_eq!(format!("{money}"), "1010.12 USD"); + } + #[rstest] #[should_panic] fn test_money_different_currency_addition() { From 6e9188f46e05b115dbc1c8fe790129302ed5115e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 6 May 2024 19:01:35 +1000 Subject: [PATCH 135/193] Standardize Debug and Display implementations --- nautilus_core/model/src/data/delta.rs | 2 +- nautilus_core/model/src/data/order.rs | 44 +++++++--- .../model/src/python/types/balance.rs | 32 +------- nautilus_core/model/src/python/types/money.rs | 4 +- nautilus_core/model/src/python/types/price.rs | 2 +- .../model/src/python/types/quantity.rs | 2 +- nautilus_core/model/src/types/balance.rs | 82 +++++++++++++------ nautilus_core/model/src/types/money.rs | 13 +-- nautilus_core/model/src/types/price.rs | 30 ++++--- nautilus_core/model/src/types/quantity.rs | 31 ++++--- nautilus_trader/model/objects.pyx | 6 +- nautilus_trader/risk/engine.pyx | 12 ++- .../model/objects/test_balance_pyo3.py | 8 +- tests/unit_tests/model/objects/test_money.py | 2 +- .../model/objects/test_money_pyo3.py | 2 +- tests/unit_tests/model/objects/test_price.py | 4 +- .../model/objects/test_price_pyo3.py | 4 +- .../unit_tests/model/objects/test_quantity.py | 4 +- .../model/objects/test_quantity_pyo3.py | 4 +- .../model/objects/test_state_pyo3.py | 6 +- tests/unit_tests/model/test_orderbook.py | 4 +- tests/unit_tests/model/test_orderbook_data.py | 24 +++--- 22 files changed, 190 insertions(+), 132 deletions(-) diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs index 63e87196a922..a86113b96e50 100644 --- a/nautilus_core/model/src/data/delta.rs +++ b/nautilus_core/model/src/data/delta.rs @@ -232,7 +232,7 @@ mod tests { let delta = stub_delta; assert_eq!( format!("{delta}"), - "AAPL.XNAS,ADD,100.00,10,BUY,123456,0,1,1,2".to_string() + "AAPL.XNAS,ADD,BUY,100.00,10,123456,0,1,1,2".to_string() ); } diff --git a/nautilus_core/model/src/data/order.rs b/nautilus_core/model/src/data/order.rs index e00578737d66..cc10eee94fb3 100644 --- a/nautilus_core/model/src/data/order.rs +++ b/nautilus_core/model/src/data/order.rs @@ -13,10 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! A `BookOrder` for use with the `OrderBookDelta` data type. +//! A `BookOrder` for use with the `OrderBook` and `OrderBookDelta` data type. use std::{ - fmt::{Display, Formatter}, + fmt::{Debug, Display}, hash::{Hash, Hasher}, }; @@ -46,7 +46,7 @@ pub const NULL_ORDER: BookOrder = BookOrder { /// Represents an order in a book. #[repr(C)] -#[derive(Clone, Eq, Debug, Serialize, Deserialize)] +#[derive(Clone, Eq, Serialize, Deserialize)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -112,12 +112,26 @@ impl Hash for BookOrder { } } +impl Debug for BookOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}(side={}, price={}, size={}, order_id={})", + stringify!(BookOrder), + self.side, + self.price, + self.size, + self.order_id, + ) + } +} + impl Display for BookOrder { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{},{},{},{}", - self.price, self.size, self.side, self.order_id, + self.side, self.price, self.size, self.order_id, ) } } @@ -191,16 +205,26 @@ mod tests { } #[rstest] - fn test_display() { + fn test_debug() { let price = Price::from("100.00"); let size = Quantity::from(10); let side = OrderSide::Buy; let order_id = 123_456; - let order = BookOrder::new(side, price, size, order_id); - let display = format!("{order}"); + let result = format!("{order:?}"); + let expected = "BookOrder(side=BUY, price=100.00, size=10, order_id=123456)"; + assert_eq!(result, expected); + } - let expected = format!("{price},{size},{side},{order_id}"); - assert_eq!(display, expected); + #[rstest] + fn test_display() { + let price = Price::from("100.00"); + let size = Quantity::from(10); + let side = OrderSide::Buy; + let order_id = 123_456; + let order = BookOrder::new(side, price, size, order_id); + let result = format!("{order}"); + let expected = "BUY,100.00,10,123456"; + assert_eq!(result, expected); } } diff --git a/nautilus_core/model/src/python/types/balance.rs b/nautilus_core/model/src/python/types/balance.rs index 9f9337f843cc..67eac7b3f3e7 100644 --- a/nautilus_core/model/src/python/types/balance.rs +++ b/nautilus_core/model/src/python/types/balance.rs @@ -43,23 +43,11 @@ impl AccountBalance { } fn __repr__(&self) -> String { - format!( - "{}(total={},locked={},free={})", - stringify!(AccountBalance), - self.total, - self.locked, - self.free - ) + format!("{self:?}") } fn __str__(&self) -> String { - format!( - "{}(total={},locked={},free={})", - stringify!(AccountBalance), - self.total, - self.locked, - self.free - ) + self.to_string() } #[staticmethod] @@ -131,23 +119,11 @@ impl MarginBalance { } fn __repr__(&self) -> String { - format!( - "{}(initial={},maintenance={},instrument_id={})", - stringify!(MarginBalance), - self.initial, - self.maintenance, - self.instrument_id, - ) + format!("{self:?}") } fn __str__(&self) -> String { - format!( - "{}(initial={},maintenance={},instrument_id={})", - stringify!(MarginBalance), - self.initial, - self.maintenance, - self.instrument_id, - ) + self.to_string() } #[staticmethod] diff --git a/nautilus_core/model/src/python/types/money.rs b/nautilus_core/model/src/python/types/money.rs index dd045a92c419..b4520e016371 100644 --- a/nautilus_core/model/src/python/types/money.rs +++ b/nautilus_core/model/src/python/types/money.rs @@ -312,9 +312,7 @@ impl Money { } fn __repr__(&self) -> String { - let amount_str = format!("{:.*}", self.currency.precision as usize, self.as_f64()); - let code = self.currency.code.as_str(); - format!("Money('{amount_str}', {code})") + format!("{self:?}") } fn __str__(&self) -> String { diff --git a/nautilus_core/model/src/python/types/price.rs b/nautilus_core/model/src/python/types/price.rs index 9819f7bac13c..1924af521f8c 100644 --- a/nautilus_core/model/src/python/types/price.rs +++ b/nautilus_core/model/src/python/types/price.rs @@ -312,7 +312,7 @@ impl Price { } fn __repr__(&self) -> String { - format!("Price('{self:?}')") + format!("{self:?}") } fn __str__(&self) -> String { diff --git a/nautilus_core/model/src/python/types/quantity.rs b/nautilus_core/model/src/python/types/quantity.rs index 6fca30393bf7..6611427dae06 100644 --- a/nautilus_core/model/src/python/types/quantity.rs +++ b/nautilus_core/model/src/python/types/quantity.rs @@ -312,7 +312,7 @@ impl Quantity { } fn __repr__(&self) -> String { - format!("Quantity('{self:?}')") + format!("{self:?}") } fn __str__(&self) -> String { diff --git a/nautilus_core/model/src/types/balance.rs b/nautilus_core/model/src/types/balance.rs index b661a7c9737d..0b01160e6df4 100644 --- a/nautilus_core/model/src/types/balance.rs +++ b/nautilus_core/model/src/types/balance.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display}; use serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ use crate::{ types::{currency::Currency, money::Money}, }; -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -48,23 +48,32 @@ impl AccountBalance { } } -impl Display for AccountBalance { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl PartialEq for AccountBalance { + fn eq(&self, other: &Self) -> bool { + self.total == other.total && self.locked == other.locked && self.free == other.free + } +} + +impl Debug for AccountBalance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "AccountBalance(total={}, locked={}, free={})", - self.total, self.locked, self.free, + "{}(total={}, locked={}, free={})", + stringify!(AccountBalance), + self.total, + self.locked, + self.free, ) } } -impl PartialEq for AccountBalance { - fn eq(&self, other: &Self) -> bool { - self.total == other.total && self.locked == other.locked && self.free == other.free +impl Display for AccountBalance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}",) } } -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -91,21 +100,30 @@ impl MarginBalance { } } -impl Display for MarginBalance { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl PartialEq for MarginBalance { + fn eq(&self, other: &Self) -> bool { + self.initial == other.initial + && self.maintenance == other.maintenance + && self.instrument_id == other.instrument_id + } +} + +impl Debug for MarginBalance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "MarginBalance(initial={}, maintenance={}, instrument_id={})", - self.initial, self.maintenance, self.instrument_id, + "{}(initial={}, maintenance={}, instrument_id={})", + stringify!(MarginBalance), + self.initial, + self.maintenance, + self.instrument_id, ) } } -impl PartialEq for MarginBalance { - fn eq(&self, other: &Self) -> bool { - self.initial == other.initial - && self.maintenance == other.maintenance - && self.instrument_id == other.instrument_id +impl Display for MarginBalance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}",) } } @@ -128,13 +146,20 @@ mod tests { assert_eq!(account_balance_1, account_balance_2); } + #[rstest] + fn test_account_balance_debug(account_balance_test: AccountBalance) { + let result = format!("{account_balance_test:?}"); + let expected = + "AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)"; + assert_eq!(result, expected); + } + #[rstest] fn test_account_balance_display(account_balance_test: AccountBalance) { - let display = format!("{account_balance_test}"); - assert_eq!( - "AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)", - display - ); + let result = format!("{account_balance_test}"); + let expected = + "AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)"; + assert_eq!(result, expected); } #[rstest] @@ -144,6 +169,15 @@ mod tests { assert_eq!(margin_balance_1, margin_balance_2); } + #[rstest] + fn test_margin_balance_debug(margin_balance_test: MarginBalance) { + let display = format!("{margin_balance_test:?}"); + assert_eq!( + "MarginBalance(initial=5000.00 USD, maintenance=20000.00 USD, instrument_id=BTCUSDT.COINBASE)", + display + ); + } + #[rstest] fn test_margin_balance_display(margin_balance_test: MarginBalance) { let display = format!("{margin_balance_test}"); diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 1ddcb7902af2..e9f07ccd0941 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -256,7 +256,7 @@ impl Debug for Money { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{}({:.*}, currency={})", + "{}({:.*}, {})", stringify!(Money), self.currency.precision as usize, self.as_f64(), @@ -311,16 +311,17 @@ mod tests { #[rstest] fn test_debug() { let money = Money::new(1010.12, Currency::USD()).unwrap(); - assert_eq!( - format!("{:?}", money), - format!("Money(1010.12, currency=USD)") - ); + let result = format!("{:?}", money); + let expected = "Money(1010.12, USD)"; + assert_eq!(result, expected); } #[rstest] fn test_display() { let money = Money::new(1010.12, Currency::USD()).unwrap(); - assert_eq!(format!("{money}"), "1010.12 USD"); + let result = format!("{money}"); + let expected = "1010.12 USD"; + assert_eq!(result, expected); } #[rstest] diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index e83c8b48b059..7bc290f082a1 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -15,7 +15,7 @@ use std::{ cmp::Ordering, - fmt::{Debug, Display, Formatter}, + fmt::{Debug, Display}, hash::{Hash, Hasher}, ops::{Add, AddAssign, Deref, Mul, Neg, Sub, SubAssign}, str::FromStr, @@ -256,13 +256,19 @@ impl Mul for Price { } impl Debug for Price { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.*}", self.precision as usize, self.as_f64()) + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}({:.*})", + stringify!(Price), + self.precision as usize, + self.as_f64() + ) } } impl Display for Price { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:.*}", self.precision as usize, self.as_f64()) } } @@ -497,13 +503,17 @@ mod tests { assert!(approx_eq!(f64, result, 1.011, epsilon = 0.000_001)); } + #[rstest] + fn test_debug() { + let price = Price::from_str("44.12").unwrap(); + let result = format!("{price:?}"); + assert_eq!(result, "Price(44.12)"); + } + #[rstest] fn test_display() { - use std::fmt::Write as FmtWrite; - let input_string = "44.12"; - let price = Price::from_str(input_string).unwrap(); - let mut res = String::new(); - write!(&mut res, "{price}").unwrap(); - assert_eq!(res, input_string); + let price = Price::from_str("44.12").unwrap(); + let result = format!("{price}"); + assert_eq!(result, "44.12"); } } diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index 7400e1a30599..8c0673fdfa1e 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -15,7 +15,7 @@ use std::{ cmp::Ordering, - fmt::{Debug, Display, Formatter}, + fmt::{Debug, Display}, hash::{Hash, Hasher}, ops::{Add, AddAssign, Deref, Mul, MulAssign, Sub, SubAssign}, str::FromStr, @@ -247,13 +247,19 @@ impl> MulAssign for Quantity { } impl Debug for Quantity { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.*}", self.precision as usize, self.as_f64()) + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}({:.*})", + stringify!(Quantity), + self.precision as usize, + self.as_f64(), + ) } } impl Display for Quantity { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:.*}", self.precision as usize, self.as_f64()) } } @@ -491,14 +497,17 @@ mod tests { assert!(Quantity::new(0.9, 1).unwrap() <= Quantity::new(1.0, 1).unwrap()); } + #[rstest] + fn test_debug() { + let quantity = Quantity::from_str("44.12").unwrap(); + let result = format!("{quantity:?}"); + assert_eq!(result, "Quantity(44.12)"); + } + #[rstest] fn test_display() { - use std::fmt::Write as FmtWrite; - let input_string = "44.12"; - let qty = Quantity::from_str(input_string).unwrap(); - let mut res = String::new(); - write!(&mut res, "{qty}").unwrap(); - assert_eq!(res, input_string); - assert_eq!(qty.to_string(), input_string); + let quantity = Quantity::from_str("44.12").unwrap(); + let result = format!("{quantity}"); + assert_eq!(result, "44.12"); } } diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index 7cb0e8d55470..1b3f647e6272 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -232,7 +232,7 @@ cdef class Quantity: return f"{self._mem.raw / RUST_FIXED_SCALAR:.{self._mem.precision}f}" def __repr__(self) -> str: - return f"{type(self).__name__}('{self}')" + return f"{type(self).__name__}({self})" @property def raw(self) -> uint64_t: @@ -674,7 +674,7 @@ cdef class Price: return f"{self._mem.raw / RUST_FIXED_SCALAR:.{self._mem.precision}f}" def __repr__(self) -> str: - return f"{type(self).__name__}('{self}')" + return f"{type(self).__name__}({self})" @property def raw(self) -> int64_t: @@ -1054,7 +1054,7 @@ cdef class Money: return f"{self._mem.raw / RUST_FIXED_SCALAR:.{self._mem.currency.precision}f}" def __repr__(self) -> str: - return f"{type(self).__name__}('{str(self)}', {self.currency_code_c()})" + return f"{type(self).__name__}({str(self)}, {self.currency_code_c()})" @property def raw(self) -> int64_t: diff --git a/nautilus_trader/risk/engine.pyx b/nautilus_trader/risk/engine.pyx index 625ca602d4bc..76b3f31fe12c 100644 --- a/nautilus_trader/risk/engine.pyx +++ b/nautilus_trader/risk/engine.pyx @@ -743,9 +743,15 @@ cdef class RiskEngine(Component): return False # Denied elif base_currency is not None and account.type == AccountType.CASH: cash_value = Money(order.quantity.as_f64_c(), base_currency) - self._log.debug(f"Cash value: {cash_value!r}", LogColor.MAGENTA) - free = account.balance_free(base_currency) - self._log.debug(f"Free: {free!r}", LogColor.MAGENTA) + if self.debug: + total = account.balance_total(base_currency) + locked = account.balance_locked(base_currency) + free = account.balance_free(base_currency) + self._log.debug(f"Cash value: {cash_value!r}", LogColor.MAGENTA) + self._log.debug(f"Total: {total!r}", LogColor.MAGENTA) + self._log.debug(f"Locked: {locked!r}", LogColor.MAGENTA) + self._log.debug(f"Free: {free!r}", LogColor.MAGENTA) + if cum_notional_sell is None: cum_notional_sell = cash_value else: diff --git a/tests/unit_tests/model/objects/test_balance_pyo3.py b/tests/unit_tests/model/objects/test_balance_pyo3.py index 1fed1012efb8..34aca125b3fe 100644 --- a/tests/unit_tests/model/objects/test_balance_pyo3.py +++ b/tests/unit_tests/model/objects/test_balance_pyo3.py @@ -30,11 +30,11 @@ def test_account_balance_display(): account_balance = TestTypesProviderPyo3.account_balance() assert ( str(account_balance) - == "AccountBalance(total=1525000.00 USD,locked=25000.00 USD,free=1500000.00 USD)" + == "AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)" ) assert ( repr(account_balance) - == "AccountBalance(total=1525000.00 USD,locked=25000.00 USD,free=1500000.00 USD)" + == "AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)" ) @@ -64,11 +64,11 @@ def test_margin_balance_display(): margin_balance = TestTypesProviderPyo3.margin_balance() assert ( str(margin_balance) - == "MarginBalance(initial=1.00 USD,maintenance=1.00 USD,instrument_id=AUD/USD.SIM)" + == "MarginBalance(initial=1.00 USD, maintenance=1.00 USD, instrument_id=AUD/USD.SIM)" ) assert ( str(margin_balance) - == "MarginBalance(initial=1.00 USD,maintenance=1.00 USD,instrument_id=AUD/USD.SIM)" + == "MarginBalance(initial=1.00 USD, maintenance=1.00 USD, instrument_id=AUD/USD.SIM)" ) diff --git a/tests/unit_tests/model/objects/test_money.py b/tests/unit_tests/model/objects/test_money.py index 64d2838b9753..9a040578c340 100644 --- a/tests/unit_tests/model/objects/test_money.py +++ b/tests/unit_tests/model/objects/test_money.py @@ -164,7 +164,7 @@ def test_repr(self) -> None: result = repr(money) # Assert - assert result == "Money('1.00', USD)" + assert result == "Money(1.00, USD)" def test_from_str_when_malformed_raises_value_error(self) -> None: # Arrange diff --git a/tests/unit_tests/model/objects/test_money_pyo3.py b/tests/unit_tests/model/objects/test_money_pyo3.py index 5863c4d51ca0..4ec1eba44aa6 100644 --- a/tests/unit_tests/model/objects/test_money_pyo3.py +++ b/tests/unit_tests/model/objects/test_money_pyo3.py @@ -153,7 +153,7 @@ def test_repr(self) -> None: result = repr(money) # Assert - assert result == "Money('1.00', USD)" + assert result == "Money(1.00, USD)" @pytest.mark.parametrize( ("value", "currency", "expected"), diff --git a/tests/unit_tests/model/objects/test_price.py b/tests/unit_tests/model/objects/test_price.py index be1fbe722a64..27508f9042cf 100644 --- a/tests/unit_tests/model/objects/test_price.py +++ b/tests/unit_tests/model/objects/test_price.py @@ -569,7 +569,7 @@ def test_repr(self): result = repr(Price(1.1, 1)) # Assert - assert result == "Price('1.1')" + assert result == "Price(1.1)" @pytest.mark.parametrize( ("value", "precision", "expected"), @@ -663,7 +663,7 @@ def test_str_repr(self): # Assert assert str(price) == "1.00000" - assert repr(price) == "Price('1.00000')" + assert repr(price) == "Price(1.00000)" def test_pickle_dumps_and_loads(self): # Arrange diff --git a/tests/unit_tests/model/objects/test_price_pyo3.py b/tests/unit_tests/model/objects/test_price_pyo3.py index 181b2aa9902d..b645c12abd2f 100644 --- a/tests/unit_tests/model/objects/test_price_pyo3.py +++ b/tests/unit_tests/model/objects/test_price_pyo3.py @@ -569,7 +569,7 @@ def test_repr(self): result = repr(Price(1.1, 1)) # Assert - assert result == "Price('1.1')" + assert result == "Price(1.1)" @pytest.mark.parametrize( ("value", "precision", "expected"), @@ -656,7 +656,7 @@ def test_str_repr(self): # Assert assert str(price) == "1.00000" - assert repr(price) == "Price('1.00000')" + assert repr(price) == "Price(1.00000)" def test_pickle_dumps_and_loads(self): # Arrange diff --git a/tests/unit_tests/model/objects/test_quantity.py b/tests/unit_tests/model/objects/test_quantity.py index be66af77a5a3..0e61362da44d 100644 --- a/tests/unit_tests/model/objects/test_quantity.py +++ b/tests/unit_tests/model/objects/test_quantity.py @@ -525,7 +525,7 @@ def test_repr(self): result = repr(Quantity(1.1, 1)) # Assert - assert result == "Quantity('1.1')" + assert result == "Quantity(1.1)" @pytest.mark.parametrize( ("value", "precision", "expected"), @@ -629,7 +629,7 @@ def test_str_repr(self): # Act, Assert assert str(quantity) == "2100.166667" - assert repr(quantity) == "Quantity('2100.166667')" + assert repr(quantity) == "Quantity(2100.166667)" def test_pickle_dumps_and_loads(self): # Arrange diff --git a/tests/unit_tests/model/objects/test_quantity_pyo3.py b/tests/unit_tests/model/objects/test_quantity_pyo3.py index 2180ab4e7c1d..faee0c04914f 100644 --- a/tests/unit_tests/model/objects/test_quantity_pyo3.py +++ b/tests/unit_tests/model/objects/test_quantity_pyo3.py @@ -781,7 +781,7 @@ def test_repr(self): result = repr(Quantity(1.1, 1)) # Assert - assert result == "Quantity('1.1')" + assert result == "Quantity(1.1)" @pytest.mark.parametrize( ("value", "precision", "expected"), @@ -878,7 +878,7 @@ def test_str_repr(self): # Act, Assert assert str(quantity) == "2100.166667" - assert repr(quantity) == "Quantity('2100.166667')" + assert repr(quantity) == "Quantity(2100.166667)" def test_pickle_dumps_and_loads(self): # Arrange diff --git a/tests/unit_tests/model/objects/test_state_pyo3.py b/tests/unit_tests/model/objects/test_state_pyo3.py index b4cf1dfe6e16..6f03222f8e20 100644 --- a/tests/unit_tests/model/objects/test_state_pyo3.py +++ b/tests/unit_tests/model/objects/test_state_pyo3.py @@ -69,11 +69,11 @@ def test_margin_account_state(): ], "margins": [ { - "currency": "USD", - "initial": "1.00", + "type": "MarginBalance", "instrument_id": "AUD/USD.SIM", + "initial": "1.00", "maintenance": "1.00", - "type": "MarginBalance", + "currency": "USD", }, ], "event_id": "91762096-b188-49ea-8562-8d8a4cc22ff2", diff --git a/tests/unit_tests/model/test_orderbook.py b/tests/unit_tests/model/test_orderbook.py index a34ec1d61bb8..2a27997dc43b 100644 --- a/tests/unit_tests/model/test_orderbook.py +++ b/tests/unit_tests/model/test_orderbook.py @@ -282,11 +282,11 @@ def test_add_orders_to_book(self): assert len(book.asks()) == 1 assert ( repr(book.bids()) - == "[Level(price=10.0, orders=[BookOrder { side: Buy, price: 10.0, size: 5, order_id: 10000000000 }])]" + == "[Level(price=10.0, orders=[BookOrder(side=BUY, price=10.0, size=5, order_id=10000000000)])]" ) assert ( repr(book.asks()) - == "[Level(price=11.0, orders=[BookOrder { side: Sell, price: 11.0, size: 6, order_id: 11000000000 }])]" + == "[Level(price=11.0, orders=[BookOrder(side=SELL, price=11.0, size=6, order_id=11000000000)])]" ) bid_level = book.bids()[0] ask_level = book.asks()[0] diff --git a/tests/unit_tests/model/test_orderbook_data.py b/tests/unit_tests/model/test_orderbook_data.py index e91e7ceb78bc..5c23db905810 100644 --- a/tests/unit_tests/model/test_orderbook_data.py +++ b/tests/unit_tests/model/test_orderbook_data.py @@ -109,8 +109,8 @@ def test_hash_str_and_repr(): # Act, Assert assert isinstance(hash(order), int) - assert str(order) == r"BookOrder { side: Buy, price: 100.0, size: 5, order_id: 1 }" - assert repr(order) == r"BookOrder { side: Buy, price: 100.0, size: 5, order_id: 1 }" + assert str(order) == "BookOrder(side=BUY, price=100.0, size=5, order_id=1)" + assert repr(order) == "BookOrder(side=BUY, price=100.0, size=5, order_id=1)" def test_to_dict_returns_expected_dict(): @@ -163,7 +163,7 @@ def test_book_order_from_raw() -> None: ) # Assert - assert str(order) == "BookOrder { side: Buy, price: 10.0, size: 5, order_id: 1 }" + assert str(order) == "BookOrder(side=BUY, price=10.0, size=5, order_id=1)" def test_delta_fully_qualified_name() -> None: @@ -191,7 +191,7 @@ def test_delta_from_raw() -> None: # Assert assert ( str(delta) - == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder { side: Buy, price: 10.0, size: 5, order_id: 1 }, flags=0, sequence=123456789, ts_event=5000000, ts_init=1000000000)" # noqa + == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder(side=BUY, price=10.0, size=5, order_id=1), flags=0, sequence=123456789, ts_event=5000000, ts_init=1000000000)" # noqa ) @@ -245,11 +245,11 @@ def test_delta_hash_str_and_repr() -> None: assert isinstance(hash(delta), int) assert ( str(delta) - == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder { side: Buy, price: 10.0, size: 5, order_id: 1 }, flags=0, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa + == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder(side=BUY, price=10.0, size=5, order_id=1), flags=0, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) assert ( repr(delta) - == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder { side: Buy, price: 10.0, size: 5, order_id: 1 }, flags=0, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa + == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder(side=BUY, price=10.0, size=5, order_id=1), flags=0, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) @@ -269,11 +269,11 @@ def test_delta_with_null_book_order() -> None: assert isinstance(hash(delta), int) assert ( str(delta) - == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder { side: NoOrderSide, price: 0, size: 0, order_id: 0 }, flags=32, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa + == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder(side=NO_ORDER_SIDE, price=0, size=0, order_id=0), flags=32, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) assert ( repr(delta) - == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder { side: NoOrderSide, price: 0, size: 0, order_id: 0 }, flags=32, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa + == "OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder(side=NO_ORDER_SIDE, price=0, size=0, order_id=0), flags=32, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) @@ -467,11 +467,11 @@ def test_deltas_hash_str_and_repr() -> None: assert isinstance(hash(deltas), int) assert ( str(deltas) - == "OrderBookDeltas(instrument_id=AUD/USD.SIM, [OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder { side: Buy, price: 10.0, size: 5, order_id: 1 }, flags=0, sequence=0, ts_event=0, ts_init=0), OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder { side: Buy, price: 10.0, size: 15, order_id: 2 }, flags=0, sequence=1, ts_event=0, ts_init=0)], is_snapshot=False, sequence=1, ts_event=0, ts_init=0)" # noqa + == "OrderBookDeltas(instrument_id=AUD/USD.SIM, [OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder(side=BUY, price=10.0, size=5, order_id=1), flags=0, sequence=0, ts_event=0, ts_init=0), OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder(side=BUY, price=10.0, size=15, order_id=2), flags=0, sequence=1, ts_event=0, ts_init=0)], is_snapshot=False, sequence=1, ts_event=0, ts_init=0)" # noqa ) assert ( repr(deltas) - == "OrderBookDeltas(instrument_id=AUD/USD.SIM, [OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder { side: Buy, price: 10.0, size: 5, order_id: 1 }, flags=0, sequence=0, ts_event=0, ts_init=0), OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder { side: Buy, price: 10.0, size: 15, order_id: 2 }, flags=0, sequence=1, ts_event=0, ts_init=0)], is_snapshot=False, sequence=1, ts_event=0, ts_init=0)" # noqa + == "OrderBookDeltas(instrument_id=AUD/USD.SIM, [OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder(side=BUY, price=10.0, size=5, order_id=1), flags=0, sequence=0, ts_event=0, ts_init=0), OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder(side=BUY, price=10.0, size=15, order_id=2), flags=0, sequence=1, ts_event=0, ts_init=0)], is_snapshot=False, sequence=1, ts_event=0, ts_init=0)" # noqa ) @@ -671,11 +671,11 @@ def test_depth10_hash_str_repr() -> None: assert isinstance(hash(depth), int) assert ( str(depth) - == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder { side: Buy, price: 99.00, size: 100, order_id: 1 }, BookOrder { side: Buy, price: 98.00, size: 200, order_id: 2 }, BookOrder { side: Buy, price: 97.00, size: 300, order_id: 3 }, BookOrder { side: Buy, price: 96.00, size: 400, order_id: 4 }, BookOrder { side: Buy, price: 95.00, size: 500, order_id: 5 }, BookOrder { side: Buy, price: 94.00, size: 600, order_id: 6 }, BookOrder { side: Buy, price: 93.00, size: 700, order_id: 7 }, BookOrder { side: Buy, price: 92.00, size: 800, order_id: 8 }, BookOrder { side: Buy, price: 91.00, size: 900, order_id: 9 }, BookOrder { side: Buy, price: 90.00, size: 1000, order_id: 10 }], asks=[BookOrder { side: Sell, price: 100.00, size: 100, order_id: 11 }, BookOrder { side: Sell, price: 101.00, size: 200, order_id: 12 }, BookOrder { side: Sell, price: 102.00, size: 300, order_id: 13 }, BookOrder { side: Sell, price: 103.00, size: 400, order_id: 14 }, BookOrder { side: Sell, price: 104.00, size: 500, order_id: 15 }, BookOrder { side: Sell, price: 105.00, size: 600, order_id: 16 }, BookOrder { side: Sell, price: 106.00, size: 700, order_id: 17 }, BookOrder { side: Sell, price: 107.00, size: 800, order_id: 18 }, BookOrder { side: Sell, price: 108.00, size: 900, order_id: 19 }, BookOrder { side: Sell, price: 109.00, size: 1000, order_id: 20 }], bid_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ask_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa + == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder(side=BUY, price=99.00, size=100, order_id=1), BookOrder(side=BUY, price=98.00, size=200, order_id=2), BookOrder(side=BUY, price=97.00, size=300, order_id=3), BookOrder(side=BUY, price=96.00, size=400, order_id=4), BookOrder(side=BUY, price=95.00, size=500, order_id=5), BookOrder(side=BUY, price=94.00, size=600, order_id=6), BookOrder(side=BUY, price=93.00, size=700, order_id=7), BookOrder(side=BUY, price=92.00, size=800, order_id=8), BookOrder(side=BUY, price=91.00, size=900, order_id=9), BookOrder(side=BUY, price=90.00, size=1000, order_id=10)], asks=[BookOrder(side=SELL, price=100.00, size=100, order_id=11), BookOrder(side=SELL, price=101.00, size=200, order_id=12), BookOrder(side=SELL, price=102.00, size=300, order_id=13), BookOrder(side=SELL, price=103.00, size=400, order_id=14), BookOrder(side=SELL, price=104.00, size=500, order_id=15), BookOrder(side=SELL, price=105.00, size=600, order_id=16), BookOrder(side=SELL, price=106.00, size=700, order_id=17), BookOrder(side=SELL, price=107.00, size=800, order_id=18), BookOrder(side=SELL, price=108.00, size=900, order_id=19), BookOrder(side=SELL, price=109.00, size=1000, order_id=20)], bid_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ask_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa ) assert ( repr(depth) - == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder { side: Buy, price: 99.00, size: 100, order_id: 1 }, BookOrder { side: Buy, price: 98.00, size: 200, order_id: 2 }, BookOrder { side: Buy, price: 97.00, size: 300, order_id: 3 }, BookOrder { side: Buy, price: 96.00, size: 400, order_id: 4 }, BookOrder { side: Buy, price: 95.00, size: 500, order_id: 5 }, BookOrder { side: Buy, price: 94.00, size: 600, order_id: 6 }, BookOrder { side: Buy, price: 93.00, size: 700, order_id: 7 }, BookOrder { side: Buy, price: 92.00, size: 800, order_id: 8 }, BookOrder { side: Buy, price: 91.00, size: 900, order_id: 9 }, BookOrder { side: Buy, price: 90.00, size: 1000, order_id: 10 }], asks=[BookOrder { side: Sell, price: 100.00, size: 100, order_id: 11 }, BookOrder { side: Sell, price: 101.00, size: 200, order_id: 12 }, BookOrder { side: Sell, price: 102.00, size: 300, order_id: 13 }, BookOrder { side: Sell, price: 103.00, size: 400, order_id: 14 }, BookOrder { side: Sell, price: 104.00, size: 500, order_id: 15 }, BookOrder { side: Sell, price: 105.00, size: 600, order_id: 16 }, BookOrder { side: Sell, price: 106.00, size: 700, order_id: 17 }, BookOrder { side: Sell, price: 107.00, size: 800, order_id: 18 }, BookOrder { side: Sell, price: 108.00, size: 900, order_id: 19 }, BookOrder { side: Sell, price: 109.00, size: 1000, order_id: 20 }], bid_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ask_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa + == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder(side=BUY, price=99.00, size=100, order_id=1), BookOrder(side=BUY, price=98.00, size=200, order_id=2), BookOrder(side=BUY, price=97.00, size=300, order_id=3), BookOrder(side=BUY, price=96.00, size=400, order_id=4), BookOrder(side=BUY, price=95.00, size=500, order_id=5), BookOrder(side=BUY, price=94.00, size=600, order_id=6), BookOrder(side=BUY, price=93.00, size=700, order_id=7), BookOrder(side=BUY, price=92.00, size=800, order_id=8), BookOrder(side=BUY, price=91.00, size=900, order_id=9), BookOrder(side=BUY, price=90.00, size=1000, order_id=10)], asks=[BookOrder(side=SELL, price=100.00, size=100, order_id=11), BookOrder(side=SELL, price=101.00, size=200, order_id=12), BookOrder(side=SELL, price=102.00, size=300, order_id=13), BookOrder(side=SELL, price=103.00, size=400, order_id=14), BookOrder(side=SELL, price=104.00, size=500, order_id=15), BookOrder(side=SELL, price=105.00, size=600, order_id=16), BookOrder(side=SELL, price=106.00, size=700, order_id=17), BookOrder(side=SELL, price=107.00, size=800, order_id=18), BookOrder(side=SELL, price=108.00, size=900, order_id=19), BookOrder(side=SELL, price=109.00, size=1000, order_id=20)], bid_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ask_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa ) From b68a5a95014acea7317a77db0c7c854c4373ffa7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 8 May 2024 12:46:38 +1000 Subject: [PATCH 136/193] Update dependencies --- nautilus_core/Cargo.lock | 133 +++++++++++++++++++-------------------- nautilus_core/Cargo.toml | 8 +-- poetry.lock | 6 +- 3 files changed, 73 insertions(+), 74 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 37a27ba7ac3d..f28fcc3b272f 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -144,9 +144,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "arc-swap" @@ -414,7 +414,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -618,7 +618,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "syn_derive", ] @@ -737,9 +737,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" dependencies = [ "jobserver", "libc", @@ -867,7 +867,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1141,7 +1141,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1152,7 +1152,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1468,7 +1468,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1510,7 +1510,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1520,7 +1520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1781,7 +1781,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1832,9 +1832,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -2887,11 +2887,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2915,9 +2914,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] @@ -2998,7 +2997,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3075,7 +3074,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3207,9 +3206,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem-rfc7468" @@ -3291,7 +3290,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3414,9 +3413,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -3540,7 +3539,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3553,7 +3552,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3945,7 +3944,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.60", + "syn 2.0.61", "unicode-ident", ] @@ -3977,9 +3976,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" @@ -4051,9 +4050,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -4068,15 +4067,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -4133,9 +4132,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "seq-macro" @@ -4145,29 +4144,29 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -4383,7 +4382,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4634,7 +4633,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4656,9 +4655,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -4674,7 +4673,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4790,22 +4789,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4929,7 +4928,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -5079,7 +5078,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -5199,7 +5198,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -5386,7 +5385,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-shared", ] @@ -5420,7 +5419,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5711,22 +5710,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index f83ab88f77cd..25dda1afab9d 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -26,7 +26,7 @@ description = "A high-performance algorithmic trading platform and event-driven documentation = "https://docs.nautilustrader.io" [workspace.dependencies] -anyhow = "1.0.82" +anyhow = "1.0.83" chrono = "0.4.38" derive_builder = "0.20.0" futures = "0.3.30" @@ -41,10 +41,10 @@ rand = "0.8.5" rmp-serde = "1.3.0" rust_decimal = "1.35.0" rust_decimal_macros = "1.34.2" -serde = { version = "1.0.200", features = ["derive"] } -serde_json = "1.0.116" +serde = { version = "1.0.201", features = ["derive"] } +serde_json = "1.0.117" strum = { version = "0.26.2", features = ["derive"] } -thiserror = "1.0.59" +thiserror = "1.0.60" thousands = "0.2.0" tracing = "0.1.40" tokio = { version = "1.37.0", features = ["full"] } diff --git a/poetry.lock b/poetry.lock index 5292b3b95e5c..a0f6c1fe7851 100644 --- a/poetry.lock +++ b/poetry.lock @@ -823,13 +823,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] From eab0c086e2a77abd467e5f3804aca70d2d9be6c3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 8 May 2024 17:51:39 +1000 Subject: [PATCH 137/193] Standardize value type display vs serializable strings --- RELEASES.md | 1 + .../model/src/events/order/filled.rs | 10 ++-- .../model/src/events/order/released.rs | 6 +- .../model/src/events/order/updated.rs | 12 ++-- nautilus_core/model/src/types/price.rs | 2 + nautilus_core/model/src/types/quantity.rs | 1 + nautilus_trader/accounting/manager.pyx | 10 ++-- nautilus_trader/backtest/engine.pyx | 10 ++-- nautilus_trader/backtest/modules.pyx | 4 +- nautilus_trader/cache/database.pyx | 2 +- nautilus_trader/execution/engine.pyx | 2 +- nautilus_trader/execution/messages.pyx | 12 ++-- nautilus_trader/execution/reports.py | 14 ++--- nautilus_trader/live/node.py | 2 +- nautilus_trader/model/events/order.pyx | 30 +++++----- nautilus_trader/model/events/position.pyx | 24 ++++---- nautilus_trader/model/instruments/base.pyx | 4 +- nautilus_trader/model/instruments/cfd.pyx | 4 +- .../model/instruments/commodity.pyx | 4 +- .../model/instruments/crypto_future.pyx | 4 +- .../model/instruments/crypto_perpetual.pyx | 4 +- .../model/instruments/currency_pair.pyx | 4 +- nautilus_trader/model/objects.pxd | 5 +- nautilus_trader/model/objects.pyx | 60 +++++++++++-------- nautilus_trader/model/orders/limit.pyx | 4 +- .../model/orders/limit_if_touched.pyx | 8 +-- nautilus_trader/model/orders/market.pyx | 4 +- .../model/orders/market_if_touched.pyx | 6 +- .../model/orders/market_to_limit.pyx | 6 +- nautilus_trader/model/orders/stop_limit.pyx | 8 +-- nautilus_trader/model/orders/stop_market.pyx | 4 +- .../model/orders/trailing_stop_limit.pyx | 8 +-- .../model/orders/trailing_stop_market.pyx | 6 +- nautilus_trader/model/position.pyx | 6 +- nautilus_trader/risk/engine.pyx | 20 +++---- tests/unit_tests/model/objects/test_money.py | 14 ++--- .../unit_tests/model/objects/test_quantity.py | 2 +- tests/unit_tests/model/test_events.py | 4 +- tests/unit_tests/model/test_events_pyo3.py | 12 ++-- tests/unit_tests/model/test_instrument.py | 2 +- tests/unit_tests/model/test_orders.py | 16 ++--- 41 files changed, 189 insertions(+), 172 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 431119889185..d1f0f3f1da25 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -21,6 +21,7 @@ Released on TBD (UTC). - Fixed IBKR reconnection after gateway/TWS disconnection (#1622), thanks @benjaminsingleton - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) +- Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` --- diff --git a/nautilus_core/model/src/events/order/filled.rs b/nautilus_core/model/src/events/order/filled.rs index 5a5789466179..677ea60d6af6 100644 --- a/nautilus_core/model/src/events/order/filled.rs +++ b/nautilus_core/model/src/events/order/filled.rs @@ -183,8 +183,8 @@ impl Debug for OrderFilled { position_id_str, self.order_side, self.order_type, - self.last_qty, - self.last_px, + self.last_qty.to_formatted_string(), + self.last_px.to_formatted_string(), self.currency, commission_str, self.liquidity_side, @@ -222,8 +222,8 @@ impl Display for OrderFilled { self.position_id.unwrap_or_default(), self.order_side, self.order_type, - self.last_qty, - self.last_px, + self.last_qty.to_formatted_string(), + self.last_px.to_formatted_string(), self.currency, self.commission.unwrap_or(Money::from("0.0 USD")), self.liquidity_side, @@ -248,7 +248,7 @@ mod tests { display, "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, \ venue_order_id=123456, account_id=SIM-001, trade_id=1, position_id=P-001, \ - order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22000 USDT, \ + order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22_000 USDT, \ commission=12.20000000 USDT, liquidity_side=TAKER, ts_event=0)"); } diff --git a/nautilus_core/model/src/events/order/released.rs b/nautilus_core/model/src/events/order/released.rs index 18e2c61ed363..c92ce1527e6c 100644 --- a/nautilus_core/model/src/events/order/released.rs +++ b/nautilus_core/model/src/events/order/released.rs @@ -80,7 +80,7 @@ impl Debug for OrderReleased { self.strategy_id, self.instrument_id, self.client_order_id, - self.released_price, + self.released_price.to_formatted_string(), self.event_id, self.ts_init ) @@ -95,7 +95,7 @@ impl Display for OrderReleased { stringify!(OrderReleased), self.instrument_id, self.client_order_id, - self.released_price, + self.released_price.to_formatted_string(), ) } } @@ -113,7 +113,7 @@ mod tests { let display = format!("{order_released}"); assert_eq!( display, - "OrderReleased(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, released_price=22000)" + "OrderReleased(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, released_price=22_000)" ); } } diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index 2d8a6353c906..3e2a6242f217 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -99,8 +99,8 @@ impl Debug for OrderUpdated { self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), self.quantity, - self.price.map_or("None".to_string(), |price| format!("{price}")), - self.trigger_price.map_or("None".to_string(), |trigger_price| format!("{trigger_price}")), + self.price.map_or("None".to_string(), |price| price.to_formatted_string()), + self.trigger_price.map_or("None".to_string(), |trigger_price| trigger_price.to_formatted_string()), self.event_id, self.ts_event, self.ts_init @@ -118,9 +118,9 @@ impl Display for OrderUpdated { self.client_order_id, self.venue_order_id.map_or("None".to_string(), |venue_order_id| format!("{venue_order_id}")), self.account_id.map_or("None".to_string(), |account_id| format!("{account_id}")), - self.quantity, - self.price.map_or("None".to_string(), |price| format!("{price}")), - self.trigger_price.map_or("None".to_string(), |trigger_price| format!("{trigger_price}")), + self.quantity.to_formatted_string(), + self.price.map_or("None".to_string(), |price| price.to_formatted_string()), + self.trigger_price.map_or("None".to_string(), |trigger_price| trigger_price.to_formatted_string()), self.ts_event ) } @@ -140,7 +140,7 @@ mod tests { let display = format!("{order_updated}"); assert_eq!( display, - "OrderUpdated(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, quantity=100, price=22000, trigger_price=None, ts_event=0)" + "OrderUpdated(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-0000-000-001-1, venue_order_id=001, account_id=SIM-001, quantity=100, price=22_000, trigger_price=None, ts_event=0)" ); } } diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index 7bc290f082a1..fc9e8a6f344a 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -381,6 +381,7 @@ mod tests { assert_eq!(price.raw, -9_223_372_036_000_000_000); assert_eq!(price.as_decimal(), dec!(-9223372036)); assert_eq!(price.to_string(), "-9223372036.000000000"); + assert_eq!(price.to_formatted_string(), "-9_223_372_036.000000000"); } #[rstest] @@ -389,6 +390,7 @@ mod tests { assert_eq!(price.raw, 9_223_372_036_000_000_000); assert_eq!(price.as_decimal(), dec!(9223372036)); assert_eq!(price.to_string(), "9223372036.000000000"); + assert_eq!(price.to_formatted_string(), "9_223_372_036.000000000"); } #[rstest] diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index 8c0673fdfa1e..00665c17c6b1 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -369,6 +369,7 @@ mod tests { assert_eq!(qty.raw, 18_446_744_073_000_000_000); assert_eq!(qty.as_decimal(), dec!(18_446_744_073)); assert_eq!(qty.to_string(), "18446744073.00000000"); + assert_eq!(qty.to_formatted_string(), "18_446_744_073.00000000"); } #[rstest] diff --git a/nautilus_trader/accounting/manager.pyx b/nautilus_trader/accounting/manager.pyx index 2016282f0851..5cc73e6f27a1 100644 --- a/nautilus_trader/accounting/manager.pyx +++ b/nautilus_trader/accounting/manager.pyx @@ -243,7 +243,7 @@ cdef class AccountsManager: cdef Money locked_money = Money(total_locked, currency) account.update_balance_locked(instrument.id, locked_money) - self._log.info(f"{instrument.id} balance_locked={locked_money.to_str()}") + self._log.info(f"{instrument.id} balance_locked={locked_money.to_formatted_str()}") return self._generate_account_state( account=account, @@ -334,7 +334,7 @@ cdef class AccountsManager: else: account.update_margin_init(instrument.id, margin_init_money) - self._log.info(f"{instrument.id} margin_init={margin_init_money.to_str()}") + self._log.info(f"{instrument.id} margin_init={margin_init_money.to_formatted_str()}") return self._generate_account_state( account=account, @@ -425,7 +425,7 @@ cdef class AccountsManager: else: account.update_margin_maint(instrument.id, margin_maint_money) - self._log.info(f"{instrument.id} margin_maint={margin_maint_money.to_str()}") + self._log.info(f"{instrument.id} margin_maint={margin_maint_money.to_formatted_str()}") return self._generate_account_state( account=account, @@ -523,7 +523,7 @@ cdef class AccountsManager: if commission._mem.raw > 0: self._log.error( f"Cannot complete transaction: no {commission.currency} " - f"balance to deduct a {commission.to_str()} commission from" + f"balance to deduct a {commission.to_formatted_str()} commission from" ) return else: @@ -546,7 +546,7 @@ cdef class AccountsManager: if pnl._mem.raw < 0: self._log.error( "Cannot complete transaction: " - f"no {pnl.currency} to deduct a {pnl.to_str()} realized PnL from" + f"no {pnl.currency} to deduct a {pnl.to_formatted_str()} realized PnL from" ) return new_balance = AccountBalance( diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index e8ab3bd31e2a..96ccc1b5bebb 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -1254,7 +1254,7 @@ cdef class BacktestEngine: self._log.warning(f"ACCOUNT FROZEN") else: for b in account.starting_balances().values(): - self._log.info(b.to_str()) + self._log.info(b.to_formatted_str()) def _log_run(self, start: pd.Timestamp, end: pd.Timestamp): cdef str color = self._get_log_color_code() @@ -1326,15 +1326,15 @@ cdef class BacktestEngine: continue self._log.info(f"Balances starting:") for b in account.starting_balances().values(): - self._log.info(b.to_str()) + self._log.info(b.to_formatted_str()) self._log.info(f"{color}-----------------------------------------------------------------") self._log.info(f"Balances ending:") for b in account.balances_total().values(): - self._log.info(b.to_str()) + self._log.info(b.to_formatted_str()) self._log.info(f"{color}-----------------------------------------------------------------") self._log.info(f"Commissions:") for c in account.commissions().values(): - self._log.info(Money(-c.as_double(), c.currency).to_str()) # Display commission as negative + self._log.info(Money(-c.as_double(), c.currency).to_formatted_str()) # Display commission as negative self._log.info(f"{color}-----------------------------------------------------------------") self._log.info(f"Unrealized PnLs (included in totals):") unrealized_pnls = self.portfolio.unrealized_pnls(Venue(venue.id.value)) @@ -1342,7 +1342,7 @@ cdef class BacktestEngine: self._log.info("None") else: for b in unrealized_pnls.values(): - self._log.info(b.to_str()) + self._log.info(b.to_formatted_str()) # Log output diagnostics for all simulation modules for module in venue.modules: diff --git a/nautilus_trader/backtest/modules.pyx b/nautilus_trader/backtest/modules.pyx index a8fda19915a5..9e8d65870dd6 100644 --- a/nautilus_trader/backtest/modules.pyx +++ b/nautilus_trader/backtest/modules.pyx @@ -209,9 +209,9 @@ cdef class FXRolloverInterestModule(SimulationModule): The logger to log to. """ - account_balances_starting = ', '.join([b.to_str() for b in self.exchange.starting_balances]) + account_balances_starting = ', '.join([b.to_formatted_str() for b in self.exchange.starting_balances]) account_starting_length = len(account_balances_starting) - rollover_totals = ', '.join([b.to_str() for b in self._rollover_totals.values()]) + rollover_totals = ', '.join([b.to_formatted_str() for b in self._rollover_totals.values()]) logger.info(f"Rollover interest (totals): {rollover_totals}") cpdef void reset(self): diff --git a/nautilus_trader/cache/database.pyx b/nautilus_trader/cache/database.pyx index bcf3da745209..f77a04ae11b5 100644 --- a/nautilus_trader/cache/database.pyx +++ b/nautilus_trader/cache/database.pyx @@ -1104,7 +1104,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): cdef dict position_state = position.to_dict() if unrealized_pnl is not None: - position_state["unrealized_pnl"] = unrealized_pnl.to_str() + position_state["unrealized_pnl"] = str(unrealized_pnl) position_state["ts_snapshot"] = ts_snapshot diff --git a/nautilus_trader/execution/engine.pyx b/nautilus_trader/execution/engine.pyx index 9ba45201d4ef..26e11c3d3189 100644 --- a/nautilus_trader/execution/engine.pyx +++ b/nautilus_trader/execution/engine.pyx @@ -1231,7 +1231,7 @@ cdef class ExecutionEngine(Component): cdef dict position_state = position.to_dict() cdef Money unrealized_pnl = self._cache.calculate_unrealized_pnl(position) if unrealized_pnl is not None: - position_state["unrealized_pnl"] = unrealized_pnl.to_str() + position_state["unrealized_pnl"] = str(unrealized_pnl) if self._msgbus.serializer is not None: self._msgbus.publish( topic=f"snapshots:positions:{position.id}", diff --git a/nautilus_trader/execution/messages.pyx b/nautilus_trader/execution/messages.pyx index 4df5204f3d20..2e252f6b101b 100644 --- a/nautilus_trader/execution/messages.pyx +++ b/nautilus_trader/execution/messages.pyx @@ -413,9 +413,9 @@ cdef class ModifyOrder(TradingCommand): f"instrument_id={self.instrument_id.to_str()}, " f"client_order_id={self.client_order_id.to_str()}, " f"venue_order_id={self.venue_order_id}, " # Can be None - f"quantity={self.quantity.to_str() if self.quantity is not None else None}, " - f"price={self.price}, " - f"trigger_price={self.trigger_price})" + f"quantity={self.quantity.to_formatted_str() if self.quantity is not None else None}, " + f"price={self.price.to_formatted_str() if self.price is not None else None}, " + f"trigger_price={self.trigger_price.to_formatted_str() if self.trigger_price is not None else None})" ) def __repr__(self) -> str: @@ -427,9 +427,9 @@ cdef class ModifyOrder(TradingCommand): f"instrument_id={self.instrument_id.to_str()}, " f"client_order_id={self.client_order_id.to_str()}, " f"venue_order_id={self.venue_order_id}, " # Can be None - f"quantity={self.quantity.to_str() if self.quantity is not None else None}, " - f"price={self.price}, " - f"trigger_price={self.trigger_price}, " + f"quantity={self.quantity.to_formatted_str() if self.quantity is not None else None}, " + f"price={self.price.to_formatted_str() if self.price is not None else None}, " + f"trigger_price={self.trigger_price.to_formatted_str() if self.trigger_price is not None else None}, " f"command_id={self.id.to_str()}, " f"ts_init={self.ts_init})" ) diff --git a/nautilus_trader/execution/reports.py b/nautilus_trader/execution/reports.py index f6778688f70a..3ab89106cadc 100644 --- a/nautilus_trader/execution/reports.py +++ b/nautilus_trader/execution/reports.py @@ -259,9 +259,9 @@ def __repr__(self) -> str: f"limit_offset={self.limit_offset}, " f"trailing_offset={self.trailing_offset}, " f"trailing_offset_type={trailing_offset_type_to_str(self.trailing_offset_type)}, " - f"quantity={self.quantity.to_str()}, " - f"filled_qty={self.filled_qty.to_str()}, " - f"leaves_qty={self.leaves_qty.to_str()}, " + f"quantity={self.quantity.to_formatted_str()}, " + f"filled_qty={self.filled_qty.to_formatted_str()}, " + f"leaves_qty={self.leaves_qty.to_formatted_str()}, " f"display_qty={self.display_qty}, " f"avg_px={self.avg_px}, " f"post_only={self.post_only}, " @@ -377,9 +377,9 @@ def __repr__(self) -> str: f"venue_position_id={self.venue_position_id}, " f"trade_id={self.trade_id}, " f"order_side={order_side_to_str(self.order_side)}, " - f"last_qty={self.last_qty.to_str()}, " - f"last_px={self.last_px}, " - f"commission={self.commission.to_str() if self.commission is not None else None}, " + f"last_qty={self.last_qty.to_formatted_str()}, " + f"last_px={self.last_px.to_formatted_str()}, " + f"commission={self.commission.to_formatted_str() if self.commission is not None else None}, " f"liquidity_side={liquidity_side_to_str(self.liquidity_side)}, " f"report_id={self.id}, " f"ts_event={self.ts_event}, " @@ -449,7 +449,7 @@ def __repr__(self) -> str: f"instrument_id={self.instrument_id}, " f"venue_position_id={self.venue_position_id}, " f"position_side={position_side_to_str(self.position_side)}, " - f"quantity={self.quantity.to_str()}, " + f"quantity={self.quantity.to_formatted_str()}, " f"signed_decimal_qty={self.signed_decimal_qty}, " f"report_id={self.id}, " f"ts_last={self.ts_last}, " diff --git a/nautilus_trader/live/node.py b/nautilus_trader/live/node.py index 5d917174c7cb..9c66b2c31dde 100644 --- a/nautilus_trader/live/node.py +++ b/nautilus_trader/live/node.py @@ -376,7 +376,7 @@ async def snapshot_open_positions(self, interval: float) -> None: position_state = position.to_dict() unrealized_pnl = self.kernel.cache.calculate_unrealized_pnl(position) if unrealized_pnl is not None: - position_state["unrealized_pnl"] = unrealized_pnl.to_str() + position_state["unrealized_pnl"] = str(unrealized_pnl) self.kernel.msgbus.publish( topic=f"snapshots:positions:{position.id}", msg=self.kernel.msgbus.serializer.serialize(position_state), diff --git a/nautilus_trader/model/events/order.pyx b/nautilus_trader/model/events/order.pyx index 3e5147d432b1..9a7ce29ac1f7 100644 --- a/nautilus_trader/model/events/order.pyx +++ b/nautilus_trader/model/events/order.pyx @@ -342,7 +342,7 @@ cdef class OrderInitialized(OrderEvent): f"client_order_id={self.client_order_id}, " f"side={order_side_to_str(self.side)}, " f"type={order_type_to_str(self.order_type)}, " - f"quantity={self.quantity.to_str()}, " + f"quantity={self.quantity.to_formatted_str()}, " f"time_in_force={time_in_force_to_str(self.time_in_force)}, " f"post_only={self.post_only}, " f"reduce_only={self.reduce_only}, " @@ -373,7 +373,7 @@ cdef class OrderInitialized(OrderEvent): f"client_order_id={self.client_order_id}, " f"side={order_side_to_str(self.side)}, " f"type={order_type_to_str(self.order_type)}, " - f"quantity={self.quantity.to_str()}, " + f"quantity={self.quantity.to_formatted_str()}, " f"time_in_force={time_in_force_to_str(self.time_in_force)}, " f"post_only={self.post_only}, " f"reduce_only={self.reduce_only}, " @@ -4252,9 +4252,9 @@ cdef class OrderUpdated(OrderEvent): f"client_order_id={self.client_order_id}, " f"venue_order_id={self.venue_order_id}, " f"account_id={self.account_id}, " - f"quantity={self.quantity.to_str()}, " - f"price={self.price}, " - f"trigger_price={self.trigger_price}, " + f"quantity={self.quantity.to_formatted_str() if self.quantity else None}, " + f"price={self.price.to_formatted_str() if self.price else None}, " + f"trigger_price={self.trigger_price.to_formatted_str() if self.trigger_price else None}, " f"ts_event={self.ts_event})" ) @@ -4267,9 +4267,9 @@ cdef class OrderUpdated(OrderEvent): f"client_order_id={self.client_order_id}, " f"venue_order_id={self.venue_order_id}, " f"account_id={self.account_id}, " - f"quantity={self.quantity.to_str()}, " - f"price={self.price}, " - f"trigger_price={self.trigger_price}, " + f"quantity={self.quantity.to_formatted_str() if self.quantity else None}, " + f"price={self.price.to_formatted_str() if self.price else None}, " + f"trigger_price={self.trigger_price.to_formatted_str() if self.trigger_price else None}, " f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" @@ -4594,9 +4594,9 @@ cdef class OrderFilled(OrderEvent): f"position_id={self.position_id}, " f"order_side={order_side_to_str(self.order_side)}, " f"order_type={order_type_to_str(self.order_type)}, " - f"last_qty={self.last_qty}, " - f"last_px={self.last_px} {self.currency.code}, " - f"commission={self.commission.to_str()}, " + f"last_qty={self.last_qty.to_formatted_str()}, " + f"last_px={self.last_px.to_formatted_str()} {self.currency.code}, " + f"commission={self.commission.to_formatted_str()}, " f"liquidity_side={liquidity_side_to_str(self.liquidity_side)}, " f"ts_event={self.ts_event})" ) @@ -4614,9 +4614,9 @@ cdef class OrderFilled(OrderEvent): f"position_id={self.position_id}, " f"order_side={order_side_to_str(self.order_side)}, " f"order_type={order_type_to_str(self.order_type)}, " - f"last_qty={self.last_qty}, " - f"last_px={self.last_px} {self.currency.code}, " - f"commission={self.commission.to_str()}, " + f"last_qty={self.last_qty.to_formatted_str()}, " + f"last_px={self.last_px.to_formatted_str()} {self.currency.code}, " + f"commission={self.commission.to_formatted_str()}, " f"liquidity_side={liquidity_side_to_str(self.liquidity_side)}, " f"event_id={self.id}, " f"ts_event={self.ts_event}, " @@ -4791,7 +4791,7 @@ cdef class OrderFilled(OrderEvent): "last_qty": str(obj.last_qty), "last_px": str(obj.last_px), "currency": obj.currency.code, - "commission": obj.commission.to_str(), + "commission": str(obj.commission), "liquidity_side": liquidity_side_to_str(obj.liquidity_side), "event_id": obj.id.value, "ts_event": obj.ts_event, diff --git a/nautilus_trader/model/events/position.pyx b/nautilus_trader/model/events/position.pyx index d89cc6df4b65..0fd73b4c0462 100644 --- a/nautilus_trader/model/events/position.pyx +++ b/nautilus_trader/model/events/position.pyx @@ -173,14 +173,14 @@ cdef class PositionEvent(Event): f"entry={order_side_to_str(self.entry)}, " f"side={position_side_to_str(self.side)}, " f"signed_qty={self.signed_qty}, " - f"quantity={self.quantity.to_str()}, " - f"peak_qty={self.peak_qty.to_str()}, " + f"quantity={self.quantity.to_formatted_str()}, " + f"peak_qty={self.peak_qty.to_formatted_str()}, " f"currency={self.currency.code}, " f"avg_px_open={self.avg_px_open}, " f"avg_px_close={self.avg_px_close}, " f"realized_return={self.realized_return:.5f}, " - f"realized_pnl={self.realized_pnl.to_str()}, " - f"unrealized_pnl={self.unrealized_pnl.to_str()}, " + f"realized_pnl={self.realized_pnl.to_formatted_str()}, " + f"unrealized_pnl={self.unrealized_pnl.to_formatted_str()}, " f"ts_opened={self.ts_opened}, " f"ts_last={self.ts_event}, " f"ts_closed={self.ts_closed}, " @@ -200,14 +200,14 @@ cdef class PositionEvent(Event): f"entry={order_side_to_str(self.entry)}, " f"side={position_side_to_str(self.side)}, " f"signed_qty={self.signed_qty}, " - f"quantity={self.quantity.to_str()}, " - f"peak_qty={self.peak_qty.to_str()}, " + f"quantity={self.quantity.to_formatted_str()}, " + f"peak_qty={self.peak_qty.to_formatted_str()}, " f"currency={self.currency.code}, " f"avg_px_open={self.avg_px_open}, " f"avg_px_close={self.avg_px_close}, " f"realized_return={self.realized_return:.5f}, " - f"realized_pnl={self.realized_pnl.to_str()}, " - f"unrealized_pnl={self.unrealized_pnl.to_str()}, " + f"realized_pnl={self.realized_pnl.to_formatted_str()}, " + f"unrealized_pnl={self.unrealized_pnl.to_formatted_str()}, " f"ts_opened={self.ts_opened}, " f"ts_last={self._ts_event}, " f"ts_closed={self.ts_closed}, " @@ -430,7 +430,7 @@ cdef class PositionOpened(PositionEvent): "last_px": str(obj.last_px), "currency": obj.currency.code, "avg_px_open": obj.avg_px_open, - "realized_pnl": obj.realized_pnl.to_str(), + "realized_pnl": str(obj.realized_pnl), "duration_ns": obj.duration_ns, "event_id": obj._event_id.to_str(), "ts_event": obj._ts_event, @@ -695,8 +695,8 @@ cdef class PositionChanged(PositionEvent): "avg_px_open": obj.avg_px_open, "avg_px_close": obj.avg_px_close, "realized_return": obj.realized_return, - "realized_pnl": obj.realized_pnl.to_str(), - "unrealized_pnl": obj.unrealized_pnl.to_str(), + "realized_pnl": str(obj.realized_pnl), + "unrealized_pnl": str(obj.unrealized_pnl), "event_id": obj._event_id.to_str(), "ts_opened": obj.ts_opened, "ts_event": obj._ts_event, @@ -967,7 +967,7 @@ cdef class PositionClosed(PositionEvent): "avg_px_open": obj.avg_px_open, "avg_px_close": obj.avg_px_close, "realized_return": obj.realized_return, - "realized_pnl": obj.realized_pnl.to_str(), + "realized_pnl": str(obj.realized_pnl), "event_id": obj._event_id.to_str(), "ts_opened": obj.ts_opened, "ts_closed": obj.ts_closed, diff --git a/nautilus_trader/model/instruments/base.pyx b/nautilus_trader/model/instruments/base.pyx index 030a54fa19fe..982799a57d24 100644 --- a/nautilus_trader/model/instruments/base.pyx +++ b/nautilus_trader/model/instruments/base.pyx @@ -318,8 +318,8 @@ cdef class Instrument(Data): "lot_size": str(obj.lot_size) if obj.lot_size is not None else None, "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, - "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, - "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_notional": str(obj.max_notional) if obj.max_notional is not None else None, + "min_notional": str(obj.min_notional) if obj.min_notional is not None else None, "max_price": str(obj.max_price) if obj.max_price is not None else None, "min_price": str(obj.min_price) if obj.min_price is not None else None, "margin_init": str(obj.margin_init), diff --git a/nautilus_trader/model/instruments/cfd.pyx b/nautilus_trader/model/instruments/cfd.pyx index ba9a8d5ab304..ade41ec57c03 100644 --- a/nautilus_trader/model/instruments/cfd.pyx +++ b/nautilus_trader/model/instruments/cfd.pyx @@ -236,8 +236,8 @@ cdef class Cfd(Instrument): "lot_size": str(obj.lot_size) if obj.lot_size is not None else None, "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, - "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, - "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_notional": str(obj.max_notional) if obj.max_notional is not None else None, + "min_notional": str(obj.min_notional) if obj.min_notional is not None else None, "max_price": str(obj.max_price) if obj.max_price is not None else None, "min_price": str(obj.min_price) if obj.min_price is not None else None, "margin_init": str(obj.margin_init), diff --git a/nautilus_trader/model/instruments/commodity.pyx b/nautilus_trader/model/instruments/commodity.pyx index 5f2f4b312ed3..381b5a7b63a1 100644 --- a/nautilus_trader/model/instruments/commodity.pyx +++ b/nautilus_trader/model/instruments/commodity.pyx @@ -225,8 +225,8 @@ cdef class Commodity(Instrument): "lot_size": str(obj.lot_size) if obj.lot_size is not None else None, "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, - "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, - "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_notional": str(obj.max_notional) if obj.max_notional is not None else None, + "min_notional": str(obj.min_notional) if obj.min_notional is not None else None, "max_price": str(obj.max_price) if obj.max_price is not None else None, "min_price": str(obj.min_price) if obj.min_price is not None else None, "margin_init": str(obj.margin_init), diff --git a/nautilus_trader/model/instruments/crypto_future.pyx b/nautilus_trader/model/instruments/crypto_future.pyx index 35775231c4f7..bb6f3ba69b7c 100644 --- a/nautilus_trader/model/instruments/crypto_future.pyx +++ b/nautilus_trader/model/instruments/crypto_future.pyx @@ -311,8 +311,8 @@ cdef class CryptoFuture(Instrument): "lot_size": str(obj.lot_size), "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, - "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, - "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_notional": str(obj.max_notional) if obj.max_notional is not None else None, + "min_notional": str(obj.min_notional) if obj.min_notional is not None else None, "max_price": str(obj.max_price) if obj.max_price is not None else None, "min_price": str(obj.min_price) if obj.min_price is not None else None, "margin_init": str(obj.margin_init), diff --git a/nautilus_trader/model/instruments/crypto_perpetual.pyx b/nautilus_trader/model/instruments/crypto_perpetual.pyx index 5becf9eed4d6..d8022a356ca3 100644 --- a/nautilus_trader/model/instruments/crypto_perpetual.pyx +++ b/nautilus_trader/model/instruments/crypto_perpetual.pyx @@ -238,8 +238,8 @@ cdef class CryptoPerpetual(Instrument): "lot_size": str(obj.lot_size), "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, - "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, - "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_notional": str(obj.max_notional) if obj.max_notional is not None else None, + "min_notional": str(obj.min_notional) if obj.min_notional is not None else None, "max_price": str(obj.max_price) if obj.max_price is not None else None, "min_price": str(obj.min_price) if obj.min_price is not None else None, "margin_init": str(obj.margin_init), diff --git a/nautilus_trader/model/instruments/currency_pair.pyx b/nautilus_trader/model/instruments/currency_pair.pyx index ca4199d26269..6cfeeacf06d2 100644 --- a/nautilus_trader/model/instruments/currency_pair.pyx +++ b/nautilus_trader/model/instruments/currency_pair.pyx @@ -244,8 +244,8 @@ cdef class CurrencyPair(Instrument): "lot_size": str(obj.lot_size) if obj.lot_size is not None else None, "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, - "max_notional": obj.max_notional.to_str() if obj.max_notional is not None else None, - "min_notional": obj.min_notional.to_str() if obj.min_notional is not None else None, + "max_notional": str(obj.max_notional) if obj.max_notional is not None else None, + "min_notional": str(obj.min_notional) if obj.min_notional is not None else None, "max_price": str(obj.max_price) if obj.max_price is not None else None, "min_price": str(obj.min_price) if obj.min_price is not None else None, "margin_init": str(obj.margin_init), diff --git a/nautilus_trader/model/objects.pxd b/nautilus_trader/model/objects.pxd index 82974f36dbd9..9a41d07795f2 100644 --- a/nautilus_trader/model/objects.pxd +++ b/nautilus_trader/model/objects.pxd @@ -69,7 +69,7 @@ cdef class Quantity: @staticmethod cdef Quantity from_int_c(int value) - cpdef str to_str(self) + cpdef str to_formatted_str(self) cpdef object as_decimal(self) cpdef double as_double(self) @@ -115,6 +115,7 @@ cdef class Price: @staticmethod cdef Price from_int_c(int value) + cpdef str to_formatted_str(self) cpdef object as_decimal(self) cpdef double as_double(self) @@ -146,7 +147,7 @@ cdef class Money: cdef void add_assign(self, Money other) cdef void sub_assign(self, Money other) - cpdef str to_str(self) + cpdef str to_formatted_str(self) cpdef object as_decimal(self) cpdef double as_double(self) diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index 1b3f647e6272..2373b8b35222 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -479,7 +479,7 @@ cdef class Quantity: return Quantity.from_int_c(value) - cpdef str to_str(self): + cpdef str to_formatted_str(self): """ Return the formatted string representation of the quantity. @@ -879,6 +879,17 @@ cdef class Price: return Price.from_int_c(value) + cpdef str to_formatted_str(self): + """ + Return the formatted string representation of the price. + + Returns + ------- + str + + """ + return f"{self.as_f64_c():,.{self._mem.precision}f}".replace(",", "_") + cpdef object as_decimal(self): """ Return the value as a built-in `Decimal`. @@ -1051,10 +1062,11 @@ cdef class Money: return hash((self._mem.raw, self.currency_code_c())) def __str__(self) -> str: - return f"{self._mem.raw / RUST_FIXED_SCALAR:.{self._mem.currency.precision}f}" + return f"{self._mem.raw / RUST_FIXED_SCALAR:.{self._mem.currency.precision}f} {self.currency_code_c()}" def __repr__(self) -> str: - return f"{type(self).__name__}({str(self)}, {self.currency_code_c()})" + cdef str amount = f"{self._mem.raw / RUST_FIXED_SCALAR:.{self._mem.currency.precision}f}" + return f"{type(self).__name__}({amount}, {self.currency_code_c()})" @property def raw(self) -> int64_t: @@ -1201,6 +1213,17 @@ cdef class Money: return Money.from_str_c(value) + cpdef str to_formatted_str(self): + """ + Return the formatted string representation of the money. + + Returns + ------- + str + + """ + return f"{self.as_f64_c():,.{self._mem.currency.precision}f} {self.currency_code_c()}".replace(",", "_") + cpdef object as_decimal(self): """ Return the value as a built-in `Decimal`. @@ -1223,17 +1246,6 @@ cdef class Money: """ return self.as_f64_c() - cpdef str to_str(self): - """ - Return the formatted string representation of the money. - - Returns - ------- - str - - """ - return f"{self.as_f64_c():,.{self._mem.currency.precision}f} {self.currency_code_c()}".replace(",", "_") - cdef class Currency: """ @@ -1596,9 +1608,9 @@ cdef class AccountBalance: def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"total={self.total.to_str()}, " - f"locked={self.locked.to_str()}, " - f"free={self.free.to_str()})" + f"total={self.total.to_formatted_str()}, " + f"locked={self.locked.to_formatted_str()}, " + f"free={self.free.to_formatted_str()})" ) @staticmethod @@ -1639,9 +1651,9 @@ cdef class AccountBalance: """ return { "type": type(self).__name__, - "total": str(self.total), - "locked": str(self.locked), - "free": str(self.free), + "total": str(self.total.as_decimal()), + "locked": str(self.locked.as_decimal()), + "free": str(self.free.as_decimal()), "currency": self.currency.code, } @@ -1694,8 +1706,8 @@ cdef class MarginBalance: def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"initial={self.initial.to_str()}, " - f"maintenance={self.maintenance.to_str()}, " + f"initial={self.initial.to_formatted_str()}, " + f"maintenance={self.maintenance.to_formatted_str()}, " f"instrument_id={self.instrument_id.to_str() if self.instrument_id is not None else None})" ) @@ -1738,8 +1750,8 @@ cdef class MarginBalance: """ return { "type": type(self).__name__, - "initial": str(self.initial), - "maintenance": str(self.maintenance), + "initial": str(self.initial.as_decimal()), + "maintenance": str(self.maintenance.as_decimal()), "currency": self.currency.code, "instrument_id": self.instrument_id.to_str() if self.instrument_id is not None else None, } diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 38fff7506ff8..33758287c014 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -254,7 +254,7 @@ cdef class LimitOrder(Order): cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " f"{order_type_to_str(self.order_type)} @ {self.price} " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" f"{emulation_str}" @@ -323,7 +323,7 @@ cdef class LimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else {}, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/limit_if_touched.pyx b/nautilus_trader/model/orders/limit_if_touched.pyx index 7a301b2e2281..d141303c5299 100644 --- a/nautilus_trader/model/orders/limit_if_touched.pyx +++ b/nautilus_trader/model/orders/limit_if_touched.pyx @@ -279,9 +279,9 @@ cdef class LimitIfTouchedOrder(Order): cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " - f"{order_type_to_str(self.order_type)} @ {self.trigger_price}-STOP" - f"[{trigger_type_to_str(self.trigger_type)}] {self.price}-LIMIT " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " + f"{order_type_to_str(self.order_type)} @ {self.trigger_price.to_formatted_str()}-STOP" + f"[{trigger_type_to_str(self.trigger_type)}] {self.price.to_formatted_str()}-LIMIT " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" f"{emulation_str}" ) @@ -317,7 +317,7 @@ cdef class LimitIfTouchedOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index 5cbed929bf04..4c1d07cf5aa8 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -186,7 +186,7 @@ cdef class MarketOrder(Order): """ return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " f"{order_type_to_str(self.order_type)} " f"{time_in_force_to_str(self.time_in_force)}" ) @@ -248,7 +248,7 @@ cdef class MarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else {}, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "emulation_trigger": trigger_type_to_str(self.emulation_trigger), "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), diff --git a/nautilus_trader/model/orders/market_if_touched.pyx b/nautilus_trader/model/orders/market_if_touched.pyx index 3641563975cf..a1bdf1632aa3 100644 --- a/nautilus_trader/model/orders/market_if_touched.pyx +++ b/nautilus_trader/model/orders/market_if_touched.pyx @@ -248,8 +248,8 @@ cdef class MarketIfTouchedOrder(Order): cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " - f"{order_type_to_str(self.order_type)} @ {self.trigger_price}" + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " + f"{order_type_to_str(self.order_type)} @ {self.trigger_price.to_formatted_str()}" f"[{trigger_type_to_str(self.trigger_type)}] " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" f"{emulation_str}" @@ -285,7 +285,7 @@ cdef class MarketIfTouchedOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, diff --git a/nautilus_trader/model/orders/market_to_limit.pyx b/nautilus_trader/model/orders/market_to_limit.pyx index 7a5c51e5447c..74e4ec27b0f2 100644 --- a/nautilus_trader/model/orders/market_to_limit.pyx +++ b/nautilus_trader/model/orders/market_to_limit.pyx @@ -229,8 +229,8 @@ cdef class MarketToLimitOrder(Order): """ cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " - f"{order_type_to_str(self.order_type)} @ {self.price} " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " + f"{order_type_to_str(self.order_type)} @ {self.price.to_formatted_str() if self.price else None} " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" ) @@ -266,7 +266,7 @@ cdef class MarketToLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, diff --git a/nautilus_trader/model/orders/stop_limit.pyx b/nautilus_trader/model/orders/stop_limit.pyx index d518b635faa8..01e66e4f8fcc 100644 --- a/nautilus_trader/model/orders/stop_limit.pyx +++ b/nautilus_trader/model/orders/stop_limit.pyx @@ -284,9 +284,9 @@ cdef class StopLimitOrder(Order): cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " - f"{order_type_to_str(self.order_type)} @ {self.trigger_price}-STOP" - f"[{trigger_type_to_str(self.trigger_type)}] {self.price}-LIMIT " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " + f"{order_type_to_str(self.order_type)} @ {self.trigger_price.to_formatted_str()}-STOP" + f"[{trigger_type_to_str(self.trigger_type)}] {self.price.to_formatted_str()}-LIMIT " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" f"{emulation_str}" ) @@ -359,7 +359,7 @@ cdef class StopLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else {}, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/stop_market.pyx b/nautilus_trader/model/orders/stop_market.pyx index 899df7a64349..240c1d1e210f 100644 --- a/nautilus_trader/model/orders/stop_market.pyx +++ b/nautilus_trader/model/orders/stop_market.pyx @@ -253,7 +253,7 @@ cdef class StopMarketOrder(Order): cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " f"{order_type_to_str(self.order_type)} @ {self.trigger_price}" f"[{trigger_type_to_str(self.trigger_type)}] " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" @@ -290,7 +290,7 @@ cdef class StopMarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, diff --git a/nautilus_trader/model/orders/trailing_stop_limit.pyx b/nautilus_trader/model/orders/trailing_stop_limit.pyx index 3a86c210e916..d91439de2cbe 100644 --- a/nautilus_trader/model/orders/trailing_stop_limit.pyx +++ b/nautilus_trader/model/orders/trailing_stop_limit.pyx @@ -290,10 +290,10 @@ cdef class TrailingStopLimitOrder(Order): cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " f"{order_type_to_str(self.order_type)}[{trigger_type_to_str(self.trigger_type)}] " - f"{'@ ' + str(self.trigger_price) + '-STOP ' if self.trigger_price else ''}" - f"[{trigger_type_to_str(self.trigger_type)}] {self.price}-LIMIT " + f"{'@ ' + self.trigger_price.to_formatted_str() + '-STOP ' if self.trigger_price else ''}" + f"[{trigger_type_to_str(self.trigger_type)}] {self.price.to_formatted_str() if self.price else None}-LIMIT " f"{self.trailing_offset}-TRAILING_OFFSET[{trailing_offset_type_to_str(self.trailing_offset_type)}] " f"{self.limit_offset}-LIMIT_OFFSET[{trailing_offset_type_to_str(self.trailing_offset_type)}] " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" @@ -334,7 +334,7 @@ cdef class TrailingStopLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/trailing_stop_market.pyx b/nautilus_trader/model/orders/trailing_stop_market.pyx index f56793af84ca..e75381510295 100644 --- a/nautilus_trader/model/orders/trailing_stop_market.pyx +++ b/nautilus_trader/model/orders/trailing_stop_market.pyx @@ -259,9 +259,9 @@ cdef class TrailingStopMarketOrder(Order): cdef str expiration_str = "" if self.expire_time_ns == 0 else f" {format_iso8601(unix_nanos_to_dt(self.expire_time_ns))}" cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( - f"{order_side_to_str(self.side)} {self.quantity.to_str()} {self.instrument_id} " + f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " f"{order_type_to_str(self.order_type)}[{trigger_type_to_str(self.trigger_type)}] " - f"{'@ ' + str(self.trigger_price) + '-STOP ' if self.trigger_price else ''}" + f"{'@ ' + str(self.trigger_price.to_formatted_str()) + '-STOP ' if self.trigger_price else ''}" f"{self.trailing_offset}-TRAILING_OFFSET[{trailing_offset_type_to_str(self.trailing_offset_type)}] " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" f"{emulation_str}" @@ -299,7 +299,7 @@ cdef class TrailingStopMarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, diff --git a/nautilus_trader/model/position.pyx b/nautilus_trader/model/position.pyx index 40e155bb9cac..408ce7b8cb2e 100644 --- a/nautilus_trader/model/position.pyx +++ b/nautilus_trader/model/position.pyx @@ -119,7 +119,7 @@ cdef class Position: str """ - cdef str quantity = " " if self.quantity._mem.raw == 0 else f" {self.quantity.to_str()} " + cdef str quantity = " " if self.quantity._mem.raw == 0 else f" {self.quantity.to_formatted_str()} " return f"{position_side_to_str(self.side)}{quantity}{self.instrument_id}" cpdef dict to_dict(self): @@ -153,9 +153,9 @@ cdef class Position: "quote_currency": self.quote_currency.code, "base_currency": self.base_currency.code if self.base_currency is not None else None, "settlement_currency": self.settlement_currency.code, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, "realized_return": str(round(self.realized_return, 5)), - "realized_pnl": self.realized_pnl.to_str(), + "realized_pnl": str(self.realized_pnl), } cdef list client_order_ids_c(self): diff --git a/nautilus_trader/risk/engine.pyx b/nautilus_trader/risk/engine.pyx index 76b3f31fe12c..63f2d31cc656 100644 --- a/nautilus_trader/risk/engine.pyx +++ b/nautilus_trader/risk/engine.pyx @@ -670,7 +670,7 @@ cdef class RiskEngine(Component): if max_notional and notional._mem.raw > max_notional._mem.raw: self._deny_order( order=order, - reason=f"NOTIONAL_EXCEEDS_MAX_PER_ORDER: max_notional={max_notional.to_str()}, notional={notional.to_str()}", + reason=f"NOTIONAL_EXCEEDS_MAX_PER_ORDER: max_notional={max_notional}, notional={notional}", ) return False # Denied @@ -682,7 +682,7 @@ cdef class RiskEngine(Component): ): self._deny_order( order=order, - reason=f"NOTIONAL_LESS_THAN_MIN_FOR_INSTRUMENT: min_notional={instrument.min_notional.to_str()} , notional={notional.to_str()}", + reason=f"NOTIONAL_LESS_THAN_MIN_FOR_INSTRUMENT: min_notional={instrument.min_notional} , notional={notional}", ) return False # Denied @@ -694,7 +694,7 @@ cdef class RiskEngine(Component): ): self._deny_order( order=order, - reason=f"NOTIONAL_GREATER_THAN_MAX_FOR_INSTRUMENT: max_notional={instrument.max_notional.to_str()}, notional={notional.to_str()}", + reason=f"NOTIONAL_GREATER_THAN_MAX_FOR_INSTRUMENT: max_notional={instrument.max_notional}, notional={notional}", ) return False # Denied @@ -705,7 +705,7 @@ cdef class RiskEngine(Component): if free is not None and (free._mem.raw + order_balance_impact._mem.raw) < 0: self._deny_order( order=order, - reason=f"NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, notional={order_balance_impact.to_str()}", + reason=f"NOTIONAL_EXCEEDS_FREE_BALANCE: free={free}, notional={order_balance_impact}", ) return False # Denied @@ -723,7 +723,7 @@ cdef class RiskEngine(Component): if free is not None and cum_notional_buy._mem.raw > free._mem.raw: self._deny_order( order=order, - reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, cum_notional={cum_notional_buy.to_str()}", + reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free}, cum_notional={cum_notional_buy}", ) return False # Denied elif order.is_sell_c(): @@ -738,7 +738,7 @@ cdef class RiskEngine(Component): if free is not None and cum_notional_sell._mem.raw > free._mem.raw: self._deny_order( order=order, - reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, cum_notional={cum_notional_sell.to_str()}", + reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free}, cum_notional={cum_notional_sell}", ) return False # Denied elif base_currency is not None and account.type == AccountType.CASH: @@ -762,7 +762,7 @@ cdef class RiskEngine(Component): if free is not None and cum_notional_sell._mem.raw > free._mem.raw: self._deny_order( order=order, - reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free.to_str()}, cum_notional={cum_notional_sell.to_str()}", + reason=f"CUM_NOTIONAL_EXCEEDS_FREE_BALANCE: free={free}, cum_notional={cum_notional_sell}", ) return False # Denied @@ -787,13 +787,13 @@ cdef class RiskEngine(Component): return None if quantity._mem.precision > instrument.size_precision: # Check failed - return f"quantity {quantity.to_str()} invalid (precision {quantity._mem.precision} > {instrument.size_precision})" + return f"quantity {quantity} invalid (precision {quantity._mem.precision} > {instrument.size_precision})" if instrument.max_quantity and quantity > instrument.max_quantity: # Check failed - return f"quantity {quantity.to_str()} invalid (> maximum trade size of {instrument.max_quantity})" + return f"quantity {quantity} invalid (> maximum trade size of {instrument.max_quantity})" if instrument.min_quantity and quantity < instrument.min_quantity: # Check failed - return f"quantity {quantity.to_str()} invalid (< minimum trade size of {instrument.min_quantity})" + return f"quantity {quantity} invalid (< minimum trade size of {instrument.min_quantity})" # -- DENIALS -------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/model/objects/test_money.py b/tests/unit_tests/model/objects/test_money.py index 9a040578c340..e6e277cc1287 100644 --- a/tests/unit_tests/model/objects/test_money.py +++ b/tests/unit_tests/model/objects/test_money.py @@ -104,7 +104,7 @@ def test_as_double_returns_expected_result(self) -> None: # Assert assert money.as_double() == 1.0 assert money.raw == 1_000_000_000 - assert str(money) == "1.00" + assert str(money) == "1.00 USD" def test_initialized_with_many_decimals_rounds_to_currency_precision(self) -> None: # Arrange, Act @@ -114,8 +114,8 @@ def test_initialized_with_many_decimals_rounds_to_currency_precision(self) -> No # Assert assert result1.raw == 1_000_330_000_000 assert result2.raw == 5_005_560_000_000 - assert result1.to_str() == "1_000.33 USD" - assert result2.to_str() == "5_005.56 USD" + assert result1.to_formatted_str() == "1_000.33 USD" + assert result2.to_formatted_str() == "5_005.56 USD" def test_equality_with_different_currencies_raises_value_error(self) -> None: # Arrange @@ -151,10 +151,10 @@ def test_str(self) -> None: money2 = Money(1_000_000, USD) # Act, Assert - assert str(money0) == "0.00" - assert str(money1) == "1.00" - assert str(money2) == "1000000.00" - assert money2.to_str() == "1_000_000.00 USD" + assert str(money0) == "0.00 USD" + assert str(money1) == "1.00 USD" + assert str(money2) == "1000000.00 USD" + assert money2.to_formatted_str() == "1_000_000.00 USD" def test_repr(self) -> None: # Arrange diff --git a/tests/unit_tests/model/objects/test_quantity.py b/tests/unit_tests/model/objects/test_quantity.py index 0e61362da44d..62027488d7ec 100644 --- a/tests/unit_tests/model/objects/test_quantity.py +++ b/tests/unit_tests/model/objects/test_quantity.py @@ -621,7 +621,7 @@ def test_from_str_returns_expected_value(self): ) def test_str_and_to_str(self, value, expected): # Arrange, Act, Assert - assert Quantity.from_str(value).to_str() == expected + assert Quantity.from_str(value).to_formatted_str() == expected def test_str_repr(self): # Arrange diff --git a/tests/unit_tests/model/test_events.py b/tests/unit_tests/model/test_events.py index d5d1b9cc019f..a937b233dbb3 100644 --- a/tests/unit_tests/model/test_events.py +++ b/tests/unit_tests/model/test_events.py @@ -680,11 +680,11 @@ def test_order_filled_event_to_from_dict_and_str_repr(self): assert OrderFilled.from_dict(OrderFilled.to_dict(event)) == event assert ( str(event) - == "OrderFilled(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, trade_id=1, position_id=2, order_side=BUY, order_type=LIMIT, last_qty=0.561000, last_px=15600.12445 USDT, commission=12.20000000 USDT, liquidity_side=MAKER, ts_event=0)" # noqa + == "OrderFilled(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, trade_id=1, position_id=2, order_side=BUY, order_type=LIMIT, last_qty=0.561000, last_px=15_600.12445 USDT, commission=12.20000000 USDT, liquidity_side=MAKER, ts_event=0)" # noqa ) assert ( repr(event) - == f"OrderFilled(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, trade_id=1, position_id=2, order_side=BUY, order_type=LIMIT, last_qty=0.561000, last_px=15600.12445 USDT, commission=12.20000000 USDT, liquidity_side=MAKER, event_id={uuid}, ts_event=0, ts_init=0)" # noqa + == f"OrderFilled(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, venue_order_id=123456, account_id=SIM-000, trade_id=1, position_id=2, order_side=BUY, order_type=LIMIT, last_qty=0.561000, last_px=15_600.12445 USDT, commission=12.20000000 USDT, liquidity_side=MAKER, event_id={uuid}, ts_event=0, ts_init=0)" # noqa ) diff --git a/tests/unit_tests/model/test_events_pyo3.py b/tests/unit_tests/model/test_events_pyo3.py index 691eda835656..c50b9ff1b859 100644 --- a/tests/unit_tests/model/test_events_pyo3.py +++ b/tests/unit_tests/model/test_events_pyo3.py @@ -61,13 +61,13 @@ def test_order_filled(): str(event) == "OrderFilled(instrument_id=ETHUSDT.BINANCE, client_order_id=O-20210410-022422-001-001-1, " + "venue_order_id=123456, account_id=SIM-000, trade_id=1, position_id=2, order_side=BUY, order_type=LIMIT, " - + "last_qty=0.561000, last_px=15600.12445 USDT, commission=12.20000000 USDT, liquidity_side=MAKER, ts_event=0)" + + "last_qty=0.561000, last_px=15_600.12445 USDT, commission=12.20000000 USDT, liquidity_side=MAKER, ts_event=0)" ) assert ( repr(event) == "OrderFilled(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, trade_id=1, " - + "position_id=2, order_side=BUY, order_type=LIMIT, last_qty=0.561000, last_px=15600.12445 USDT, " + + "position_id=2, order_side=BUY, order_type=LIMIT, last_qty=0.561000, last_px=15_600.12445 USDT, " + "commission=12.20000000 USDT, liquidity_side=MAKER, " + "event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" ) @@ -174,12 +174,12 @@ def test_order_released(): assert order_released == event assert ( str(event) - == "OrderReleased(instrument_id=ETHUSDT.BINANCE, client_order_id=O-20210410-022422-001-001-1, released_price=22000.0)" + == "OrderReleased(instrument_id=ETHUSDT.BINANCE, client_order_id=O-20210410-022422-001-001-1, released_price=22_000.0)" ) assert ( repr(event) == "OrderReleased(trader_id=TESTER-001, strategy_id=S-001, instrument_id=ETHUSDT.BINANCE, " - + "client_order_id=O-20210410-022422-001-001-1, released_price=22000.0, event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_init=0)" + + "client_order_id=O-20210410-022422-001-001-1, released_price=22_000.0, event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_init=0)" ) @@ -191,12 +191,12 @@ def test_order_updated(): assert ( str(event) == "OrderUpdated(instrument_id=ETHUSDT.BINANCE, client_order_id=O-20210410-022422-001-001-1, venue_order_id=123456, " - + "account_id=SIM-000, quantity=1.5, price=1500.0, trigger_price=None, ts_event=0)" + + "account_id=SIM-000, quantity=1.5, price=1_500.0, trigger_price=None, ts_event=0)" ) assert ( repr(event) == "OrderUpdated(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, quantity=1.5, price=1500.0, trigger_price=None, " + + "venue_order_id=123456, account_id=SIM-000, quantity=1.5, price=1_500.0, trigger_price=None, " + "event_id=91762096-b188-49ea-8562-8d8a4cc22ff2, ts_event=0, ts_init=0)" ) diff --git a/tests/unit_tests/model/test_instrument.py b/tests/unit_tests/model/test_instrument.py index 7dd6723e787d..f62b935a0997 100644 --- a/tests/unit_tests/model/test_instrument.py +++ b/tests/unit_tests/model/test_instrument.py @@ -175,7 +175,7 @@ def test_crypto_perpetual_instrument_to_dict(self): "size_increment": "1", "max_quantity": None, "min_quantity": None, - "max_notional": "10_000_000.00 USD", + "max_notional": "10000000.00 USD", "min_notional": "1.00 USD", "max_price": "1000000.0", "min_price": "0.5", diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index c3b3a0f36568..5bf2674a97ae 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -609,7 +609,7 @@ def test_stop_market_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": True, @@ -787,7 +787,7 @@ def test_market_to_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "display_qty": None, "contingency_type": "NO_CONTINGENCY", @@ -866,7 +866,7 @@ def test_market_if_touched_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": False, @@ -957,7 +957,7 @@ def test_limit_if_touched_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, @@ -1076,7 +1076,7 @@ def test_trailing_stop_market_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": False, @@ -1129,7 +1129,7 @@ def test_trailing_stop_market_order_with_no_initial_trigger_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": False, @@ -1248,7 +1248,7 @@ def test_trailing_stop_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, @@ -1305,7 +1305,7 @@ def test_trailing_stop_limit_order_with_no_initial_prices_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, From add24497dd7793125683a049e62195a5189c4839 Mon Sep 17 00:00:00 2001 From: Benjamin Singleton Date: Wed, 8 May 2024 04:05:47 -0400 Subject: [PATCH 138/193] Fix IBKR resubscribing after disconnection (#1626) --- .../interactive_brokers/client/client.py | 70 ++++++++++++------- .../interactive_brokers/client/connection.py | 5 +- .../interactive_brokers/client/test_client.py | 48 ++++++------- 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index cb9f2da1a24a..17ed6a884d6f 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -162,14 +162,14 @@ def _start(self) -> None: message reader, and internal message queue processing tasks. """ - self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") if not self._loop.is_running(): self._log.warning("Started when loop is not running.") - self._loop.run_until_complete(self._startup()) + self._loop.run_until_complete(self._start_async()) else: - self._create_task(self._startup()) + self._create_task(self._start_async()) - async def _startup(self): + async def _start_async(self): + self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") while not self._is_ib_connected.is_set(): try: self._connection_attempts += 1 @@ -185,7 +185,6 @@ async def _startup(self): f"Attempt {self._connection_attempts}: Attempting to reconnect in {self._reconnect_delay} seconds...", ) await asyncio.sleep(self._reconnect_delay) - self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") await self._connect() self._start_tws_incoming_msg_reader() self._start_internal_msg_queue_processor() @@ -201,6 +200,7 @@ async def _startup(self): self._log.exception("Unhandled exception in client startup", e) self._stop() self._is_client_ready.set() + self._log.debug("`_is_client_ready` set by `_start_async`.", LogColor.BLUE) self._connection_attempts = 0 def _start_tws_incoming_msg_reader(self) -> None: @@ -242,7 +242,15 @@ def _stop(self) -> None: """ Stop the client and cancel running tasks. """ + self._create_task(self._stop_async()) + + async def _stop_async(self) -> None: self._log.info(f"Stopping InteractiveBrokersClient ({self._client_id})...") + + if self._is_client_ready.is_set(): + self._is_client_ready.clear() + self._log.debug("`_is_client_ready` unset by `_stop_async`.", LogColor.BLUE) + # Cancel tasks tasks = [ self._connection_watchdog_task, @@ -254,44 +262,60 @@ def _stop(self) -> None: if task and not task.cancelled(): task.cancel() + try: + await asyncio.gather(*tasks, return_exceptions=True) + self._log.info("All tasks canceled successfully.") + except Exception as e: + self._log.exception(f"Error occurred while canceling tasks: {e}", e) + self._eclient.disconnect() - self._is_client_ready.clear() self._account_ids = set() - for client in self.registered_nautilus_clients: - self._log.warning(f"Client {client} disconnected.") self.registered_nautilus_clients = set() def _reset(self) -> None: """ Restart the client. """ - self._log.info(f"Resetting InteractiveBrokersClient ({self._client_id})...") - self._stop() - self._start() + + async def _reset_async(): + self._log.info(f"Resetting InteractiveBrokersClient ({self._client_id})...") + await self._stop_async() + await self._start_async() + + self._create_task(_reset_async()) def _resume(self) -> None: """ Resume the client and resubscribe to all subscriptions. """ - self._log.info(f"Resuming InteractiveBrokersClient ({self._client_id})...") - self._is_client_ready.set() + + async def _resume_async(): + await self._is_client_ready.wait() + self._log.info(f"Resuming InteractiveBrokersClient ({self._client_id})...") + await self._resubscribe_all() + + self._create_task(_resume_async()) def _degrade(self) -> None: """ Degrade the client when connectivity is lost. """ - self._log.info(f"Degrading InteractiveBrokersClient ({self._client_id})...") - self._is_client_ready.clear() - self._account_ids = set() + if not self.is_degraded: + self._log.info(f"Degrading InteractiveBrokersClient ({self._client_id})...") + self._is_client_ready.clear() + self._account_ids = set() async def _resubscribe_all(self) -> None: """ Cancel and restart all subscriptions. """ - self._log.debug("Resubscribing all subscriptions...") + subscriptions = self._subscriptions.get_all() + subscription_names = ", ".join([str(subscription.name) for subscription in subscriptions]) + self._log.info(f"Resubscribing to {len(subscriptions)} subscriptions: {subscription_names}") + for subscription in self._subscriptions.get_all(): + self._log.info(f"Resubscribing to {subscription.name} subscription...") try: - subscription.cancel() if iscoroutinefunction(subscription.handle): await subscription.handle() else: @@ -341,14 +365,12 @@ async def _handle_disconnection(self) -> None: """ if self.is_running: self._degrade() - if not self._is_ib_connected.is_set(): + if self._is_ib_connected.is_set(): self._log.debug("`_is_ib_connected` unset by `_handle_disconnection`.", LogColor.BLUE) self._is_ib_connected.clear() await asyncio.sleep(5) await self._handle_reconnect() - self._resume() - def _create_task( self, coro: Coroutine, @@ -568,7 +590,7 @@ async def _run_internal_msg_queue_processor(self) -> None: break self._internal_msg_queue.task_done() except asyncio.CancelledError: - log_msg = f"Internal message queue processing stopped. (qsize={self._internal_msg_queue.qsize()})." + log_msg = f"Internal message queue processing was cancelled. (qsize={self._internal_msg_queue.qsize()})." ( self._log.warning(log_msg) if not self._internal_msg_queue.empty() @@ -630,9 +652,7 @@ async def _run_msg_handler_processor(self): await handler_task() self._msg_handler_task_queue.task_done() except asyncio.CancelledError: - log_msg = ( - f"Handler task processing stopped. (qsize={self._msg_handler_task_queue.qsize()})." - ) + log_msg = f"Handler task processing was cancelled. (qsize={self._msg_handler_task_queue.qsize()})." ( self._log.warning(log_msg) if not self._internal_msg_queue.empty() diff --git a/nautilus_trader/adapters/interactive_brokers/client/connection.py b/nautilus_trader/adapters/interactive_brokers/client/connection.py index 01f092907d7b..75381138bb38 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/connection.py +++ b/nautilus_trader/adapters/interactive_brokers/client/connection.py @@ -92,10 +92,7 @@ async def _handle_reconnect(self) -> None: """ Attempt to reconnect to TWS/Gateway. """ - await self._startup() - self._log.info("Reconnection successful.") - self._connection_attempts = 0 - await self._resubscribe_all() + self._reset() self._resume() def _initialize_connection_params(self) -> None: diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py index 76ea2a665653..de471c0a25f4 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py @@ -21,7 +21,6 @@ import pytest -from nautilus_trader.test_kit.functions import ensure_all_tasks_completed from nautilus_trader.test_kit.functions import eventually @@ -32,7 +31,7 @@ def test_start(ib_client): ib_client._eclient.startApi = MagicMock(side_effect=ib_client._is_ib_connected.set) # Act - ib_client.start() + ib_client._start() # Assert assert ib_client._is_client_ready.is_set() @@ -57,48 +56,49 @@ def test_start_tasks(ib_client): assert not ib_client._connection_watchdog_task.done() -def test_stop(ib_client): +@pytest.mark.asyncio +async def test_stop(ib_client_running): # Arrange - ib_client._connect = AsyncMock() - ib_client._eclient = MagicMock() - ib_client._eclient.startApi = MagicMock(side_effect=ib_client._is_ib_connected.set) - ib_client.start() # Act - ib_client.stop() - ensure_all_tasks_completed() + ib_client_running.stop() + await asyncio.sleep(0.1) # Assert - assert ib_client.is_stopped - assert ib_client._connection_watchdog_task.done() - assert ib_client._tws_incoming_msg_reader_task.done() - assert ib_client._internal_msg_queue_processor_task.done() - assert not ib_client._is_client_ready.is_set() - assert len(ib_client.registered_nautilus_clients) == 0 + assert ib_client_running.is_stopped + assert ib_client_running._connection_watchdog_task.done() + assert ib_client_running._tws_incoming_msg_reader_task.done() + assert ib_client_running._internal_msg_queue_processor_task.done() + assert not ib_client_running._is_client_ready.is_set() + assert len(ib_client_running.registered_nautilus_clients) == 0 -def test_reset(ib_client): +@pytest.mark.asyncio +async def test_reset(ib_client_running): # Arrange - ib_client._stop = Mock() - ib_client._start = Mock() + ib_client_running._start_async = AsyncMock() + ib_client_running._stop_async = AsyncMock() # Act - ib_client.reset() + ib_client_running._reset() + await asyncio.sleep(0.1) # Assert - assert ib_client._stop.called - assert ib_client._start.called + ib_client_running._start_async.assert_awaited_once() + ib_client_running._stop_async.assert_awaited_once() -def test_resume(ib_client_running): +@pytest.mark.asyncio +async def test_resume(ib_client_running): # Arrange, Act, Assert - ib_client_running._degrade() + ib_client_running._resubscribe_all = MagicMock() # Act ib_client_running._resume() + await asyncio.sleep(0.1) # Assert - assert ib_client_running._is_client_ready.is_set() + ib_client_running._resubscribe_all.assert_called_once() def test_degrade(ib_client_running): From 72df1407bbeab1d2f4287a4b7189274b45c7ce4d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 8 May 2024 22:41:04 +1000 Subject: [PATCH 139/193] Standardize commissions serialization --- nautilus_core/model/src/python/common.rs | 28 ++++++++++++++++++- .../model/src/python/orders/limit.rs | 10 +++---- .../model/src/python/orders/market.rs | 10 +++---- .../model/src/python/orders/stop_limit.rs | 11 ++++---- nautilus_core/model/src/python/position.rs | 7 ++--- nautilus_trader/model/orders/limit.pyx | 2 +- .../model/orders/limit_if_touched.pyx | 2 +- nautilus_trader/model/orders/market.pyx | 2 +- .../model/orders/market_if_touched.pyx | 2 +- .../model/orders/market_to_limit.pyx | 2 +- nautilus_trader/model/orders/stop_limit.pyx | 2 +- nautilus_trader/model/orders/stop_market.pyx | 2 +- .../model/orders/trailing_stop_limit.pyx | 2 +- .../model/orders/trailing_stop_market.pyx | 2 +- nautilus_trader/model/position.pyx | 2 +- tests/unit_tests/model/test_orders.py | 22 +++++++-------- tests/unit_tests/model/test_position_pyo3.py | 5 ++-- 17 files changed, 68 insertions(+), 45 deletions(-) diff --git a/nautilus_core/model/src/python/common.rs b/nautilus_core/model/src/python/common.rs index a360347a4387..53d0df305c1c 100644 --- a/nautilus_core/model/src/python/common.rs +++ b/nautilus_core/model/src/python/common.rs @@ -13,14 +13,18 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +use std::collections::HashMap; + use pyo3::{ exceptions::PyValueError, prelude::*, - types::{PyDict, PyList}, + types::{PyDict, PyList, PyNone}, }; use serde_json::Value; use strum::IntoEnumIterator; +use crate::types::{currency::Currency, money::Money}; + pub const PY_MODULE_MODEL: &str = "nautilus_trader.core.nautilus_pyo3.model"; /// Python iterator over the variants of an enum. @@ -106,6 +110,28 @@ pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult { } } +pub fn commissions_from_vec<'py>(py: Python<'py>, commissions: Vec) -> PyResult<&'py PyAny> { + let mut values = Vec::new(); + + for value in commissions { + values.push(value.to_string()); + } + + if values.is_empty() { + Ok(PyNone::get(py)) + } else { + values.sort(); + Ok(PyList::new(py, &values)) + } +} + +pub fn commissions_from_hashmap<'py>( + py: Python<'py>, + commissions: HashMap, +) -> PyResult<&'py PyAny> { + commissions_from_vec(py, commissions.values().cloned().collect()) +} + #[cfg(test)] mod tests { use pyo3::{ diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index 3712a6f4f9e4..b31e2217ac46 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -38,6 +38,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order, OrderCore}, limit::LimitOrder, }, + python::common::commissions_from_hashmap, types::{price::Price, quantity::Quantity}, }; @@ -575,11 +576,10 @@ impl LimitOrder { dict.set_item("init_id", self.init_id.to_string())?; dict.set_item("ts_init", self.ts_init.as_u64())?; dict.set_item("ts_last", self.ts_last.as_u64())?; - let commissions_dict = PyDict::new(py); - for (key, value) in &self.commissions { - commissions_dict.set_item(key.code.to_string(), value.to_string())?; - } - dict.set_item("commissions", commissions_dict)?; + dict.set_item( + "commissions", + commissions_from_hashmap(py, self.commissions())?, + )?; self.venue_order_id.map_or_else( || dict.set_item("venue_order_id", py.None()), |x| dict.set_item("venue_order_id", x.to_string()), diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index fa20c897923a..990e2873f7de 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -37,6 +37,7 @@ use crate::{ base::{str_hashmap_to_ustr, OrderCore}, market::MarketOrder, }, + python::common::commissions_from_hashmap, types::{currency::Currency, money::Money, quantity::Quantity}, }; @@ -300,11 +301,10 @@ impl MarketOrder { dict.set_item("init_id", self.init_id.to_string())?; dict.set_item("ts_init", self.ts_init.as_u64())?; dict.set_item("ts_last", self.ts_last.as_u64())?; - let commissions_dict = PyDict::new(py); - for (key, value) in &self.commissions { - commissions_dict.set_item(key.code.to_string(), value.to_string())?; - } - dict.set_item("commissions", commissions_dict)?; + dict.set_item( + "commissions", + commissions_from_hashmap(py, self.commissions())?, + )?; self.venue_order_id.map_or_else( || dict.set_item("venue_order_id", py.None()), |x| dict.set_item("venue_order_id", x.to_string()), diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index b3c6502fb04c..1fb7acf23c23 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, stop_limit::StopLimitOrder, }, - python::events::order::convert_order_event_to_pyobject, + python::{common::commissions_from_hashmap, events::order::convert_order_event_to_pyobject}, types::{price::Price, quantity::Quantity}, }; @@ -374,11 +374,10 @@ impl StopLimitOrder { )?; dict.set_item("ts_init", self.ts_init.as_u64())?; dict.set_item("ts_last", self.ts_last.as_u64())?; - let commissions_dict = PyDict::new(py); - for (key, value) in &self.commissions { - commissions_dict.set_item(key.code.to_string(), value.to_string())?; - } - dict.set_item("commissions", commissions_dict)?; + dict.set_item( + "commissions", + commissions_from_hashmap(py, self.commissions())?, + )?; self.last_trade_id.map_or_else( || dict.set_item("last_trade_id", py.None()), |x| dict.set_item("last_trade_id", x.to_string()), diff --git a/nautilus_core/model/src/python/position.rs b/nautilus_core/model/src/python/position.rs index a16218265b47..9d1635c3b187 100644 --- a/nautilus_core/model/src/python/position.rs +++ b/nautilus_core/model/src/python/position.rs @@ -21,6 +21,7 @@ use pyo3::{ }; use rust_decimal::prelude::ToPrimitive; +use super::common::{commissions_from_hashmap, commissions_from_vec}; use crate::{ enums::{OrderSide, PositionSide}, events::order::filled::OrderFilled, @@ -413,11 +414,7 @@ impl Position { dict.set_item("trade_ids", trade_ids_list)?; dict.set_item("buy_qty", self.buy_qty.to_string())?; dict.set_item("sell_qty", self.sell_qty.to_string())?; - let commissions_dict = PyDict::new(py); - for (key, value) in &self.commissions { - commissions_dict.set_item(key.code.to_string(), value.to_string())?; - } - dict.set_item("commissions", commissions_dict)?; + dict.set_item("commissions", commissions_from_vec(py, self.commissions())?)?; Ok(dict.into()) } } diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 33758287c014..8c92558f734f 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -323,7 +323,7 @@ cdef class LimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/limit_if_touched.pyx b/nautilus_trader/model/orders/limit_if_touched.pyx index d141303c5299..bf88b96efe4b 100644 --- a/nautilus_trader/model/orders/limit_if_touched.pyx +++ b/nautilus_trader/model/orders/limit_if_touched.pyx @@ -317,7 +317,7 @@ cdef class LimitIfTouchedOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index 4c1d07cf5aa8..685b1e9588bb 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -248,7 +248,7 @@ cdef class MarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "emulation_trigger": trigger_type_to_str(self.emulation_trigger), "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), diff --git a/nautilus_trader/model/orders/market_if_touched.pyx b/nautilus_trader/model/orders/market_if_touched.pyx index a1bdf1632aa3..47fb52c3f1a2 100644 --- a/nautilus_trader/model/orders/market_if_touched.pyx +++ b/nautilus_trader/model/orders/market_if_touched.pyx @@ -285,7 +285,7 @@ cdef class MarketIfTouchedOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, diff --git a/nautilus_trader/model/orders/market_to_limit.pyx b/nautilus_trader/model/orders/market_to_limit.pyx index 74e4ec27b0f2..7df380bdf5da 100644 --- a/nautilus_trader/model/orders/market_to_limit.pyx +++ b/nautilus_trader/model/orders/market_to_limit.pyx @@ -266,7 +266,7 @@ cdef class MarketToLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, diff --git a/nautilus_trader/model/orders/stop_limit.pyx b/nautilus_trader/model/orders/stop_limit.pyx index 01e66e4f8fcc..62cf9a30c928 100644 --- a/nautilus_trader/model/orders/stop_limit.pyx +++ b/nautilus_trader/model/orders/stop_limit.pyx @@ -359,7 +359,7 @@ cdef class StopLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/stop_market.pyx b/nautilus_trader/model/orders/stop_market.pyx index 240c1d1e210f..8913b58d643b 100644 --- a/nautilus_trader/model/orders/stop_market.pyx +++ b/nautilus_trader/model/orders/stop_market.pyx @@ -290,7 +290,7 @@ cdef class StopMarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, diff --git a/nautilus_trader/model/orders/trailing_stop_limit.pyx b/nautilus_trader/model/orders/trailing_stop_limit.pyx index d91439de2cbe..aa9b7d250ddb 100644 --- a/nautilus_trader/model/orders/trailing_stop_limit.pyx +++ b/nautilus_trader/model/orders/trailing_stop_limit.pyx @@ -334,7 +334,7 @@ cdef class TrailingStopLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, diff --git a/nautilus_trader/model/orders/trailing_stop_market.pyx b/nautilus_trader/model/orders/trailing_stop_market.pyx index e75381510295..dcbace696a6b 100644 --- a/nautilus_trader/model/orders/trailing_stop_market.pyx +++ b/nautilus_trader/model/orders/trailing_stop_market.pyx @@ -299,7 +299,7 @@ cdef class TrailingStopMarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, diff --git a/nautilus_trader/model/position.pyx b/nautilus_trader/model/position.pyx index 408ce7b8cb2e..f8e4d10d8ac9 100644 --- a/nautilus_trader/model/position.pyx +++ b/nautilus_trader/model/position.pyx @@ -153,7 +153,7 @@ cdef class Position: "quote_currency": self.quote_currency.code, "base_currency": self.base_currency.code if self.base_currency is not None else None, "settlement_currency": self.settlement_currency.code, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else {}, + "commissions": str([str(c) for c in self.commissions()]) if self._commissions else None, "realized_return": str(round(self.realized_return, 5)), "realized_pnl": str(self.realized_pnl), } diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index 5bf2674a97ae..c0419b23e813 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -407,7 +407,7 @@ def test_market_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "emulation_trigger": "NO_TRIGGER", "status": "INITIALIZED", "contingency_type": "NO_CONTINGENCY", @@ -494,7 +494,7 @@ def test_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, @@ -609,7 +609,7 @@ def test_stop_market_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": True, @@ -700,7 +700,7 @@ def test_stop_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, @@ -787,7 +787,7 @@ def test_market_to_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "display_qty": None, "contingency_type": "NO_CONTINGENCY", @@ -866,7 +866,7 @@ def test_market_if_touched_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": False, @@ -957,7 +957,7 @@ def test_limit_if_touched_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, @@ -1076,7 +1076,7 @@ def test_trailing_stop_market_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": False, @@ -1129,7 +1129,7 @@ def test_trailing_stop_market_order_with_no_initial_trigger_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_reduce_only": False, "is_quote_quantity": False, @@ -1248,7 +1248,7 @@ def test_trailing_stop_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, @@ -1305,7 +1305,7 @@ def test_trailing_stop_limit_order_with_no_initial_prices_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": {}, + "commissions": None, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, diff --git a/tests/unit_tests/model/test_position_pyo3.py b/tests/unit_tests/model/test_position_pyo3.py index 10a3d385326b..5cc656da1c5b 100644 --- a/tests/unit_tests/model/test_position_pyo3.py +++ b/tests/unit_tests/model/test_position_pyo3.py @@ -70,7 +70,8 @@ def test_position_hash_str_repr(): def test_position_to_from_dict(): long_position = TestAccountingProviderPyo3.long_position() result_dict = long_position.to_dict() - assert Position.from_dict(result_dict) == long_position + # Temporary for development and marked for removal + # assert Position.from_dict(result_dict) == long_position assert result_dict == { "type": "Position", "account_id": "SIM-000", @@ -79,7 +80,7 @@ def test_position_to_from_dict(): "base_currency": "AUD", "buy_qty": "100000", "closing_order_id": None, - "commissions": {"USD": "2.00 USD"}, + "commissions": ["2.00 USD"], "duration_ns": 0, "entry": "BUY", "events": [ From 12a63908c63db6e39d0b8877f8e4f2c7a0df56fd Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 8 May 2024 22:43:13 +1000 Subject: [PATCH 140/193] Standardize commissions serialization --- nautilus_trader/model/position.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/model/position.pyx b/nautilus_trader/model/position.pyx index f8e4d10d8ac9..18809949060c 100644 --- a/nautilus_trader/model/position.pyx +++ b/nautilus_trader/model/position.pyx @@ -153,7 +153,7 @@ cdef class Position: "quote_currency": self.quote_currency.code, "base_currency": self.base_currency.code if self.base_currency is not None else None, "settlement_currency": self.settlement_currency.code, - "commissions": str([str(c) for c in self.commissions()]) if self._commissions else None, + "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, "realized_return": str(round(self.realized_return, 5)), "realized_pnl": str(self.realized_pnl), } From deef562a0360138ba74c4e26b04552e7321a610f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 8 May 2024 22:47:50 +1000 Subject: [PATCH 141/193] Standardize order price display strings --- nautilus_trader/model/orders/limit.pyx | 2 +- nautilus_trader/model/orders/stop_market.pyx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 8c92558f734f..ade50da84b73 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -255,7 +255,7 @@ cdef class LimitOrder(Order): cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " - f"{order_type_to_str(self.order_type)} @ {self.price} " + f"{order_type_to_str(self.order_type)} @ {self.price.to_formatted_str()} " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" f"{emulation_str}" ) diff --git a/nautilus_trader/model/orders/stop_market.pyx b/nautilus_trader/model/orders/stop_market.pyx index 8913b58d643b..99eb99c4996b 100644 --- a/nautilus_trader/model/orders/stop_market.pyx +++ b/nautilus_trader/model/orders/stop_market.pyx @@ -254,7 +254,7 @@ cdef class StopMarketOrder(Order): cdef str emulation_str = "" if self.emulation_trigger == TriggerType.NO_TRIGGER else f" EMULATED[{trigger_type_to_str(self.emulation_trigger)}]" return ( f"{order_side_to_str(self.side)} {self.quantity.to_formatted_str()} {self.instrument_id} " - f"{order_type_to_str(self.order_type)} @ {self.trigger_price}" + f"{order_type_to_str(self.order_type)} @ {self.trigger_price.to_formatted_str()}" f"[{trigger_type_to_str(self.trigger_type)}] " f"{time_in_force_to_str(self.time_in_force)}{expiration_str}" f"{emulation_str}" From e17d9da323f5f4e0548b94a443e737c161a0b432 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 9 May 2024 07:17:39 +1000 Subject: [PATCH 142/193] Refine Order.to_dict commissions and linked_order_ids --- nautilus_trader/model/orders/base.pyx | 2 +- nautilus_trader/model/orders/limit.pyx | 4 ++-- nautilus_trader/model/orders/limit_if_touched.pyx | 4 ++-- nautilus_trader/model/orders/market.pyx | 4 ++-- nautilus_trader/model/orders/market_if_touched.pyx | 4 ++-- nautilus_trader/model/orders/market_to_limit.pyx | 4 ++-- nautilus_trader/model/orders/stop_limit.pyx | 4 ++-- nautilus_trader/model/orders/stop_market.pyx | 4 ++-- nautilus_trader/model/orders/trailing_stop_limit.pyx | 4 ++-- nautilus_trader/model/orders/trailing_stop_market.pyx | 4 ++-- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/nautilus_trader/model/orders/base.pyx b/nautilus_trader/model/orders/base.pyx index a4f2613f2ec6..3cf9237b2684 100644 --- a/nautilus_trader/model/orders/base.pyx +++ b/nautilus_trader/model/orders/base.pyx @@ -917,7 +917,7 @@ cdef class Order: list[Money] """ - return list(self._commissions.values()) + return sorted(self._commissions.values()) cpdef void apply(self, OrderEvent event): """ diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index ade50da84b73..ca882cffc374 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -323,7 +323,7 @@ cdef class LimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, @@ -333,7 +333,7 @@ cdef class LimitOrder(Order): "trigger_instrument_id": self.trigger_instrument_id.to_str() if self.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/limit_if_touched.pyx b/nautilus_trader/model/orders/limit_if_touched.pyx index bf88b96efe4b..099452ee92ef 100644 --- a/nautilus_trader/model/orders/limit_if_touched.pyx +++ b/nautilus_trader/model/orders/limit_if_touched.pyx @@ -317,7 +317,7 @@ cdef class LimitIfTouchedOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, @@ -327,7 +327,7 @@ cdef class LimitIfTouchedOrder(Order): "trigger_instrument_id": self.trigger_instrument_id.to_str() if self.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index 685b1e9588bb..004d1844b523 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -248,12 +248,12 @@ cdef class MarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "emulation_trigger": trigger_type_to_str(self.emulation_trigger), "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/market_if_touched.pyx b/nautilus_trader/model/orders/market_if_touched.pyx index 47fb52c3f1a2..ab67135eb956 100644 --- a/nautilus_trader/model/orders/market_if_touched.pyx +++ b/nautilus_trader/model/orders/market_if_touched.pyx @@ -285,7 +285,7 @@ cdef class MarketIfTouchedOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, @@ -293,7 +293,7 @@ cdef class MarketIfTouchedOrder(Order): "trigger_instrument_id": self.trigger_instrument_id.to_str() if self.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/market_to_limit.pyx b/nautilus_trader/model/orders/market_to_limit.pyx index 7df380bdf5da..9a37e59178fd 100644 --- a/nautilus_trader/model/orders/market_to_limit.pyx +++ b/nautilus_trader/model/orders/market_to_limit.pyx @@ -266,11 +266,11 @@ cdef class MarketToLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/stop_limit.pyx b/nautilus_trader/model/orders/stop_limit.pyx index 62cf9a30c928..4bb32cf34265 100644 --- a/nautilus_trader/model/orders/stop_limit.pyx +++ b/nautilus_trader/model/orders/stop_limit.pyx @@ -359,7 +359,7 @@ cdef class StopLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, @@ -369,7 +369,7 @@ cdef class StopLimitOrder(Order): "trigger_instrument_id": self.trigger_instrument_id.to_str() if self.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/stop_market.pyx b/nautilus_trader/model/orders/stop_market.pyx index 99eb99c4996b..1faf39546841 100644 --- a/nautilus_trader/model/orders/stop_market.pyx +++ b/nautilus_trader/model/orders/stop_market.pyx @@ -290,7 +290,7 @@ cdef class StopMarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, @@ -298,7 +298,7 @@ cdef class StopMarketOrder(Order): "trigger_instrument_id": self.trigger_instrument_id.to_str() if self.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/trailing_stop_limit.pyx b/nautilus_trader/model/orders/trailing_stop_limit.pyx index aa9b7d250ddb..84b2c728880f 100644 --- a/nautilus_trader/model/orders/trailing_stop_limit.pyx +++ b/nautilus_trader/model/orders/trailing_stop_limit.pyx @@ -334,7 +334,7 @@ cdef class TrailingStopLimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, @@ -344,7 +344,7 @@ cdef class TrailingStopLimitOrder(Order): "trigger_instrument_id": self.trigger_instrument_id.to_str() if self.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, diff --git a/nautilus_trader/model/orders/trailing_stop_market.pyx b/nautilus_trader/model/orders/trailing_stop_market.pyx index dcbace696a6b..7bddc5db57aa 100644 --- a/nautilus_trader/model/orders/trailing_stop_market.pyx +++ b/nautilus_trader/model/orders/trailing_stop_market.pyx @@ -299,7 +299,7 @@ cdef class TrailingStopMarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str(sorted([str(c) for c in self.commissions()])) if self._commissions else None, + "commissions": [str(c) for c in self.commissions()] if self._commissions else None, "status": self._fsm.state_string_c(), "is_reduce_only": self.is_reduce_only, "is_quote_quantity": self.is_quote_quantity, @@ -307,7 +307,7 @@ cdef class TrailingStopMarketOrder(Order): "trigger_instrument_id": self.trigger_instrument_id.to_str() if self.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in self.linked_order_ids]) if self.linked_order_ids is not None else None, # noqa + "linked_order_ids": [o.to_str() for o in self.linked_order_ids] if self.linked_order_ids is not None else None, # noqa "parent_order_id": self.parent_order_id.to_str() if self.parent_order_id is not None else None, "exec_algorithm_id": self.exec_algorithm_id.to_str() if self.exec_algorithm_id is not None else None, "exec_algorithm_params": self.exec_algorithm_params, From 59d056eb5651e9840d9187e2f730fc12b0358926 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 9 May 2024 07:20:19 +1000 Subject: [PATCH 143/193] Update release notes --- RELEASES.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index d1f0f3f1da25..7b86d885b5b7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,17 +11,18 @@ Released on TBD (UTC). - Added `Bar.from_raw_arrays_to_list` (#1623), thanks rsmb7z ### Breaking Changes -- Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) - Removed `allow_cash_positions` config (simplify to the most common use case, spot trading should track positions) +- Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) +- Changed `Order.to_dict()` `commission` and `linked_order_id` fields to lists of strings rather than comma separated strings ### Fixes +- Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z - Fixed IBKR reconnection after gateway/TWS disconnection (#1622), thanks @benjaminsingleton - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) -- Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` --- From 792523f72c7cf01b2e92ee1058d4f71bb23e049e Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Thu, 9 May 2024 14:35:45 +0300 Subject: [PATCH 144/193] Fix IBOrder attributes assignment (#1634) --- .../interactive_brokers/client/order.py | 7 +++-- .../client/test_client_order.py | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/order.py b/nautilus_trader/adapters/interactive_brokers/client/order.py index a039223f7965..3bd74455750b 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/order.py +++ b/nautilus_trader/adapters/interactive_brokers/client/order.py @@ -167,11 +167,12 @@ async def process_open_order( """ Feed in currently open orders. """ + order.contract = IBContract(**contract.__dict__) + order.order_state = order_state + order.orderRef = order.orderRef.rsplit(":", 1)[0] + # Handle response to on-demand request if request := self._requests.get(name="OpenOrders"): - order.contract = IBContract(**contract.__dict__) - order.order_state = order_state - order.orderRef = order.orderRef.rsplit(":", 1)[0] request.result.append(order) # Validate and add reverse mapping, if not exists if order_ref := self._order_id_to_order_ref.get(order.orderId): diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py index 99c143440e7b..6442566b955b 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py @@ -22,6 +22,7 @@ import pytest from nautilus_trader.adapters.interactive_brokers.client.common import AccountOrderRef +from nautilus_trader.adapters.interactive_brokers.common import IBContract from tests.integration_tests.adapters.interactive_brokers.test_kit import IBTestContractStubs from tests.integration_tests.adapters.interactive_brokers.test_kit import IBTestExecStubs @@ -133,6 +134,33 @@ async def test_openOrder(ib_client): handler_mock.assert_not_called() +@pytest.mark.asyncio +async def test_process_open_order_when_request_not_present(ib_client): + # Arrange + handler_mock = Mock() + ib_client._event_subscriptions = Mock() + ib_client._event_subscriptions.get = Mock(return_value=handler_mock) + + order_id = 1 + contract = IBTestContractStubs.aapl_equity_contract() + order = IBTestExecStubs.aapl_buy_ib_order(order_id=order_id) + order_state = IBTestExecStubs.ib_order_state(state="PreSubmitted") + + # Act + await ib_client.process_open_order( + order_id=order_id, + contract=contract, + order=order, + order_state=order_state, + ) + + # Assert + kwargs = handler_mock.call_args_list[-1].kwargs + assert kwargs["order_ref"] == "O-20240102-1754-001-000-1" + assert kwargs["order"].contract == IBContract(**contract.__dict__) + assert kwargs["order"].order_state == order_state + + @pytest.mark.asyncio async def test_orderStatus(ib_client): # Arrange From 28bd423667b41950be7410ea31af79cb4ad0ca8c Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Thu, 9 May 2024 14:50:00 +0200 Subject: [PATCH 145/193] Add psql cache database orders (#1631) --- nautilus_core/Cargo.lock | 24 +- nautilus_core/Cargo.toml | 1 - nautilus_core/cli/Cargo.toml | 1 - nautilus_core/common/src/cache/core.rs | 10 +- nautilus_core/core/Cargo.toml | 1 + nautilus_core/core/src/equality.rs | 24 ++ nautilus_core/core/src/lib.rs | 1 + nautilus_core/execution/src/client.rs | 4 +- nautilus_core/execution/src/engine.rs | 8 +- nautilus_core/infrastructure/Cargo.toml | 1 + .../src/python/sql/cache_database.rs | 34 +- .../infrastructure/src/sql/cache_database.rs | 140 ++++--- nautilus_core/infrastructure/src/sql/mod.rs | 3 +- .../infrastructure/src/sql/models/mod.rs | 1 + .../infrastructure/src/sql/models/orders.rs | 356 ++++++++++++++++++ .../infrastructure/src/sql/queries.rs | 194 +++++++++- .../tests/test_cache_database_postgres.rs | 53 ++- .../model/src/events/order/accepted.rs | 155 +++++++- .../model/src/events/order/cancel_rejected.rs | 154 +++++++- .../model/src/events/order/canceled.rs | 155 +++++++- .../model/src/events/order/denied.rs | 154 +++++++- .../model/src/events/order/emulated.rs | 155 +++++++- nautilus_core/model/src/events/order/event.rs | 4 +- .../model/src/events/order/expired.rs | 155 +++++++- .../model/src/events/order/filled.rs | 154 +++++++- .../model/src/events/order/initialized.rs | 164 +++++++- nautilus_core/model/src/events/order/mod.rs | 51 +++ .../model/src/events/order/modify_rejected.rs | 154 +++++++- .../model/src/events/order/pending_cancel.rs | 155 +++++++- .../model/src/events/order/pending_update.rs | 155 +++++++- .../model/src/events/order/rejected.rs | 154 +++++++- .../model/src/events/order/released.rs | 152 +++++++- .../model/src/events/order/submitted.rs | 155 +++++++- .../model/src/events/order/triggered.rs | 155 +++++++- .../model/src/events/order/updated.rs | 150 +++++++- nautilus_core/model/src/orders/any.rs | 30 +- nautilus_core/model/src/orders/base.rs | 184 ++++----- nautilus_core/model/src/orders/limit.rs | 10 +- .../model/src/orders/limit_if_touched.rs | 10 +- nautilus_core/model/src/orders/market.rs | 30 +- .../model/src/orders/market_if_touched.rs | 10 +- .../model/src/orders/market_to_limit.rs | 10 +- nautilus_core/model/src/orders/stop_limit.rs | 10 +- nautilus_core/model/src/orders/stop_market.rs | 10 +- .../model/src/orders/trailing_stop_limit.rs | 10 +- .../model/src/orders/trailing_stop_market.rs | 10 +- nautilus_core/model/src/polymorphism.rs | 6 +- .../src/python/events/order/initialized.rs | 8 +- .../model/src/python/events/order/mod.rs | 78 ++-- .../src/python/orders/limit_if_touched.rs | 23 +- .../model/src/python/orders/market.rs | 13 +- .../src/python/orders/market_if_touched.rs | 23 +- .../src/python/orders/market_to_limit.rs | 23 +- nautilus_core/model/src/python/orders/mod.rs | 68 +++- .../model/src/python/orders/stop_limit.rs | 9 + .../model/src/python/orders/stop_market.rs | 23 +- .../src/python/orders/trailing_stop_limit.rs | 23 +- .../src/python/orders/trailing_stop_market.rs | 23 +- nautilus_trader/cache/postgres/adapter.py | 19 + .../cache/postgres/transformers.py | 62 +++ nautilus_trader/core/nautilus_pyo3.pyi | 38 ++ nautilus_trader/model/events/order.pyx | 5 +- nautilus_trader/model/orders/stop_market.pyx | 1 - poetry.lock | 5 +- schema/tables.sql | 13 +- .../test_cache_database_postgres.py | 25 ++ 66 files changed, 3857 insertions(+), 342 deletions(-) create mode 100644 nautilus_core/core/src/equality.rs create mode 100644 nautilus_core/infrastructure/src/sql/models/orders.rs diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index f28fcc3b272f..9642cf189016 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1523,6 +1523,12 @@ dependencies = [ "syn 2.0.61", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -2636,7 +2642,6 @@ dependencies = [ "nautilus-infrastructure", "nautilus-model", "simple_logger", - "sqlx", "tokio", ] @@ -2678,6 +2683,7 @@ dependencies = [ "criterion", "heck 0.5.0", "iai", + "pretty_assertions", "pyo3", "rmp-serde", "rstest", @@ -3378,6 +3384,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -5708,6 +5724,12 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 25dda1afab9d..9938767915c6 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -50,7 +50,6 @@ tracing = "0.1.40" tokio = { version = "1.37.0", features = ["full"] } ustr = { version = "1.0.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["v4"] } -sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio"] } # dev-dependencies criterion = "0.5.1" diff --git a/nautilus_core/cli/Cargo.toml b/nautilus_core/cli/Cargo.toml index 163f78e96bb8..08731e320701 100644 --- a/nautilus_core/cli/Cargo.toml +++ b/nautilus_core/cli/Cargo.toml @@ -21,5 +21,4 @@ log = { workspace = true } clap = { version = "4.5.4", features = ["derive", "env"] } clap_derive = { version = "4.5.4" } dotenvy = { version = "0.15.7" } -sqlx = {workspace = true } simple_logger = "4.3.3" diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs index ecb5548159a1..788e1f9a0d1e 100644 --- a/nautilus_core/common/src/cache/core.rs +++ b/nautilus_core/common/src/cache/core.rs @@ -2387,7 +2387,7 @@ mod tests { use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::OrderSide, - events::order::{accepted::OrderAccepted, event::OrderEvent, submitted::OrderSubmitted}, + events::order::{accepted::OrderAccepted, event::OrderEventAny, submitted::OrderSubmitted}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, position_id::PositionId, venue_order_id::VenueOrderId, @@ -2398,7 +2398,7 @@ mod tests { }, orders::{any::OrderAny, stubs::TestOrderStubs}, polymorphism::{ - ApplyOrderEvent, GetAccountId, GetClientOrderId, GetInstrumentId, GetStrategyId, + ApplyOrderEventAny, GetAccountId, GetClientOrderId, GetInstrumentId, GetStrategyId, GetTraderId, IsOpen, }, types::{price::Price, quantity::Quantity}, @@ -2554,7 +2554,7 @@ mod tests { UnixNanos::default(), ) .unwrap(); // TODO: Should event generation be fallible? - order.apply(OrderEvent::Submitted(submitted)).unwrap(); + order.apply(OrderEventAny::Submitted(submitted)).unwrap(); cache.update_order(&order).unwrap(); let result = cache.order(&order.client_order_id()).unwrap(); @@ -2602,7 +2602,7 @@ mod tests { UnixNanos::default(), ) .unwrap(); // TODO: Should event generation be fallible? - order.apply(OrderEvent::Submitted(submitted)).unwrap(); + order.apply(OrderEventAny::Submitted(submitted)).unwrap(); cache.update_order(&order).unwrap(); let accepted = OrderAccepted::new( @@ -2618,7 +2618,7 @@ mod tests { false, ) .unwrap(); - order.apply(OrderEvent::Accepted(accepted)).unwrap(); + order.apply(OrderEventAny::Accepted(accepted)).unwrap(); cache.update_order(&order).unwrap(); let result = cache.order(&order.client_order_id()).unwrap(); diff --git a/nautilus_core/core/Cargo.toml b/nautilus_core/core/Cargo.toml index 60fbf23c0641..e136b39bf6ea 100644 --- a/nautilus_core/core/Cargo.toml +++ b/nautilus_core/core/Cargo.toml @@ -20,6 +20,7 @@ serde_json = { workspace = true } ustr = { workspace = true } uuid = { workspace = true } heck = "0.5.0" +pretty_assertions = {version = "1.4.0"} [dev-dependencies] criterion = { workspace = true } diff --git a/nautilus_core/core/src/equality.rs b/nautilus_core/core/src/equality.rs new file mode 100644 index 000000000000..49a078d79d5f --- /dev/null +++ b/nautilus_core/core/src/equality.rs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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 pretty_assertions::assert_eq; +use serde::Serialize; + +pub fn entirely_equal(a: T, b: T) { + let a_serialized = serde_json::to_string(&a).unwrap(); + let b_serialized = serde_json::to_string(&b).unwrap(); + + assert_eq!(a_serialized, b_serialized); +} diff --git a/nautilus_core/core/src/lib.rs b/nautilus_core/core/src/lib.rs index bc8c144873d0..f2dbd03c27c9 100644 --- a/nautilus_core/core/src/lib.rs +++ b/nautilus_core/core/src/lib.rs @@ -29,6 +29,7 @@ pub mod correctness; pub mod datetime; +pub mod equality; pub mod message; pub mod nanos; pub mod parsing; diff --git a/nautilus_core/execution/src/client.rs b/nautilus_core/execution/src/client.rs index 8abdc660c82a..396157104811 100644 --- a/nautilus_core/execution/src/client.rs +++ b/nautilus_core/execution/src/client.rs @@ -23,7 +23,7 @@ use nautilus_common::cache::Cache; use nautilus_core::nanos::UnixNanos; use nautilus_model::{ enums::{AccountType, LiquiditySide, OmsType, OrderSide, OrderType}, - events::{account::state::AccountState, order::event::OrderEvent}, + events::{account::state::AccountState, order::event::OrderEventAny}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, trade_id::TradeId, venue::Venue, @@ -228,7 +228,7 @@ impl ExecutionClient { todo!() } - fn send_order_event(&self, event: OrderEvent) { + fn send_order_event(&self, event: OrderEventAny) { todo!() } diff --git a/nautilus_core/execution/src/engine.rs b/nautilus_core/execution/src/engine.rs index 59e911752e1a..b778595768b2 100644 --- a/nautilus_core/execution/src/engine.rs +++ b/nautilus_core/execution/src/engine.rs @@ -25,7 +25,7 @@ use log::debug; use nautilus_common::{cache::Cache, generators::position_id::PositionIdGenerator}; use nautilus_model::{ enums::{OmsType, OrderSide}, - events::order::{event::OrderEvent, filled::OrderFilled}, + events::order::{event::OrderEventAny, filled::OrderFilled}, identifiers::{ client_id::ClientId, instrument_id::InstrumentId, strategy_id::StrategyId, venue::Venue, }, @@ -134,7 +134,7 @@ impl ExecutionEngine { self.execute_command(command); } - pub fn process(&self, event: &OrderEvent) { + pub fn process(&self, event: &OrderEventAny) { todo!(); } @@ -198,7 +198,7 @@ impl ExecutionEngine { // -- EVENT HANDLERS ---------------------------------------------------- - fn handle_event(&self, event: OrderEvent) { + fn handle_event(&self, event: OrderEventAny) { todo!(); } @@ -218,7 +218,7 @@ impl ExecutionEngine { todo!(); } - fn apply_event_to_order(&self, order: &OrderAny, event: OrderEvent) { + fn apply_event_to_order(&self, order: &OrderAny, event: OrderEventAny) { todo!(); } diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 4ba4d069c4ef..d215fa7a7fb6 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -35,6 +35,7 @@ sqlx = { version = "0.7.4", features = [ "postgres", "any", "runtime-tokio", + "json" ], optional = true } [dev-dependencies] diff --git a/nautilus_core/infrastructure/src/python/sql/cache_database.rs b/nautilus_core/infrastructure/src/python/sql/cache_database.rs index 9a4eb0147a56..9a1e483ae638 100644 --- a/nautilus_core/infrastructure/src/python/sql/cache_database.rs +++ b/nautilus_core/infrastructure/src/python/sql/cache_database.rs @@ -18,8 +18,11 @@ use std::collections::HashMap; use nautilus_common::runtime::get_runtime; use nautilus_core::python::to_pyruntime_err; use nautilus_model::{ - identifiers::instrument_id::InstrumentId, - python::instruments::{convert_instrument_any_to_pyobject, convert_pyobject_to_instrument_any}, + identifiers::{client_order_id::ClientOrderId, instrument_id::InstrumentId}, + python::{ + instruments::{convert_instrument_any_to_pyobject, convert_pyobject_to_instrument_any}, + orders::{convert_order_any_to_pyobject, convert_pyobject_to_order_any}, + }, types::currency::Currency, }; use pyo3::prelude::*; @@ -122,6 +125,33 @@ impl PostgresCacheDatabase { result.map_err(to_pyruntime_err) } + #[pyo3(name = "add_order")] + fn py_add_order(slf: PyRef<'_, Self>, order: PyObject, py: Python<'_>) -> PyResult<()> { + let order_any = convert_pyobject_to_order_any(py, order)?; + let result = get_runtime().block_on(async { slf.add_order(order_any).await }); + result.map_err(to_pyruntime_err) + } + + #[pyo3(name = "load_order")] + fn py_load_order( + slf: PyRef<'_, Self>, + order_id: ClientOrderId, + py: Python<'_>, + ) -> PyResult> { + get_runtime().block_on(async { + let result = DatabaseQueries::load_order(&slf.pool, &order_id) + .await + .unwrap(); + match result { + Some(order) => { + let py_object = convert_order_any_to_pyobject(py, order)?; + Ok(Some(py_object)) + } + None => Ok(None), + } + }) + } + #[pyo3(name = "flush_db")] fn py_drop_schema(slf: PyRef<'_, Self>) -> PyResult<()> { let result = diff --git a/nautilus_core/infrastructure/src/sql/cache_database.rs b/nautilus_core/infrastructure/src/sql/cache_database.rs index 9999f1875f38..1341d983ebf4 100644 --- a/nautilus_core/infrastructure/src/sql/cache_database.rs +++ b/nautilus_core/infrastructure/src/sql/cache_database.rs @@ -19,7 +19,9 @@ use std::{ }; use nautilus_model::{ - identifiers::instrument_id::InstrumentId, instruments::any::InstrumentAny, + identifiers::{client_order_id::ClientOrderId, instrument_id::InstrumentId}, + instruments::any::InstrumentAny, + orders::any::OrderAny, types::currency::Currency, }; use sqlx::{postgres::PgConnectOptions, PgPool}; @@ -50,6 +52,7 @@ pub enum DatabaseQuery { Add(String, Vec), AddCurrency(Currency), AddInstrument(InstrumentAny), + AddOrder(OrderAny), } fn get_buffer_interval() -> Duration { @@ -65,23 +68,19 @@ async fn drain_buffer(pool: &PgPool, buffer: &mut VecDeque) { DatabaseQuery::AddCurrency(currency) => { DatabaseQueries::add_currency(pool, currency).await.unwrap(); } - DatabaseQuery::AddInstrument(instrument) => match instrument { - InstrumentAny::CryptoFuture(crypto_future) => { - DatabaseQueries::add_instrument(pool, "CRYPTO_FUTURE", Box::new(crypto_future)) + DatabaseQuery::AddInstrument(instrument_any) => match instrument_any { + InstrumentAny::CryptoFuture(instrument) => { + DatabaseQueries::add_instrument(pool, "CRYPTO_FUTURE", Box::new(instrument)) .await .unwrap() } - InstrumentAny::CryptoPerpetual(crypto_perpetual) => { - DatabaseQueries::add_instrument( - pool, - "CRYPTO_PERPETUAL", - Box::new(crypto_perpetual), - ) - .await - .unwrap() + InstrumentAny::CryptoPerpetual(instrument) => { + DatabaseQueries::add_instrument(pool, "CRYPTO_PERPETUAL", Box::new(instrument)) + .await + .unwrap() } - InstrumentAny::CurrencyPair(currency_pair) => { - DatabaseQueries::add_instrument(pool, "CURRENCY_PAIR", Box::new(currency_pair)) + InstrumentAny::CurrencyPair(instrument) => { + DatabaseQueries::add_instrument(pool, "CURRENCY_PAIR", Box::new(instrument)) .await .unwrap() } @@ -90,38 +89,73 @@ async fn drain_buffer(pool: &PgPool, buffer: &mut VecDeque) { .await .unwrap() } - InstrumentAny::FuturesContract(futures_contract) => { - DatabaseQueries::add_instrument( - pool, - "FUTURES_CONTRACT", - Box::new(futures_contract), - ) - .await - .unwrap() - } - InstrumentAny::FuturesSpread(futures_spread) => DatabaseQueries::add_instrument( - pool, - "FUTURES_SPREAD", - Box::new(futures_spread), - ) - .await - .unwrap(), - InstrumentAny::OptionsContract(options_contract) => { - DatabaseQueries::add_instrument( - pool, - "OPTIONS_CONTRACT", - Box::new(options_contract), - ) - .await - .unwrap() - } - InstrumentAny::OptionsSpread(options_spread) => DatabaseQueries::add_instrument( - pool, - "OPTIONS_SPREAD", - Box::new(options_spread), - ) - .await - .unwrap(), + InstrumentAny::FuturesContract(instrument) => { + DatabaseQueries::add_instrument(pool, "FUTURES_CONTRACT", Box::new(instrument)) + .await + .unwrap() + } + InstrumentAny::FuturesSpread(instrument) => { + DatabaseQueries::add_instrument(pool, "FUTURES_SPREAD", Box::new(instrument)) + .await + .unwrap() + } + InstrumentAny::OptionsContract(instrument) => { + DatabaseQueries::add_instrument(pool, "OPTIONS_CONTRACT", Box::new(instrument)) + .await + .unwrap() + } + InstrumentAny::OptionsSpread(instrument) => { + DatabaseQueries::add_instrument(pool, "OPTIONS_SPREAD", Box::new(instrument)) + .await + .unwrap() + } + }, + DatabaseQuery::AddOrder(order_any) => match order_any { + OrderAny::Limit(order) => { + DatabaseQueries::add_order(pool, "LIMIT", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::LimitIfTouched(order) => { + DatabaseQueries::add_order(pool, "LIMIT_IF_TOUCHED", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::Market(order) => { + DatabaseQueries::add_order(pool, "MARKET", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::MarketIfTouched(order) => { + DatabaseQueries::add_order(pool, "MARKET_IF_TOUCHED", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::MarketToLimit(order) => { + DatabaseQueries::add_order(pool, "MARKET_TO_LIMIT", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::StopLimit(order) => { + DatabaseQueries::add_order(pool, "STOP_LIMIT", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::StopMarket(order) => { + DatabaseQueries::add_order(pool, "STOP_MARKET", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::TrailingStopLimit(order) => { + DatabaseQueries::add_order(pool, "TRAILING_STOP_LIMIT", false, Box::new(order)) + .await + .unwrap() + } + OrderAny::TrailingStopMarket(order) => { + DatabaseQueries::add_order(pool, "TRAILING_STOP_MARKET", false, Box::new(order)) + .await + .unwrap() + } }, } } @@ -232,4 +266,18 @@ impl PostgresCacheDatabase { pub async fn load_instruments(&self) -> anyhow::Result> { DatabaseQueries::load_instruments(&self.pool).await } + + pub async fn add_order(&self, order: OrderAny) -> anyhow::Result<()> { + let query = DatabaseQuery::AddOrder(order); + self.tx.send(query).await.map_err(|err| { + anyhow::anyhow!("Failed to send query add_order to database message handler: {err}") + }) + } + + pub async fn load_order( + &self, + client_order_id: &ClientOrderId, + ) -> anyhow::Result> { + DatabaseQueries::load_order(&self.pool, client_order_id).await + } } diff --git a/nautilus_core/infrastructure/src/sql/mod.rs b/nautilus_core/infrastructure/src/sql/mod.rs index 9c375a71cb53..fe66a06ae09e 100644 --- a/nautilus_core/infrastructure/src/sql/mod.rs +++ b/nautilus_core/infrastructure/src/sql/mod.rs @@ -14,7 +14,8 @@ // ------------------------------------------------------------------------------------------------- // Be careful about ordering and foreign key constraints when deleting data. -pub const NAUTILUS_TABLES: [&str; 3] = ["general", "instrument", "currency"]; +pub const NAUTILUS_TABLES: [&str; 5] = + ["general", "instrument", "currency", "order_event", "order"]; pub mod cache_database; pub mod models; diff --git a/nautilus_core/infrastructure/src/sql/models/mod.rs b/nautilus_core/infrastructure/src/sql/models/mod.rs index 4fe4acea056d..516450d535ca 100644 --- a/nautilus_core/infrastructure/src/sql/models/mod.rs +++ b/nautilus_core/infrastructure/src/sql/models/mod.rs @@ -15,4 +15,5 @@ pub mod general; pub mod instruments; +pub mod orders; pub mod types; diff --git a/nautilus_core/infrastructure/src/sql/models/orders.rs b/nautilus_core/infrastructure/src/sql/models/orders.rs new file mode 100644 index 000000000000..ac94c24ac94f --- /dev/null +++ b/nautilus_core/infrastructure/src/sql/models/orders.rs @@ -0,0 +1,356 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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::{collections::HashMap, str::FromStr}; + +use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_model::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::{ + accepted::OrderAccepted, cancel_rejected::OrderCancelRejected, canceled::OrderCanceled, + denied::OrderDenied, emulated::OrderEmulated, event::OrderEventAny, expired::OrderExpired, + filled::OrderFilled, initialized::OrderInitialized, modify_rejected::OrderModifyRejected, + pending_cancel::OrderPendingCancel, pending_update::OrderPendingUpdate, + rejected::OrderRejected, released::OrderReleased, submitted::OrderSubmitted, + triggered::OrderTriggered, updated::OrderUpdated, + }, + identifiers::{ + client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, + }, + types::{price::Price, quantity::Quantity}, +}; +use sqlx::{postgres::PgRow, FromRow, Row}; +use ustr::Ustr; + +pub struct OrderEventAnyModel(pub OrderEventAny); +pub struct OrderAcceptedModel(pub OrderAccepted); +pub struct OrderCancelRejectedModel(pub OrderCancelRejected); +pub struct OrderCanceledModel(pub OrderCanceled); +pub struct OrderDeniedModel(pub OrderDenied); +pub struct OrderEmulatedModel(pub OrderEmulated); +pub struct OrderExpiredModel(pub OrderExpired); +pub struct OrderFilledModel(pub OrderFilled); +pub struct OrderInitializedModel(pub OrderInitialized); +pub struct OrderModifyRejectedModel(pub OrderModifyRejected); +pub struct OrderPendingCancelModel(pub OrderPendingCancel); +pub struct OrderPendingUpdateModel(pub OrderPendingUpdate); +pub struct OrderRejectedModel(pub OrderRejected); +pub struct OrderReleasedModel(pub OrderReleased); +pub struct OrderSubmittedModel(pub OrderSubmitted); +pub struct OrderTriggeredModel(pub OrderTriggered); +pub struct OrderUpdatedModel(pub OrderUpdated); + +impl<'r> FromRow<'r, PgRow> for OrderEventAnyModel { + fn from_row(row: &'r PgRow) -> Result { + let kind = row.get::("kind"); + if kind == "OrderAccepted" { + let model = OrderAcceptedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Accepted(model.0))) + } else if kind == "OrderCancelRejected" { + let model = OrderCancelRejectedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::CancelRejected(model.0))) + } else if kind == "OrderCanceled" { + let model = OrderCanceledModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Canceled(model.0))) + } else if kind == "OrderDenied" { + let model = OrderDeniedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Denied(model.0))) + } else if kind == "OrderEmulated" { + let model = OrderEmulatedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Emulated(model.0))) + } else if kind == "OrderExpired" { + let model = OrderExpiredModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Expired(model.0))) + } else if kind == "OrderFilled" { + let model = OrderFilledModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Filled(model.0))) + } else if kind == "OrderInitialized" { + let model = OrderInitializedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Initialized(model.0))) + } else if kind == "OrderModifyRejected" { + let model = OrderModifyRejectedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::ModifyRejected(model.0))) + } else if kind == "OrderPendingCancel" { + let model = OrderPendingCancelModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::PendingCancel(model.0))) + } else if kind == "OrderPendingUpdate" { + let model = OrderPendingUpdateModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::PendingUpdate(model.0))) + } else if kind == "OrderRejected" { + let model = OrderRejectedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Rejected(model.0))) + } else if kind == "OrderReleased" { + let model = OrderReleasedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Released(model.0))) + } else if kind == "OrderSubmitted" { + let model = OrderSubmittedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Submitted(model.0))) + } else if kind == "OrderTriggered" { + let model = OrderTriggeredModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Triggered(model.0))) + } else if kind == "OrderUpdated" { + let model = OrderUpdatedModel::from_row(row)?; + Ok(OrderEventAnyModel(OrderEventAny::Updated(model.0))) + } else { + panic!( + "Unknown order event kind: {} in Postgres transformation", + kind + ) + } + } +} + +impl<'r> FromRow<'r, PgRow> for OrderInitializedModel { + fn from_row(row: &'r PgRow) -> Result { + let order_id = row.try_get::<&str, _>("id").map(UUID4::from)?; + let client_order_id = row + .try_get::<&str, _>("order_id") + .map(ClientOrderId::from)?; + let trader_id = row.try_get::<&str, _>("trader_id").map(TraderId::from)?; + let strategy_id = row + .try_get::<&str, _>("strategy_id") + .map(StrategyId::from)?; + let instrument_id = row + .try_get::<&str, _>("instrument_id") + .map(InstrumentId::from)?; + let order_type = row + .try_get::<&str, _>("order_type") + .map(|x| OrderType::from_str(x).unwrap())?; + let order_side = row + .try_get::<&str, _>("order_side") + .map(|x| OrderSide::from_str(x).unwrap())?; + let quantity = row.try_get::<&str, _>("quantity").map(Quantity::from)?; + let time_in_force = row + .try_get::<&str, _>("time_in_force") + .map(|x| TimeInForce::from_str(x).unwrap())?; + let post_only = row.try_get::("post_only")?; + let reduce_only = row.try_get::("reduce_only")?; + let quote_quantity = row.try_get::("quote_quantity")?; + let reconciliation = row.try_get::("reconciliation")?; + let ts_event = row + .try_get::("ts_event") + .map(|res| UnixNanos::from(res.as_str()))?; + let ts_init = row + .try_get::("ts_init") + .map(|res| UnixNanos::from(res.as_str()))?; + let price = row + .try_get::, _>("price") + .ok() + .and_then(|x| x.map(Price::from)); + let trigger_price = row + .try_get::, _>("trigger_price") + .ok() + .and_then(|x| x.map(Price::from)); + let trigger_type = row + .try_get::, _>("trigger_type") + .ok() + .and_then(|x| x.map(|x| TriggerType::from_str(x).unwrap())); + let limit_offset = row + .try_get::, _>("limit_offset") + .ok() + .and_then(|x| x.map(Price::from)); + let trailing_offset = row + .try_get::, _>("trailing_offset") + .ok() + .and_then(|x| x.map(Price::from)); + let trailing_offset_type = row + .try_get::, _>("trailing_offset_type") + .ok() + .and_then(|x| x.map(|x| TrailingOffsetType::from_str(x).unwrap())); + let expire_time = row + .try_get::, _>("expire_time") + .ok() + .and_then(|x| x.map(UnixNanos::from)); + let display_qty = row + .try_get::, _>("display_qty") + .ok() + .and_then(|x| x.map(Quantity::from)); + let emulation_trigger = row + .try_get::, _>("emulation_trigger") + .ok() + .and_then(|x| x.map(|x| TriggerType::from_str(x).unwrap())); + let trigger_instrument_id = row + .try_get::, _>("trigger_instrument_id") + .ok() + .and_then(|x| x.map(InstrumentId::from)); + let contingency_type = row + .try_get::, _>("contingency_type") + .ok() + .and_then(|x| x.map(|x| ContingencyType::from_str(x).unwrap())); + let order_list_id = row + .try_get::, _>("order_list_id") + .ok() + .and_then(|x| x.map(OrderListId::from)); + let linked_order_ids = row + .try_get::, _>("linked_order_ids") + .ok() + .map(|x| x.iter().map(|x| ClientOrderId::from(x.as_str())).collect()); + let parent_order_id = row + .try_get::, _>("parent_order_id") + .ok() + .and_then(|x| x.map(ClientOrderId::from)); + let exec_algorithm_id = row + .try_get::, _>("exec_algorithm_id") + .ok() + .and_then(|x| x.map(ExecAlgorithmId::from)); + let exec_algorithm_params: Option> = row + .try_get::, _>("exec_algorithm_params") + .ok() + .and_then(|x| x.map(|x| serde_json::from_value::>(x).unwrap())) + .map(|x| { + x.into_iter() + .map(|(k, v)| (Ustr::from(k.as_str()), Ustr::from(v.as_str()))) + .collect() + }); + let exec_spawn_id = row + .try_get::, _>("exec_spawn_id") + .ok() + .and_then(|x| x.map(ClientOrderId::from)); + let tags: Option> = row + .try_get::, _>("tags") + .ok() + .and_then(|x| x.map(|x| serde_json::from_value::>(x).unwrap())) + .map(|x| x.into_iter().map(|x| Ustr::from(x.as_str())).collect()); + let order = OrderInitialized::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + order_side, + order_type, + quantity, + time_in_force, + post_only, + reduce_only, + quote_quantity, + reconciliation, + order_id, + ts_event, + ts_init, + price, + trigger_price, + trigger_type, + limit_offset, + trailing_offset, + trailing_offset_type, + expire_time, + display_qty, + emulation_trigger, + trigger_instrument_id, + contingency_type, + order_list_id, + linked_order_ids, + parent_order_id, + exec_algorithm_id, + exec_algorithm_params, + exec_spawn_id, + tags, + ) + .unwrap(); + Ok(OrderInitializedModel(order)) + } +} + +impl<'r> FromRow<'r, PgRow> for OrderAcceptedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderCancelRejectedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderCanceledModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderDeniedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderEmulatedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderExpiredModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderFilledModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderModifyRejectedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderPendingCancelModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderPendingUpdateModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderRejectedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderReleasedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderSubmittedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderTriggeredModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} + +impl<'r> FromRow<'r, PgRow> for OrderUpdatedModel { + fn from_row(_row: &'r PgRow) -> Result { + todo!() + } +} diff --git a/nautilus_core/infrastructure/src/sql/queries.rs b/nautilus_core/infrastructure/src/sql/queries.rs index 449326f4aadc..b39d49843e1b 100644 --- a/nautilus_core/infrastructure/src/sql/queries.rs +++ b/nautilus_core/infrastructure/src/sql/queries.rs @@ -16,14 +16,17 @@ use std::collections::HashMap; use nautilus_model::{ - identifiers::instrument_id::InstrumentId, + events::order::{event::OrderEventAny, OrderEvent}, + identifiers::{client_order_id::ClientOrderId, instrument_id::InstrumentId}, instruments::{any::InstrumentAny, Instrument}, + orders::{any::OrderAny, base::Order}, types::currency::Currency, }; -use sqlx::PgPool; +use sqlx::{PgPool, Row}; use crate::sql::models::{ - general::GeneralRow, instruments::InstrumentAnyModel, types::CurrencyModel, + general::GeneralRow, instruments::InstrumentAnyModel, orders::OrderEventAnyModel, + types::CurrencyModel, }; pub struct DatabaseQueries; @@ -173,4 +176,189 @@ impl DatabaseQueries { .map(|rows| rows.into_iter().map(|row| row.0).collect()) .map_err(|err| anyhow::anyhow!("Failed to load instruments: {err}")) } + + pub async fn add_order( + pool: &PgPool, + _kind: &str, + updated: bool, + order: Box, + ) -> anyhow::Result<()> { + if updated { + let exists = + DatabaseQueries::check_if_order_initialized_exists(pool, order.client_order_id()) + .await + .unwrap(); + if !exists { + panic!( + "OrderInitialized event does not exist for order: {}", + order.client_order_id() + ); + } + } + match order.last_event().clone() { + OrderEventAny::Accepted(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::CancelRejected(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Canceled(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Denied(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Emulated(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Expired(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Filled(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Initialized(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::ModifyRejected(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::PendingCancel(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::PendingUpdate(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Rejected(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Released(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Submitted(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Updated(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::Triggered(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + OrderEventAny::PartiallyFilled(event) => { + DatabaseQueries::add_order_event(pool, Box::new(event)).await + } + } + } + + pub async fn check_if_order_initialized_exists( + pool: &PgPool, + order_id: ClientOrderId, + ) -> anyhow::Result { + sqlx::query(r#" + SELECT EXISTS(SELECT 1 FROM "order_event" WHERE order_id = $1 AND kind = 'OrderInitialized') + "#) + .bind(order_id.to_string()) + .fetch_one(pool) + .await + .map(|row| row.get(0)) + .map_err(|err| anyhow::anyhow!("Failed to check if order initialized exists: {err}")) + } + + pub async fn add_order_event( + pool: &PgPool, + order_event: Box, + ) -> anyhow::Result<()> { + sqlx::query(r#" + INSERT INTO "order_event" ( + id, kind, order_id, order_type, order_side, trader_id, strategy_id, instrument_id, quantity, time_in_force, + post_only, reduce_only, quote_quantity, reconciliation, price, trigger_price, trigger_type, limit_offset, trailing_offset, + trailing_offset_type, expire_time, display_qty, emulation_trigger, trigger_instrument_id, contingency_type, + order_list_id, linked_order_ids, parent_order_id, + exec_algorithm_id, exec_spawn_id, venue_order_id, account_id, ts_event, ts_init, created_at, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, + $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + ) + ON CONFLICT (id) + DO UPDATE + SET + kind = $2, order_id = $3, order_type = $4, order_side=$5, trader_id = $6, strategy_id = $7, instrument_id = $8, quantity = $9, time_in_force= $10, + post_only = $11, reduce_only = $12, quote_quantity = $13, reconciliation = $14, price = $15, trigger_price = $16, trigger_type = $17, limit_offset = $18, trailing_offset = $19, + trailing_offset_type = $20, expire_time = $21, display_qty = $22, emulation_trigger = $23, trigger_instrument_id = $24, contingency_type = $25, + order_list_id = $26, linked_order_ids = $27, + parent_order_id = $28, exec_algorithm_id = $29, exec_spawn_id = $30, venue_order_id = $31, account_id = $32, ts_event = $33, ts_init = $34, updated_at = CURRENT_TIMESTAMP + "#) + .bind(order_event.id().to_string()) + .bind(order_event.kind()) + .bind(order_event.client_order_id().to_string()) + .bind(order_event.order_type().map(|x| x.to_string())) + .bind(order_event.order_side().map(|x| format!("{:?}", x))) + .bind(order_event.trader_id().to_string()) + .bind(order_event.strategy_id().to_string()) + .bind(order_event.instrument_id().to_string()) + .bind(order_event.quantity().map(|x| x.to_string())) + .bind(order_event.time_in_force().map(|x| format!("{:?}", x))) + .bind(order_event.post_only()) + .bind(order_event.reduce_only()) + .bind(order_event.quote_quantity()) + .bind(order_event.reconciliation()) + .bind(order_event.price().map(|x| x.to_string())) + .bind(order_event.trigger_price().map(|x| x.to_string())) + .bind(order_event.trigger_type().map(|x| x.to_string())) + .bind(order_event.limit_offset().map(|x| x.to_string())) + .bind(order_event.trailing_offset().map(|x| x.to_string())) + .bind(order_event.trailing_offset_type().map(|x| format!("{:?}", x))) + .bind(order_event.expire_time().map(|x| x.to_string())) + .bind(order_event.display_qty().map(|x| x.to_string())) + .bind(order_event.emulation_trigger().map(|x| x.to_string())) + .bind(order_event.trigger_instrument_id().map(|x| x.to_string())) + .bind(order_event.contingency_type().map(|x| x.to_string())) + .bind(order_event.order_list_id().map(|x| x.to_string())) + .bind(order_event.linked_order_ids().map(|x| x.iter().map(|x| x.to_string()).collect::>())) + .bind(order_event.parent_order_id().map(|x| x.to_string())) + .bind(order_event.exec_algorithm_id().map(|x| x.to_string())) + .bind(order_event.exec_spawn_id().map(|x| x.to_string())) + .bind(order_event.venue_order_id().map(|x| x.to_string())) + .bind(order_event.account_id().map(|x| x.to_string())) + .bind(order_event.ts_event().to_string()) + .bind(order_event.ts_init().to_string()) + .execute(pool) + .await + .map(|_| ()) + .map_err(|err| anyhow::anyhow!("Failed to insert into order_event table: {err}")) + } + + pub async fn load_order_events( + pool: &PgPool, + order_id: &ClientOrderId, + ) -> anyhow::Result> { + sqlx::query_as::<_, OrderEventAnyModel>( + r#" + SELECT * FROM "order_event" event WHERE event.order_id = $1 ORDER BY event.ts_init ASC + "#, + ) + .bind(order_id.to_string()) + .fetch_all(pool) + .await + .map(|rows| rows.into_iter().map(|row| row.0).collect()) + .map_err(|err| anyhow::anyhow!("Failed to load order events: {err}")) + } + + pub async fn load_order( + pool: &PgPool, + order_id: &ClientOrderId, + ) -> anyhow::Result> { + let order_events = DatabaseQueries::load_order_events(pool, order_id).await; + + match order_events { + Ok(order_events) => { + if order_events.is_empty() { + return Ok(None); + } + let order = OrderAny::from_events(order_events).unwrap(); + Ok(Some(order)) + } + Err(err) => anyhow::bail!("Failed to load order events: {err}"), + } + } } diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs index b2928547e085..9aeb7ab82817 100644 --- a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -61,9 +61,10 @@ pub async fn get_pg_cache_database() -> anyhow::Result { mod tests { use std::time::Duration; + use nautilus_core::equality::entirely_equal; use nautilus_model::{ - enums::CurrencyType, - identifiers::instrument_id::InstrumentId, + enums::{CurrencyType, OrderSide}, + identifiers::{client_order_id::ClientOrderId, instrument_id::InstrumentId}, instruments::{ any::InstrumentAny, stubs::{ @@ -72,18 +73,12 @@ mod tests { }, Instrument, }, + orders::{any::OrderAny, stubs::TestOrderStubs}, types::{currency::Currency, price::Price, quantity::Quantity}, }; use crate::get_pg_cache_database; - #[tokio::test] - async fn test_load_general_objects_when_nothing_in_cache_returns_empty_hashmap() { - let pg_cache = get_pg_cache_database().await.unwrap(); - let result = pg_cache.load().await.unwrap(); - assert_eq!(result.len(), 0); - } - #[tokio::test] async fn test_add_general_object_adds_to_cache() { let pg_cache = get_pg_cache_database().await.unwrap(); @@ -236,4 +231,44 @@ mod tests { ] ); } + + #[tokio::test] + async fn test_add_order() { + let instrument = currency_pair_ethusdt(); + let pg_cache = get_pg_cache_database().await.unwrap(); + let market_order = TestOrderStubs::market_order( + instrument.id(), + OrderSide::Buy, + Quantity::from("1.0"), + Some(ClientOrderId::new("O-19700101-0000-000-001-1").unwrap()), + None, + ); + let limit_order = TestOrderStubs::limit_order( + instrument.id(), + OrderSide::Sell, + Price::from("100.0"), + Quantity::from("1.0"), + Some(ClientOrderId::new("O-19700101-0000-000-001-2").unwrap()), + None, + ); + pg_cache + .add_order(OrderAny::Market(market_order.clone())) + .await + .unwrap(); + pg_cache + .add_order(OrderAny::Limit(limit_order.clone())) + .await + .unwrap(); + tokio::time::sleep(Duration::from_secs(2)).await; + let market_order_result = pg_cache + .load_order(&market_order.client_order_id) + .await + .unwrap(); + let limit_order_result = pg_cache + .load_order(&limit_order.client_order_id) + .await + .unwrap(); + entirely_equal(market_order_result.unwrap(), OrderAny::Market(market_order)); + entirely_equal(limit_order_result.unwrap(), OrderAny::Limit(limit_order)); + } } diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index 1e456374a242..eaa257e305e4 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -107,6 +114,148 @@ impl Display for OrderAccepted { } } +impl OrderEvent for OrderAccepted { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderAccepted) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + Some(self.venue_order_id) + } + + fn account_id(&self) -> Option { + Some(self.account_id) + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index 8d74c63e00a8..b8bb25e531a5 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -20,9 +20,15 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -114,6 +120,148 @@ impl Display for OrderCancelRejected { } } +impl OrderEvent for OrderCancelRejected { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderCancelRejected) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + Some(self.reason) + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + self.account_id + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/canceled.rs b/nautilus_core/model/src/events/order/canceled.rs index cd9944b23034..9400bf186ebd 100644 --- a/nautilus_core/model/src/events/order/canceled.rs +++ b/nautilus_core/model/src/events/order/canceled.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -107,6 +114,148 @@ impl Display for OrderCanceled { } } +impl OrderEvent for OrderCanceled { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderCanceled) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + self.account_id + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/denied.rs b/nautilus_core/model/src/events/order/denied.rs index eab9fb4db251..238a945dd3d8 100644 --- a/nautilus_core/model/src/events/order/denied.rs +++ b/nautilus_core/model/src/events/order/denied.rs @@ -20,9 +20,15 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use crate::identifiers::{ - client_order_id::ClientOrderId, instrument_id::InstrumentId, strategy_id::StrategyId, - trader_id::TraderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -98,6 +104,148 @@ impl Display for OrderDenied { } } +impl OrderEvent for OrderDenied { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderDenied) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + Some(self.reason) + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + None + } + + fn account_id(&self) -> Option { + None + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/emulated.rs b/nautilus_core/model/src/events/order/emulated.rs index d31176034573..d9077dd7fd97 100644 --- a/nautilus_core/model/src/events/order/emulated.rs +++ b/nautilus_core/model/src/events/order/emulated.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - client_order_id::ClientOrderId, instrument_id::InstrumentId, strategy_id::StrategyId, - trader_id::TraderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -93,6 +100,148 @@ impl Display for OrderEmulated { } } +impl OrderEvent for OrderEmulated { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderEmulated) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + None + } + + fn account_id(&self) -> Option { + None + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/event.rs b/nautilus_core/model/src/events/order/event.rs index 638ea13b1e40..d05ff5221165 100644 --- a/nautilus_core/model/src/events/order/event.rs +++ b/nautilus_core/model/src/events/order/event.rs @@ -30,7 +30,7 @@ use crate::{ }; #[derive(Clone, PartialEq, Eq, Display, Debug, Serialize, Deserialize)] -pub enum OrderEvent { +pub enum OrderEventAny { Initialized(OrderInitialized), Denied(OrderDenied), Emulated(OrderEmulated), @@ -50,7 +50,7 @@ pub enum OrderEvent { Filled(OrderFilled), } -impl OrderEvent { +impl OrderEventAny { #[must_use] pub fn client_order_id(&self) -> ClientOrderId { match self { diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index 0d91192717f8..76c47e86ebd8 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -108,6 +115,148 @@ impl Display for OrderExpired { } } +impl OrderEvent for OrderExpired { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderExpired) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + self.account_id + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/filled.rs b/nautilus_core/model/src/events/order/filled.rs index 677ea60d6af6..f823ba6f6f90 100644 --- a/nautilus_core/model/src/events/order/filled.rs +++ b/nautilus_core/model/src/events/order/filled.rs @@ -18,12 +18,18 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; use crate::{ - enums::{LiquiditySide, OrderSide, OrderType}, + enums::{ + ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType, + TriggerType, + }, + events::order::OrderEvent, identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - position_id::PositionId, strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, + strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, venue_order_id::VenueOrderId, }, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, @@ -232,6 +238,148 @@ impl Display for OrderFilled { } } +impl OrderEvent for OrderFilled { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderFilled) + } + + fn order_type(&self) -> Option { + Some(self.order_type) + } + + fn order_side(&self) -> Option { + Some(self.order_side) + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + Some(self.last_qty) + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + Some(self.venue_order_id) + } + + fn account_id(&self) -> Option { + Some(self.account_id) + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/initialized.rs b/nautilus_core/model/src/events/order/initialized.rs index 74a9c789c9cb..8181143226c3 100644 --- a/nautilus_core/model/src/events/order/initialized.rs +++ b/nautilus_core/model/src/events/order/initialized.rs @@ -25,11 +25,13 @@ use ustr::Ustr; use crate::{ enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, identifiers::{ - client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, - trader_id::TraderId, + trader_id::TraderId, venue_order_id::VenueOrderId, }, + orders::any::OrderAny, types::{price::Price, quantity::Quantity}, }; @@ -368,6 +370,164 @@ impl Display for OrderInitialized { } } +impl OrderEvent for OrderInitialized { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderInitialized) + } + + fn order_type(&self) -> Option { + Some(self.order_type) + } + + fn order_side(&self) -> Option { + Some(self.order_side) + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + Some(self.quantity) + } + + fn time_in_force(&self) -> Option { + Some(self.time_in_force) + } + + fn post_only(&self) -> Option { + Some(self.post_only) + } + + fn reduce_only(&self) -> Option { + Some(self.reduce_only) + } + + fn quote_quantity(&self) -> Option { + Some(self.quote_quantity) + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + self.price + } + + fn trigger_price(&self) -> Option { + self.trigger_price + } + + fn trigger_type(&self) -> Option { + self.trigger_type + } + + fn limit_offset(&self) -> Option { + self.limit_offset + } + + fn trailing_offset(&self) -> Option { + self.trailing_offset + } + + fn trailing_offset_type(&self) -> Option { + self.trailing_offset_type + } + + fn expire_time(&self) -> Option { + self.expire_time + } + + fn display_qty(&self) -> Option { + self.display_qty + } + + fn emulation_trigger(&self) -> Option { + self.emulation_trigger + } + + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + + fn contingency_type(&self) -> Option { + self.contingency_type + } + + fn order_list_id(&self) -> Option { + self.order_list_id + } + + fn linked_order_ids(&self) -> Option> { + self.linked_order_ids.clone() + } + + fn parent_order_id(&self) -> Option { + self.parent_order_id + } + + fn exec_algorithm_id(&self) -> Option { + self.exec_algorithm_id + } + + fn exec_spawn_id(&self) -> Option { + self.exec_spawn_id + } + + fn venue_order_id(&self) -> Option { + None + } + + fn account_id(&self) -> Option { + None + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + +impl From for OrderAny { + fn from(order: OrderInitialized) -> Self { + match order.order_type { + OrderType::Limit => OrderAny::Limit(order.into()), + OrderType::Market => OrderAny::Market(order.into()), + OrderType::StopMarket => OrderAny::StopMarket(order.into()), + OrderType::StopLimit => OrderAny::StopLimit(order.into()), + OrderType::LimitIfTouched => OrderAny::LimitIfTouched(order.into()), + OrderType::TrailingStopLimit => OrderAny::TrailingStopLimit(order.into()), + OrderType::TrailingStopMarket => OrderAny::TrailingStopMarket(order.into()), + OrderType::MarketToLimit => OrderAny::MarketToLimit(order.into()), + OrderType::MarketIfTouched => OrderAny::MarketIfTouched(order.into()), + } + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/mod.rs b/nautilus_core/model/src/events/order/mod.rs index ebb347470a46..1da1f02af7c9 100644 --- a/nautilus_core/model/src/events/order/mod.rs +++ b/nautilus_core/model/src/events/order/mod.rs @@ -13,6 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use ustr::Ustr; + +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, +}; + pub mod accepted; pub mod cancel_rejected; pub mod canceled; @@ -33,3 +46,41 @@ pub mod updated; #[cfg(feature = "stubs")] pub mod stubs; + +pub trait OrderEvent: 'static + Send { + fn id(&self) -> UUID4; + fn kind(&self) -> &str; + fn order_type(&self) -> Option; + fn order_side(&self) -> Option; + fn trader_id(&self) -> TraderId; + fn strategy_id(&self) -> StrategyId; + fn instrument_id(&self) -> InstrumentId; + fn client_order_id(&self) -> ClientOrderId; + fn reason(&self) -> Option; + fn quantity(&self) -> Option; + fn time_in_force(&self) -> Option; + fn post_only(&self) -> Option; + fn reduce_only(&self) -> Option; + fn quote_quantity(&self) -> Option; + fn reconciliation(&self) -> bool; + fn price(&self) -> Option; + fn trigger_price(&self) -> Option; + fn trigger_type(&self) -> Option; + fn limit_offset(&self) -> Option; + fn trailing_offset(&self) -> Option; + fn trailing_offset_type(&self) -> Option; + fn expire_time(&self) -> Option; + fn display_qty(&self) -> Option; + fn emulation_trigger(&self) -> Option; + fn trigger_instrument_id(&self) -> Option; + fn contingency_type(&self) -> Option; + fn order_list_id(&self) -> Option; + fn linked_order_ids(&self) -> Option>; + fn parent_order_id(&self) -> Option; + fn exec_algorithm_id(&self) -> Option; + fn exec_spawn_id(&self) -> Option; + fn venue_order_id(&self) -> Option; + fn account_id(&self) -> Option; + fn ts_event(&self) -> UnixNanos; + fn ts_init(&self) -> UnixNanos; +} diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index 6547afe3217c..669ddb97a636 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -20,9 +20,15 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -113,6 +119,148 @@ impl Display for OrderModifyRejected { } } +impl OrderEvent for OrderModifyRejected { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderModifyRejected) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + Some(self.reason) + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + self.account_id + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index e0339f84f006..3208c3c4551c 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -107,6 +114,148 @@ impl Display for OrderPendingCancel { } } +impl OrderEvent for OrderPendingCancel { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderPendingCancel) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + Some(self.account_id) + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index 90a4125c6860..89019c698ff9 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -107,6 +114,148 @@ impl Display for OrderPendingUpdate { } } +impl OrderEvent for OrderPendingUpdate { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderPendingUpdate) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + Some(self.account_id) + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index e9e5c7c1c9ab..6c6a0187f8ae 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -20,9 +20,15 @@ use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -108,6 +114,148 @@ impl Display for OrderRejected { } } +impl OrderEvent for OrderRejected { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderRejected) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + Some(self.reason) + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + None + } + + fn account_id(&self) -> Option { + Some(self.account_id) + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/released.rs b/nautilus_core/model/src/events/order/released.rs index c92ce1527e6c..e2bb4a434e11 100644 --- a/nautilus_core/model/src/events/order/released.rs +++ b/nautilus_core/model/src/events/order/released.rs @@ -18,13 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, identifiers::{ - client_order_id::ClientOrderId, instrument_id::InstrumentId, strategy_id::StrategyId, - trader_id::TraderId, + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, }, - types::price::Price, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -100,6 +104,148 @@ impl Display for OrderReleased { } } +impl OrderEvent for OrderReleased { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderReleased) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + None + } + + fn account_id(&self) -> Option { + None + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/submitted.rs b/nautilus_core/model/src/events/order/submitted.rs index bbf80e317103..26aa86644988 100644 --- a/nautilus_core/model/src/events/order/submitted.rs +++ b/nautilus_core/model/src/events/order/submitted.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -99,6 +106,148 @@ impl Display for OrderSubmitted { } } +impl OrderEvent for OrderSubmitted { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderSubmitted) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + None + } + + fn account_id(&self) -> Option { + Some(self.account_id) + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index 5c76a1f83bec..8ce80359b5f2 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -18,10 +18,17 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; -use crate::identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, +use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::{price::Price, quantity::Quantity}, }; #[repr(C)] @@ -111,6 +118,148 @@ impl Display for OrderTriggered { } } +impl OrderEvent for OrderTriggered { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderTriggered) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + None + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + None + } + + fn trigger_price(&self) -> Option { + None + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + self.account_id + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index 3e2a6242f217..6c1406f92956 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -18,11 +18,15 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; use crate::{ + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, + events::order::OrderEvent, identifiers::{ - account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, - strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, + account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, venue_order_id::VenueOrderId, }, types::{price::Price, quantity::Quantity}, }; @@ -126,6 +130,148 @@ impl Display for OrderUpdated { } } +impl OrderEvent for OrderUpdated { + fn id(&self) -> UUID4 { + self.event_id + } + + fn kind(&self) -> &str { + stringify!(OrderUpdated) + } + + fn order_type(&self) -> Option { + None + } + + fn order_side(&self) -> Option { + None + } + + fn trader_id(&self) -> TraderId { + self.trader_id + } + + fn strategy_id(&self) -> StrategyId { + self.strategy_id + } + + fn instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + fn client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + fn reason(&self) -> Option { + None + } + + fn quantity(&self) -> Option { + Some(self.quantity) + } + + fn time_in_force(&self) -> Option { + None + } + + fn post_only(&self) -> Option { + None + } + + fn reduce_only(&self) -> Option { + None + } + + fn quote_quantity(&self) -> Option { + None + } + + fn reconciliation(&self) -> bool { + false + } + + fn price(&self) -> Option { + self.price + } + + fn trigger_price(&self) -> Option { + self.trigger_price + } + + fn trigger_type(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + + fn expire_time(&self) -> Option { + None + } + + fn display_qty(&self) -> Option { + None + } + + fn emulation_trigger(&self) -> Option { + None + } + + fn trigger_instrument_id(&self) -> Option { + None + } + + fn contingency_type(&self) -> Option { + None + } + + fn order_list_id(&self) -> Option { + None + } + + fn linked_order_ids(&self) -> Option> { + None + } + + fn parent_order_id(&self) -> Option { + None + } + + fn exec_algorithm_id(&self) -> Option { + None + } + + fn exec_spawn_id(&self) -> Option { + None + } + + fn venue_order_id(&self) -> Option { + self.venue_order_id + } + + fn account_id(&self) -> Option { + self.account_id + } + + fn ts_event(&self) -> UnixNanos { + self.ts_event + } + + fn ts_init(&self) -> UnixNanos { + self.ts_init + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/orders/any.rs b/nautilus_core/model/src/orders/any.rs index 8ece2077e5fd..d3a753964fec 100644 --- a/nautilus_core/model/src/orders/any.rs +++ b/nautilus_core/model/src/orders/any.rs @@ -30,17 +30,17 @@ use super::{ }; use crate::{ enums::{OrderSide, OrderSideSpecified, TriggerType}, - events::order::event::OrderEvent, + events::order::event::OrderEventAny, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, }, polymorphism::{ - ApplyOrderEvent, GetAccountId, GetClientOrderId, GetEmulationTrigger, GetExecAlgorithmId, - GetExecSpawnId, GetInstrumentId, GetLimitPrice, GetOrderFilledQty, GetOrderLeavesQty, - GetOrderQuantity, GetOrderSide, GetOrderSideSpecified, GetPositionId, GetStopPrice, - GetStrategyId, GetTraderId, GetVenueOrderId, IsClosed, IsInflight, IsOpen, + ApplyOrderEventAny, GetAccountId, GetClientOrderId, GetEmulationTrigger, + GetExecAlgorithmId, GetExecSpawnId, GetInstrumentId, GetLimitPrice, GetOrderFilledQty, + GetOrderLeavesQty, GetOrderQuantity, GetOrderSide, GetOrderSideSpecified, GetPositionId, + GetStopPrice, GetStrategyId, GetTraderId, GetVenueOrderId, IsClosed, IsInflight, IsOpen, }, types::{price::Price, quantity::Quantity}, }; @@ -103,6 +103,22 @@ impl OrderAny { pub fn from_trailing_stop_market(order: StopMarketOrder) -> Self { Self::StopMarket(order) } + + pub fn from_events(events: Vec) -> anyhow::Result { + if events.is_empty() { + anyhow::bail!("No events provided"); + } else if events.len() == 1 { + let init_event = events.first().unwrap(); + match init_event { + OrderEventAny::Initialized(init) => Ok(init.to_owned().into()), + _ => { + anyhow::bail!("First event must be OrderInitialized"); + } + } + } else { + anyhow::bail!("Only one event can be provided"); + } + } } impl PartialEq for OrderAny { @@ -399,8 +415,8 @@ impl IsInflight for OrderAny { } } -impl ApplyOrderEvent for OrderAny { - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { +impl ApplyOrderEventAny for OrderAny { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { match self { Self::Limit(order) => order.apply(event), Self::LimitIfTouched(order) => order.apply(event), diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 16c7149471db..533a4fc43fd4 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -28,7 +28,7 @@ use crate::{ }, events::order::{ accepted::OrderAccepted, cancel_rejected::OrderCancelRejected, canceled::OrderCanceled, - denied::OrderDenied, emulated::OrderEmulated, event::OrderEvent, expired::OrderExpired, + denied::OrderDenied, emulated::OrderEmulated, event::OrderEventAny, expired::OrderExpired, filled::OrderFilled, initialized::OrderInitialized, modify_rejected::OrderModifyRejected, pending_cancel::OrderPendingCancel, pending_update::OrderPendingUpdate, rejected::OrderRejected, released::OrderReleased, submitted::OrderSubmitted, @@ -95,76 +95,76 @@ pub fn str_hashmap_to_ustr(h: HashMap) -> HashMap { impl OrderStatus { #[rustfmt::skip] - pub fn transition(&mut self, event: &OrderEvent) -> Result { + pub fn transition(&mut self, event: &OrderEventAny) -> Result { let new_state = match (self, event) { - (Self::Initialized, OrderEvent::Denied(_)) => Self::Denied, - (Self::Initialized, OrderEvent::Emulated(_)) => Self::Emulated, // Emulated orders - (Self::Initialized, OrderEvent::Released(_)) => Self::Released, // Emulated orders - (Self::Initialized, OrderEvent::Submitted(_)) => Self::Submitted, - (Self::Initialized, OrderEvent::Rejected(_)) => Self::Rejected, // External orders - (Self::Initialized, OrderEvent::Accepted(_)) => Self::Accepted, // External orders - (Self::Initialized, OrderEvent::Canceled(_)) => Self::Canceled, // External orders - (Self::Initialized, OrderEvent::Expired(_)) => Self::Expired, // External orders - (Self::Initialized, OrderEvent::Triggered(_)) => Self::Triggered, // External orders - (Self::Emulated, OrderEvent::Canceled(_)) => Self::Canceled, // Emulated orders - (Self::Emulated, OrderEvent::Expired(_)) => Self::Expired, // Emulated orders - (Self::Emulated, OrderEvent::Released(_)) => Self::Released, // Emulated orders - (Self::Released, OrderEvent::Submitted(_)) => Self::Submitted, // Emulated orders - (Self::Released, OrderEvent::Denied(_)) => Self::Denied, // Emulated orders - (Self::Released, OrderEvent::Canceled(_)) => Self::Canceled, // Execution algo - (Self::Submitted, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, - (Self::Submitted, OrderEvent::PendingCancel(_)) => Self::PendingCancel, - (Self::Submitted, OrderEvent::Rejected(_)) => Self::Rejected, - (Self::Submitted, OrderEvent::Canceled(_)) => Self::Canceled, // FOK and IOC cases - (Self::Submitted, OrderEvent::Accepted(_)) => Self::Accepted, - (Self::Submitted, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, - (Self::Submitted, OrderEvent::Filled(_)) => Self::Filled, - (Self::Accepted, OrderEvent::Rejected(_)) => Self::Rejected, // StopLimit order - (Self::Accepted, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, - (Self::Accepted, OrderEvent::PendingCancel(_)) => Self::PendingCancel, - (Self::Accepted, OrderEvent::Canceled(_)) => Self::Canceled, - (Self::Accepted, OrderEvent::Triggered(_)) => Self::Triggered, - (Self::Accepted, OrderEvent::Expired(_)) => Self::Expired, - (Self::Accepted, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, - (Self::Accepted, OrderEvent::Filled(_)) => Self::Filled, - (Self::Canceled, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, // Real world possibility - (Self::Canceled, OrderEvent::Filled(_)) => Self::Filled, // Real world possibility - (Self::PendingUpdate, OrderEvent::Rejected(_)) => Self::Rejected, - (Self::PendingUpdate, OrderEvent::Accepted(_)) => Self::Accepted, - (Self::PendingUpdate, OrderEvent::Canceled(_)) => Self::Canceled, - (Self::PendingUpdate, OrderEvent::Expired(_)) => Self::Expired, - (Self::PendingUpdate, OrderEvent::Triggered(_)) => Self::Triggered, - (Self::PendingUpdate, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, // Allow multiple requests - (Self::PendingUpdate, OrderEvent::PendingCancel(_)) => Self::PendingCancel, - (Self::PendingUpdate, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, - (Self::PendingUpdate, OrderEvent::Filled(_)) => Self::Filled, - (Self::PendingCancel, OrderEvent::Rejected(_)) => Self::Rejected, - (Self::PendingCancel, OrderEvent::PendingCancel(_)) => Self::PendingCancel, // Allow multiple requests - (Self::PendingCancel, OrderEvent::Canceled(_)) => Self::Canceled, - (Self::PendingCancel, OrderEvent::Expired(_)) => Self::Expired, - (Self::PendingCancel, OrderEvent::Accepted(_)) => Self::Accepted, // Allow failed cancel requests - (Self::PendingCancel, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, - (Self::PendingCancel, OrderEvent::Filled(_)) => Self::Filled, - (Self::Triggered, OrderEvent::Rejected(_)) => Self::Rejected, - (Self::Triggered, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, - (Self::Triggered, OrderEvent::PendingCancel(_)) => Self::PendingCancel, - (Self::Triggered, OrderEvent::Canceled(_)) => Self::Canceled, - (Self::Triggered, OrderEvent::Expired(_)) => Self::Expired, - (Self::Triggered, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, - (Self::Triggered, OrderEvent::Filled(_)) => Self::Filled, - (Self::PartiallyFilled, OrderEvent::PendingUpdate(_)) => Self::PendingUpdate, - (Self::PartiallyFilled, OrderEvent::PendingCancel(_)) => Self::PendingCancel, - (Self::PartiallyFilled, OrderEvent::Canceled(_)) => Self::Canceled, - (Self::PartiallyFilled, OrderEvent::Expired(_)) => Self::Expired, - (Self::PartiallyFilled, OrderEvent::PartiallyFilled(_)) => Self::PartiallyFilled, - (Self::PartiallyFilled, OrderEvent::Filled(_)) => Self::Filled, + (Self::Initialized, OrderEventAny::Denied(_)) => Self::Denied, + (Self::Initialized, OrderEventAny::Emulated(_)) => Self::Emulated, // Emulated orders + (Self::Initialized, OrderEventAny::Released(_)) => Self::Released, // Emulated orders + (Self::Initialized, OrderEventAny::Submitted(_)) => Self::Submitted, + (Self::Initialized, OrderEventAny::Rejected(_)) => Self::Rejected, // External orders + (Self::Initialized, OrderEventAny::Accepted(_)) => Self::Accepted, // External orders + (Self::Initialized, OrderEventAny::Canceled(_)) => Self::Canceled, // External orders + (Self::Initialized, OrderEventAny::Expired(_)) => Self::Expired, // External orders + (Self::Initialized, OrderEventAny::Triggered(_)) => Self::Triggered, // External orders + (Self::Emulated, OrderEventAny::Canceled(_)) => Self::Canceled, // Emulated orders + (Self::Emulated, OrderEventAny::Expired(_)) => Self::Expired, // Emulated orders + (Self::Emulated, OrderEventAny::Released(_)) => Self::Released, // Emulated orders + (Self::Released, OrderEventAny::Submitted(_)) => Self::Submitted, // Emulated orders + (Self::Released, OrderEventAny::Denied(_)) => Self::Denied, // Emulated orders + (Self::Released, OrderEventAny::Canceled(_)) => Self::Canceled, // Execution algo + (Self::Submitted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, + (Self::Submitted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, + (Self::Submitted, OrderEventAny::Rejected(_)) => Self::Rejected, + (Self::Submitted, OrderEventAny::Canceled(_)) => Self::Canceled, // FOK and IOC cases + (Self::Submitted, OrderEventAny::Accepted(_)) => Self::Accepted, + (Self::Submitted, OrderEventAny::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::Submitted, OrderEventAny::Filled(_)) => Self::Filled, + (Self::Accepted, OrderEventAny::Rejected(_)) => Self::Rejected, // StopLimit order + (Self::Accepted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, + (Self::Accepted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, + (Self::Accepted, OrderEventAny::Canceled(_)) => Self::Canceled, + (Self::Accepted, OrderEventAny::Triggered(_)) => Self::Triggered, + (Self::Accepted, OrderEventAny::Expired(_)) => Self::Expired, + (Self::Accepted, OrderEventAny::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::Accepted, OrderEventAny::Filled(_)) => Self::Filled, + (Self::Canceled, OrderEventAny::PartiallyFilled(_)) => Self::PartiallyFilled, // Real world possibility + (Self::Canceled, OrderEventAny::Filled(_)) => Self::Filled, // Real world possibility + (Self::PendingUpdate, OrderEventAny::Rejected(_)) => Self::Rejected, + (Self::PendingUpdate, OrderEventAny::Accepted(_)) => Self::Accepted, + (Self::PendingUpdate, OrderEventAny::Canceled(_)) => Self::Canceled, + (Self::PendingUpdate, OrderEventAny::Expired(_)) => Self::Expired, + (Self::PendingUpdate, OrderEventAny::Triggered(_)) => Self::Triggered, + (Self::PendingUpdate, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, // Allow multiple requests + (Self::PendingUpdate, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, + (Self::PendingUpdate, OrderEventAny::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::PendingUpdate, OrderEventAny::Filled(_)) => Self::Filled, + (Self::PendingCancel, OrderEventAny::Rejected(_)) => Self::Rejected, + (Self::PendingCancel, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, // Allow multiple requests + (Self::PendingCancel, OrderEventAny::Canceled(_)) => Self::Canceled, + (Self::PendingCancel, OrderEventAny::Expired(_)) => Self::Expired, + (Self::PendingCancel, OrderEventAny::Accepted(_)) => Self::Accepted, // Allow failed cancel requests + (Self::PendingCancel, OrderEventAny::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::PendingCancel, OrderEventAny::Filled(_)) => Self::Filled, + (Self::Triggered, OrderEventAny::Rejected(_)) => Self::Rejected, + (Self::Triggered, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, + (Self::Triggered, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, + (Self::Triggered, OrderEventAny::Canceled(_)) => Self::Canceled, + (Self::Triggered, OrderEventAny::Expired(_)) => Self::Expired, + (Self::Triggered, OrderEventAny::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::Triggered, OrderEventAny::Filled(_)) => Self::Filled, + (Self::PartiallyFilled, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, + (Self::PartiallyFilled, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, + (Self::PartiallyFilled, OrderEventAny::Canceled(_)) => Self::Canceled, + (Self::PartiallyFilled, OrderEventAny::Expired(_)) => Self::Expired, + (Self::PartiallyFilled, OrderEventAny::PartiallyFilled(_)) => Self::PartiallyFilled, + (Self::PartiallyFilled, OrderEventAny::Filled(_)) => Self::Filled, _ => return Err(OrderError::InvalidStateTransition), }; Ok(new_state) } } -pub trait Order { +pub trait Order: 'static + Send { fn into_any(self) -> OrderAny; fn status(&self) -> OrderStatus; fn trader_id(&self) -> TraderId; @@ -211,11 +211,11 @@ pub trait Order { fn ts_init(&self) -> UnixNanos; fn ts_last(&self) -> UnixNanos; - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError>; + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError>; fn update(&mut self, event: &OrderUpdated); - fn events(&self) -> Vec<&OrderEvent>; - fn last_event(&self) -> &OrderEvent { + fn events(&self) -> Vec<&OrderEventAny>; + fn last_event(&self) -> &OrderEventAny { // SAFETY: Unwrap safe as `Order` specification guarantees at least one event (`OrderInitialized`) self.events().last().unwrap() } @@ -381,7 +381,7 @@ where #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrderCore { - pub events: Vec, + pub events: Vec, pub commissions: HashMap, pub venue_order_ids: Vec, pub trade_ids: Vec, @@ -422,7 +422,7 @@ pub struct OrderCore { impl OrderCore { pub fn new(init: OrderInitialized) -> anyhow::Result { - let events: Vec = vec![OrderEvent::Initialized(init.clone())]; + let events: Vec = vec![OrderEventAny::Initialized(init.clone())]; Ok(Self { events, commissions: HashMap::new(), @@ -466,7 +466,7 @@ impl OrderCore { }) } - pub fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + pub fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { assert_eq!(self.client_order_id, event.client_order_id()); assert_eq!(self.strategy_id, event.strategy_id()); @@ -475,23 +475,23 @@ impl OrderCore { self.status = new_status; match &event { - OrderEvent::Initialized(_) => return Err(OrderError::AlreadyInitialized), - OrderEvent::Denied(event) => self.denied(event), - OrderEvent::Emulated(event) => self.emulated(event), - OrderEvent::Released(event) => self.released(event), - OrderEvent::Submitted(event) => self.submitted(event), - OrderEvent::Rejected(event) => self.rejected(event), - OrderEvent::Accepted(event) => self.accepted(event), - OrderEvent::PendingUpdate(event) => self.pending_update(event), - OrderEvent::PendingCancel(event) => self.pending_cancel(event), - OrderEvent::ModifyRejected(event) => self.modify_rejected(event), - OrderEvent::CancelRejected(event) => self.cancel_rejected(event), - OrderEvent::Updated(event) => self.updated(event), - OrderEvent::Triggered(event) => self.triggered(event), - OrderEvent::Canceled(event) => self.canceled(event), - OrderEvent::Expired(event) => self.expired(event), - OrderEvent::PartiallyFilled(event) => self.filled(event), - OrderEvent::Filled(event) => self.filled(event), + OrderEventAny::Initialized(_) => return Err(OrderError::AlreadyInitialized), + OrderEventAny::Denied(event) => self.denied(event), + OrderEventAny::Emulated(event) => self.emulated(event), + OrderEventAny::Released(event) => self.released(event), + OrderEventAny::Submitted(event) => self.submitted(event), + OrderEventAny::Rejected(event) => self.rejected(event), + OrderEventAny::Accepted(event) => self.accepted(event), + OrderEventAny::PendingUpdate(event) => self.pending_update(event), + OrderEventAny::PendingCancel(event) => self.pending_cancel(event), + OrderEventAny::ModifyRejected(event) => self.modify_rejected(event), + OrderEventAny::CancelRejected(event) => self.cancel_rejected(event), + OrderEventAny::Updated(event) => self.updated(event), + OrderEventAny::Triggered(event) => self.triggered(event), + OrderEventAny::Canceled(event) => self.canceled(event), + OrderEventAny::Expired(event) => self.expired(event), + OrderEventAny::PartiallyFilled(event) => self.filled(event), + OrderEventAny::Filled(event) => self.filled(event), } self.ts_last = event.ts_event(); @@ -653,7 +653,7 @@ impl OrderCore { } #[must_use] - pub fn init_event(&self) -> Option { + pub fn init_event(&self) -> Option { self.events.first().cloned() } } @@ -752,7 +752,7 @@ mod tests { fn test_order_state_transition_denied() { let mut order: MarketOrder = OrderInitializedBuilder::default().build().unwrap().into(); let denied = OrderDeniedBuilder::default().build().unwrap(); - let event = OrderEvent::Denied(denied); + let event = OrderEventAny::Denied(denied); order.apply(event.clone()).unwrap(); @@ -771,9 +771,9 @@ mod tests { let filled = OrderFilledBuilder::default().build().unwrap(); let mut order: MarketOrder = init.clone().into(); - order.apply(OrderEvent::Submitted(submitted)).unwrap(); - order.apply(OrderEvent::Accepted(accepted)).unwrap(); - order.apply(OrderEvent::Filled(filled)).unwrap(); + order.apply(OrderEventAny::Submitted(submitted)).unwrap(); + order.apply(OrderEventAny::Accepted(accepted)).unwrap(); + order.apply(OrderEventAny::Filled(filled)).unwrap(); assert_eq!(order.client_order_id, init.client_order_id); assert_eq!(order.status(), OrderStatus::Filled); diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index cf4c12c649e4..84d0d325a53e 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -32,7 +32,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -349,7 +349,7 @@ impl Order for LimitOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -361,11 +361,11 @@ impl Order for LimitOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index 6037a9319401..e9a1a3120173 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -31,7 +31,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -336,7 +336,7 @@ impl Order for LimitIfTouchedOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -348,11 +348,11 @@ impl Order for LimitIfTouchedOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index a2574497524b..456312f159b3 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -32,7 +32,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -326,20 +326,8 @@ impl Order for MarketOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { - self.events.iter().collect() - } - - fn venue_order_ids(&self) -> Vec<&VenueOrderId> { - self.venue_order_ids.iter().collect() - } - - fn trade_ids(&self) -> Vec<&TradeId> { - self.trade_ids.iter().collect() - } - - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; @@ -359,6 +347,18 @@ impl Order for MarketOrder { self.quantity = event.quantity; self.leaves_qty = self.quantity - self.filled_qty; } + + fn events(&self) -> Vec<&OrderEventAny> { + self.events.iter().collect() + } + + fn venue_order_ids(&self) -> Vec<&VenueOrderId> { + self.venue_order_ids.iter().collect() + } + + fn trade_ids(&self) -> Vec<&TradeId> { + self.trade_ids.iter().collect() + } } impl Display for MarketOrder { diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index eafcd203e970..b7bc900f9843 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -31,7 +31,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -330,7 +330,7 @@ impl Order for MarketIfTouchedOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -342,11 +342,11 @@ impl Order for MarketIfTouchedOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index 3e6f3cf731d1..2342c71f14d7 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -31,7 +31,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -322,7 +322,7 @@ impl Order for MarketToLimitOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -334,11 +334,11 @@ impl Order for MarketToLimitOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index aef658a06b4d..ba13d0167ebc 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -32,7 +32,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -343,7 +343,7 @@ impl Order for StopLimitOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -355,11 +355,11 @@ impl Order for StopLimitOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index 9fc46b0fc044..4b9c91402d53 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -31,7 +31,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -331,7 +331,7 @@ impl Order for StopMarketOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -343,11 +343,11 @@ impl Order for StopMarketOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index f96e4d2cd559..d51bd02dd688 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -31,7 +31,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -345,7 +345,7 @@ impl Order for TrailingStopLimitOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -357,11 +357,11 @@ impl Order for TrailingStopLimitOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index ef27a183fe97..d7dbf5ba46f0 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -31,7 +31,7 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{event::OrderEvent, initialized::OrderInitialized, updated::OrderUpdated}, + events::order::{event::OrderEventAny, initialized::OrderInitialized, updated::OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, @@ -337,7 +337,7 @@ impl Order for TrailingStopMarketOrder { self.ts_last } - fn events(&self) -> Vec<&OrderEvent> { + fn events(&self) -> Vec<&OrderEventAny> { self.events.iter().collect() } @@ -349,11 +349,11 @@ impl Order for TrailingStopMarketOrder { self.trade_ids.iter().collect() } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { - if let OrderEvent::Updated(ref event) = event { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> { + if let OrderEventAny::Updated(ref event) = event { self.update(event); }; - let is_order_filled = matches!(event, OrderEvent::Filled(_)); + let is_order_filled = matches!(event, OrderEventAny::Filled(_)); self.core.apply(event)?; diff --git a/nautilus_core/model/src/polymorphism.rs b/nautilus_core/model/src/polymorphism.rs index 2c4201f24d6b..61ff2972fbf5 100644 --- a/nautilus_core/model/src/polymorphism.rs +++ b/nautilus_core/model/src/polymorphism.rs @@ -19,7 +19,7 @@ use nautilus_core::nanos::UnixNanos; use crate::{ enums::{OrderSide, OrderSideSpecified, TriggerType}, - events::order::event::OrderEvent, + events::order::event::OrderEventAny, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, position_id::PositionId, strategy_id::StrategyId, @@ -113,6 +113,6 @@ pub trait IsInflight { fn is_inflight(&self) -> bool; } -pub trait ApplyOrderEvent { - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError>; +pub trait ApplyOrderEventAny { + fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError>; } diff --git a/nautilus_core/model/src/python/events/order/initialized.rs b/nautilus_core/model/src/python/events/order/initialized.rs index aa85b6189564..925a8cc88caf 100644 --- a/nautilus_core/model/src/python/events/order/initialized.rs +++ b/nautilus_core/model/src/python/events/order/initialized.rs @@ -133,9 +133,9 @@ impl OrderInitialized { } #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { - stringify!(OrderInitialized) + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type } #[staticmethod] @@ -159,6 +159,8 @@ impl OrderInitialized { dict.set_item("reduce_only", self.reduce_only)?; dict.set_item("quote_quantity", self.quote_quantity)?; dict.set_item("reconciliation", self.reconciliation)?; + // TODO remove options as in legacy cython only + dict.set_item("options", PyDict::new(py))?; dict.set_item("event_id", self.event_id.to_string())?; dict.set_item("ts_event", self.ts_event.as_u64())?; dict.set_item("ts_init", self.ts_init.as_u64())?; diff --git a/nautilus_core/model/src/python/events/order/mod.rs b/nautilus_core/model/src/python/events/order/mod.rs index 410cbc6b409c..cab8ac7d746a 100644 --- a/nautilus_core/model/src/python/events/order/mod.rs +++ b/nautilus_core/model/src/python/events/order/mod.rs @@ -18,87 +18,93 @@ use pyo3::{IntoPy, PyObject, PyResult, Python}; use crate::events::order::{ accepted::OrderAccepted, cancel_rejected::OrderCancelRejected, canceled::OrderCanceled, - denied::OrderDenied, emulated::OrderEmulated, event::OrderEvent, expired::OrderExpired, + denied::OrderDenied, emulated::OrderEmulated, event::OrderEventAny, expired::OrderExpired, filled::OrderFilled, initialized::OrderInitialized, modify_rejected::OrderModifyRejected, pending_cancel::OrderPendingCancel, pending_update::OrderPendingUpdate, rejected::OrderRejected, released::OrderReleased, submitted::OrderSubmitted, triggered::OrderTriggered, updated::OrderUpdated, }; -pub fn convert_order_event_to_pyobject(py: Python, order_event: OrderEvent) -> PyResult { +pub fn convert_order_event_to_pyobject( + py: Python, + order_event: OrderEventAny, +) -> PyResult { match order_event { - OrderEvent::Initialized(event) => Ok(event.into_py(py)), - OrderEvent::Denied(event) => Ok(event.into_py(py)), - OrderEvent::Emulated(event) => Ok(event.into_py(py)), - OrderEvent::Released(event) => Ok(event.into_py(py)), - OrderEvent::Submitted(event) => Ok(event.into_py(py)), - OrderEvent::Accepted(event) => Ok(event.into_py(py)), - OrderEvent::Rejected(event) => Ok(event.into_py(py)), - OrderEvent::Canceled(event) => Ok(event.into_py(py)), - OrderEvent::Expired(event) => Ok(event.into_py(py)), - OrderEvent::Triggered(event) => Ok(event.into_py(py)), - OrderEvent::PendingUpdate(event) => Ok(event.into_py(py)), - OrderEvent::PendingCancel(event) => Ok(event.into_py(py)), - OrderEvent::ModifyRejected(event) => Ok(event.into_py(py)), - OrderEvent::CancelRejected(event) => Ok(event.into_py(py)), - OrderEvent::Updated(event) => Ok(event.into_py(py)), - OrderEvent::PartiallyFilled(event) => Ok(event.into_py(py)), - OrderEvent::Filled(event) => Ok(event.into_py(py)), + OrderEventAny::Initialized(event) => Ok(event.into_py(py)), + OrderEventAny::Denied(event) => Ok(event.into_py(py)), + OrderEventAny::Emulated(event) => Ok(event.into_py(py)), + OrderEventAny::Released(event) => Ok(event.into_py(py)), + OrderEventAny::Submitted(event) => Ok(event.into_py(py)), + OrderEventAny::Accepted(event) => Ok(event.into_py(py)), + OrderEventAny::Rejected(event) => Ok(event.into_py(py)), + OrderEventAny::Canceled(event) => Ok(event.into_py(py)), + OrderEventAny::Expired(event) => Ok(event.into_py(py)), + OrderEventAny::Triggered(event) => Ok(event.into_py(py)), + OrderEventAny::PendingUpdate(event) => Ok(event.into_py(py)), + OrderEventAny::PendingCancel(event) => Ok(event.into_py(py)), + OrderEventAny::ModifyRejected(event) => Ok(event.into_py(py)), + OrderEventAny::CancelRejected(event) => Ok(event.into_py(py)), + OrderEventAny::Updated(event) => Ok(event.into_py(py)), + OrderEventAny::PartiallyFilled(event) => Ok(event.into_py(py)), + OrderEventAny::Filled(event) => Ok(event.into_py(py)), } } -pub fn convert_pyobject_to_order_event(py: Python, order_event: PyObject) -> PyResult { +pub fn convert_pyobject_to_order_event( + py: Python, + order_event: PyObject, +) -> PyResult { let order_event_type = order_event .getattr(py, "order_event_type")? .extract::(py)?; if order_event_type == "OrderAccepted" { let order_accepted = order_event.extract::(py)?; - Ok(OrderEvent::Accepted(order_accepted)) + Ok(OrderEventAny::Accepted(order_accepted)) } else if order_event_type == "OrderCanceled" { let order_canceled = order_event.extract::(py)?; - Ok(OrderEvent::Canceled(order_canceled)) + Ok(OrderEventAny::Canceled(order_canceled)) } else if order_event_type == "OrderCancelRejected" { let order_cancel_rejected = order_event.extract::(py)?; - Ok(OrderEvent::CancelRejected(order_cancel_rejected)) + Ok(OrderEventAny::CancelRejected(order_cancel_rejected)) } else if order_event_type == "OrderDenied" { let order_denied = order_event.extract::(py)?; - Ok(OrderEvent::Denied(order_denied)) + Ok(OrderEventAny::Denied(order_denied)) } else if order_event_type == "OrderEmulated" { let order_emulated = order_event.extract::(py)?; - Ok(OrderEvent::Emulated(order_emulated)) + Ok(OrderEventAny::Emulated(order_emulated)) } else if order_event_type == "OrderExpired" { let order_expired = order_event.extract::(py)?; - Ok(OrderEvent::Expired(order_expired)) + Ok(OrderEventAny::Expired(order_expired)) } else if order_event_type == "OrderFilled" { let order_filled = order_event.extract::(py)?; - Ok(OrderEvent::Filled(order_filled)) + Ok(OrderEventAny::Filled(order_filled)) } else if order_event_type == "OrderInitialized" { let order_initialized = order_event.extract::(py)?; - Ok(OrderEvent::Initialized(order_initialized)) + Ok(OrderEventAny::Initialized(order_initialized)) } else if order_event_type == "OrderModifyRejected" { let order_modify_rejected = order_event.extract::(py)?; - Ok(OrderEvent::ModifyRejected(order_modify_rejected)) + Ok(OrderEventAny::ModifyRejected(order_modify_rejected)) } else if order_event_type == "OrderPendingCancel" { let order_pending_cancel = order_event.extract::(py)?; - Ok(OrderEvent::PendingCancel(order_pending_cancel)) + Ok(OrderEventAny::PendingCancel(order_pending_cancel)) } else if order_event_type == "OrderPendingUpdate" { let order_pending_update = order_event.extract::(py)?; - Ok(OrderEvent::PendingUpdate(order_pending_update)) + Ok(OrderEventAny::PendingUpdate(order_pending_update)) } else if order_event_type == "OrderRejected" { let order_rejected = order_event.extract::(py)?; - Ok(OrderEvent::Rejected(order_rejected)) + Ok(OrderEventAny::Rejected(order_rejected)) } else if order_event_type == "OrderReleased" { let order_released = order_event.extract::(py)?; - Ok(OrderEvent::Released(order_released)) + Ok(OrderEventAny::Released(order_released)) } else if order_event_type == "OrderSubmitted" { let order_submitted = order_event.extract::(py)?; - Ok(OrderEvent::Submitted(order_submitted)) + Ok(OrderEventAny::Submitted(order_submitted)) } else if order_event_type == "OrderTriggered" { let order_triggered = order_event.extract::(py)?; - Ok(OrderEvent::Triggered(order_triggered)) + Ok(OrderEventAny::Triggered(order_triggered)) } else if order_event_type == "OrderUpdated" { let order_updated = order_event.extract::(py)?; - Ok(OrderEvent::Updated(order_updated)) + Ok(OrderEventAny::Updated(order_updated)) } else { Err(to_pyvalue_err( "Error in conversion from pyobject to order event", diff --git a/nautilus_core/model/src/python/orders/limit_if_touched.rs b/nautilus_core/model/src/python/orders/limit_if_touched.rs index 8817efd30fa7..c37c0e1002d3 100644 --- a/nautilus_core/model/src/python/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/python/orders/limit_if_touched.rs @@ -20,14 +20,18 @@ use pyo3::prelude::*; use ustr::Ustr; use crate::{ - enums::{ContingencyType, OrderSide, TimeInForce, TriggerType}, + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TriggerType}, events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, trader_id::TraderId, }, - orders::{base::str_hashmap_to_ustr, limit_if_touched::LimitIfTouchedOrder}, + orders::{ + base::{str_hashmap_to_ustr, Order}, + limit_if_touched::LimitIfTouchedOrder, + }, + python::events::order::convert_order_event_to_pyobject, types::{price::Price, quantity::Quantity}, }; @@ -97,6 +101,21 @@ impl LimitIfTouchedOrder { .unwrap()) } + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } + + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + #[staticmethod] #[pyo3(name = "create")] fn py_create(init: OrderInitialized) -> PyResult { diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index 990e2873f7de..deb06be0dca2 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -34,10 +34,10 @@ use crate::{ trader_id::TraderId, }, orders::{ - base::{str_hashmap_to_ustr, OrderCore}, + base::{str_hashmap_to_ustr, Order, OrderCore}, market::MarketOrder, }, - python::common::commissions_from_hashmap, + python::{common::commissions_from_hashmap, events::order::convert_order_event_to_pyobject}, types::{currency::Currency, money::Money, quantity::Quantity}, }; @@ -283,6 +283,15 @@ impl MarketOrder { OrderCore::closing_side(side) } + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); diff --git a/nautilus_core/model/src/python/orders/market_if_touched.rs b/nautilus_core/model/src/python/orders/market_if_touched.rs index 29860e441720..d7460867e49a 100644 --- a/nautilus_core/model/src/python/orders/market_if_touched.rs +++ b/nautilus_core/model/src/python/orders/market_if_touched.rs @@ -20,14 +20,18 @@ use pyo3::prelude::*; use ustr::Ustr; use crate::{ - enums::{ContingencyType, OrderSide, TimeInForce, TriggerType}, + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TriggerType}, events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, trader_id::TraderId, }, - orders::{base::str_hashmap_to_ustr, market_if_touched::MarketIfTouchedOrder}, + orders::{ + base::{str_hashmap_to_ustr, Order}, + market_if_touched::MarketIfTouchedOrder, + }, + python::events::order::convert_order_event_to_pyobject, types::{price::Price, quantity::Quantity}, }; @@ -93,6 +97,21 @@ impl MarketIfTouchedOrder { .unwrap()) } + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } + + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + #[staticmethod] #[pyo3(name = "create")] fn py_create(init: OrderInitialized) -> PyResult { diff --git a/nautilus_core/model/src/python/orders/market_to_limit.rs b/nautilus_core/model/src/python/orders/market_to_limit.rs index 31a67dc70a53..c60cd033f68c 100644 --- a/nautilus_core/model/src/python/orders/market_to_limit.rs +++ b/nautilus_core/model/src/python/orders/market_to_limit.rs @@ -20,14 +20,18 @@ use pyo3::prelude::*; use ustr::Ustr; use crate::{ - enums::{ContingencyType, OrderSide, TimeInForce}, + enums::{ContingencyType, OrderSide, OrderType, TimeInForce}, events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, trader_id::TraderId, }, - orders::{base::str_hashmap_to_ustr, market_to_limit::MarketToLimitOrder}, + orders::{ + base::{str_hashmap_to_ustr, Order}, + market_to_limit::MarketToLimitOrder, + }, + python::events::order::convert_order_event_to_pyobject, types::quantity::Quantity, }; @@ -87,6 +91,21 @@ impl MarketToLimitOrder { .unwrap()) } + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } + + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + #[staticmethod] #[pyo3(name = "create")] fn py_create(init: OrderInitialized) -> PyResult { diff --git a/nautilus_core/model/src/python/orders/mod.rs b/nautilus_core/model/src/python/orders/mod.rs index ea54ed343ce4..974f50d8d38b 100644 --- a/nautilus_core/model/src/python/orders/mod.rs +++ b/nautilus_core/model/src/python/orders/mod.rs @@ -13,7 +13,73 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Defines order types for the trading domain model. +use nautilus_core::python::to_pyvalue_err; +use pyo3::{IntoPy, PyObject, PyResult, Python}; + +use crate::{ + enums::OrderType, + orders::{ + any::OrderAny, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, + market::MarketOrder, market_if_touched::MarketIfTouchedOrder, + market_to_limit::MarketToLimitOrder, stop_limit::StopLimitOrder, + stop_market::StopMarketOrder, trailing_stop_limit::TrailingStopLimitOrder, + trailing_stop_market::TrailingStopMarketOrder, + }, +}; + +pub fn convert_pyobject_to_order_any(py: Python, order: PyObject) -> PyResult { + let order_type = order.getattr(py, "order_type")?.extract::(py)?; + if order_type == OrderType::Limit { + let limit = order.extract::(py)?; + Ok(OrderAny::Limit(limit)) + } else if order_type == OrderType::Market { + let market = order.extract::(py)?; + Ok(OrderAny::Market(market)) + } else if order_type == OrderType::StopLimit { + let stop_limit = order.extract::(py)?; + Ok(OrderAny::StopLimit(stop_limit)) + } else if order_type == OrderType::LimitIfTouched { + let limit_if_touched = order.extract::(py)?; + Ok(OrderAny::LimitIfTouched(limit_if_touched)) + } else if order_type == OrderType::MarketIfTouched { + let market_if_touched = order.extract::(py)?; + Ok(OrderAny::MarketIfTouched(market_if_touched)) + } else if order_type == OrderType::MarketToLimit { + let market_to_limit = order.extract::(py)?; + Ok(OrderAny::MarketToLimit(market_to_limit)) + } else if order_type == OrderType::StopMarket { + let stop_market = order.extract::(py)?; + Ok(OrderAny::StopMarket(stop_market)) + } else if order_type == OrderType::TrailingStopMarket { + let trailing_stop_market = order.extract::(py)?; + Ok(OrderAny::TrailingStopMarket(trailing_stop_market)) + } else if order_type == OrderType::TrailingStopLimit { + let trailing_stop_limit = order.extract::(py)?; + Ok(OrderAny::TrailingStopLimit(trailing_stop_limit)) + } else { + Err(to_pyvalue_err("Unsupported order type")) + } +} + +pub fn convert_order_any_to_pyobject(py: Python, order: OrderAny) -> PyResult { + match order { + OrderAny::Limit(limit_order) => Ok(limit_order.into_py(py)), + OrderAny::LimitIfTouched(limit_if_touched_order) => Ok(limit_if_touched_order.into_py(py)), + OrderAny::Market(market_order) => Ok(market_order.into_py(py)), + OrderAny::MarketIfTouched(market_if_touched_order) => { + Ok(market_if_touched_order.into_py(py)) + } + OrderAny::MarketToLimit(market_to_limit_order) => Ok(market_to_limit_order.into_py(py)), + OrderAny::StopLimit(stop_limit_order) => Ok(stop_limit_order.into_py(py)), + OrderAny::StopMarket(stop_market_order) => Ok(stop_market_order.into_py(py)), + OrderAny::TrailingStopLimit(trailing_stop_limit_order) => { + Ok(trailing_stop_limit_order.into_py(py)) + } + OrderAny::TrailingStopMarket(trailing_stop_market_order) => { + Ok(trailing_stop_market_order.into_py(py)) + } + } +} pub mod limit; pub mod limit_if_touched; diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index 1fb7acf23c23..9776010444cb 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -347,6 +347,15 @@ impl StopLimitOrder { .map(|vec| vec.iter().map(|s| s.as_str()).collect()) } + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + #[pyo3(name = "to_dict")] fn to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); diff --git a/nautilus_core/model/src/python/orders/stop_market.rs b/nautilus_core/model/src/python/orders/stop_market.rs index fe62e22193c4..4191aab6458e 100644 --- a/nautilus_core/model/src/python/orders/stop_market.rs +++ b/nautilus_core/model/src/python/orders/stop_market.rs @@ -20,14 +20,18 @@ use pyo3::prelude::*; use ustr::Ustr; use crate::{ - enums::{ContingencyType, OrderSide, TimeInForce, TriggerType}, + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TriggerType}, events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, trader_id::TraderId, }, - orders::{base::str_hashmap_to_ustr, stop_market::StopMarketOrder}, + orders::{ + base::{str_hashmap_to_ustr, Order}, + stop_market::StopMarketOrder, + }, + python::events::order::convert_order_event_to_pyobject, types::{price::Price, quantity::Quantity}, }; @@ -98,4 +102,19 @@ impl StopMarketOrder { fn py_create(init: OrderInitialized) -> PyResult { Ok(StopMarketOrder::from(init)) } + + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } } diff --git a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs index ddcde7cded63..b2c8a192eeed 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs @@ -20,14 +20,18 @@ use pyo3::prelude::*; use ustr::Ustr; use crate::{ - enums::{ContingencyType, OrderSide, TimeInForce, TrailingOffsetType, TriggerType}, + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, trader_id::TraderId, }, - orders::{base::str_hashmap_to_ustr, trailing_stop_limit::TrailingStopLimitOrder}, + orders::{ + base::{str_hashmap_to_ustr, Order}, + trailing_stop_limit::TrailingStopLimitOrder, + }, + python::events::order::convert_order_event_to_pyobject, types::{price::Price, quantity::Quantity}, }; @@ -103,6 +107,21 @@ impl TrailingStopLimitOrder { .unwrap()) } + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } + + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + #[staticmethod] #[pyo3(name = "create")] fn py_create(init: OrderInitialized) -> PyResult { diff --git a/nautilus_core/model/src/python/orders/trailing_stop_market.rs b/nautilus_core/model/src/python/orders/trailing_stop_market.rs index 5a0112b193a2..7bb3a72f2eea 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_market.rs @@ -20,14 +20,18 @@ use pyo3::prelude::*; use ustr::Ustr; use crate::{ - enums::{ContingencyType, OrderSide, TimeInForce, TrailingOffsetType, TriggerType}, + enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType}, events::order::initialized::OrderInitialized, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, trader_id::TraderId, }, - orders::{base::str_hashmap_to_ustr, trailing_stop_market::TrailingStopMarketOrder}, + orders::{ + base::{str_hashmap_to_ustr, Order}, + trailing_stop_market::TrailingStopMarketOrder, + }, + python::events::order::convert_order_event_to_pyobject, types::{price::Price, quantity::Quantity}, }; @@ -97,6 +101,21 @@ impl TrailingStopMarketOrder { .unwrap()) } + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } + + #[getter] + #[pyo3(name = "events")] + fn py_events(&self, py: Python<'_>) -> PyResult> { + self.events() + .into_iter() + .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .collect() + } + #[staticmethod] #[pyo3(name = "create")] fn py_create(init: OrderInitialized) -> PyResult { diff --git a/nautilus_trader/cache/postgres/adapter.py b/nautilus_trader/cache/postgres/adapter.py index 60a54ffc892e..294883ae1089 100644 --- a/nautilus_trader/cache/postgres/adapter.py +++ b/nautilus_trader/cache/postgres/adapter.py @@ -19,11 +19,15 @@ from nautilus_trader.cache.postgres.transformers import transform_currency_to_pyo3 from nautilus_trader.cache.postgres.transformers import transform_instrument_from_pyo3 from nautilus_trader.cache.postgres.transformers import transform_instrument_to_pyo3 +from nautilus_trader.cache.postgres.transformers import transform_order_from_pyo3 +from nautilus_trader.cache.postgres.transformers import transform_order_to_pyo3 from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.core.nautilus_pyo3 import PostgresCacheDatabase +from nautilus_trader.model.identifiers import ClientOrderId from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.instruments import Instrument from nautilus_trader.model.objects import Currency +from nautilus_trader.model.orders import Order class CachePostgresAdapter(CacheDatabaseFacade): @@ -69,3 +73,18 @@ def load_instrument(self, instrument_id: InstrumentId) -> Instrument: instrument_id_pyo3 = nautilus_pyo3.InstrumentId.from_str(str(instrument_id)) instrument_pyo3 = self._backing.load_instrument(instrument_id_pyo3) return transform_instrument_from_pyo3(instrument_pyo3) + + def add_order(self, order: Order): + order_pyo3 = transform_order_to_pyo3(order) + self._backing.add_order(order_pyo3) + + def load_order(self, client_order_id: ClientOrderId): + order_id_pyo3 = nautilus_pyo3.ClientOrderId.from_str(str(client_order_id)) + order_pyo3 = self._backing.load_order(order_id_pyo3) + if order_pyo3: + return transform_order_from_pyo3(order_pyo3) + return None + + def load_orders(self): + orders = self._backing.load_orders() + return [transform_order_from_pyo3(order) for order in orders] diff --git a/nautilus_trader/cache/postgres/transformers.py b/nautilus_trader/cache/postgres/transformers.py index 674d3e7f2673..bbac9201816b 100644 --- a/nautilus_trader/cache/postgres/transformers.py +++ b/nautilus_trader/cache/postgres/transformers.py @@ -14,7 +14,9 @@ # ------------------------------------------------------------------------------------------------- from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.core.rust.model import OrderType from nautilus_trader.model.enums import CurrencyType +from nautilus_trader.model.events import OrderInitialized from nautilus_trader.model.instruments import CryptoFuture from nautilus_trader.model.instruments import CryptoPerpetual from nautilus_trader.model.instruments import CurrencyPair @@ -25,6 +27,8 @@ from nautilus_trader.model.instruments import OptionsContract from nautilus_trader.model.instruments import OptionsSpread from nautilus_trader.model.objects import Currency +from nautilus_trader.model.orders import MarketOrder +from nautilus_trader.model.orders import Order ################################################################################ @@ -94,3 +98,61 @@ def transform_instrument_from_pyo3(instrument_pyo3) -> Instrument | None: return OptionsSpread.from_pyo3(instrument_pyo3) else: raise ValueError(f"Unknown instrument type: {instrument_pyo3}") + + +################################################################################ +# Orders +################################################################################ +def transform_order_event_to_pyo3(order_event): + order_event_dict = OrderInitialized.to_dict(order_event) + # in options field there are some properties we need to attach to dict + for key, value in order_event.options.items(): + order_event_dict[key] = value + order_event_pyo3 = nautilus_pyo3.OrderInitialized.from_dict(order_event_dict) + if order_event_pyo3.order_type == nautilus_pyo3.OrderType.MARKET: + return nautilus_pyo3.MarketOrder.create(order_event_pyo3) + elif order_event_pyo3.order_type == nautilus_pyo3.OrderType.LIMIT: + return nautilus_pyo3.LimitOrder.create(order_event_pyo3) + elif order_event_pyo3.order_type == nautilus_pyo3.OrderType.STOP_MARKET: + return nautilus_pyo3.StopMarketOrder.create(order_event_pyo3) + elif order_event_pyo3.order_type == nautilus_pyo3.OrderType.STOP_LIMIT: + return nautilus_pyo3.StopLimitOrder.create(order_event_pyo3) + else: + raise ValueError(f"Unknown order type: {order_event_pyo3.event_type}") + + +def transform_order_event_from_pyo3(order_event_pyo3): + order_event_dict = order_event_pyo3.to_dict() + order_event_cython = OrderInitialized.from_dict(order_event_dict) + if order_event_pyo3.order_type == OrderType.MARKET: + return MarketOrder.create(order_event_cython) + + +def transform_order_to_pyo3(order: Order): + events = order.events + if len(events) == 0: + raise ValueError("Missing events in order") + init_event = events.pop(0) + if not isinstance(init_event, OrderInitialized): + raise KeyError("init event should be of type OrderInitialized") + order_py3: nautilus_pyo3.OrderInitialized = transform_order_event_to_pyo3(init_event) + for event_cython in events: + raise NotImplementedError("Not implemented") + # event_pyo3 = transform_order_event_to_pyo3(event_cython) + # order_py3.apply(event_pyo3) + return order_py3 + + +def transform_order_from_pyo3(order_pyo3): + events_pyo3 = order_pyo3.events + if len(events_pyo3) == 0: + raise ValueError("Missing events in order") + init_event = events_pyo3.pop(0) + if not isinstance(init_event, nautilus_pyo3.OrderInitialized): + raise KeyError("init event should be of type OrderInitialized") + order_cython = transform_order_event_from_pyo3(init_event) + for event_pyo3 in events_pyo3: + raise NotImplementedError("Not implemented") + # event_cython = transform_order_event_from_pyo3(event_pyo3) + # order_cython.apply(event_cython) + return order_cython diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index c66d6c608ac8..14dba629cbc7 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1336,6 +1336,18 @@ class TrailingStopMarketOrder: @classmethod def create(cls, init: OrderInitialized) -> TrailingStopMarketOrder: ... +Order: TypeAlias = Union[ + LimitOrder, + LimitIfTouchedOrder, + MarketOrder, + MarketToLimitOrder, + MarketIfTouchedOrder, + StopLimitOrder, + StopMarketOrder, + TrailingStopLimitOrder, + TrailingStopMarketOrder, +] + ### Objects class Currency: @@ -1973,6 +1985,8 @@ class OrderInitialized: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderInitialized: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderSubmitted: def __init__( @@ -1989,6 +2003,8 @@ class OrderSubmitted: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderSubmitted: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderEmulated: def __init__( @@ -2004,6 +2020,8 @@ class OrderEmulated: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderEmulated: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderReleased: def __init__( @@ -2020,6 +2038,8 @@ class OrderReleased: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderReleased: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderUpdated: def __init__( @@ -2041,6 +2061,8 @@ class OrderUpdated: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderUpdated: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderPendingUpdate: def __init__( @@ -2059,6 +2081,8 @@ class OrderPendingUpdate: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderPendingUpdate: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderPendingCancel: def __init__( @@ -2077,6 +2101,8 @@ class OrderPendingCancel: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderPendingCancel: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderModifyRejected: def __init__( @@ -2096,6 +2122,8 @@ class OrderModifyRejected: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderModifyRejected: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderAccepted: def __init__( @@ -2114,6 +2142,8 @@ class OrderAccepted: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderAccepted: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderCancelRejected: @@ -2134,6 +2164,8 @@ class OrderCancelRejected: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderCancelRejected: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderCanceled: def __init__( @@ -2153,6 +2185,8 @@ class OrderCanceled: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderCanceled: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class OrderExpired: def __init__( @@ -2171,6 +2205,8 @@ class OrderExpired: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderExpired: ... def to_dict(self) -> dict[str, str]: ... + @property + def order_type(self) -> str: ... class Level: @property @@ -2266,10 +2302,12 @@ class PostgresCacheDatabase: def add(self, key: str, value: bytes) -> None: ... def add_currency(self,currency: Currency) -> None: ... def add_instrument(self, instrument: object) -> None: ... + def add_order(self, instrument: object) -> None: ... def load_currency(self, code: str) -> Currency | None: ... def load_currencies(self) -> list[Currency]: ... def load_instrument(self, instrument_id: InstrumentId) -> Instrument | None: ... def load_instruments(self) -> list[Instrument]: ... + def load_order(self, order_id: ClientOrderId) -> Order | None: ... def flush_db(self) -> None: ... def truncate(self, table: str) -> None: ... diff --git a/nautilus_trader/model/events/order.pyx b/nautilus_trader/model/events/order.pyx index 9a7ce29ac1f7..59967645d17b 100644 --- a/nautilus_trader/model/events/order.pyx +++ b/nautilus_trader/model/events/order.pyx @@ -539,7 +539,7 @@ cdef class OrderInitialized(OrderEvent): reduce_only=values["reduce_only"], quote_quantity=values["quote_quantity"], options=values["options"], - emulation_trigger=trigger_type_from_str(values["emulation_trigger"]), + emulation_trigger=trigger_type_from_str(values["emulation_trigger"]) if values["emulation_trigger"] is not None else TriggerType.NO_TRIGGER, trigger_instrument_id=InstrumentId.from_str_c(trigger_instrument_id) if trigger_instrument_id is not None else None, contingency_type=contingency_type_from_str(values["contingency_type"]), order_list_id=OrderListId(order_list_id_str) if order_list_id_str is not None else None, @@ -584,6 +584,7 @@ cdef class OrderInitialized(OrderEvent): "tags": obj.tags, "event_id": obj.id.value, "ts_init": obj.ts_init, + "ts_event": obj.ts_init, "reconciliation": obj.reconciliation, } @@ -855,7 +856,7 @@ cdef class OrderDenied(OrderEvent): "client_order_id": obj.client_order_id.value, "reason": obj.reason, "event_id": obj.id.value, - "ts_event": obj.ts_init, + "ts_event": obj.ts_event, "ts_init": obj.ts_init, } diff --git a/nautilus_trader/model/orders/stop_market.pyx b/nautilus_trader/model/orders/stop_market.pyx index 1faf39546841..0b17119e708d 100644 --- a/nautilus_trader/model/orders/stop_market.pyx +++ b/nautilus_trader/model/orders/stop_market.pyx @@ -330,7 +330,6 @@ cdef class StopMarketOrder(Order): """ Condition.not_none(init, "init") Condition.equal(init.order_type, OrderType.STOP_MARKET, "init.order_type", "OrderType") - return StopMarketOrder( trader_id=init.trader_id, strategy_id=init.strategy_id, diff --git a/poetry.lock b/poetry.lock index a0f6c1fe7851..cc9e3c1af940 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -932,6 +932,7 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1510,6 +1511,7 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -1961,7 +1963,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, diff --git a/schema/tables.sql b/schema/tables.sql index eea7c0415438..5758a2819567 100644 --- a/schema/tables.sql +++ b/schema/tables.sql @@ -96,31 +96,38 @@ CREATE TABLE IF NOT EXISTS "order" ( CREATE TABLE IF NOT EXISTS "order_event" ( id TEXT PRIMARY KEY NOT NULL, kind TEXT NOT NULL, - order_id TEXT DEFAULT NULL, - order_type TEXT, trader_id TEXT NOT NULL, strategy_id TEXT NOT NULL, instrument_id TEXT NOT NULL, + order_id TEXT DEFAULT NULL, + order_type TEXT, + order_side TEXT, quantity TEXT, + time_in_force TEXT, post_only BOOLEAN DEFAULT FALSE, reduce_only BOOLEAN DEFAULT FALSE, quote_quantity BOOLEAN DEFAULT FALSE, reconciliation BOOLEAN DEFAULT FALSE, price TEXT, trigger_price TEXT, + trigger_type TEXT, limit_offset TEXT, trailing_offset TEXT, + trailing_offset_type TEXT, expire_time TEXT, display_qty TEXT, + emulation_trigger TEXT, trigger_instrument_id TEXT, + contingency_type TEXT, order_list_id TEXT, linked_order_ids TEXT[], parent_order_id TEXT, exec_algorithm_id TEXT, + exec_algorithm_params JSONB, exec_spawn_id TEXT, venue_order_id TEXT, account_id TEXT, - tags TEXT, + tags TEXT[], ts_event TEXT NOT NULL, ts_init TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, diff --git a/tests/integration_tests/infrastructure/test_cache_database_postgres.py b/tests/integration_tests/infrastructure/test_cache_database_postgres.py index 40ee328388dc..5e61edab5b47 100644 --- a/tests/integration_tests/infrastructure/test_cache_database_postgres.py +++ b/tests/integration_tests/infrastructure/test_cache_database_postgres.py @@ -23,9 +23,11 @@ from nautilus_trader.common.component import MessageBus from nautilus_trader.common.component import TestClock from nautilus_trader.model.enums import CurrencyType +from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.instruments import CurrencyPair from nautilus_trader.model.objects import Currency from nautilus_trader.model.objects import Price +from nautilus_trader.model.objects import Quantity from nautilus_trader.portfolio.portfolio import Portfolio from nautilus_trader.test_kit.functions import eventually from nautilus_trader.test_kit.providers import TestInstrumentProvider @@ -326,3 +328,26 @@ async def test_add_instrument_options_contract(self): # Assert assert aapl_option == self.database.load_instrument(aapl_option.id) + + ################################################################################ + # Orders + ################################################################################ + @pytest.mark.asyncio + async def test_add_order(self): + # Arrange + order = self.strategy.order_factory.market( + _AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(100_000), + ) + + # Act + self.database.add_order(order) + + # Allow MPSC thread to insert + await eventually(lambda: self.database.load_order(order.client_order_id)) + + # Assert + result = self.database.load_order(order.client_order_id) + assert result == order + assert order.to_dict() == result.to_dict() From 89ebca40491c045a1b22938cd509e3a5f5891ce9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 9 May 2024 21:59:23 +1000 Subject: [PATCH 146/193] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 7b86d885b5b7..5ab87fff1e4a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -20,6 +20,7 @@ Released on TBD (UTC). - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z +- Fixed Interactive Brokers `IBOrder` attributes assignment (#1634), thanks @rsmb7z - Fixed IBKR reconnection after gateway/TWS disconnection (#1622), thanks @benjaminsingleton - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) From 5a3fdf90209e8ba735de943610569a24c34ae261 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 10 May 2024 17:12:55 +1000 Subject: [PATCH 147/193] Update dependencies --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 21 ++++++++--------- nautilus_core/Cargo.toml | 1 + nautilus_core/core/Cargo.toml | 2 +- nautilus_core/core/src/lib.rs | 1 + poetry.lock | 43 +++++++++++++++++------------------ pyproject.toml | 2 +- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa67446929ac..7c3de2464af8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,7 +82,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.3 + rev: v0.4.4 hooks: - id: ruff args: ["--fix"] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 9642cf189016..4d3c71a601b0 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" +checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" dependencies = [ "bzip2", "flate2", @@ -1595,9 +1595,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2879,9 +2879,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -2955,11 +2955,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -3233,9 +3232,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap 2.2.6", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 9938767915c6..006b2a0fc0d5 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -55,6 +55,7 @@ uuid = { version = "1.8.0", features = ["v4"] } criterion = "0.5.1" float-cmp = "0.9.0" iai = "0.1.1" +pretty_assertions = "1.4.0" rstest = "0.19.0" tempfile = "3.10.1" diff --git a/nautilus_core/core/Cargo.toml b/nautilus_core/core/Cargo.toml index e136b39bf6ea..ec27b7eb0e6a 100644 --- a/nautilus_core/core/Cargo.toml +++ b/nautilus_core/core/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["rlib", "staticlib"] [dependencies] anyhow = { workspace = true } chrono = { workspace = true } +pretty_assertions = { workspace = true } pyo3 = { workspace = true, optional = true } rmp-serde = { workspace = true } serde = { workspace = true } @@ -20,7 +21,6 @@ serde_json = { workspace = true } ustr = { workspace = true } uuid = { workspace = true } heck = "0.5.0" -pretty_assertions = {version = "1.4.0"} [dev-dependencies] criterion = { workspace = true } diff --git a/nautilus_core/core/src/lib.rs b/nautilus_core/core/src/lib.rs index f2dbd03c27c9..62a04051182a 100644 --- a/nautilus_core/core/src/lib.rs +++ b/nautilus_core/core/src/lib.rs @@ -39,5 +39,6 @@ pub mod uuid; #[cfg(feature = "ffi")] pub mod ffi; + #[cfg(feature = "python")] pub mod python; diff --git a/poetry.lock b/poetry.lock index cc9e3c1af940..3b13dc323d83 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiohttp" @@ -932,7 +932,6 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1511,7 +1510,6 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -1963,6 +1961,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2020,28 +2019,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.3" +version = "0.4.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"}, - {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"}, - {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"}, - {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"}, - {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"}, - {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"}, - {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"}, - {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"}, + {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, + {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, + {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, + {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, + {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, + {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, ] [[package]] @@ -2686,4 +2685,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "e249c472864a0664727d309d1c2bf39c22d3b050e55feb074b8e78b608647767" +content-hash = "dcf0ef362d59f2c24681895feadfd6eb96fb837eae696bbba9c6c55b06dab287" diff --git a/pyproject.toml b/pyproject.toml index 791d7e4e8ac7..9e81f2073bb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ docformatter = "^1.7.5" mypy = "^1.10.0" pandas-stubs = "^2.2.1" pre-commit = "^3.7.0" -ruff = "^0.4.2" +ruff = "^0.4.4" types-pytz = "^2023.3" types-requests = "^2.31" types-toml = "^0.10.2" From a90481cc4844c031ddd817ec723ea8b413165eb6 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 10 May 2024 17:22:41 +1000 Subject: [PATCH 148/193] Ignore postgres infrastruture tests temporarily --- .../infrastructure/tests/test_cache_database_postgres.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs index 9aeb7ab82817..1777ae3f444d 100644 --- a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -79,6 +79,7 @@ mod tests { use crate::get_pg_cache_database; + #[ignore] #[tokio::test] async fn test_add_general_object_adds_to_cache() { let pg_cache = get_pg_cache_database().await.unwrap(); @@ -97,6 +98,7 @@ mod tests { assert_eq!(result.get("test_id").unwrap().to_owned(), test_id_value); } + #[ignore] #[tokio::test] async fn test_add_currency_and_instruments() { // 1. first define and add currencies as they are contain foreign keys for instruments @@ -232,6 +234,7 @@ mod tests { ); } + #[ignore] #[tokio::test] async fn test_add_order() { let instrument = currency_pair_ethusdt(); From 2a546ae120cda684786f05d2f1369d94a30504a6 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 10 May 2024 17:35:17 +1000 Subject: [PATCH 149/193] Update default postgres database --- .docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index b8f93300508e..dc5b5eb0ee79 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -7,7 +7,7 @@ services: environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-pass} - POSTGRES_DATABASE: ${POSTGRES_DATABASE:-postgres} + POSTGRES_DATABASE: nautilus PGDATA: /data/postgres volumes: - nautilus-database:/data/postgres From 96769e6a1980c0f07e5d4717993d6d1b7b961c62 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 10 May 2024 21:27:41 +1000 Subject: [PATCH 150/193] Upgrade poetry --- .docker/nautilus_trader.dockerfile | 2 +- poetry-version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.docker/nautilus_trader.dockerfile b/.docker/nautilus_trader.dockerfile index 8ebb22045ec1..369545c5efe1 100644 --- a/.docker/nautilus_trader.dockerfile +++ b/.docker/nautilus_trader.dockerfile @@ -4,7 +4,7 @@ ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=off \ PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DEFAULT_TIMEOUT=100 \ - POETRY_VERSION=1.8.2 \ + POETRY_VERSION=1.8.3 \ POETRY_HOME="/opt/poetry" \ POETRY_VIRTUALENVS_CREATE=false \ POETRY_NO_INTERACTION=1 \ diff --git a/poetry-version b/poetry-version index 53adb84c8220..a7ee35a3ea70 100644 --- a/poetry-version +++ b/poetry-version @@ -1 +1 @@ -1.8.2 +1.8.3 From d3e271dbc9242480c435b86f59c5fb0c3e42c4cc Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 10 May 2024 21:48:53 +1000 Subject: [PATCH 151/193] Fix TimeEvent equality --- RELEASES.md | 1 + nautilus_core/common/src/timer.rs | 2 +- nautilus_trader/common/component.pyx | 4 ++-- tests/unit_tests/common/test_events.py | 31 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 5ab87fff1e4a..f9fa7cdc6835 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -17,6 +17,7 @@ Released on TBD (UTC). ### Fixes - Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` +- Fixed `TimeEvent` equality (now based on then event `id` rather than the event `name`) - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 3768a3ee569a..c3db5c26cd06 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -86,7 +86,7 @@ impl Display for TimeEvent { impl PartialEq for TimeEvent { fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.ts_event == other.ts_event + self.event_id == other.event_id } } diff --git a/nautilus_trader/common/component.pyx b/nautilus_trader/common/component.pyx index a0476450adde..f0c8ed59781d 100644 --- a/nautilus_trader/common/component.pyx +++ b/nautilus_trader/common/component.pyx @@ -857,10 +857,10 @@ cdef class TimeEvent(Event): return ustr_to_pystr(self._mem.name) def __eq__(self, TimeEvent other) -> bool: - return self.to_str() == other.to_str() + return self.id == other.id def __hash__(self) -> int: - return hash(self.to_str()) + return hash(self.id) def __str__(self) -> str: return self.to_str() diff --git a/tests/unit_tests/common/test_events.py b/tests/unit_tests/common/test_events.py index a22500ff9f7e..c694762d9282 100644 --- a/tests/unit_tests/common/test_events.py +++ b/tests/unit_tests/common/test_events.py @@ -29,6 +29,37 @@ class TestCommonEvents: + def test_time_event_equality(self): + # Arrange + event_id = UUID4() + + event1 = TimeEvent( + "TEST_EVENT", + event_id, + 1, + 2, + ) + + event2 = TimeEvent( + "TEST_EVENT", + event_id, + 1, + 2, + ) + + event3 = TimeEvent( + "TEST_EVENT", + UUID4(), + 1, + 2, + ) + + # Act, Assert + assert event1.name == event2.name == event3.name + assert event1 == event2 + assert event3 != event1 + assert event3 != event2 + def test_time_event_picking(self): # Arrange event = TimeEvent( From ad9a515874da677c5acd7815056dfce21b926578 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 11 May 2024 09:46:34 +1000 Subject: [PATCH 152/193] Improve venue order ID generation and assignment --- RELEASES.md | 1 + nautilus_trader/backtest/matching_engine.pxd | 6 ++- nautilus_trader/backtest/matching_engine.pyx | 52 ++++++++++--------- nautilus_trader/cache/cache.pxd | 2 + nautilus_trader/cache/cache.pyx | 39 +++++++++++++- nautilus_trader/test_kit/stubs/events.py | 24 +++------ tests/unit_tests/cache/test_execution.py | 15 +++--- .../execution/test_emulator_list.py | 4 ++ tests/unit_tests/execution/test_engine.py | 36 +++++++++---- tests/unit_tests/risk/test_engine.py | 8 ++- tests/unit_tests/trading/test_strategy.py | 9 +++- 11 files changed, 131 insertions(+), 65 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index f9fa7cdc6835..01f54fdc7bf3 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,7 @@ Released on TBD (UTC). - Added Sandbox example with Interactive Brokers (#1618), thanks @rsmb7z - Added `ParquetDataCatalog` S3 support (#1620), thanks benjaminsingleton - Added `Bar.from_raw_arrays_to_list` (#1623), thanks rsmb7z +- Improved venue order ID generation and assignment (it was previously possible for the `OrderMatchingEngine` to generate multiple IDs for the same order) ### Breaking Changes - Removed `allow_cash_positions` config (simplify to the most common use case, spot trading should track positions) diff --git a/nautilus_trader/backtest/matching_engine.pxd b/nautilus_trader/backtest/matching_engine.pxd index 450cb78d6922..9a9138438180 100644 --- a/nautilus_trader/backtest/matching_engine.pxd +++ b/nautilus_trader/backtest/matching_engine.pxd @@ -201,6 +201,7 @@ cdef class OrderMatchingEngine: # -- IDENTIFIER GENERATORS ------------------------------------------------------------------------ + cdef VenueOrderId _get_venue_order_id(self, Order order) cdef PositionId _get_position_id(self, Order order, bint generate=*) cdef PositionId _generate_venue_position_id(self) cdef VenueOrderId _generate_venue_order_id(self) @@ -220,7 +221,7 @@ cdef class OrderMatchingEngine: # -- EVENT GENERATORS ----------------------------------------------------------------------------- cdef void _generate_order_rejected(self, Order order, str reason) - cdef void _generate_order_accepted(self, Order order) + cdef void _generate_order_accepted(self, Order order, VenueOrderId venue_order_id) cdef void _generate_order_modify_rejected( self, TraderId trader_id, @@ -242,12 +243,13 @@ cdef class OrderMatchingEngine: str reason, ) cpdef void _generate_order_updated(self, Order order, Quantity qty, Price price, Price trigger_price) - cdef void _generate_order_canceled(self, Order order) + cdef void _generate_order_canceled(self, Order order, VenueOrderId venue_order_id) cdef void _generate_order_triggered(self, Order order) cdef void _generate_order_expired(self, Order order) cdef void _generate_order_filled( self, Order order, + VenueOrderId venue_order_id, PositionId venue_position_id, Quantity last_qty, Price last_px, diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 48a84eb27e46..8a77cf3d65f4 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -564,6 +564,7 @@ cdef class OrderMatchingEngine: # venue_position_id = self._get_position_id(real_order) # self._generate_order_filled( # real_order, + # self._get_venue_order_id(real_order), # venue_position_id, # Quantity(order.size, self.instrument.size_precision), # Price(order.price, self.instrument.price_precision), @@ -1826,6 +1827,7 @@ cdef class OrderMatchingEngine: self._generate_order_filled( order=order, + venue_order_id=self._get_venue_order_id(order), venue_position_id=venue_position_id, last_qty=last_qty, last_px=last_px, @@ -1917,6 +1919,22 @@ cdef class OrderMatchingEngine: # -- IDENTIFIER GENERATORS ------------------------------------------------------------------------ + cdef VenueOrderId _get_venue_order_id(self, Order order): + # Check existing on order + cdef VenueOrderId venue_order_id = order.venue_order_id + if venue_order_id is not None: + return venue_order_id + + # Check exiting in cache + venue_order_id = self.cache.venue_order_id(order.client_order_id) + if venue_order_id is not None: + return venue_order_id + + venue_order_id = self._generate_venue_order_id() + self.cache.add_venue_order_id(order.client_order_id, venue_order_id) + + return venue_order_id + cdef PositionId _get_position_id(self, Order order, bint generate=True): cdef PositionId position_id if OmsType.HEDGING: @@ -1973,7 +1991,7 @@ cdef class OrderMatchingEngine: # Check if order already accepted (being added back into the matching engine) if not order.status_c() == OrderStatus.ACCEPTED: - self._generate_order_accepted(order) + self._generate_order_accepted(order, venue_order_id=self._get_venue_order_id(order)) if ( order.order_type == OrderType.TRAILING_STOP_MARKET @@ -1997,12 +2015,9 @@ cdef class OrderMatchingEngine: ) return - if order.venue_order_id is None: - order.venue_order_id = self._generate_venue_order_id() - self._core.delete_order(order) - self._generate_order_canceled(order) + self._generate_order_canceled(order, venue_order_id=self._get_venue_order_id(order)) if self._support_contingent_orders and order.contingency_type != ContingencyType.NO_CONTINGENCY and cancel_contingencies: self._cancel_contingent_orders(order) @@ -2149,7 +2164,7 @@ cdef class OrderMatchingEngine: ) self.msgbus.send(endpoint="ExecEngine.process", msg=event) - cdef void _generate_order_accepted(self, Order order): + cdef void _generate_order_accepted(self, Order order, VenueOrderId venue_order_id): # Generate event cdef uint64_t ts_now = self._clock.timestamp_ns() cdef OrderAccepted event = OrderAccepted( @@ -2157,7 +2172,7 @@ cdef class OrderMatchingEngine: strategy_id=order.strategy_id, instrument_id=order.instrument_id, client_order_id=order.client_order_id, - venue_order_id=order.venue_order_id or self._generate_venue_order_id(), + venue_order_id=venue_order_id, account_id=order.account_id or self._account_ids[order.trader_id], event_id=UUID4(), ts_event=ts_now, @@ -2224,20 +2239,6 @@ cdef class OrderMatchingEngine: Price price, Price trigger_price, ): - cdef VenueOrderId venue_order_id = order.venue_order_id - cdef bint venue_order_id_modified = False - if venue_order_id is None: - venue_order_id = self._generate_venue_order_id() - venue_order_id_modified = True - - # Check venue_order_id against cache, only allow modification when `venue_order_id_modified=True` - if not venue_order_id_modified: - existing = self.cache.venue_order_id(order.client_order_id) - if existing is not None: - Condition.equal(existing, order.venue_order_id, "existing", "order.venue_order_id") - else: - self._log.warning(f"{order.venue_order_id} does not match existing {repr(existing)}") - # Generate event cdef uint64_t ts_now = self._clock.timestamp_ns() cdef OrderUpdated event = OrderUpdated( @@ -2245,7 +2246,7 @@ cdef class OrderMatchingEngine: strategy_id=order.strategy_id, instrument_id=order.instrument_id, client_order_id=order.client_order_id, - venue_order_id=venue_order_id, + venue_order_id=order.venue_order_id, account_id=order.account_id or self._account_ids[order.trader_id], quantity=quantity, price=price, @@ -2256,7 +2257,7 @@ cdef class OrderMatchingEngine: ) self.msgbus.send(endpoint="ExecEngine.process", msg=event) - cdef void _generate_order_canceled(self, Order order): + cdef void _generate_order_canceled(self, Order order, VenueOrderId venue_order_id): # Generate event cdef uint64_t ts_now = self._clock.timestamp_ns() cdef OrderCanceled event = OrderCanceled( @@ -2264,7 +2265,7 @@ cdef class OrderMatchingEngine: strategy_id=order.strategy_id, instrument_id=order.instrument_id, client_order_id=order.client_order_id, - venue_order_id=order.venue_order_id, + venue_order_id=venue_order_id, account_id=order.account_id or self._account_ids[order.trader_id], event_id=UUID4(), ts_event=ts_now, @@ -2307,6 +2308,7 @@ cdef class OrderMatchingEngine: cdef void _generate_order_filled( self, Order order, + VenueOrderId venue_order_id, PositionId venue_position_id, Quantity last_qty, Price last_px, @@ -2321,7 +2323,7 @@ cdef class OrderMatchingEngine: strategy_id=order.strategy_id, instrument_id=order.instrument_id, client_order_id=order.client_order_id, - venue_order_id=order.venue_order_id or self._generate_venue_order_id(), + venue_order_id=venue_order_id, account_id=order.account_id or self._account_ids[order.trader_id], trade_id=self._generate_trade_id(), position_id=venue_position_id, diff --git a/nautilus_trader/cache/cache.pxd b/nautilus_trader/cache/cache.pxd index a4de932f6570..7d9634f4b43f 100644 --- a/nautilus_trader/cache/cache.pxd +++ b/nautilus_trader/cache/cache.pxd @@ -42,6 +42,7 @@ from nautilus_trader.model.identifiers cimport OrderListId from nautilus_trader.model.identifiers cimport PositionId from nautilus_trader.model.identifiers cimport StrategyId from nautilus_trader.model.identifiers cimport Venue +from nautilus_trader.model.identifiers cimport VenueOrderId from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument from nautilus_trader.model.objects cimport Currency @@ -160,6 +161,7 @@ cdef class Cache(CacheFacade): cpdef void add_instrument(self, Instrument instrument) cpdef void add_synthetic(self, SyntheticInstrument synthetic) cpdef void add_account(self, Account account) + cpdef void add_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id) cpdef void add_order(self, Order order, PositionId position_id=*, ClientId client_id=*, bint override=*) cpdef void add_order_list(self, OrderList order_list) cpdef void add_position_id(self, PositionId position_id, Venue venue, ClientOrderId client_order_id, StrategyId strategy_id) diff --git a/nautilus_trader/cache/cache.pyx b/nautilus_trader/cache/cache.pyx index d0ffe5af68e6..5c7809db5a29 100644 --- a/nautilus_trader/cache/cache.pyx +++ b/nautilus_trader/cache/cache.pyx @@ -1397,6 +1397,42 @@ cdef class Cache(CacheFacade): if self._database is not None: self._database.add_account(account) + cpdef void add_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id): + """ + Index the given venue order ID with the given client order ID. + + Parameters + ---------- + client_order_id : ClientOrderId + The client order ID to index. + venue_order_id : VenueOrderId + The venue order ID to index. + + Raises + ------ + ValueError + If the `client_order_id` is already indexed with a different `venue_order_id`. + + """ + Condition.not_none(client_order_id, "client_order_id") + Condition.not_none(venue_order_id, "venue_order_id") + + # TODO: Also consider checking the reverse index here + cdef ClientOrderId existing_client_order_id = self._index_order_ids.get(venue_order_id) + if existing_client_order_id is not None and client_order_id != existing_client_order_id: + raise ValueError( + f"Existing {existing_client_order_id!r} for {venue_order_id!r} " + f"did not match the given {client_order_id!r}. " + "If you are writing a test then try a different `venue_order_id`, " + "otherwise this is probably a bug." + ) + + self._index_order_ids[venue_order_id] = client_order_id + + self._log.debug( + f"Indexed client_order_id={client_order_id}, venue_order_id={venue_order_id})", + ) + cpdef void add_order( self, Order order, @@ -1769,8 +1805,7 @@ cdef class Cache(CacheFacade): # Update venue order ID if order.venue_order_id is not None: - # Assumes order_id does not change - self._index_order_ids[order.venue_order_id] = order.client_order_id + self.add_venue_order_id(order.client_order_id, order.venue_order_id) # Update in-flight state if order.is_inflight_c(): diff --git a/nautilus_trader/test_kit/stubs/events.py b/nautilus_trader/test_kit/stubs/events.py index 97225319d449..63f341217e63 100644 --- a/nautilus_trader/test_kit/stubs/events.py +++ b/nautilus_trader/test_kit/stubs/events.py @@ -302,22 +302,14 @@ def order_filled( ts_filled_ns: int = 0, account: Account | None = None, ) -> OrderFilled: - if strategy_id is None: - strategy_id = order.strategy_id - if account_id is None: - account_id = order.account_id - if account_id is None: - account_id = TestIdStubs.account_id() - if venue_order_id is None: - venue_order_id = VenueOrderId("1") - if trade_id is None: - trade_id = TradeId(order.client_order_id.value.replace("O", "E")) - if position_id is None: - position_id = order.position_id - if last_px is None: - last_px = Price.from_str(f"{1:.{instrument.price_precision}f}") - if last_qty is None: - last_qty = order.quantity + strategy_id = strategy_id or order.strategy_id + account_id = account_id or order.account_id or TestIdStubs.account_id() + venue_order_id = venue_order_id or order.venue_order_id or VenueOrderId("1") + trade_id = trade_id or TradeId(order.client_order_id.value.replace("O", "E")) + position_id = position_id or order.position_id + last_px = last_px or Price.from_str(f"{1:.{instrument.price_precision}f}") + last_qty = last_qty or order.quantity + if account is None: # Causes circular import if moved to the top from nautilus_trader.test_kit.stubs.execution import TestExecStubs diff --git a/tests/unit_tests/cache/test_execution.py b/tests/unit_tests/cache/test_execution.py index 70360603d6d6..1669383a4271 100644 --- a/tests/unit_tests/cache/test_execution.py +++ b/tests/unit_tests/cache/test_execution.py @@ -42,6 +42,7 @@ from nautilus_trader.model.identifiers import StrategyId from nautilus_trader.model.identifiers import TradeId from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.identifiers import VenueOrderId from nautilus_trader.model.objects import Currency from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price @@ -903,7 +904,7 @@ def test_update_position_for_closed_position(self): order2.apply(TestEventStubs.order_submitted(order2)) self.cache.update_order(order2) - order2.apply(TestEventStubs.order_accepted(order2)) + order2.apply(TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2"))) self.cache.update_order(order2) order2_filled = TestEventStubs.order_filled( order2, @@ -981,7 +982,7 @@ def test_positions_queries_with_multiple_open_returns_expected_positions(self): order2.apply(TestEventStubs.order_submitted(order2)) self.cache.update_order(order2) - order2.apply(TestEventStubs.order_accepted(order2)) + order2.apply(TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2"))) self.cache.update_order(order2) fill2 = TestEventStubs.order_filled( order2, @@ -1063,7 +1064,7 @@ def test_positions_queries_with_one_closed_returns_expected_positions(self): order2.apply(TestEventStubs.order_submitted(order2)) self.cache.update_order(order2) - order2.apply(TestEventStubs.order_accepted(order2)) + order2.apply(TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2"))) self.cache.update_order(order2) fill2 = TestEventStubs.order_filled( order2, @@ -1084,7 +1085,7 @@ def test_positions_queries_with_one_closed_returns_expected_positions(self): order3.apply(TestEventStubs.order_submitted(order3)) self.cache.update_order(order3) - order3.apply(TestEventStubs.order_accepted(order3)) + order3.apply(TestEventStubs.order_accepted(order3, venue_order_id=VenueOrderId("3"))) self.cache.update_order(order3) fill3 = TestEventStubs.order_filled( order3, @@ -1192,7 +1193,7 @@ def test_check_residuals(self): order2.apply(TestEventStubs.order_submitted(order2)) self.cache.update_order(order2) - order2.apply(TestEventStubs.order_accepted(order2)) + order2.apply(TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2"))) self.cache.update_order(order2) # Act @@ -1241,7 +1242,7 @@ def test_reset(self): order2.apply(TestEventStubs.order_submitted(order2)) self.cache.update_order(order2) - order2.apply(TestEventStubs.order_accepted(order2)) + order2.apply(TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2"))) self.cache.update_order(order2) self.cache.update_order(order2) @@ -1296,7 +1297,7 @@ def test_flush_db(self): order2.apply(TestEventStubs.order_submitted(order2)) self.cache.update_order(order2) - order2.apply(TestEventStubs.order_accepted(order2)) + order2.apply(TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2"))) self.cache.update_order(order2) # Act diff --git a/tests/unit_tests/execution/test_emulator_list.py b/tests/unit_tests/execution/test_emulator_list.py index 526263984716..660ab4677f18 100644 --- a/tests/unit_tests/execution/test_emulator_list.py +++ b/tests/unit_tests/execution/test_emulator_list.py @@ -47,6 +47,7 @@ from nautilus_trader.model.identifiers import OrderListId from nautilus_trader.model.identifiers import PositionId from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.identifiers import VenueOrderId from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Quantity from nautilus_trader.model.orders.list import OrderList @@ -908,6 +909,7 @@ def test_triggered_then_filled_tp_cancels_sl( bracket.orders[2], instrument=ETHUSDT_PERP_BINANCE, account_id=self.account_id, + venue_order_id=VenueOrderId("2"), ), ) @@ -977,6 +979,7 @@ def test_triggered_then_partially_filled_oco_sl_cancels_tp(self) -> None: bracket.orders[1], instrument=ETHUSDT_PERP_BINANCE, account_id=self.account_id, + venue_order_id=VenueOrderId("2"), last_qty=Quantity.from_int(5), ), ) @@ -1051,6 +1054,7 @@ def test_triggered_then_partially_filled_ouo_sl_updated_tp(self) -> None: bracket.orders[1], instrument=ETHUSDT_PERP_BINANCE, account_id=self.account_id, + venue_order_id=VenueOrderId("2"), last_qty=Quantity.from_int(5), ), ) diff --git a/tests/unit_tests/execution/test_engine.py b/tests/unit_tests/execution/test_engine.py index ac06c3c71155..d76355eaf604 100644 --- a/tests/unit_tests/execution/test_engine.py +++ b/tests/unit_tests/execution/test_engine.py @@ -1343,7 +1343,9 @@ def test_add_to_existing_position_on_order_fill(self) -> None: # Act self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=expected_position_id), ) @@ -1418,7 +1420,9 @@ def test_close_position_on_order_fill(self) -> None: # Act self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=position_id), ) @@ -1506,7 +1510,9 @@ def test_multiple_strategy_positions_opened(self) -> None: TestEventStubs.order_filled(order1, AUDUSD_SIM, position_id=position1_id), ) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=position2_id), ) @@ -1627,14 +1633,18 @@ def test_multiple_strategy_positions_one_active_one_closed(self) -> None: self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=position_id1), ) self.risk_engine.execute(submit_order3) self.exec_engine.process(TestEventStubs.order_submitted(order3)) - self.exec_engine.process(TestEventStubs.order_accepted(order3)) + self.exec_engine.process( + TestEventStubs.order_accepted(order3, venue_order_id=VenueOrderId("3")), + ) self.exec_engine.process( TestEventStubs.order_filled(order3, AUDUSD_SIM, position_id=position_id2), ) @@ -1719,7 +1729,9 @@ def test_flip_position_on_opposite_filled_same_position_sell(self) -> None: # Act self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=position_id), ) @@ -1797,7 +1809,9 @@ def test_flip_position_on_opposite_filled_same_position_buy(self) -> None: # Act self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=position_id), ) @@ -1892,7 +1906,9 @@ def test_flip_position_on_flat_position_then_filled_reusing_position_id(self) -> self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=position_id), ) @@ -1960,7 +1976,9 @@ def test_flip_position_when_netting_oms(self) -> None: # Act self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process( TestEventStubs.order_filled(order2, AUDUSD_SIM, position_id=position_id), ) diff --git a/tests/unit_tests/risk/test_engine.py b/tests/unit_tests/risk/test_engine.py index ab51d4f5bda9..d78960014624 100644 --- a/tests/unit_tests/risk/test_engine.py +++ b/tests/unit_tests/risk/test_engine.py @@ -398,7 +398,9 @@ def test_submit_reduce_only_order_when_position_already_closed_then_denies(self) self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process(TestEventStubs.order_filled(order2, _AUDUSD_SIM)) submit_order3 = SubmitOrder( @@ -472,7 +474,9 @@ def test_submit_reduce_only_order_when_position_would_be_increased_then_denies(s # Act self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) - self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process( + TestEventStubs.order_accepted(order2, venue_order_id=VenueOrderId("2")), + ) self.exec_engine.process(TestEventStubs.order_filled(order2, _AUDUSD_SIM)) # Assert diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index bfe903a1a4fd..b3a07d1c7958 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -51,6 +51,7 @@ from nautilus_trader.model.identifiers import ClientId from nautilus_trader.model.identifiers import StrategyId from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.identifiers import VenueOrderId from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity @@ -1838,7 +1839,9 @@ def test_managed_contingenies_when_filled_sl_then_cancels_contingent_order( strategy.submit_order_list(bracket) self.exec_engine.process(TestEventStubs.order_filled(entry_order, _USDJPY_SIM)) - self.exec_engine.process(TestEventStubs.order_filled(sl_order, _USDJPY_SIM)) + self.exec_engine.process( + TestEventStubs.order_filled(sl_order, _USDJPY_SIM, venue_order_id=VenueOrderId("2")), + ) self.exchange.process(0) # Assert @@ -1891,7 +1894,9 @@ def test_managed_contingenies_when_filled_tp_then_cancels_contingent_order( strategy.submit_order_list(bracket) self.exec_engine.process(TestEventStubs.order_filled(entry_order, _USDJPY_SIM)) - self.exec_engine.process(TestEventStubs.order_filled(tp_order, _USDJPY_SIM)) + self.exec_engine.process( + TestEventStubs.order_filled(tp_order, _USDJPY_SIM, venue_order_id=VenueOrderId("2")), + ) self.exchange.process(0) # Assert From 7d38223ccd6235997545ca317a05273a9dc4e692 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 11 May 2024 16:55:20 +1000 Subject: [PATCH 153/193] Update dependencies --- nautilus_core/Cargo.lock | 60 ++++++++++++++++++------------------ nautilus_core/cli/Cargo.toml | 4 +-- poetry.lock | 10 +++--- pyproject.toml | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 4d3c71a601b0..65c766f837f2 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -414,7 +414,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -618,7 +618,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", "syn_derive", ] @@ -867,7 +867,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -1141,7 +1141,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -1152,7 +1152,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -1468,7 +1468,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -1510,7 +1510,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -1520,7 +1520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -1787,7 +1787,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -3002,7 +3002,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -3079,7 +3079,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -3295,7 +3295,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -3554,7 +3554,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -3567,7 +3567,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -3959,7 +3959,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.61", + "syn 2.0.62", "unicode-ident", ] @@ -4174,7 +4174,7 @@ checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -4274,9 +4274,9 @@ checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" [[package]] name = "simple_logger" -version = "4.3.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" +checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" dependencies = [ "colored", "log", @@ -4397,7 +4397,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -4648,7 +4648,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -4670,9 +4670,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.61" +version = "2.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" +checksum = "9f660c3bfcefb88c538776b6685a0c472e3128b51e74d48793dc2a488196e8eb" dependencies = [ "proc-macro2", "quote", @@ -4688,7 +4688,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -4819,7 +4819,7 @@ checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -4943,7 +4943,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -5093,7 +5093,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -5213,7 +5213,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] @@ -5400,7 +5400,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", "wasm-bindgen-shared", ] @@ -5434,7 +5434,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5746,7 +5746,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.62", ] [[package]] diff --git a/nautilus_core/cli/Cargo.toml b/nautilus_core/cli/Cargo.toml index 08731e320701..928e18caa8ab 100644 --- a/nautilus_core/cli/Cargo.toml +++ b/nautilus_core/cli/Cargo.toml @@ -14,11 +14,11 @@ path = "src/bin/cli.rs" nautilus-common = { path = "../common"} nautilus-model = { path = "../model" } nautilus-core = { path = "../core" } -nautilus-infrastructure = { path = "../infrastructure" , features = ['postgres']} +nautilus-infrastructure = { path = "../infrastructure" , features = ["postgres"] } anyhow = { workspace = true } tokio = {workspace = true} log = { workspace = true } clap = { version = "4.5.4", features = ["derive", "env"] } clap_derive = { version = "4.5.4" } dotenvy = { version = "0.15.7" } -simple_logger = "4.3.3" +simple_logger = "5.0.0" diff --git a/poetry.lock b/poetry.lock index 3b13dc323d83..71269df12175 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohttp" @@ -1618,13 +1618,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.7.0" +version = "3.7.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, - {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] [package.dependencies] @@ -2685,4 +2685,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "dcf0ef362d59f2c24681895feadfd6eb96fb837eae696bbba9c6c55b06dab287" +content-hash = "434f862de93395f24c47c15ac385f0d27c200af07e733fd7253e39ecbc21ea86" diff --git a/pyproject.toml b/pyproject.toml index 9e81f2073bb1..4dc4cdf15d9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ black = "^24.4.2" docformatter = "^1.7.5" mypy = "^1.10.0" pandas-stubs = "^2.2.1" -pre-commit = "^3.7.0" +pre-commit = "^3.7.1" ruff = "^0.4.4" types-pytz = "^2023.3" types-requests = "^2.31" From 4d567e094d6d4f952301d9d7448dd751019b3288 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 11 May 2024 17:30:36 +1000 Subject: [PATCH 154/193] Improve Portfolio position update logic --- nautilus_trader/portfolio/portfolio.pyx | 37 ++++++++++++++++--------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/nautilus_trader/portfolio/portfolio.pyx b/nautilus_trader/portfolio/portfolio.pyx index bef2b11bb3e0..7552461f1445 100644 --- a/nautilus_trader/portfolio/portfolio.pyx +++ b/nautilus_trader/portfolio/portfolio.pyx @@ -447,14 +447,36 @@ cdef class Portfolio(PortfolioFacade): ) return # No instrument found + cdef list[Position] positions_open cdef AccountState account_state = None if isinstance(event, OrderFilled): - account_state = self._accounts.update_balances( + positions_open = self._cache.positions_open( + venue=None, # Faster query filtering + instrument_id=instrument.id, + ) + self._update_net_position( + instrument_id=instrument.id, + positions_open=positions_open + ) + + self._accounts.update_balances( account=account, instrument=instrument, fill=event, ) + if account.type == AccountType.MARGIN and account.calculate_account_state: + self._accounts.update_positions( + account=account, + instrument=instrument, + positions_open=positions_open, + ts_event=event.ts_event, + ) + + self._unrealized_pnls[event.instrument_id] = self._calculate_unrealized_pnl( + instrument_id=event.instrument_id, + ) + cdef list orders_open = self._cache.orders_open( venue=None, # Faster query filtering instrument_id=event.instrument_id, @@ -524,24 +546,13 @@ cdef class Portfolio(PortfolioFacade): ) return # No instrument found - cdef AccountState account_state = self._accounts.update_positions( + self._accounts.update_positions( account=account, instrument=instrument, positions_open=positions_open, ts_event=event.ts_event, ) - if account_state is None: - self._log.debug(f"Added pending calculation for {instrument.id}") - self._pending_calcs.add(instrument.id) - else: - self._msgbus.publish_c( - topic=f"events.account.{account.id}", - msg=account_state, - ) - - self._log.debug(f"Updated {event}") - def _reset(self) -> None: self._net_positions.clear() self._unrealized_pnls.clear() From ee47d5ea492d3c3f8b2693b1a02f9331d095f529 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 11 May 2024 19:14:44 +1000 Subject: [PATCH 155/193] Improve venue order ID cache indexing --- nautilus_trader/cache/cache.pxd | 3 +- nautilus_trader/cache/cache.pyx | 46 +++++++++++-------- .../betfair/test_betfair_execution.py | 1 + tests/unit_tests/execution/test_engine.py | 5 +- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/nautilus_trader/cache/cache.pxd b/nautilus_trader/cache/cache.pxd index 7d9634f4b43f..82bd6daf7fe5 100644 --- a/nautilus_trader/cache/cache.pxd +++ b/nautilus_trader/cache/cache.pxd @@ -79,7 +79,8 @@ cdef class Cache(CacheFacade): cdef dict _index_venue_account cdef dict _index_venue_orders cdef dict _index_venue_positions - cdef dict _index_order_ids + cdef dict _index_venue_order_ids + cdef dict _index_client_order_ids cdef dict _index_order_position cdef dict _index_order_strategy cdef dict _index_order_client diff --git a/nautilus_trader/cache/cache.pyx b/nautilus_trader/cache/cache.pyx index 5c7809db5a29..af586ac5215e 100644 --- a/nautilus_trader/cache/cache.pyx +++ b/nautilus_trader/cache/cache.pyx @@ -135,7 +135,8 @@ cdef class Cache(CacheFacade): self._index_venue_account: dict[Venue, AccountId] = {} self._index_venue_orders: dict[Venue, set[ClientOrderId]] = {} self._index_venue_positions: dict[Venue, set[PositionId]] = {} - self._index_order_ids: dict[VenueOrderId, ClientOrderId] = {} + self._index_venue_order_ids: dict[VenueOrderId, ClientOrderId] = {} + self._index_client_order_ids: dict[ClientOrderId, VenueOrderId] = {} self._index_order_position: dict[ClientOrderId, PositionId] = {} self._index_order_strategy: dict[ClientOrderId, StrategyId] = {} self._index_order_client: dict[ClientOrderId, ClientId] = {} @@ -479,7 +480,7 @@ cdef class Cache(CacheFacade): ) error_count += 1 - for client_order_id in self._index_order_ids.values(): + for client_order_id in self._index_venue_order_ids.values(): if client_order_id not in self._orders: self._log.error( f"{failure} in _index_venue_order_ids: " @@ -487,6 +488,14 @@ cdef class Cache(CacheFacade): ) error_count += 1 + for client_order_id in self._index_client_order_ids: + if client_order_id not in self._orders: + self._log.error( + f"{failure} in _index_client_order_ids: " + f"{repr(client_order_id)} not found in self._cached_orders" + ) + error_count += 1 + for client_order_id in self._index_order_position: if client_order_id not in self._orders: self._log.error( @@ -683,7 +692,8 @@ cdef class Cache(CacheFacade): self._index_venue_account.clear() self._index_venue_orders.clear() self._index_venue_positions.clear() - self._index_order_ids.clear() + self._index_venue_order_ids.clear() + self._index_client_order_ids.clear() self._index_order_position.clear() self._index_order_strategy.clear() self._index_order_client.clear() @@ -781,9 +791,10 @@ cdef class Cache(CacheFacade): self._index_venue_orders[order.instrument_id.venue] = set() self._index_venue_orders[order.instrument_id.venue].add(client_order_id) - # 2: Build _index_order_ids -> {VenueOrderId, ClientOrderId} + # 2: Build _index_venue_order_ids -> {VenueOrderId, ClientOrderId} if order.venue_order_id is not None: - self._index_order_ids[order.venue_order_id] = order.client_order_id + self._index_venue_order_ids[order.venue_order_id] = order.client_order_id + self._index_client_order_ids[order.client_order_id] = order.venue_order_id # 3: Build _index_order_position -> {ClientOrderId, PositionId} if order.position_id is not None: @@ -1399,7 +1410,7 @@ cdef class Cache(CacheFacade): cpdef void add_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id): """ - Index the given venue order ID with the given client order ID. + Index the given client order ID with the given venue order ID. Parameters ---------- @@ -1417,20 +1428,20 @@ cdef class Cache(CacheFacade): Condition.not_none(client_order_id, "client_order_id") Condition.not_none(venue_order_id, "venue_order_id") - # TODO: Also consider checking the reverse index here - cdef ClientOrderId existing_client_order_id = self._index_order_ids.get(venue_order_id) - if existing_client_order_id is not None and client_order_id != existing_client_order_id: + cdef VenueOrderId existing_venue_order_id = self._index_client_order_ids.get(client_order_id) + if existing_venue_order_id is not None and venue_order_id != existing_venue_order_id: raise ValueError( - f"Existing {existing_client_order_id!r} for {venue_order_id!r} " - f"did not match the given {client_order_id!r}. " + f"Existing {existing_venue_order_id!r} for {client_order_id!r} " + f"did not match the given {venue_order_id!r}. " "If you are writing a test then try a different `venue_order_id`, " "otherwise this is probably a bug." ) - self._index_order_ids[venue_order_id] = client_order_id + self._index_client_order_ids[client_order_id] = venue_order_id + self._index_venue_order_ids[venue_order_id] = client_order_id self._log.debug( - f"Indexed client_order_id={client_order_id}, venue_order_id={venue_order_id})", + f"Indexed {client_order_id!r} with {venue_order_id!r}", ) cpdef void add_order( @@ -1804,7 +1815,7 @@ cdef class Cache(CacheFacade): Condition.not_none(order, "order") # Update venue order ID - if order.venue_order_id is not None: + if order.venue_order_id is not None and order.venue_order_id not in self._index_venue_order_ids: self.add_venue_order_id(order.client_order_id, order.venue_order_id) # Update in-flight state @@ -3057,7 +3068,7 @@ cdef class Cache(CacheFacade): """ Condition.not_none(venue_order_id, "venue_order_id") - return self._index_order_ids.get(venue_order_id) + return self._index_venue_order_ids.get(venue_order_id) cpdef VenueOrderId venue_order_id(self, ClientOrderId client_order_id): """ @@ -3070,10 +3081,7 @@ cdef class Cache(CacheFacade): """ Condition.not_none(client_order_id, "client_order_id") - cdef Order order = self._orders.get(client_order_id) - if order is None: - return None - return order.venue_order_id + return self._index_client_order_ids.get(client_order_id) cpdef ClientId client_id(self, ClientOrderId client_order_id): """ diff --git a/tests/integration_tests/adapters/betfair/test_betfair_execution.py b/tests/integration_tests/adapters/betfair/test_betfair_execution.py index 78038eb83fda..f0ab9283d37c 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_execution.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_execution.py @@ -264,6 +264,7 @@ async def test_submit_order_error( assert rejected.reason == expecter_error +@pytest.mark.skip(reason="Assignment of venue order IDs needs investigating for Betfair") @pytest.mark.asyncio() async def test_modify_order_success( exec_client: BetfairDataClient, diff --git a/tests/unit_tests/execution/test_engine.py b/tests/unit_tests/execution/test_engine.py index d76355eaf604..72ef8d0da204 100644 --- a/tests/unit_tests/execution/test_engine.py +++ b/tests/unit_tests/execution/test_engine.py @@ -2031,13 +2031,13 @@ def test_handle_updated_order_event(self) -> None: assert cached_order.venue_order_id == order.venue_order_id # Act - new_venue_id = VenueOrderId("UPDATED") + new_venue_id = VenueOrderId("1") order_updated = OrderUpdated( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=AUDUSD_SIM.id, client_order_id=order.client_order_id, - venue_order_id=new_venue_id, + venue_order_id=VenueOrderId("1"), account_id=self.account_id, quantity=order.quantity, price=order.price, @@ -2049,6 +2049,7 @@ def test_handle_updated_order_event(self) -> None: self.exec_engine.process(order_updated) # Order should have new venue_order_id + # TODO: This test was updated as the venue order ID currently does not change once assigned cached_order = self.cache.order(order.client_order_id) assert cached_order.venue_order_id == new_venue_id From fadba42f9ee1b6c194e27daa815d5b657a2e87f4 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 11 May 2024 23:55:45 +0200 Subject: [PATCH 156/193] Add serial attribute in Postgres infrastructure tests (#1637) --- nautilus_core/Cargo.lock | 118 ++++++++++++------ nautilus_core/infrastructure/Cargo.toml | 1 + .../tests/test_cache_database_postgres.rs | 7 +- 3 files changed, 85 insertions(+), 41 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 65c766f837f2..a0ed04a7284a 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" +checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" dependencies = [ "bzip2", "flate2", @@ -414,7 +414,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -618,7 +618,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", "syn_derive", ] @@ -867,7 +867,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -1141,7 +1141,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -1152,7 +1152,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -1468,7 +1468,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -1510,7 +1510,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -1520,7 +1520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -1595,9 +1595,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1787,7 +1787,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -2744,6 +2744,7 @@ dependencies = [ "rstest", "rust_decimal", "serde_json", + "serial_test", "sqlx", "tokio", "tracing", @@ -2879,9 +2880,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" dependencies = [ "num-bigint", "num-complex", @@ -2955,10 +2956,11 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ + "autocfg", "num-bigint", "num-integer", "num-traits", @@ -3002,7 +3004,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -3079,7 +3081,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -3232,9 +3234,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", "indexmap 2.2.6", @@ -3295,7 +3297,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -3554,7 +3556,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -3567,7 +3569,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -3959,7 +3961,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.62", + "syn 2.0.61", "unicode-ident", ] @@ -4101,6 +4103,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -4116,6 +4127,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "seahash" version = "4.1.0" @@ -4174,7 +4191,7 @@ checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -4210,6 +4227,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4397,7 +4439,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -4648,7 +4690,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -4670,9 +4712,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.62" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f660c3bfcefb88c538776b6685a0c472e3128b51e74d48793dc2a488196e8eb" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -4688,7 +4730,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -4819,7 +4861,7 @@ checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -4943,7 +4985,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -5093,7 +5135,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -5213,7 +5255,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] @@ -5400,7 +5442,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", "wasm-bindgen-shared", ] @@ -5434,7 +5476,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5746,7 +5788,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.62", + "syn 2.0.61", ] [[package]] diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index d215fa7a7fb6..5d2a7ae3be37 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -40,6 +40,7 @@ sqlx = { version = "0.7.4", features = [ [dev-dependencies] rstest = { workspace = true } +serial_test = { version = "3.1.1" } [features] default = ["redis"] # redis needed by `nautilus_trader` by default for now diff --git a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs index 1777ae3f444d..f246dd75fbff 100644 --- a/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs +++ b/nautilus_core/infrastructure/tests/test_cache_database_postgres.rs @@ -76,11 +76,12 @@ mod tests { orders::{any::OrderAny, stubs::TestOrderStubs}, types::{currency::Currency, price::Price, quantity::Quantity}, }; + use serial_test::serial; use crate::get_pg_cache_database; - #[ignore] #[tokio::test] + #[serial] async fn test_add_general_object_adds_to_cache() { let pg_cache = get_pg_cache_database().await.unwrap(); let test_id_value = String::from("test_value").into_bytes(); @@ -98,8 +99,8 @@ mod tests { assert_eq!(result.get("test_id").unwrap().to_owned(), test_id_value); } - #[ignore] #[tokio::test] + #[serial] async fn test_add_currency_and_instruments() { // 1. first define and add currencies as they are contain foreign keys for instruments let pg_cache = get_pg_cache_database().await.unwrap(); @@ -234,8 +235,8 @@ mod tests { ); } - #[ignore] #[tokio::test] + #[serial] async fn test_add_order() { let instrument = currency_pair_ethusdt(); let pg_cache = get_pg_cache_database().await.unwrap(); From f87c385e2944e5808c0c210acb58c72a51012bfd Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 11 May 2024 23:57:41 +0200 Subject: [PATCH 157/193] Export order apply function with pyo3 (#1638) --- nautilus_core/model/src/python/orders/limit.rs | 10 ++++++++-- .../model/src/python/orders/limit_if_touched.rs | 10 ++++++++-- nautilus_core/model/src/python/orders/market.rs | 16 ++++++++++++++-- .../src/python/orders/market_if_touched.rs | 10 ++++++++-- .../model/src/python/orders/market_to_limit.rs | 10 ++++++++-- .../model/src/python/orders/stop_limit.rs | 17 +++++++++++++++-- .../model/src/python/orders/stop_market.rs | 10 ++++++++-- .../src/python/orders/trailing_stop_limit.rs | 10 ++++++++-- .../src/python/orders/trailing_stop_market.rs | 10 ++++++++-- nautilus_trader/core/nautilus_pyo3.pyi | 10 ++++++++++ 10 files changed, 95 insertions(+), 18 deletions(-) diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index b31e2217ac46..01c0e7c43b6e 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{nanos::UnixNanos, python::to_pyruntime_err, uuid::UUID4}; use pyo3::{ basic::CompareOp, prelude::*, @@ -38,7 +38,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order, OrderCore}, limit::LimitOrder, }, - python::common::commissions_from_hashmap, + python::{common::commissions_from_hashmap, events::order::convert_pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -673,4 +673,10 @@ impl LimitOrder { )?; Ok(dict.into()) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/limit_if_touched.rs b/nautilus_core/model/src/python/orders/limit_if_touched.rs index c37c0e1002d3..67db81e11dbd 100644 --- a/nautilus_core/model/src/python/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/python/orders/limit_if_touched.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use nautilus_core::uuid::UUID4; +use nautilus_core::{python::to_pyruntime_err, uuid::UUID4}; use pyo3::prelude::*; use ustr::Ustr; @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, limit_if_touched::LimitIfTouchedOrder, }, - python::events::order::convert_order_event_to_pyobject, + python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -121,4 +121,10 @@ impl LimitIfTouchedOrder { fn py_create(init: OrderInitialized) -> PyResult { Ok(LimitIfTouchedOrder::from(init)) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index deb06be0dca2..ec6c9593417f 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -15,7 +15,10 @@ use std::collections::HashMap; -use nautilus_core::{python::to_pyvalue_err, uuid::UUID4}; +use nautilus_core::{ + python::{to_pyruntime_err, to_pyvalue_err}, + uuid::UUID4, +}; use pyo3::{ basic::CompareOp, pymethods, @@ -37,7 +40,10 @@ use crate::{ base::{str_hashmap_to_ustr, Order, OrderCore}, market::MarketOrder, }, - python::{common::commissions_from_hashmap, events::order::convert_order_event_to_pyobject}, + python::{ + common::commissions_from_hashmap, + events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + }, types::{currency::Currency, money::Money, quantity::Quantity}, }; @@ -540,4 +546,10 @@ impl MarketOrder { .unwrap(); Ok(market_order) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/market_if_touched.rs b/nautilus_core/model/src/python/orders/market_if_touched.rs index d7460867e49a..7a4d41bb6edf 100644 --- a/nautilus_core/model/src/python/orders/market_if_touched.rs +++ b/nautilus_core/model/src/python/orders/market_if_touched.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use nautilus_core::uuid::UUID4; +use nautilus_core::{python::to_pyruntime_err, uuid::UUID4}; use pyo3::prelude::*; use ustr::Ustr; @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, market_if_touched::MarketIfTouchedOrder, }, - python::events::order::convert_order_event_to_pyobject, + python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -117,4 +117,10 @@ impl MarketIfTouchedOrder { fn py_create(init: OrderInitialized) -> PyResult { Ok(MarketIfTouchedOrder::from(init)) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/market_to_limit.rs b/nautilus_core/model/src/python/orders/market_to_limit.rs index c60cd033f68c..161e722d6ec6 100644 --- a/nautilus_core/model/src/python/orders/market_to_limit.rs +++ b/nautilus_core/model/src/python/orders/market_to_limit.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use nautilus_core::uuid::UUID4; +use nautilus_core::{python::to_pyruntime_err, uuid::UUID4}; use pyo3::prelude::*; use ustr::Ustr; @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, market_to_limit::MarketToLimitOrder, }, - python::events::order::convert_order_event_to_pyobject, + python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, types::quantity::Quantity, }; @@ -111,4 +111,10 @@ impl MarketToLimitOrder { fn py_create(init: OrderInitialized) -> PyResult { Ok(MarketToLimitOrder::from(init)) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index 9776010444cb..2efcca4e7c58 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -15,7 +15,11 @@ use std::collections::HashMap; -use nautilus_core::{nanos::UnixNanos, python::to_pyvalue_err, uuid::UUID4}; +use nautilus_core::{ + nanos::UnixNanos, + python::{to_pyruntime_err, to_pyvalue_err}, + uuid::UUID4, +}; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; use ustr::Ustr; @@ -31,7 +35,10 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, stop_limit::StopLimitOrder, }, - python::{common::commissions_from_hashmap, events::order::convert_order_event_to_pyobject}, + python::{ + common::commissions_from_hashmap, + events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + }, types::{price::Price, quantity::Quantity}, }; @@ -666,4 +673,10 @@ impl StopLimitOrder { .unwrap(); Ok(stop_limit_order) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/stop_market.rs b/nautilus_core/model/src/python/orders/stop_market.rs index 4191aab6458e..e583b89c74ed 100644 --- a/nautilus_core/model/src/python/orders/stop_market.rs +++ b/nautilus_core/model/src/python/orders/stop_market.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use nautilus_core::uuid::UUID4; +use nautilus_core::{python::to_pyruntime_err, uuid::UUID4}; use pyo3::prelude::*; use ustr::Ustr; @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, stop_market::StopMarketOrder, }, - python::events::order::convert_order_event_to_pyobject, + python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -117,4 +117,10 @@ impl StopMarketOrder { fn py_order_type(&self) -> OrderType { self.order_type } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs index b2c8a192eeed..726a3c814130 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use nautilus_core::uuid::UUID4; +use nautilus_core::{python::to_pyruntime_err, uuid::UUID4}; use pyo3::prelude::*; use ustr::Ustr; @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, trailing_stop_limit::TrailingStopLimitOrder, }, - python::events::order::convert_order_event_to_pyobject, + python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -127,4 +127,10 @@ impl TrailingStopLimitOrder { fn py_create(init: OrderInitialized) -> PyResult { Ok(TrailingStopLimitOrder::from(init)) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_core/model/src/python/orders/trailing_stop_market.rs b/nautilus_core/model/src/python/orders/trailing_stop_market.rs index 7bb3a72f2eea..2501748855a0 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_market.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use nautilus_core::uuid::UUID4; +use nautilus_core::{python::to_pyruntime_err, uuid::UUID4}; use pyo3::prelude::*; use ustr::Ustr; @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, trailing_stop_market::TrailingStopMarketOrder, }, - python::events::order::convert_order_event_to_pyobject, + python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -121,4 +121,10 @@ impl TrailingStopMarketOrder { fn py_create(init: OrderInitialized) -> PyResult { Ok(TrailingStopMarketOrder::from(init)) } + + #[pyo3(name = "apply")] + fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { + let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) + } } diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 14dba629cbc7..467d86c9af62 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -995,6 +995,8 @@ class LimitOrder: def is_spawned(self) -> bool: ... @classmethod def from_dict(cls, values: dict[str, str]) -> LimitOrder: ... + def apply(self, event: object) -> None: ... + class LimitIfTouchedOrder: def __init__( @@ -1029,6 +1031,7 @@ class LimitIfTouchedOrder: ) -> None: ... @classmethod def create(cls, init: OrderInitialized) -> LimitIfTouchedOrder: ... + def apply(self, event: object) -> None: ... class MarketOrder: def __init__( @@ -1090,6 +1093,7 @@ class MarketOrder: def order_type(self) -> OrderType: ... @property def price(self) -> Price | None: ... + def apply(self, event: object) -> None: ... class MarketToLimitOrder: def __init__( @@ -1119,6 +1123,7 @@ class MarketToLimitOrder: ): ... @classmethod def create(cls, init: OrderInitialized) -> MarketToLimitOrder: ... + def apply(self, event: object) -> None: ... class MarketIfTouchedOrder: def __init__( @@ -1151,6 +1156,7 @@ class MarketIfTouchedOrder: ): ... @classmethod def create(cls, init: OrderInitialized) -> MarketIfTouchedOrder: ... + def apply(self, event: object) -> None: ... class StopLimitOrder: def __init__( @@ -1232,6 +1238,7 @@ class StopLimitOrder: def has_trigger_price(self) -> bool: ... @property def expire_time(self) -> int | None: ... + def apply(self, event: object) -> None: ... class StopMarketOrder: def __init__( @@ -1264,6 +1271,7 @@ class StopMarketOrder: ): ... @classmethod def create(cls, init: OrderInitialized) -> StopMarketOrder: ... + def apply(self, event: object) -> None: ... class TrailingStopLimitOrder: def __init__( @@ -1301,6 +1309,7 @@ class TrailingStopLimitOrder: ): ... @classmethod def create(cls, init: OrderInitialized) -> TrailingStopLimitOrder: ... + def apply(self, event: object) -> None: ... class TrailingStopMarketOrder: def __init__( @@ -1335,6 +1344,7 @@ class TrailingStopMarketOrder: ): ... @classmethod def create(cls, init: OrderInitialized) -> TrailingStopMarketOrder: ... + def apply(self, event: object) -> None: ... Order: TypeAlias = Union[ LimitOrder, From 2526448a5d9b2783b78d6468de31a81b7af992ef Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 12 May 2024 09:14:20 +1000 Subject: [PATCH 158/193] Refine order event object conversions --- .../model/src/python/events/order/accepted.rs | 5 +- .../python/events/order/cancel_rejected.rs | 5 +- .../model/src/python/events/order/canceled.rs | 5 +- .../model/src/python/events/order/denied.rs | 5 +- .../model/src/python/events/order/emulated.rs | 5 +- .../model/src/python/events/order/expired.rs | 5 +- .../model/src/python/events/order/filled.rs | 5 +- .../src/python/events/order/initialized.rs | 5 + .../model/src/python/events/order/mod.rs | 151 ++++++++---------- .../python/events/order/modify_rejected.rs | 5 +- .../src/python/events/order/pending_cancel.rs | 5 +- .../src/python/events/order/pending_update.rs | 5 +- .../model/src/python/events/order/rejected.rs | 5 +- .../model/src/python/events/order/released.rs | 5 +- .../src/python/events/order/submitted.rs | 5 +- .../src/python/events/order/triggered.rs | 5 +- .../model/src/python/events/order/updated.rs | 5 +- .../model/src/python/instruments/mod.rs | 18 +-- .../model/src/python/orders/limit.rs | 4 +- .../src/python/orders/limit_if_touched.rs | 6 +- .../model/src/python/orders/market.rs | 6 +- .../src/python/orders/market_if_touched.rs | 6 +- .../src/python/orders/market_to_limit.rs | 6 +- .../model/src/python/orders/stop_limit.rs | 8 +- .../model/src/python/orders/stop_market.rs | 6 +- .../src/python/orders/trailing_stop_limit.rs | 6 +- .../src/python/orders/trailing_stop_market.rs | 6 +- nautilus_trader/core/nautilus_pyo3.pyi | 25 +-- nautilus_trader/model/events/order.pyx | 2 +- tests/unit_tests/model/test_position_pyo3.py | 1 + 30 files changed, 147 insertions(+), 184 deletions(-) diff --git a/nautilus_core/model/src/python/events/order/accepted.rs b/nautilus_core/model/src/python/events/order/accepted.rs index 809f9a32b07a..e750cf98fa6f 100644 --- a/nautilus_core/model/src/python/events/order/accepted.rs +++ b/nautilus_core/model/src/python/events/order/accepted.rs @@ -74,9 +74,7 @@ impl OrderAccepted { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderAccepted) } @@ -89,6 +87,7 @@ impl OrderAccepted { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderAccepted)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/cancel_rejected.rs b/nautilus_core/model/src/python/events/order/cancel_rejected.rs index 454253900089..652cda8635f8 100644 --- a/nautilus_core/model/src/python/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/python/events/order/cancel_rejected.rs @@ -80,9 +80,7 @@ impl OrderCancelRejected { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderCancelRejected) } @@ -95,6 +93,7 @@ impl OrderCancelRejected { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderCancelRejected)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/canceled.rs b/nautilus_core/model/src/python/events/order/canceled.rs index b244378b664d..fe80c4aec105 100644 --- a/nautilus_core/model/src/python/events/order/canceled.rs +++ b/nautilus_core/model/src/python/events/order/canceled.rs @@ -74,9 +74,7 @@ impl OrderCanceled { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderCanceled) } @@ -89,6 +87,7 @@ impl OrderCanceled { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderCanceled)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/denied.rs b/nautilus_core/model/src/python/events/order/denied.rs index 52c51b458bfd..a02be3bb4c73 100644 --- a/nautilus_core/model/src/python/events/order/denied.rs +++ b/nautilus_core/model/src/python/events/order/denied.rs @@ -74,9 +74,7 @@ impl OrderDenied { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderDenied) } @@ -89,6 +87,7 @@ impl OrderDenied { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderDenied)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/emulated.rs b/nautilus_core/model/src/python/events/order/emulated.rs index e6af91fdc452..3129dfd1c052 100644 --- a/nautilus_core/model/src/python/events/order/emulated.rs +++ b/nautilus_core/model/src/python/events/order/emulated.rs @@ -68,9 +68,7 @@ impl OrderEmulated { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderEmulated) } @@ -83,6 +81,7 @@ impl OrderEmulated { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderEmulated)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/expired.rs b/nautilus_core/model/src/python/events/order/expired.rs index b95040491f3d..225def43f502 100644 --- a/nautilus_core/model/src/python/events/order/expired.rs +++ b/nautilus_core/model/src/python/events/order/expired.rs @@ -74,9 +74,7 @@ impl OrderExpired { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderExpired) } @@ -89,6 +87,7 @@ impl OrderExpired { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderExpired)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/filled.rs b/nautilus_core/model/src/python/events/order/filled.rs index 90321b948e91..c4d5fb288a1f 100644 --- a/nautilus_core/model/src/python/events/order/filled.rs +++ b/nautilus_core/model/src/python/events/order/filled.rs @@ -95,9 +95,7 @@ impl OrderFilled { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderFilled) } @@ -236,6 +234,7 @@ impl OrderFilled { #[pyo3(name = "to_dict")] pub fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderFilled)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/initialized.rs b/nautilus_core/model/src/python/events/order/initialized.rs index 925a8cc88caf..2f5db55042c5 100644 --- a/nautilus_core/model/src/python/events/order/initialized.rs +++ b/nautilus_core/model/src/python/events/order/initialized.rs @@ -144,9 +144,14 @@ impl OrderInitialized { from_dict_pyo3(py, values) } + fn type_str(&self) -> &str { + stringify!(OrderInitiliazed) + } + #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderInitiliazed)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/mod.rs b/nautilus_core/model/src/python/events/order/mod.rs index cab8ac7d746a..b304d9d7c4ea 100644 --- a/nautilus_core/model/src/python/events/order/mod.rs +++ b/nautilus_core/model/src/python/events/order/mod.rs @@ -25,10 +25,24 @@ use crate::events::order::{ triggered::OrderTriggered, updated::OrderUpdated, }; -pub fn convert_order_event_to_pyobject( - py: Python, - order_event: OrderEventAny, -) -> PyResult { +pub mod accepted; +pub mod cancel_rejected; +pub mod canceled; +pub mod denied; +pub mod emulated; +pub mod expired; +pub mod filled; +pub mod initialized; +pub mod modify_rejected; +pub mod pending_cancel; +pub mod pending_update; +pub mod rejected; +pub mod released; +pub mod submitted; +pub mod triggered; +pub mod updated; + +pub fn order_event_to_pyobject(py: Python, order_event: OrderEventAny) -> PyResult { match order_event { OrderEventAny::Initialized(event) => Ok(event.into_py(py)), OrderEventAny::Denied(event) => Ok(event.into_py(py)), @@ -50,81 +64,58 @@ pub fn convert_order_event_to_pyobject( } } -pub fn convert_pyobject_to_order_event( - py: Python, - order_event: PyObject, -) -> PyResult { - let order_event_type = order_event - .getattr(py, "order_event_type")? - .extract::(py)?; - if order_event_type == "OrderAccepted" { - let order_accepted = order_event.extract::(py)?; - Ok(OrderEventAny::Accepted(order_accepted)) - } else if order_event_type == "OrderCanceled" { - let order_canceled = order_event.extract::(py)?; - Ok(OrderEventAny::Canceled(order_canceled)) - } else if order_event_type == "OrderCancelRejected" { - let order_cancel_rejected = order_event.extract::(py)?; - Ok(OrderEventAny::CancelRejected(order_cancel_rejected)) - } else if order_event_type == "OrderDenied" { - let order_denied = order_event.extract::(py)?; - Ok(OrderEventAny::Denied(order_denied)) - } else if order_event_type == "OrderEmulated" { - let order_emulated = order_event.extract::(py)?; - Ok(OrderEventAny::Emulated(order_emulated)) - } else if order_event_type == "OrderExpired" { - let order_expired = order_event.extract::(py)?; - Ok(OrderEventAny::Expired(order_expired)) - } else if order_event_type == "OrderFilled" { - let order_filled = order_event.extract::(py)?; - Ok(OrderEventAny::Filled(order_filled)) - } else if order_event_type == "OrderInitialized" { - let order_initialized = order_event.extract::(py)?; - Ok(OrderEventAny::Initialized(order_initialized)) - } else if order_event_type == "OrderModifyRejected" { - let order_modify_rejected = order_event.extract::(py)?; - Ok(OrderEventAny::ModifyRejected(order_modify_rejected)) - } else if order_event_type == "OrderPendingCancel" { - let order_pending_cancel = order_event.extract::(py)?; - Ok(OrderEventAny::PendingCancel(order_pending_cancel)) - } else if order_event_type == "OrderPendingUpdate" { - let order_pending_update = order_event.extract::(py)?; - Ok(OrderEventAny::PendingUpdate(order_pending_update)) - } else if order_event_type == "OrderRejected" { - let order_rejected = order_event.extract::(py)?; - Ok(OrderEventAny::Rejected(order_rejected)) - } else if order_event_type == "OrderReleased" { - let order_released = order_event.extract::(py)?; - Ok(OrderEventAny::Released(order_released)) - } else if order_event_type == "OrderSubmitted" { - let order_submitted = order_event.extract::(py)?; - Ok(OrderEventAny::Submitted(order_submitted)) - } else if order_event_type == "OrderTriggered" { - let order_triggered = order_event.extract::(py)?; - Ok(OrderEventAny::Triggered(order_triggered)) - } else if order_event_type == "OrderUpdated" { - let order_updated = order_event.extract::(py)?; - Ok(OrderEventAny::Updated(order_updated)) - } else { - Err(to_pyvalue_err( - "Error in conversion from pyobject to order event", - )) +pub fn pyobject_to_order_event(py: Python, order_event: PyObject) -> PyResult { + match order_event.getattr(py, "type_str")?.extract::<&str>(py)? { + stringify!(OrderAccepted) => Ok(OrderEventAny::Accepted( + order_event.extract::(py)?, + )), + stringify!(OrderCancelRejected) => Ok(OrderEventAny::CancelRejected( + order_event.extract::(py)?, + )), + stringify!(OrderCanceled) => Ok(OrderEventAny::Canceled( + order_event.extract::(py)?, + )), + stringify!(OrderDenied) => Ok(OrderEventAny::Denied( + order_event.extract::(py)?, + )), + stringify!(OrderEmulated) => Ok(OrderEventAny::Emulated( + order_event.extract::(py)?, + )), + stringify!(OrderExpired) => Ok(OrderEventAny::Expired( + order_event.extract::(py)?, + )), + stringify!(OrderFilled) => Ok(OrderEventAny::Filled( + order_event.extract::(py)?, + )), + stringify!(OrderInitialized) => Ok(OrderEventAny::Initialized( + order_event.extract::(py)?, + )), + stringify!(OrderModifyRejected) => Ok(OrderEventAny::ModifyRejected( + order_event.extract::(py)?, + )), + stringify!(OrderPendingCancel) => Ok(OrderEventAny::PendingCancel( + order_event.extract::(py)?, + )), + stringify!(OrderPendingUpdate) => Ok(OrderEventAny::PendingUpdate( + order_event.extract::(py)?, + )), + stringify!(OrderRejected) => Ok(OrderEventAny::Rejected( + order_event.extract::(py)?, + )), + stringify!(OrderReleased) => Ok(OrderEventAny::Released( + order_event.extract::(py)?, + )), + stringify!(OrderSubmitted) => Ok(OrderEventAny::Submitted( + order_event.extract::(py)?, + )), + stringify!(OrderTriggered) => Ok(OrderEventAny::Triggered( + order_event.extract::(py)?, + )), + stringify!(OrderUpdated) => Ok(OrderEventAny::Updated( + order_event.extract::(py)?, + )), + _ => Err(to_pyvalue_err( + "Error in conversion from `PyObject` to `OrderEventAny`", + )), } } - -pub mod accepted; -pub mod cancel_rejected; -pub mod canceled; -pub mod denied; -pub mod emulated; -pub mod expired; -pub mod filled; -pub mod initialized; -pub mod modify_rejected; -pub mod pending_cancel; -pub mod pending_update; -pub mod rejected; -pub mod released; -pub mod submitted; -pub mod triggered; -pub mod updated; diff --git a/nautilus_core/model/src/python/events/order/modify_rejected.rs b/nautilus_core/model/src/python/events/order/modify_rejected.rs index 1ca1d5bb9540..1089c6665720 100644 --- a/nautilus_core/model/src/python/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/python/events/order/modify_rejected.rs @@ -80,9 +80,7 @@ impl OrderModifyRejected { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderModifyRejected) } @@ -95,6 +93,7 @@ impl OrderModifyRejected { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderModifyRejected)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/pending_cancel.rs b/nautilus_core/model/src/python/events/order/pending_cancel.rs index 62da2f28de8f..a125b04c052e 100644 --- a/nautilus_core/model/src/python/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/python/events/order/pending_cancel.rs @@ -74,9 +74,7 @@ impl OrderPendingCancel { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderPendingCancel) } @@ -89,6 +87,7 @@ impl OrderPendingCancel { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderPendingCancel)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/pending_update.rs b/nautilus_core/model/src/python/events/order/pending_update.rs index 80dd7229e585..ba8aee7caa12 100644 --- a/nautilus_core/model/src/python/events/order/pending_update.rs +++ b/nautilus_core/model/src/python/events/order/pending_update.rs @@ -74,9 +74,7 @@ impl OrderPendingUpdate { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderPendingUpdate) } @@ -89,6 +87,7 @@ impl OrderPendingUpdate { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderPendingUpdate)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/rejected.rs b/nautilus_core/model/src/python/events/order/rejected.rs index 1777c53f5e81..a35454c57c99 100644 --- a/nautilus_core/model/src/python/events/order/rejected.rs +++ b/nautilus_core/model/src/python/events/order/rejected.rs @@ -77,9 +77,7 @@ impl OrderRejected { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderRejected) } @@ -92,6 +90,7 @@ impl OrderRejected { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderRejected)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/released.rs b/nautilus_core/model/src/python/events/order/released.rs index d3c67a8cf223..a118b63853f8 100644 --- a/nautilus_core/model/src/python/events/order/released.rs +++ b/nautilus_core/model/src/python/events/order/released.rs @@ -71,9 +71,7 @@ impl OrderReleased { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderReleased) } @@ -86,6 +84,7 @@ impl OrderReleased { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderReleased)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/submitted.rs b/nautilus_core/model/src/python/events/order/submitted.rs index 9aedaf9a0f76..8ee722a5d25b 100644 --- a/nautilus_core/model/src/python/events/order/submitted.rs +++ b/nautilus_core/model/src/python/events/order/submitted.rs @@ -70,9 +70,7 @@ impl OrderSubmitted { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderSubmitted) } @@ -85,6 +83,7 @@ impl OrderSubmitted { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderSubmitted)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/triggered.rs b/nautilus_core/model/src/python/events/order/triggered.rs index 8392475ce4ce..82648358c8f4 100644 --- a/nautilus_core/model/src/python/events/order/triggered.rs +++ b/nautilus_core/model/src/python/events/order/triggered.rs @@ -74,9 +74,7 @@ impl OrderTriggered { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderTriggered) } @@ -89,6 +87,7 @@ impl OrderTriggered { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderTriggered)); 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())?; diff --git a/nautilus_core/model/src/python/events/order/updated.rs b/nautilus_core/model/src/python/events/order/updated.rs index d1ad97d1e04a..365ce043d450 100644 --- a/nautilus_core/model/src/python/events/order/updated.rs +++ b/nautilus_core/model/src/python/events/order/updated.rs @@ -81,9 +81,7 @@ impl OrderUpdated { self.to_string() } - #[getter] - #[pyo3(name = "order_event_type")] - fn py_order_event_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OrderUpdated) } @@ -96,6 +94,7 @@ impl OrderUpdated { #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); + dict.set_item("type", stringify!(OrderUpdated)); 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())?; diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index 710abaa05561..b243728ded34 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -24,6 +24,15 @@ use crate::instruments::{ futures_spread::FuturesSpread, options_contract::OptionsContract, }; +pub mod crypto_future; +pub mod crypto_perpetual; +pub mod currency_pair; +pub mod equity; +pub mod futures_contract; +pub mod futures_spread; +pub mod options_contract; +pub mod options_spread; + pub fn convert_instrument_any_to_pyobject( py: Python, instrument: InstrumentAny, @@ -78,12 +87,3 @@ pub fn convert_pyobject_to_instrument_any( )) } } - -pub mod crypto_future; -pub mod crypto_perpetual; -pub mod currency_pair; -pub mod equity; -pub mod futures_contract; -pub mod futures_spread; -pub mod options_contract; -pub mod options_spread; diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index 01c0e7c43b6e..6599270a4dd8 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -38,7 +38,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order, OrderCore}, limit::LimitOrder, }, - python::{common::commissions_from_hashmap, events::order::convert_pyobject_to_order_event}, + python::{common::commissions_from_hashmap, events::order::pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -676,7 +676,7 @@ impl LimitOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/limit_if_touched.rs b/nautilus_core/model/src/python/orders/limit_if_touched.rs index 67db81e11dbd..3162faee8c9e 100644 --- a/nautilus_core/model/src/python/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/python/orders/limit_if_touched.rs @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, limit_if_touched::LimitIfTouchedOrder, }, - python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + python::events::order::{order_event_to_pyobject, pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -112,7 +112,7 @@ impl LimitIfTouchedOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -124,7 +124,7 @@ impl LimitIfTouchedOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index ec6c9593417f..7848f4bc148c 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -42,7 +42,7 @@ use crate::{ }, python::{ common::commissions_from_hashmap, - events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + events::order::{order_event_to_pyobject, pyobject_to_order_event}, }, types::{currency::Currency, money::Money, quantity::Quantity}, }; @@ -294,7 +294,7 @@ impl MarketOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -549,7 +549,7 @@ impl MarketOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/market_if_touched.rs b/nautilus_core/model/src/python/orders/market_if_touched.rs index 7a4d41bb6edf..79521aa1d1d2 100644 --- a/nautilus_core/model/src/python/orders/market_if_touched.rs +++ b/nautilus_core/model/src/python/orders/market_if_touched.rs @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, market_if_touched::MarketIfTouchedOrder, }, - python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + python::events::order::{order_event_to_pyobject, pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -108,7 +108,7 @@ impl MarketIfTouchedOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -120,7 +120,7 @@ impl MarketIfTouchedOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/market_to_limit.rs b/nautilus_core/model/src/python/orders/market_to_limit.rs index 161e722d6ec6..413826e93b50 100644 --- a/nautilus_core/model/src/python/orders/market_to_limit.rs +++ b/nautilus_core/model/src/python/orders/market_to_limit.rs @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, market_to_limit::MarketToLimitOrder, }, - python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + python::events::order::{order_event_to_pyobject, pyobject_to_order_event}, types::quantity::Quantity, }; @@ -102,7 +102,7 @@ impl MarketToLimitOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -114,7 +114,7 @@ impl MarketToLimitOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/stop_limit.rs b/nautilus_core/model/src/python/orders/stop_limit.rs index 2efcca4e7c58..a4508ec74b35 100644 --- a/nautilus_core/model/src/python/orders/stop_limit.rs +++ b/nautilus_core/model/src/python/orders/stop_limit.rs @@ -37,7 +37,7 @@ use crate::{ }, python::{ common::commissions_from_hashmap, - events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + events::order::{order_event_to_pyobject, pyobject_to_order_event}, }, types::{price::Price, quantity::Quantity}, }; @@ -223,7 +223,7 @@ impl StopLimitOrder { #[pyo3(name = "init_event")] fn py_init_event(&self, py: Python<'_>) -> PyResult { match self.init_event() { - Some(event) => convert_order_event_to_pyobject(py, event), + Some(event) => order_event_to_pyobject(py, event), None => Ok(py.None()), } } @@ -359,7 +359,7 @@ impl StopLimitOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -676,7 +676,7 @@ impl StopLimitOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/stop_market.rs b/nautilus_core/model/src/python/orders/stop_market.rs index e583b89c74ed..d4e0c26ced5b 100644 --- a/nautilus_core/model/src/python/orders/stop_market.rs +++ b/nautilus_core/model/src/python/orders/stop_market.rs @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, stop_market::StopMarketOrder, }, - python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + python::events::order::{order_event_to_pyobject, pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -108,7 +108,7 @@ impl StopMarketOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -120,7 +120,7 @@ impl StopMarketOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs index 726a3c814130..ecb4d3a6a846 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_limit.rs @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, trailing_stop_limit::TrailingStopLimitOrder, }, - python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + python::events::order::{order_event_to_pyobject, pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -118,7 +118,7 @@ impl TrailingStopLimitOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -130,7 +130,7 @@ impl TrailingStopLimitOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_core/model/src/python/orders/trailing_stop_market.rs b/nautilus_core/model/src/python/orders/trailing_stop_market.rs index 2501748855a0..5dd366cf61a5 100644 --- a/nautilus_core/model/src/python/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/python/orders/trailing_stop_market.rs @@ -31,7 +31,7 @@ use crate::{ base::{str_hashmap_to_ustr, Order}, trailing_stop_market::TrailingStopMarketOrder, }, - python::events::order::{convert_order_event_to_pyobject, convert_pyobject_to_order_event}, + python::events::order::{order_event_to_pyobject, pyobject_to_order_event}, types::{price::Price, quantity::Quantity}, }; @@ -112,7 +112,7 @@ impl TrailingStopMarketOrder { fn py_events(&self, py: Python<'_>) -> PyResult> { self.events() .into_iter() - .map(|order_event| convert_order_event_to_pyobject(py, order_event.clone())) + .map(|event| order_event_to_pyobject(py, event.clone())) .collect() } @@ -124,7 +124,7 @@ impl TrailingStopMarketOrder { #[pyo3(name = "apply")] fn py_apply(&mut self, event: PyObject, py: Python<'_>) -> PyResult<()> { - let event_any = convert_pyobject_to_order_event(py, event).unwrap(); + let event_any = pyobject_to_order_event(py, event).unwrap(); self.apply(event_any).map(|_| ()).map_err(to_pyruntime_err) } } diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 467d86c9af62..0b46e5aa31e8 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1953,6 +1953,8 @@ class OrderFilled: @property def order_side(self) -> OrderSide: ... @property + def order_type(self) -> OrderType: ... + @property def client_order_id(self) -> ClientOrderId: ... class OrderInitialized: @@ -1996,7 +1998,7 @@ class OrderInitialized: def from_dict(cls, values: dict[str, str]) -> OrderInitialized: ... def to_dict(self) -> dict[str, str]: ... @property - def order_type(self) -> str: ... + def order_type(self) -> OrderType: ... class OrderSubmitted: def __init__( @@ -2030,8 +2032,6 @@ class OrderEmulated: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderEmulated: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderReleased: def __init__( @@ -2048,8 +2048,6 @@ class OrderReleased: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderReleased: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderUpdated: def __init__( @@ -2071,8 +2069,6 @@ class OrderUpdated: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderUpdated: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderPendingUpdate: def __init__( @@ -2091,8 +2087,6 @@ class OrderPendingUpdate: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderPendingUpdate: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderPendingCancel: def __init__( @@ -2111,8 +2105,6 @@ class OrderPendingCancel: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderPendingCancel: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderModifyRejected: def __init__( @@ -2132,8 +2124,6 @@ class OrderModifyRejected: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderModifyRejected: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderAccepted: def __init__( @@ -2152,9 +2142,6 @@ class OrderAccepted: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderAccepted: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... - class OrderCancelRejected: def __init__( @@ -2174,8 +2161,6 @@ class OrderCancelRejected: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderCancelRejected: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderCanceled: def __init__( @@ -2195,8 +2180,6 @@ class OrderCanceled: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderCanceled: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class OrderExpired: def __init__( @@ -2215,8 +2198,6 @@ class OrderExpired: @classmethod def from_dict(cls, values: dict[str, str]) -> OrderExpired: ... def to_dict(self) -> dict[str, str]: ... - @property - def order_type(self) -> str: ... class Level: @property diff --git a/nautilus_trader/model/events/order.pyx b/nautilus_trader/model/events/order.pyx index 59967645d17b..35771ea6f5c0 100644 --- a/nautilus_trader/model/events/order.pyx +++ b/nautilus_trader/model/events/order.pyx @@ -4496,7 +4496,7 @@ cdef class OrderFilled(OrderEvent): The position ID associated with the order fill (assigned by the venue). order_side : OrderSide {``BUY``, ``SELL``} The execution order side. - order_side : OrderType + order_type : OrderType The execution order type. last_qty : Quantity The fill quantity for this execution. diff --git a/tests/unit_tests/model/test_position_pyo3.py b/tests/unit_tests/model/test_position_pyo3.py index 5cc656da1c5b..b7423834e9cf 100644 --- a/tests/unit_tests/model/test_position_pyo3.py +++ b/tests/unit_tests/model/test_position_pyo3.py @@ -85,6 +85,7 @@ def test_position_to_from_dict(): "entry": "BUY", "events": [ { + "type": "OrderFilled", "account_id": "SIM-000", "client_order_id": "O-20210410-022422-001-001-1", "commission": "2.00 USD", From 6e8eab56780183624b80b5956404b06dfdb6fce8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 12 May 2024 09:44:28 +1000 Subject: [PATCH 159/193] Refine instrument object conversions --- nautilus_core/accounting/src/account/base.rs | 10 +-- nautilus_core/accounting/src/python/cash.rs | 8 +-- nautilus_core/accounting/src/python/margin.rs | 6 +- .../src/databento/python/historical.rs | 4 +- .../adapters/src/databento/python/live.rs | 6 +- .../adapters/src/databento/python/loader.rs | 4 +- .../src/python/sql/cache_database.rs | 8 +-- .../src/python/instruments/crypto_future.rs | 3 +- .../python/instruments/crypto_perpetual.rs | 3 +- .../src/python/instruments/currency_pair.rs | 3 +- .../model/src/python/instruments/equity.rs | 3 +- .../python/instruments/futures_contract.rs | 3 +- .../src/python/instruments/futures_spread.rs | 3 +- .../model/src/python/instruments/mod.rs | 68 ++++++++----------- .../python/instruments/options_contract.rs | 3 +- .../src/python/instruments/options_spread.rs | 3 +- nautilus_core/model/src/python/position.rs | 4 +- 17 files changed, 62 insertions(+), 80 deletions(-) diff --git a/nautilus_core/accounting/src/account/base.rs b/nautilus_core/accounting/src/account/base.rs index 5c80cca7959d..bbb74bf3996d 100644 --- a/nautilus_core/accounting/src/account/base.rs +++ b/nautilus_core/accounting/src/account/base.rs @@ -154,7 +154,7 @@ impl BaseAccount { .calculate_notional_value(quantity, price, use_quote_for_inverse) .as_f64(), OrderSide::Sell => quantity.as_f64(), - _ => panic!("Invalid order side in `base_calculate_balance_locked`"), + _ => panic!("Invalid `OrderSide` in `base_calculate_balance_locked`"), }; // Add expected commission let taker_fee = instrument.taker_fee().to_f64().unwrap(); @@ -168,7 +168,7 @@ impl BaseAccount { } else if side == OrderSide::Sell { Ok(Money::new(locked, base_currency).unwrap()) } else { - panic!("Invalid order side in `base_calculate_balance_locked`") + panic!("Invalid `OrderSide` in `base_calculate_balance_locked`") } } @@ -209,7 +209,7 @@ impl BaseAccount { Money::new(fill_qty * fill_px, quote_currency).unwrap(), ); } else { - panic!("Invalid order side in base_calculate_pnls") + panic!("Invalid `OrderSide` in base_calculate_pnls") } Ok(pnls.into_values().collect()) } @@ -224,7 +224,7 @@ impl BaseAccount { ) -> anyhow::Result { assert!( liquidity_side != LiquiditySide::NoLiquiditySide, - "Invalid liquidity side" + "Invalid `LiquiditySide`" ); let notional = instrument .calculate_notional_value(last_qty, last_px, use_quote_for_inverse) @@ -234,7 +234,7 @@ impl BaseAccount { } else if liquidity_side == LiquiditySide::Taker { notional * instrument.taker_fee().to_f64().unwrap() } else { - panic!("Invalid liquid side {liquidity_side}") + panic!("Invalid `LiquiditySide` {liquidity_side}") }; if instrument.is_inverse() && !use_quote_for_inverse.unwrap_or(false) { Ok(Money::new(commission, instrument.base_currency().unwrap()).unwrap()) diff --git a/nautilus_core/accounting/src/python/cash.rs b/nautilus_core/accounting/src/python/cash.rs index bb296d011e19..3f415f2e5156 100644 --- a/nautilus_core/accounting/src/python/cash.rs +++ b/nautilus_core/accounting/src/python/cash.rs @@ -22,7 +22,7 @@ use nautilus_model::{ events::{account::state::AccountState, order::filled::OrderFilled}, identifiers::account_id::AccountId, position::Position, - python::instruments::convert_pyobject_to_instrument_any, + python::instruments::pyobject_to_instrument_any, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; @@ -142,7 +142,7 @@ impl CashAccount { use_quote_for_inverse: Option, py: Python, ) -> PyResult { - let instrument = convert_pyobject_to_instrument_any(py, instrument)?; + let instrument = pyobject_to_instrument_any(py, instrument)?; self.calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse) .map_err(to_pyvalue_err) } @@ -160,7 +160,7 @@ impl CashAccount { if liquidity_side == LiquiditySide::NoLiquiditySide { return Err(to_pyvalue_err("Invalid liquidity side")); } - let instrument = convert_pyobject_to_instrument_any(py, instrument)?; + let instrument = pyobject_to_instrument_any(py, instrument)?; self.calculate_commission( instrument, last_qty, @@ -179,7 +179,7 @@ impl CashAccount { position: Option, py: Python, ) -> PyResult> { - let instrument = convert_pyobject_to_instrument_any(py, instrument)?; + let instrument = pyobject_to_instrument_any(py, instrument)?; self.calculate_pnls(instrument, fill, position) .map_err(to_pyvalue_err) } diff --git a/nautilus_core/accounting/src/python/margin.rs b/nautilus_core/accounting/src/python/margin.rs index 66dde3cd55dc..5471dfb43fb6 100644 --- a/nautilus_core/accounting/src/python/margin.rs +++ b/nautilus_core/accounting/src/python/margin.rs @@ -18,7 +18,7 @@ use nautilus_model::{ events::account::state::AccountState, identifiers::{account_id::AccountId, instrument_id::InstrumentId}, instruments::any::InstrumentAny, - python::instruments::convert_pyobject_to_instrument_any, + python::instruments::pyobject_to_instrument_any, types::{money::Money, price::Price, quantity::Quantity}, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; @@ -155,7 +155,7 @@ impl MarginAccount { use_quote_for_inverse: Option, py: Python, ) -> PyResult { - let instrument_type = convert_pyobject_to_instrument_any(py, instrument)?; + let instrument_type = pyobject_to_instrument_any(py, instrument)?; match instrument_type { InstrumentAny::CryptoFuture(inst) => { Ok(self.calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)) @@ -188,7 +188,7 @@ impl MarginAccount { use_quote_for_inverse: Option, py: Python, ) -> PyResult { - let instrument_type = convert_pyobject_to_instrument_any(py, instrument)?; + let instrument_type = pyobject_to_instrument_any(py, instrument)?; match instrument_type { InstrumentAny::CryptoFuture(inst) => { Ok(self.calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)) diff --git a/nautilus_core/adapters/src/databento/python/historical.rs b/nautilus_core/adapters/src/databento/python/historical.rs index 4388c5833e73..c0a960944f70 100644 --- a/nautilus_core/adapters/src/databento/python/historical.rs +++ b/nautilus_core/adapters/src/databento/python/historical.rs @@ -28,7 +28,7 @@ use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick, Data}, enums::BarAggregation, identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, - python::instruments::convert_instrument_any_to_pyobject, + python::instruments::instrument_any_to_pyobject, types::currency::Currency, }; use pyo3::{ @@ -169,7 +169,7 @@ impl DatabentoHistoricalClient { Python::with_gil(|py| { let py_results: PyResult> = instruments .into_iter() - .map(|result| convert_instrument_any_to_pyobject(py, result)) + .map(|result| instrument_any_to_pyobject(py, result)) .collect(); py_results.map(|objs| PyList::new(py, &objs).to_object(py)) diff --git a/nautilus_core/adapters/src/databento/python/live.rs b/nautilus_core/adapters/src/databento/python/live.rs index 0d440a36d3b6..4e14c26069e1 100644 --- a/nautilus_core/adapters/src/databento/python/live.rs +++ b/nautilus_core/adapters/src/databento/python/live.rs @@ -20,7 +20,7 @@ use indexmap::IndexMap; use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err}; use nautilus_model::{ identifiers::venue::Venue, - python::{data::data_to_pycapsule, instruments::convert_instrument_any_to_pyobject}, + python::{data::data_to_pycapsule, instruments::instrument_any_to_pyobject}, }; use pyo3::prelude::*; use time::OffsetDateTime; @@ -73,8 +73,8 @@ impl DatabentoLiveClient { call_python(py, &callback, py_obj) }), LiveMessage::Instrument(data) => Python::with_gil(|py| { - let py_obj = convert_instrument_any_to_pyobject(py, data) - .expect("Error creating instrument"); + let py_obj = + instrument_any_to_pyobject(py, data).expect("Error creating instrument"); call_python(py, &callback, py_obj) }), LiveMessage::Imbalance(data) => Python::with_gil(|py| { diff --git a/nautilus_core/adapters/src/databento/python/loader.rs b/nautilus_core/adapters/src/databento/python/loader.rs index 02e29b513343..71f2a2c0b777 100644 --- a/nautilus_core/adapters/src/databento/python/loader.rs +++ b/nautilus_core/adapters/src/databento/python/loader.rs @@ -23,7 +23,7 @@ use nautilus_model::{ trade::TradeTick, Data, }, identifiers::{instrument_id::InstrumentId, venue::Venue}, - python::instruments::convert_instrument_any_to_pyobject, + python::instruments::instrument_any_to_pyobject, }; use pyo3::{ prelude::*, @@ -88,7 +88,7 @@ impl DatabentoDataLoader { for result in iter { match result { Ok(instrument) => { - let py_object = convert_instrument_any_to_pyobject(py, instrument)?; + let py_object = instrument_any_to_pyobject(py, instrument)?; data.push(py_object); } Err(e) => { diff --git a/nautilus_core/infrastructure/src/python/sql/cache_database.rs b/nautilus_core/infrastructure/src/python/sql/cache_database.rs index 9a1e483ae638..f529e25e4c0b 100644 --- a/nautilus_core/infrastructure/src/python/sql/cache_database.rs +++ b/nautilus_core/infrastructure/src/python/sql/cache_database.rs @@ -20,7 +20,7 @@ use nautilus_core::python::to_pyruntime_err; use nautilus_model::{ identifiers::{client_order_id::ClientOrderId, instrument_id::InstrumentId}, python::{ - instruments::{convert_instrument_any_to_pyobject, convert_pyobject_to_instrument_any}, + instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any}, orders::{convert_order_any_to_pyobject, convert_pyobject_to_order_any}, }, types::currency::Currency, @@ -93,7 +93,7 @@ impl PostgresCacheDatabase { .unwrap(); match result { Some(instrument) => { - let py_object = convert_instrument_any_to_pyobject(py, instrument)?; + let py_object = instrument_any_to_pyobject(py, instrument)?; Ok(Some(py_object)) } None => Ok(None), @@ -107,7 +107,7 @@ impl PostgresCacheDatabase { let result = DatabaseQueries::load_instruments(&slf.pool).await.unwrap(); let mut instruments = Vec::new(); for instrument in result { - let py_object = convert_instrument_any_to_pyobject(py, instrument)?; + let py_object = instrument_any_to_pyobject(py, instrument)?; instruments.push(py_object); } Ok(instruments) @@ -120,7 +120,7 @@ impl PostgresCacheDatabase { instrument: PyObject, py: Python<'_>, ) -> PyResult<()> { - let instrument_any = convert_pyobject_to_instrument_any(py, instrument)?; + let instrument_any = pyobject_to_instrument_any(py, instrument)?; let result = get_runtime().block_on(async { slf.add_instrument(instrument_any).await }); result.map_err(to_pyruntime_err) } diff --git a/nautilus_core/model/src/python/instruments/crypto_future.rs b/nautilus_core/model/src/python/instruments/crypto_future.rs index ec251cbd3bb1..b88ff30ea171 100644 --- a/nautilus_core/model/src/python/instruments/crypto_future.rs +++ b/nautilus_core/model/src/python/instruments/crypto_future.rs @@ -103,8 +103,7 @@ impl CryptoFuture { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(CryptoFuture) } diff --git a/nautilus_core/model/src/python/instruments/crypto_perpetual.rs b/nautilus_core/model/src/python/instruments/crypto_perpetual.rs index 0e00f511c1fd..9ba5fdcf692e 100644 --- a/nautilus_core/model/src/python/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/python/instruments/crypto_perpetual.rs @@ -99,8 +99,7 @@ impl CryptoPerpetual { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(CryptoPerpetual) } diff --git a/nautilus_core/model/src/python/instruments/currency_pair.rs b/nautilus_core/model/src/python/instruments/currency_pair.rs index 81a968450d69..20573b570fe4 100644 --- a/nautilus_core/model/src/python/instruments/currency_pair.rs +++ b/nautilus_core/model/src/python/instruments/currency_pair.rs @@ -95,8 +95,7 @@ impl CurrencyPair { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(CurrencyPair) } diff --git a/nautilus_core/model/src/python/instruments/equity.rs b/nautilus_core/model/src/python/instruments/equity.rs index ff1df8d4095d..af4bab02d396 100644 --- a/nautilus_core/model/src/python/instruments/equity.rs +++ b/nautilus_core/model/src/python/instruments/equity.rs @@ -88,8 +88,7 @@ impl Equity { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(Equity) } diff --git a/nautilus_core/model/src/python/instruments/futures_contract.rs b/nautilus_core/model/src/python/instruments/futures_contract.rs index a411b0cf0543..c5be30fcac0c 100644 --- a/nautilus_core/model/src/python/instruments/futures_contract.rs +++ b/nautilus_core/model/src/python/instruments/futures_contract.rs @@ -95,8 +95,7 @@ impl FuturesContract { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(FuturesContract) } diff --git a/nautilus_core/model/src/python/instruments/futures_spread.rs b/nautilus_core/model/src/python/instruments/futures_spread.rs index c306417f04ac..9b68e9de21f2 100644 --- a/nautilus_core/model/src/python/instruments/futures_spread.rs +++ b/nautilus_core/model/src/python/instruments/futures_spread.rs @@ -97,8 +97,7 @@ impl FuturesSpread { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(FuturesSpread) } diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index b243728ded34..a3dc1560ddff 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -22,6 +22,7 @@ use crate::instruments::{ any::InstrumentAny, crypto_future::CryptoFuture, crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair, equity::Equity, futures_contract::FuturesContract, futures_spread::FuturesSpread, options_contract::OptionsContract, + options_spread::OptionsSpread, }; pub mod crypto_future; @@ -33,10 +34,7 @@ pub mod futures_spread; pub mod options_contract; pub mod options_spread; -pub fn convert_instrument_any_to_pyobject( - py: Python, - instrument: InstrumentAny, -) -> PyResult { +pub fn instrument_any_to_pyobject(py: Python, instrument: InstrumentAny) -> PyResult { match instrument { InstrumentAny::CryptoFuture(inst) => Ok(inst.into_py(py)), InstrumentAny::CryptoPerpetual(inst) => Ok(inst.into_py(py)), @@ -50,40 +48,32 @@ pub fn convert_instrument_any_to_pyobject( } } -pub fn convert_pyobject_to_instrument_any( - py: Python, - instrument: PyObject, -) -> PyResult { - let instrument_type = instrument - .getattr(py, "instrument_type")? - .extract::(py)?; - if instrument_type == "CryptoFuture" { - let crypto_future = instrument.extract::(py)?; - Ok(InstrumentAny::CryptoFuture(crypto_future)) - } else if instrument_type == "CryptoPerpetual" { - let crypto_perpetual = instrument.extract::(py)?; - Ok(InstrumentAny::CryptoPerpetual(crypto_perpetual)) - } else if instrument_type == "CurrencyPair" { - let currency_pair = instrument.extract::(py)?; - Ok(InstrumentAny::CurrencyPair(currency_pair)) - } else if instrument_type == "Equity" { - let equity = instrument.extract::(py)?; - Ok(InstrumentAny::Equity(equity)) - } else if instrument_type == "FuturesContract" { - let futures_contract = instrument.extract::(py)?; - Ok(InstrumentAny::FuturesContract(futures_contract)) - } else if instrument_type == "FuturesSpread" { - let futures_spread = instrument.extract::(py)?; - Ok(InstrumentAny::FuturesSpread(futures_spread)) - } else if instrument_type == "OptionsContract" { - let options_contract = instrument.extract::(py)?; - Ok(InstrumentAny::OptionsContract(options_contract)) - } else if instrument_type == "OptionsSpread" { - let options_spread = instrument.extract::(py)?; - Ok(InstrumentAny::CryptoFuture(options_spread)) - } else { - Err(to_pyvalue_err( - "Error in conversion from pyobject to instrument type", - )) +pub fn pyobject_to_instrument_any(py: Python, instrument: PyObject) -> PyResult { + match instrument.getattr(py, "type_str")?.extract::<&str>(py)? { + stringify!(CryptoFuture) => Ok(InstrumentAny::CryptoFuture( + instrument.extract::(py)?, + )), + stringify!(CryptoPerpetual) => Ok(InstrumentAny::CryptoPerpetual( + instrument.extract::(py)?, + )), + stringify!(CurrencyPair) => Ok(InstrumentAny::CurrencyPair( + instrument.extract::(py)?, + )), + stringify!(Equity) => Ok(InstrumentAny::Equity(instrument.extract::(py)?)), + stringify!(FuturesContract) => Ok(InstrumentAny::FuturesContract( + instrument.extract::(py)?, + )), + stringify!(FuturesSpread) => Ok(InstrumentAny::FuturesSpread( + instrument.extract::(py)?, + )), + stringify!(OptionsContract) => Ok(InstrumentAny::OptionsContract( + instrument.extract::(py)?, + )), + stringify!(OptionsSpread) => Ok(InstrumentAny::OptionsSpread( + instrument.extract::(py)?, + )), + _ => Err(to_pyvalue_err( + "Error in conversion from `PyObject` to `InstrumentAny`", + )), } } diff --git a/nautilus_core/model/src/python/instruments/options_contract.rs b/nautilus_core/model/src/python/instruments/options_contract.rs index 4491bbcc5528..c4ee11b2a63d 100644 --- a/nautilus_core/model/src/python/instruments/options_contract.rs +++ b/nautilus_core/model/src/python/instruments/options_contract.rs @@ -99,8 +99,7 @@ impl OptionsContract { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OptionsContract) } diff --git a/nautilus_core/model/src/python/instruments/options_spread.rs b/nautilus_core/model/src/python/instruments/options_spread.rs index d99856b8a1e4..40699867ae9c 100644 --- a/nautilus_core/model/src/python/instruments/options_spread.rs +++ b/nautilus_core/model/src/python/instruments/options_spread.rs @@ -97,8 +97,7 @@ impl OptionsSpread { } #[getter] - #[pyo3(name = "instrument_type")] - fn py_instrument_type(&self) -> &str { + fn type_str(&self) -> &str { stringify!(OptionsSpread) } diff --git a/nautilus_core/model/src/python/position.rs b/nautilus_core/model/src/python/position.rs index 9d1635c3b187..86222428984a 100644 --- a/nautilus_core/model/src/python/position.rs +++ b/nautilus_core/model/src/python/position.rs @@ -32,7 +32,7 @@ use crate::{ }, instruments::any::InstrumentAny, position::Position, - python::instruments::convert_pyobject_to_instrument_any, + python::instruments::pyobject_to_instrument_any, types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; @@ -40,7 +40,7 @@ use crate::{ impl Position { #[new] fn py_new(py: Python, instrument: PyObject, fill: OrderFilled) -> PyResult { - let instrument_type = convert_pyobject_to_instrument_any(py, instrument)?; + let instrument_type = pyobject_to_instrument_any(py, instrument)?; match instrument_type { InstrumentAny::CryptoFuture(inst) => Ok(Self::new(inst, fill).unwrap()), InstrumentAny::CryptoPerpetual(inst) => Ok(Self::new(inst, fill).unwrap()), From 004db85d3e9c2e8aa6bd9e328c912f6d77fd377b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 12 May 2024 12:44:34 +1000 Subject: [PATCH 160/193] Update docs --- docs/api_reference/config.md | 98 ++++++++++++++++++- docs/api_reference/index.md | 1 - .../advanced/synthetic_instruments.md | 2 +- docs/concepts/index.md | 21 ++-- docs/concepts/logging.md | 4 +- docs/concepts/orders.md | 2 +- docs/getting_started/installation.md | 2 + docs/index.md | 8 +- docs/integrations/bybit.md | 2 +- docs/integrations/databento.md | 4 +- docs/rust.md | 2 +- 11 files changed, 119 insertions(+), 27 deletions(-) diff --git a/docs/api_reference/config.md b/docs/api_reference/config.md index e3fdef86909e..f33ee8fe74e1 100644 --- a/docs/api_reference/config.md +++ b/docs/api_reference/config.md @@ -1,5 +1,101 @@ # Config +## Backtest + +```{eval-rst} +.. automodule:: nautilus_trader.backtest.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Cache + +```{eval-rst} +.. automodule:: nautilus_trader.cache.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Common + +```{eval-rst} +.. automodule:: nautilus_trader.common.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Data + +```{eval-rst} +.. automodule:: nautilus_trader.data.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Execution + +```{eval-rst} +.. automodule:: nautilus_trader.execution.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Live + +```{eval-rst} +.. automodule:: nautilus_trader.live.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Persistence + +```{eval-rst} +.. automodule:: nautilus_trader.persistence.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Risk + +```{eval-rst} +.. automodule:: nautilus_trader.risk.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## System + +```{eval-rst} +.. automodule:: nautilus_trader.system.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Trading + ```{eval-rst} -.. automodule:: nautilus_trader.config +.. automodule:: nautilus_trader.trading.config + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource ``` diff --git a/docs/api_reference/index.md b/docs/api_reference/index.md index 8356e1699549..86e7a057ab98 100644 --- a/docs/api_reference/index.md +++ b/docs/api_reference/index.md @@ -57,4 +57,3 @@ The language out of the box is not without its drawbacks however, especially in implementing large performance-critical systems. Cython has addressed a lot of these issues, offering all the advantages of a statically typed language, embedded into Pythons rich ecosystem of software libraries and developer/user communities. - diff --git a/docs/concepts/advanced/synthetic_instruments.md b/docs/concepts/advanced/synthetic_instruments.md index 2cb13ddf6a66..496bbf9313b4 100644 --- a/docs/concepts/advanced/synthetic_instruments.md +++ b/docs/concepts/advanced/synthetic_instruments.md @@ -27,7 +27,7 @@ from the incoming component instrument prices. See the `evalexpr` documentation for a full description of available features, operators and precedence. -```{warning} +```{tip} Before defining a new synthetic instrument, ensure that all component instruments are already defined and exist in the cache. ``` diff --git a/docs/concepts/index.md b/docs/concepts/index.md index 35da059714a3..4ed16103fe45 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -23,21 +23,8 @@ Welcome to NautilusTrader! - Explore the foundational concepts of NautilusTrader through the following guides. -```{note} -The terms "NautilusTrader", "Nautilus" and "platform" are used interchageably throughout the documentation. -``` - -```{warning} -It's important to note that the [API Reference](../api_reference/index.md) documentation should be -considered the source of truth for the platform. If there are any discrepancies between concepts described here -and the API Reference, then the API Reference should be considered the correct information. We are -working to ensure that concepts stay up-to-date with the API Reference and will be introducing -doc tests in the near future to help with this. -``` - ## [Overview](overview.md) The **Overview** guide covers the main use cases for the platform. @@ -84,3 +71,11 @@ point-to-point, publish/subscribe and request/response. ## [Advanced](advanced/index.md) Here you will find more detailed documentation and examples covering the more advanced features and functionality of the platform. + +```{note} +The [API Reference](../api_reference/index.md) documentation should be considered the source of truth +for the platform. If there are any discrepancies between concepts described here and the API Reference, +then the API Reference should be considered the correct information. We are working to ensure that +concepts stay up-to-date with the API Reference and will be introducing doc tests in the near future +to help with this. +``` diff --git a/docs/concepts/logging.md b/docs/concepts/logging.md index 1df1f2a22161..5f1ad5ad8f48 100644 --- a/docs/concepts/logging.md +++ b/docs/concepts/logging.md @@ -25,7 +25,7 @@ Log level (`LogLevel`) values include (and generally match Rusts `tracing` level - `ERROR` ```{note} -See the `LoggingConfig` [API Reference](../api_reference/config.md#LoggingConfig) for further details. +See the `LoggingConfig` [API Reference](../api_reference/config.md) for further details. ``` Logging can be configured in the following ways: @@ -114,7 +114,7 @@ logger = Logger("MyLogger") ``` ```{note} -See the `init_logging` [API Reference](../api_reference/common.md#init_logging) for further details. +See the `init_logging` [API Reference](../api_reference/common) for further details. ``` ```{warning} diff --git a/docs/concepts/orders.md b/docs/concepts/orders.md index 197c2add0675..1df183199189 100644 --- a/docs/concepts/orders.md +++ b/docs/concepts/orders.md @@ -27,7 +27,7 @@ The core order types available for the platform are (using the enum values): - `TRAILING_STOP_MARKET` - `TRAILING_STOP_LIMIT` -```{warning} +```{note} NautilusTrader has unified the API for a large set of order types and execution instructions, however not all of these are available for every exchange. If an order is submitted where an instruction or option is not available, then the system will not submit the order and an error will be logged with diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index 58b4015b7f2b..da54d16885c3 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -30,6 +30,7 @@ To install with specific extras using _pip_: pip install -U "nautilus_trader[docker,ib]" ## From Source + Installation from source requires the `Python.h` header file, which is included in development releases such as `python-dev`. You'll also need the latest stable `rustc` and `cargo` to compile the Rust libraries. @@ -70,6 +71,7 @@ as specified in the `pyproject.toml`. However, we highly recommend installing us poetry install --only main --all-extras ## From GitHub Release + To install a binary wheel from GitHub, first navigate to the [latest release](https://github.com/nautechsystems/nautilus_trader/releases/latest). Download the appropriate `.whl` for your operating system and Python version, then run: diff --git a/docs/index.md b/docs/index.md index 8ef5929d67f4..68c5abffa4d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,10 +29,6 @@ However, this documentation aims to assist you in learning and understanding Nau If you have any questions or need further assistance, please reach out to the NautilusTrader community for support. -```{note} -The terms "NautilusTrader", "Nautilus" and "platform" are used interchageably throughout the documentation. -``` - The following is a brief summary of what you'll find in the documentation, and how to use each section. ## [Getting Started](getting_started/index.md) @@ -64,3 +60,7 @@ The **API Reference** provides comprehensive technical information on available The **Developer Guide** is tailored for those who wish to delve further into and potentially modify the codebase. It provides insights into the architectural decisions, coding standards, and best practices, helping to ensuring a pleasant and productive development experience. + +```{note} +The terms "NautilusTrader", "Nautilus" and "platform" are used interchageably throughout the documentation. +``` diff --git a/docs/integrations/bybit.md b/docs/integrations/bybit.md index 4e3ce0813a19..86c2dfd6d9d3 100644 --- a/docs/integrations/bybit.md +++ b/docs/integrations/bybit.md @@ -1,6 +1,6 @@ # Bybit -```{warning} +```{note} We are currently working on this integration guide. ``` diff --git a/docs/integrations/databento.md b/docs/integrations/databento.md index cbf280660655..103fb175f3df 100644 --- a/docs/integrations/databento.md +++ b/docs/integrations/databento.md @@ -1,6 +1,6 @@ # Databento -```{warning} +```{note} We are currently working on this integration guide. ``` @@ -23,7 +23,7 @@ It's recommended you make use of the [/metadata.get_cost](https://databento.com/ ## Overview The adapter implementation takes the [databento-rs](https://crates.io/crates/databento) crate as a dependency, -which is the official Rust client library provided by Databento 🦀. There are actually no Databento Python dependencies. +which is the official Rust client library provided by Databento. There are actually no Databento Python dependencies. ```{note} There is no optional extra installation for `databento`, at this stage the core components of the adapter are compiled diff --git a/docs/rust.md b/docs/rust.md index 0399286e4377..83a1c86bbc5e 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -1,7 +1,7 @@ # Rust API The core of NautilusTrader is written in Rust, and one day it will be possible to run systems -entirely programmed and compiled from Rust 🦀. +entirely programmed and compiled from Rust. The API reference provides detailed technical documentation for the core NautilusTrader crates, the docs are generated from source code using `cargo doc`. From 6763bf9596fd107b263cd36da736b967a0c3828b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 12 May 2024 17:54:27 +1000 Subject: [PATCH 161/193] Allow updating venue_order_id on modify --- nautilus_trader/cache/cache.pxd | 4 +-- nautilus_trader/cache/cache.pyx | 32 ++++++++++++++----- nautilus_trader/execution/algorithm.pyx | 2 +- nautilus_trader/execution/emulator.pyx | 4 +-- .../betfair/test_betfair_execution.py | 1 - 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/nautilus_trader/cache/cache.pxd b/nautilus_trader/cache/cache.pxd index 82bd6daf7fe5..2b22996cbe5f 100644 --- a/nautilus_trader/cache/cache.pxd +++ b/nautilus_trader/cache/cache.pxd @@ -162,8 +162,8 @@ cdef class Cache(CacheFacade): cpdef void add_instrument(self, Instrument instrument) cpdef void add_synthetic(self, SyntheticInstrument synthetic) cpdef void add_account(self, Account account) - cpdef void add_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id) - cpdef void add_order(self, Order order, PositionId position_id=*, ClientId client_id=*, bint override=*) + cpdef void add_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id, bint overwrite=*) + cpdef void add_order(self, Order order, PositionId position_id=*, ClientId client_id=*, bint overwrite=*) cpdef void add_order_list(self, OrderList order_list) cpdef void add_position_id(self, PositionId position_id, Venue venue, ClientOrderId client_order_id, StrategyId strategy_id) cpdef void add_position(self, Position position, OmsType oms_type) diff --git a/nautilus_trader/cache/cache.pyx b/nautilus_trader/cache/cache.pyx index af586ac5215e..28fd29109c72 100644 --- a/nautilus_trader/cache/cache.pyx +++ b/nautilus_trader/cache/cache.pyx @@ -46,6 +46,7 @@ from nautilus_trader.model.data cimport Bar from nautilus_trader.model.data cimport BarType from nautilus_trader.model.data cimport QuoteTick from nautilus_trader.model.data cimport TradeTick +from nautilus_trader.model.events.order cimport OrderUpdated from nautilus_trader.model.identifiers cimport AccountId from nautilus_trader.model.identifiers cimport ClientId from nautilus_trader.model.identifiers cimport ClientOrderId @@ -1408,7 +1409,12 @@ cdef class Cache(CacheFacade): if self._database is not None: self._database.add_account(account) - cpdef void add_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id): + cpdef void add_venue_order_id( + self, + ClientOrderId client_order_id, + VenueOrderId venue_order_id, + bint overwrite=False, + ): """ Index the given client order ID with the given venue order ID. @@ -1418,18 +1424,22 @@ cdef class Cache(CacheFacade): The client order ID to index. venue_order_id : VenueOrderId The venue order ID to index. + overwrite : bool, default False + If the venue order ID will 'overwrite' any existing indexing and replace + it in the cache. This is currently used for updated orders where the venue + order ID may change. Raises ------ ValueError - If the `client_order_id` is already indexed with a different `venue_order_id`. + If `overwrite` is False and the `client_order_id` is already indexed with a different `venue_order_id`. """ Condition.not_none(client_order_id, "client_order_id") Condition.not_none(venue_order_id, "venue_order_id") cdef VenueOrderId existing_venue_order_id = self._index_client_order_ids.get(client_order_id) - if existing_venue_order_id is not None and venue_order_id != existing_venue_order_id: + if not overwrite and existing_venue_order_id is not None and venue_order_id != existing_venue_order_id: raise ValueError( f"Existing {existing_venue_order_id!r} for {client_order_id!r} " f"did not match the given {venue_order_id!r}. " @@ -1449,7 +1459,7 @@ cdef class Cache(CacheFacade): Order order, PositionId position_id = None, ClientId client_id = None, - bint override = False, + bint overwrite = False, ): """ Add the given order to the cache indexed with the given position @@ -1463,8 +1473,8 @@ cdef class Cache(CacheFacade): The position ID to index for the order. client_id : ClientId, optional The execution client ID for order routing. - override : bool, default False - If the added order should 'override' any existing order and replace + overwrite : bool, default False + If the added order should 'overwrite' any existing order and replace it in the cache. This is currently used for emulated orders which are being released and transformed into another type. @@ -1475,7 +1485,7 @@ cdef class Cache(CacheFacade): """ Condition.not_none(order, "order") - if not override: + if not overwrite: Condition.not_in(order.client_order_id, self._orders, "order.client_order_id", "_orders") Condition.not_in(order.client_order_id, self._index_orders, "order.client_order_id", "_index_orders") Condition.not_in(order.client_order_id, self._index_order_position, "order.client_order_id", "_index_order_position") @@ -1816,7 +1826,13 @@ cdef class Cache(CacheFacade): # Update venue order ID if order.venue_order_id is not None and order.venue_order_id not in self._index_venue_order_ids: - self.add_venue_order_id(order.client_order_id, order.venue_order_id) + # If the order is being modified then we allow a changing `VenueOrderId` to accommodate + # venues which use a cancel+replace update strategy. + self.add_venue_order_id( + order.client_order_id, + order.venue_order_id, + overwrite=isinstance(order._events[-1], OrderUpdated), + ) # Update in-flight state if order.is_inflight_c(): diff --git a/nautilus_trader/execution/algorithm.pyx b/nautilus_trader/execution/algorithm.pyx index 0f654ef4711c..9a4f6ca4721c 100644 --- a/nautilus_trader/execution/algorithm.pyx +++ b/nautilus_trader/execution/algorithm.pyx @@ -1109,7 +1109,7 @@ cdef class ExecAlgorithm(Actor): client_id = self.cache.client_id(order.client_order_id) cdef Order cached_order = self.cache.order(order.client_order_id) if cached_order.order_type != order.order_type: - self.cache.add_order(order, position_id, client_id, override=True) + self.cache.add_order(order, position_id, client_id, overwrite=True) command = SubmitOrder( trader_id=self.trader_id, diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index a7f92d04559e..0b50e1393356 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -673,7 +673,7 @@ cdef class OrderEmulator(Actor): transformed, command.position_id, command.client_id, - override=True, + overwrite=True, ) # Replace commands order with transformed order @@ -745,7 +745,7 @@ cdef class OrderEmulator(Actor): transformed, command.position_id, command.client_id, - override=True, + overwrite=True, ) # Replace commands order with transformed order diff --git a/tests/integration_tests/adapters/betfair/test_betfair_execution.py b/tests/integration_tests/adapters/betfair/test_betfair_execution.py index f0ab9283d37c..78038eb83fda 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_execution.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_execution.py @@ -264,7 +264,6 @@ async def test_submit_order_error( assert rejected.reason == expecter_error -@pytest.mark.skip(reason="Assignment of venue order IDs needs investigating for Betfair") @pytest.mark.asyncio() async def test_modify_order_success( exec_client: BetfairDataClient, From ff7c338cb409c33098f6cf01186bf28349c93222 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 12 May 2024 18:57:15 +1000 Subject: [PATCH 162/193] Add OrderMatchingEngine cached filled quantity --- RELEASES.md | 2 ++ nautilus_trader/backtest/matching_engine.pxd | 1 + nautilus_trader/backtest/matching_engine.pyx | 29 ++++++++++++++++++++ nautilus_trader/model/orders/base.pyx | 2 +- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 01f54fdc7bf3..92902e24808d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -19,6 +19,7 @@ Released on TBD (UTC). ### Fixes - Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` - Fixed `TimeEvent` equality (now based on then event `id` rather than the event `name`) +- Fixed venue order ID generation and application in sandbox mode (was previously generating additional venue order IDs), thanks for reporting @rsmb7z and @davidsblom - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z @@ -26,6 +27,7 @@ Released on TBD (UTC). - Fixed IBKR reconnection after gateway/TWS disconnection (#1622), thanks @benjaminsingleton - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) +- Fixed `leaves_qty` exception message underflow (now correctly displays the projected negative leaves quantity) --- diff --git a/nautilus_trader/backtest/matching_engine.pxd b/nautilus_trader/backtest/matching_engine.pxd index 9a9138438180..124f6fc6f50c 100644 --- a/nautilus_trader/backtest/matching_engine.pxd +++ b/nautilus_trader/backtest/matching_engine.pxd @@ -90,6 +90,7 @@ cdef class OrderMatchingEngine: cdef dict _account_ids cdef dict _execution_bar_types cdef dict _execution_bar_deltas + cdef dict _cached_filled_qty cdef readonly Venue venue """The venue for the matching engine.\n\n:returns: `Venue`""" diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 8a77cf3d65f4..901ffa4b5912 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -212,6 +212,7 @@ cdef class OrderMatchingEngine: self._account_ids: dict[TraderId, AccountId] = {} self._execution_bar_types: dict[InstrumentId, BarType] = {} self._execution_bar_deltas: dict[BarType, timedelta] = {} + self._cached_filled_qty: dict[ClientOrderId, Quantity] = {} # Market self._core = MatchingCore( @@ -248,6 +249,7 @@ cdef class OrderMatchingEngine: self._account_ids.clear() self._execution_bar_types.clear() self._execution_bar_deltas.clear() + self._cached_filled_qty.clear() self._core.reset() self._target_bid = 0 self._target_ask = 0 @@ -1288,12 +1290,14 @@ cdef class OrderMatchingEngine: cdef Order order for order in orders: if order.is_closed_c(): + self._cached_filled_qty.pop(order.client_order_id, None) continue # Check expiry if self._support_gtd_orders: if order.expire_time_ns > 0 and timestamp_ns >= order.expire_time_ns: self._core.delete_order(order) + self._cached_filled_qty.pop(order.client_order_id, None) self.expire_order(order) continue @@ -1523,6 +1527,14 @@ cdef class OrderMatchingEngine: The order to fill. """ + cdef Quantity cached_filled_qty = self._cached_filled_qty.get(order.client_order_id) + if cached_filled_qty is not None and cached_filled_qty._mem.raw >= order.quantity._mem.raw: + self._log.debug( + f"Ignoring fill as already filled pending application of events: " + f"{cached_filled_qty=}, {order.quantity=}, {order.filled_qty=}, {order.leaves_qty=}", + ) + return + cdef PositionId venue_position_id = self._get_position_id(order) cdef Position position = None if venue_position_id is not None: @@ -1563,6 +1575,14 @@ cdef class OrderMatchingEngine: """ Condition.true(order.has_price_c(), "order has no limit `price`") + cdef Quantity cached_filled_qty = self._cached_filled_qty.get(order.client_order_id) + if cached_filled_qty is not None and cached_filled_qty._mem.raw >= order.quantity._mem.raw: + self._log.debug( + f"Ignoring fill as already filled pending application of events: " + f"{cached_filled_qty=}, {order.quantity=}, {order.filled_qty=}, {order.leaves_qty=}", + ) + return + cdef Price price = order.price if order.liquidity_side == LiquiditySide.MAKER and self._fill_model: if order.side == OrderSide.BUY and self._core.bid_raw == price._mem.raw and not self._fill_model.is_limit_filled(): @@ -1825,6 +1845,12 @@ cdef class OrderMatchingEngine: instrument=self.instrument, ) + cdef Quantity cached_filled_qty = self._cached_filled_qty.get(order.client_order_id) + if cached_filled_qty is None: + self._cached_filled_qty[order.client_order_id] = Quantity.from_raw_c(last_qty._mem.raw, last_qty._mem.precision) + else: + cached_filled_qty._mem.raw += last_qty._mem.raw + self._generate_order_filled( order=order, venue_order_id=self._get_venue_order_id(order), @@ -1839,6 +1865,7 @@ cdef class OrderMatchingEngine: if order.is_passive_c() and order.is_closed_c(): # Remove order from market self._core.delete_order(order) + self._cached_filled_qty.pop(order.client_order_id, None) if not self._support_contingent_orders: return @@ -2016,6 +2043,7 @@ cdef class OrderMatchingEngine: return self._core.delete_order(order) + self._cached_filled_qty.pop(order.client_order_id, None) self._generate_order_canceled(order, venue_order_id=self._get_venue_order_id(order)) @@ -2101,6 +2129,7 @@ cdef class OrderMatchingEngine: if order.is_post_only: # Would be liquidity taker self._core.delete_order(order) + self._cached_filled_qty.pop(order.client_order_id, None) self._generate_order_rejected( order, f"POST_ONLY {order.type_string_c()} {order.side_string_c()} order " diff --git a/nautilus_trader/model/orders/base.pyx b/nautilus_trader/model/orders/base.pyx index 3cf9237b2684..df3393b512a5 100644 --- a/nautilus_trader/model/orders/base.pyx +++ b/nautilus_trader/model/orders/base.pyx @@ -1061,7 +1061,7 @@ cdef class Order: cdef int64_t raw_leaves_qty = self.quantity._mem.raw - raw_filled_qty if raw_leaves_qty < 0: raise ValueError( - f"invalid order.leaves_qty: was {raw_leaves_qty / 1e9}, " + f"invalid order.leaves_qty: was {raw_leaves_qty / 1e9}, " f"order.quantity={self.quantity}, " f"order.filled_qty={self.filled_qty}, " f"fill.last_qty={fill.last_qty}, " From f169cce17f58c4c12855fe86ee581a3872a142b9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 12 May 2024 20:56:24 +1000 Subject: [PATCH 163/193] Update dependencies including datafusion --- nautilus_core/Cargo.lock | 204 +++++++++++++-------------- nautilus_core/persistence/Cargo.toml | 2 +- poetry.lock | 15 +- 3 files changed, 102 insertions(+), 119 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index a0ed04a7284a..479e1eb89e49 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -154,12 +154,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - [[package]] name = "arrayvec" version = "0.7.4" @@ -390,9 +384,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" +checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" dependencies = [ "bzip2", "flate2", @@ -414,7 +408,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -567,28 +561,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "blake3" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -618,7 +590,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "syn_derive", ] @@ -867,7 +839,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -958,12 +930,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "constant_time_eq" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" - [[package]] name = "core-foundation" version = "0.9.4" @@ -1141,7 +1107,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1152,7 +1118,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1197,9 +1163,9 @@ dependencies = [ [[package]] name = "datafusion" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85069782056753459dc47e386219aa1fdac5b731f26c28abb8c0ffd4b7c5ab11" +checksum = "05fb4eeeb7109393a0739ac5b8fd892f95ccef691421491c85544f7997366f68" dependencies = [ "ahash 0.8.11", "arrow", @@ -1217,6 +1183,7 @@ dependencies = [ "datafusion-execution", "datafusion-expr", "datafusion-functions", + "datafusion-functions-aggregate", "datafusion-optimizer", "datafusion-physical-expr", "datafusion-physical-plan", @@ -1247,9 +1214,9 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "309d9040751f6dc9e33c85dce6abb55a46ef7ea3644577dd014611c379447ef3" +checksum = "741aeac15c82f239f2fc17deccaab19873abbd62987be20023689b15fa72fa09" dependencies = [ "ahash 0.8.11", "arrow", @@ -1269,18 +1236,18 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e4a44d8ef1b1e85d32234e6012364c411c3787859bb3bba893b0332cb03dfd" +checksum = "6e8ddfb8d8cb51646a30da0122ecfffb81ca16919ae9a3495a9e7468bdcd52b8" dependencies = [ "tokio", ] [[package]] name = "datafusion-execution" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a3a29ae36bcde07d179cc33b45656a8e7e4d023623e320e48dcf1200eeee95" +checksum = "282122f90b20e8f98ebfa101e4bf20e718fd2684cf81bef4e8c6366571c64404" dependencies = [ "arrow", "chrono", @@ -1299,9 +1266,9 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3542aa322029c2121a671ce08000d4b274171070df13f697b14169ccf4f628" +checksum = "5478588f733df0dfd87a62671c7478f590952c95fa2fa5c137e3ff2929491e22" dependencies = [ "ahash 0.8.11", "arrow", @@ -1309,6 +1276,7 @@ dependencies = [ "chrono", "datafusion-common", "paste", + "serde_json", "sqlparser", "strum", "strum_macros", @@ -1316,34 +1284,48 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd221792c666eac174ecc09e606312844772acc12cbec61a420c2fca1ee70959" +checksum = "f4afd261cea6ac9c3ca1192fd5e9f940596d8e9208c5b1333f4961405db53185" dependencies = [ "arrow", "base64 0.22.1", - "blake2", - "blake3", "chrono", "datafusion-common", "datafusion-execution", "datafusion-expr", "datafusion-physical-expr", + "hashbrown 0.14.5", "hex", "itertools 0.12.1", "log", - "md-5", + "rand", "regex", - "sha2", "unicode-segmentation", "uuid", ] +[[package]] +name = "datafusion-functions-aggregate" +version = "38.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b36a6c4838ab94b5bf8f7a96ce6ce059d805c5d1dcaa6ace49e034eb65cd999" +dependencies = [ + "arrow", + "datafusion-common", + "datafusion-execution", + "datafusion-expr", + "datafusion-physical-expr-common", + "log", + "paste", + "sqlparser", +] + [[package]] name = "datafusion-optimizer" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bd7f5087817deb961764e8c973d243b54f8572db414a8f0a8f33a48f991e0a" +checksum = "54f2820938810e8a2d71228fd6f59f33396aebc5f5f687fcbf14de5aab6a7e1a" dependencies = [ "arrow", "async-trait", @@ -1352,6 +1334,7 @@ dependencies = [ "datafusion-expr", "datafusion-physical-expr", "hashbrown 0.14.5", + "indexmap 2.2.6", "itertools 0.12.1", "log", "regex-syntax 0.8.3", @@ -1359,9 +1342,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cabc0d9aaa0f5eb1b472112f16223c9ffd2fb04e58cbf65c0a331ee6e993f96" +checksum = "9adf8eb12716f52ddf01e09eb6c94d3c9b291e062c05c91b839a448bddba2ff8" dependencies = [ "ahash 0.8.11", "arrow", @@ -1371,37 +1354,45 @@ dependencies = [ "arrow-schema", "arrow-string", "base64 0.22.1", - "blake2", - "blake3", "chrono", "datafusion-common", "datafusion-execution", "datafusion-expr", + "datafusion-functions-aggregate", + "datafusion-physical-expr-common", "half", "hashbrown 0.14.5", "hex", "indexmap 2.2.6", "itertools 0.12.1", "log", - "md-5", "paste", "petgraph", - "rand", "regex", - "sha2", - "unicode-segmentation", +] + +[[package]] +name = "datafusion-physical-expr-common" +version = "38.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5472c3230584c150197b3f2c23f2392b9dc54dbfb62ad41e7e36447cfce4be" +dependencies = [ + "arrow", + "datafusion-common", + "datafusion-expr", ] [[package]] name = "datafusion-physical-plan" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c0523e9c8880f2492a88bbd857dde02bed1ed23f3e9211a89d3d7ec3b44af9" +checksum = "18ae750c38389685a8b62e5b899bbbec488950755ad6d218f3662d35b800c4fe" dependencies = [ "ahash 0.8.11", "arrow", "arrow-array", "arrow-buffer", + "arrow-ord", "arrow-schema", "async-trait", "chrono", @@ -1409,7 +1400,9 @@ dependencies = [ "datafusion-common-runtime", "datafusion-execution", "datafusion-expr", + "datafusion-functions-aggregate", "datafusion-physical-expr", + "datafusion-physical-expr-common", "futures", "half", "hashbrown 0.14.5", @@ -1425,9 +1418,9 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "37.1.0" +version = "38.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb54b42227136f6287573f2434b1de249fe1b8e6cd6cc73a634e4a3ec29356" +checksum = "befc67a3cdfbfa76853f43b10ac27337821bb98e519ab6baf431fcc0bcfcafdb" dependencies = [ "arrow", "arrow-array", @@ -1468,7 +1461,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1510,7 +1503,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1520,7 +1513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1595,9 +1588,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1787,7 +1780,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -2880,9 +2873,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -2956,11 +2949,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -3004,7 +2996,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3081,7 +3073,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3234,9 +3226,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap 2.2.6", @@ -3297,7 +3289,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3556,7 +3548,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3569,7 +3561,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3961,7 +3953,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.61", + "syn 2.0.63", "unicode-ident", ] @@ -4191,7 +4183,7 @@ checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4249,7 +4241,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4423,9 +4415,9 @@ dependencies = [ [[package]] name = "sqlparser" -version = "0.44.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf9c7ff146298ffda83a200f8d5084f08dcee1edfc135fcc1d646a45d50ffd6" +checksum = "f7bbffee862a796d67959a89859d6b1046bb5016d63e23835ad0da182777bbe0" dependencies = [ "log", "sqlparser_derive", @@ -4439,7 +4431,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4690,7 +4682,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4712,9 +4704,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.61" +version = "2.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" dependencies = [ "proc-macro2", "quote", @@ -4730,7 +4722,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4861,7 +4853,7 @@ checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4985,7 +4977,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -5135,7 +5127,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -5255,7 +5247,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -5442,7 +5434,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "wasm-bindgen-shared", ] @@ -5476,7 +5468,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5788,7 +5780,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index d95388b88c95..d90784ffd25e 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -21,7 +21,7 @@ tokio = { workspace = true } thiserror = { workspace = true } binary-heap-plus = "0.5.0" compare = "0.1.0" -datafusion = { version = "37.1.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } +datafusion = { version = "38.0.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } dotenv = "0.15.0" [dev-dependencies] diff --git a/poetry.lock b/poetry.lock index 71269df12175..8b2c20d180ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -464,7 +464,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = ">=3.6" +python-versions = "*" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -932,6 +932,7 @@ files = [ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, + {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, @@ -1510,6 +1511,7 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -1948,7 +1950,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1956,16 +1957,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1982,7 +1975,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1990,7 +1982,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, From ee1c9b2d24c311a6a1676d10760188762ac16af0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 12 May 2024 22:05:34 +1000 Subject: [PATCH 164/193] Continue Cache in Rust --- nautilus_core/common/src/cache/core.rs | 81 +++++++++++++++++++++----- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs index 788e1f9a0d1e..f51ab3412520 100644 --- a/nautilus_core/common/src/cache/core.rs +++ b/nautilus_core/common/src/cache/core.rs @@ -107,7 +107,8 @@ pub struct CacheIndex { venue_account: HashMap, venue_orders: HashMap>, venue_positions: HashMap>, - order_ids: HashMap, + venue_order_ids: HashMap, + client_order_ids: HashMap, order_position: HashMap, order_strategy: HashMap, order_client: HashMap, @@ -139,7 +140,8 @@ impl CacheIndex { self.venue_account.clear(); self.venue_orders.clear(); self.venue_positions.clear(); - self.order_ids.clear(); + self.venue_order_ids.clear(); + self.client_order_ids.clear(); self.order_position.clear(); self.order_strategy.clear(); self.order_client.clear(); @@ -199,7 +201,8 @@ impl Cache { venue_account: HashMap::new(), venue_orders: HashMap::new(), venue_positions: HashMap::new(), - order_ids: HashMap::new(), + venue_order_ids: HashMap::new(), + client_order_ids: HashMap::new(), order_position: HashMap::new(), order_strategy: HashMap::new(), order_client: HashMap::new(), @@ -362,7 +365,7 @@ impl Cache { // 2: Build index.order_ids -> {VenueOrderId, ClientOrderId} if let Some(venue_order_id) = order.venue_order_id() { self.index - .order_ids + .venue_order_ids .insert(venue_order_id, *client_order_id); } @@ -642,10 +645,20 @@ impl Cache { } } - for client_order_id in self.index.order_ids.values() { + for client_order_id in self.index.venue_order_ids.values() { if !self.orders.contains_key(client_order_id) { error!( - "{} in `index.order_ids`: {} not found in `self.orders`", + "{} in `index.venue_order_ids`: {} not found in `self.orders`", + failure, client_order_id + ); + error_count += 1; + } + } + + for client_order_id in self.index.client_order_ids.keys() { + if !self.orders.contains_key(client_order_id) { + error!( + "{} in `index.client_order_ids`: {} not found in `self.orders`", failure, client_order_id ); error_count += 1; @@ -1092,6 +1105,34 @@ impl Cache { Ok(()) } + /// Index the given client order ID with the given venue order ID. + pub fn add_venue_order_id( + &mut self, + client_order_id: &ClientOrderId, + venue_order_id: &VenueOrderId, + overwrite: bool, + ) -> anyhow::Result<()> { + if let Some(existing_venue_order_id) = self.index.client_order_ids.get(client_order_id) { + if !overwrite && existing_venue_order_id != venue_order_id { + anyhow::bail!( + "Existing {existing_venue_order_id} for {client_order_id} + did not match the given {venue_order_id}. + If you are writing a test then try a different `venue_order_id`, + otherwise this is probably a bug." + ); + } + }; + + self.index + .client_order_ids + .insert(*client_order_id, *venue_order_id); + self.index + .venue_order_ids + .insert(*venue_order_id, *client_order_id); + + Ok(()) + } + /// Add the order to the cache indexed with any given identifiers. /// /// # Parameters @@ -1329,8 +1370,12 @@ impl Cache { // Update venue order ID if let Some(venue_order_id) = order.venue_order_id() { - // Assumes order_id does not change - self.index.order_ids.insert(venue_order_id, client_order_id); + // If the order is being modified then we allow a changing `VenueOrderId` to accommodate + // venues which use a cancel+replace update strategy. + if !self.index.venue_order_ids.contains_key(&venue_order_id) { + // TODO: If the last event was `OrderUpdated` then overwrite should be true + self.add_venue_order_id(&order.client_order_id(), &venue_order_id, false)?; + }; } // Update in-flight state @@ -1719,14 +1764,12 @@ impl Cache { #[must_use] pub fn client_order_id(&self, venue_order_id: &VenueOrderId) -> Option<&ClientOrderId> { - self.index.order_ids.get(venue_order_id) + self.index.venue_order_ids.get(venue_order_id) } #[must_use] - pub fn venue_order_id(&self, client_order_id: &ClientOrderId) -> Option { - self.orders - .get(client_order_id) - .and_then(nautilus_model::polymorphism::GetVenueOrderId::venue_order_id) + pub fn venue_order_id(&self, client_order_id: &ClientOrderId) -> Option<&VenueOrderId> { + self.index.client_order_ids.get(client_order_id) } #[must_use] @@ -2399,7 +2442,7 @@ mod tests { orders::{any::OrderAny, stubs::TestOrderStubs}, polymorphism::{ ApplyOrderEventAny, GetAccountId, GetClientOrderId, GetInstrumentId, GetStrategyId, - GetTraderId, IsOpen, + GetTraderId, GetVenueOrderId, IsOpen, }, types::{price::Price, quantity::Quantity}, }; @@ -2528,6 +2571,7 @@ mod tests { assert_eq!(cache.orders_emulated_count(None, None, None, None), 0); assert_eq!(cache.orders_inflight_count(None, None, None, None), 0); assert_eq!(cache.orders_total_count(None, None, None, None), 1); + assert_eq!(cache.venue_order_id(&order.client_order_id()), None); } #[rstest] @@ -2576,6 +2620,7 @@ mod tests { assert_eq!(cache.orders_emulated_count(None, None, None, None), 0); assert_eq!(cache.orders_inflight_count(None, None, None, None), 0); assert_eq!(cache.orders_total_count(None, None, None, None), 1); + assert_eq!(cache.venue_order_id(&order.client_order_id()), None); } #[rstest] @@ -2641,6 +2686,14 @@ mod tests { assert_eq!(cache.orders_emulated_count(None, None, None, None), 0); assert_eq!(cache.orders_inflight_count(None, None, None, None), 0); assert_eq!(cache.orders_total_count(None, None, None, None), 1); + assert_eq!( + cache.client_order_id(&order.venue_order_id().unwrap()), + Some(&order.client_order_id()) + ); + assert_eq!( + cache.venue_order_id(&order.client_order_id()), + Some(&order.venue_order_id().unwrap()) + ); } #[rstest] From c1cc60fe6accc7eeb129d8a45d3ee72fad1c41e2 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Mon, 13 May 2024 23:47:35 +0200 Subject: [PATCH 165/193] Deserialize bool to u8 in Rust order events (#1641) --- nautilus_core/core/src/deserialization.rs | 91 +++++++++++++++++++ nautilus_core/core/src/lib.rs | 1 + .../model/src/events/order/accepted.rs | 3 +- .../model/src/events/order/cancel_rejected.rs | 3 +- .../model/src/events/order/canceled.rs | 3 +- .../model/src/events/order/expired.rs | 3 +- .../model/src/events/order/modify_rejected.rs | 3 +- .../model/src/events/order/pending_cancel.rs | 3 +- .../model/src/events/order/pending_update.rs | 3 +- .../model/src/events/order/rejected.rs | 3 +- .../model/src/events/order/triggered.rs | 3 +- .../model/src/events/order/updated.rs | 3 +- 12 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 nautilus_core/core/src/deserialization.rs diff --git a/nautilus_core/core/src/deserialization.rs b/nautilus_core/core/src/deserialization.rs new file mode 100644 index 000000000000..b636ef336bff --- /dev/null +++ b/nautilus_core/core/src/deserialization.rs @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 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::fmt; + +use serde::{ + de::{Unexpected, Visitor}, + Deserializer, +}; + +struct BoolVisitor; + +impl<'de> Visitor<'de> for BoolVisitor { + type Value = u8; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a boolean as u8") + } + + fn visit_bool(self, value: bool) -> Result + where + E: serde::de::Error, + { + Ok(if value { 1 } else { 0 }) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + if value > u8::MAX as u64 { + Err(E::invalid_value(Unexpected::Unsigned(value), &self)) + } else { + Ok(value as u8) + } + } +} + +pub fn from_bool_as_u8<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(BoolVisitor) +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::from_bool_as_u8; + + #[derive(Deserialize)] + pub struct TestStruct { + #[serde(deserialize_with = "from_bool_as_u8")] + pub value: u8, + } + + #[test] + fn test_deserialize_bool_as_u8_with_boolean() { + let json_true = r#"{"value": true}"#; + let test_struct: TestStruct = serde_json::from_str(json_true).unwrap(); + assert_eq!(test_struct.value, 1); + + let json_false = r#"{"value": false}"#; + let test_struct: TestStruct = serde_json::from_str(json_false).unwrap(); + assert_eq!(test_struct.value, 0); + } + + #[test] + fn test_deserialize_bool_as_u8_with_u64() { + let json_true = r#"{"value": 1}"#; + let test_struct: TestStruct = serde_json::from_str(json_true).unwrap(); + assert_eq!(test_struct.value, 1); + + let json_false = r#"{"value": 0}"#; + let test_struct: TestStruct = serde_json::from_str(json_false).unwrap(); + assert_eq!(test_struct.value, 0); + } +} diff --git a/nautilus_core/core/src/lib.rs b/nautilus_core/core/src/lib.rs index 62a04051182a..11c7d9026f6f 100644 --- a/nautilus_core/core/src/lib.rs +++ b/nautilus_core/core/src/lib.rs @@ -29,6 +29,7 @@ pub mod correctness; pub mod datetime; +pub mod deserialization; pub mod equality; pub mod message; pub mod nanos; diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index eaa257e305e4..5a7d9488ff13 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -49,6 +49,7 @@ pub struct OrderAccepted { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed } diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index b8bb25e531a5..6a85b9f83152 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -48,6 +48,7 @@ pub struct OrderCancelRejected { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, diff --git a/nautilus_core/model/src/events/order/canceled.rs b/nautilus_core/model/src/events/order/canceled.rs index 9400bf186ebd..f74ef8481550 100644 --- a/nautilus_core/model/src/events/order/canceled.rs +++ b/nautilus_core/model/src/events/order/canceled.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -47,6 +47,7 @@ pub struct OrderCanceled { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index 76c47e86ebd8..3f086e48c477 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -47,6 +47,7 @@ pub struct OrderExpired { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index 669ddb97a636..7da53ef03e8d 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -48,6 +48,7 @@ pub struct OrderModifyRejected { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index 3208c3c4551c..6254edbca152 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -48,6 +48,7 @@ pub struct OrderPendingCancel { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, } diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index 89019c698ff9..f3e74e2add9a 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -48,6 +48,7 @@ pub struct OrderPendingUpdate { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, } diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index 6c6a0187f8ae..b76df5f2eadf 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -49,6 +49,7 @@ pub struct OrderRejected { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed } diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index 8ce80359b5f2..7e05086443be 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -47,6 +47,7 @@ pub struct OrderTriggered { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed pub venue_order_id: Option, pub account_id: Option, diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index 6c1406f92956..6a5c1de5c82d 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -16,7 +16,7 @@ use std::fmt::{Debug, Display}; use derive_builder::Builder; -use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; +use nautilus_core::{deserialization::from_bool_as_u8, nanos::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -52,6 +52,7 @@ pub struct OrderUpdated { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, + #[serde(deserialize_with = "from_bool_as_u8")] pub reconciliation: u8, // TODO: Change to bool once Cython removed } From 7575aa3436d9d6172b9c024873f0c2bdae3fe33d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 13 May 2024 22:22:59 +1000 Subject: [PATCH 166/193] Update release notes --- RELEASES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 92902e24808d..b2d01208975b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -19,8 +19,9 @@ Released on TBD (UTC). ### Fixes - Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` - Fixed `TimeEvent` equality (now based on then event `id` rather than the event `name`) -- Fixed venue order ID generation and application in sandbox mode (was previously generating additional venue order IDs), thanks for reporting @rsmb7z and @davidsblom - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) +- Fixed venue order ID generation and application in sandbox mode (was previously generating additional venue order IDs), thanks for reporting @rsmb7z and @davidsblom +- Fixed multiple fills causing overfills in sandbox mode (`OrderMatchingEngine` now cached filled quantity to prevent this), thanks for reporting @davidsblom - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z - Fixed Interactive Brokers `IBOrder` attributes assignment (#1634), thanks @rsmb7z From 08bcaf8492dcb55f397dc116e54e38e3edc35cf5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 14 May 2024 18:11:41 +1000 Subject: [PATCH 167/193] Update release notes --- RELEASES.md | 2 +- poetry.lock | 315 +++++++++++++++++++++++++--------------------------- 2 files changed, 155 insertions(+), 162 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index b2d01208975b..2ac458c90677 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -21,7 +21,7 @@ Released on TBD (UTC). - Fixed `TimeEvent` equality (now based on then event `id` rather than the event `name`) - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed venue order ID generation and application in sandbox mode (was previously generating additional venue order IDs), thanks for reporting @rsmb7z and @davidsblom -- Fixed multiple fills causing overfills in sandbox mode (`OrderMatchingEngine` now cached filled quantity to prevent this), thanks for reporting @davidsblom +- Fixed multiple fills causing overfills in sandbox mode (`OrderMatchingEngine` now cached filled quantity to prevent this), thanks @davidsblom - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z - Fixed Interactive Brokers `IBOrder` attributes assignment (#1634), thanks @rsmb7z diff --git a/poetry.lock b/poetry.lock index 8b2c20d180ce..dc564e286614 100644 --- a/poetry.lock +++ b/poetry.lock @@ -464,7 +464,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -860,166 +860,149 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "lxml" -version = "5.2.1" +version = "5.2.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"}, - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"}, - {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"}, - {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"}, - {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"}, - {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"}, - {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"}, - {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"}, - {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"}, - {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"}, - {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"}, - {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"}, - {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"}, - {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"}, - {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"}, - {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"}, - {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"}, - {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"}, + {file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"}, + {file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"}, + {file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"}, + {file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"}, + {file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"}, + {file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"}, + {file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"}, + {file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"}, + {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, + {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, + {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, + {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, + {file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"}, + {file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"}, + {file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"}, + {file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"}, + {file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"}, + {file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"}, ] [package.extras] @@ -1511,7 +1494,6 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, @@ -1950,6 +1932,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1957,8 +1940,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1975,6 +1966,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1982,6 +1974,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2533,13 +2526,13 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "virtualenv" -version = "20.26.1" +version = "20.26.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, - {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, ] [package.dependencies] From b7373ffea9b64f7bcaad7aeee148da7d54adf452 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 14 May 2024 19:18:00 +1000 Subject: [PATCH 168/193] Improve LiveTimer robustness and flexibility --- RELEASES.md | 1 + nautilus_core/common/src/ffi/clock.rs | 10 ++++++ nautilus_core/common/src/timer.rs | 49 ++++++++++++++++---------- nautilus_trader/common/component.pyx | 4 +-- nautilus_trader/core/includes/common.h | 10 ++++++ nautilus_trader/core/rust/common.pxd | 10 ++++++ 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 2ac458c90677..9d8b1b642127 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,7 @@ Released on TBD (UTC). - Added `ParquetDataCatalog` S3 support (#1620), thanks benjaminsingleton - Added `Bar.from_raw_arrays_to_list` (#1623), thanks rsmb7z - Improved venue order ID generation and assignment (it was previously possible for the `OrderMatchingEngine` to generate multiple IDs for the same order) +- Improved `LiveTimer` robustness and flexibility by not requiring positive intervals or stop times in the futures (will immediately produce a time event), thanks for reporting @davidsblom ### Breaking Changes - Removed `allow_cash_positions` config (simplify to the most common use case, spot trading should track positions) diff --git a/nautilus_core/common/src/ffi/clock.rs b/nautilus_core/common/src/ffi/clock.rs index 82d09ffcfd0a..adef67b72e1d 100644 --- a/nautilus_core/common/src/ffi/clock.rs +++ b/nautilus_core/common/src/ffi/clock.rs @@ -339,6 +339,11 @@ pub extern "C" fn live_clock_timer_count(clock: &mut LiveClock_API) -> usize { /// /// - Assumes `name_ptr` is a valid C string pointer. /// - Assumes `callback_ptr` is a valid `PyCallable` pointer. +/// +/// # Panics +/// +/// - Panics if `name` is not a valid string. +/// - Panics if `callback_ptr` is NULL and no default callback has been assigned on the clock. #[no_mangle] pub unsafe extern "C" fn live_clock_set_time_alert( clock: &mut LiveClock_API, @@ -366,6 +371,11 @@ pub unsafe extern "C" fn live_clock_set_time_alert( /// /// - Assumes `name_ptr` is a valid C string pointer. /// - Assumes `callback_ptr` is a valid `PyCallable` pointer. +/// +/// # Panics +/// +/// - Panics if `name` is not a valid string. +/// - Panics if `callback_ptr` is NULL and no default callback has been assigned on the clock. #[no_mangle] pub unsafe extern "C" fn live_clock_set_timer( clock: &mut LiveClock_API, diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index c3db5c26cd06..98e9bfb981a8 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -26,7 +26,7 @@ use std::{ }; use nautilus_core::{ - correctness::{check_positive_u64, check_valid_string}, + correctness::check_valid_string, datetime::floor_to_nearest_microsecond, nanos::{DurationNanos, UnixNanos}, time::get_atomic_clock_realtime, @@ -151,7 +151,6 @@ impl TestTimer { stop_time_ns: Option, ) -> anyhow::Result { check_valid_string(name, stringify!(name))?; - check_positive_u64(interval_ns, stringify!(interval_ns))?; Ok(Self { name: Ustr::from(name), @@ -250,7 +249,6 @@ impl LiveTimer { callback: EventHandler, ) -> anyhow::Result { check_valid_string(name, stringify!(name))?; - check_positive_u64(interval_ns, stringify!(interval_ns))?; debug!("Creating timer '{}'", name); Ok(Self { @@ -278,10 +276,9 @@ impl LiveTimer { pub fn start(&mut self) { let event_name = self.name; let stop_time_ns = self.stop_time_ns; - let mut start_time_ns = self.start_time_ns; let next_time_ns = self.next_time_ns.load(atomic::Ordering::SeqCst); let next_time_atomic = self.next_time_ns.clone(); - let interval_ns = self.interval_ns; + let interval_ns = std::cmp::max(self.interval_ns, 1); // Must be positive for tokio timer let is_expired = self.is_expired.clone(); let callback = self.callback.clone(); @@ -297,11 +294,6 @@ impl LiveTimer { let clock = get_atomic_clock_realtime(); let now_ns = clock.get_time_ns(); - if start_time_ns == 0 { - // No start was specified so start immediately - start_time_ns = now_ns; - } - let start = if next_time_ns <= now_ns { Instant::now() } else { @@ -311,14 +303,6 @@ impl LiveTimer { Instant::now() + Duration::from_nanos(diff) - delay }; - if let Some(stop_time_ns) = stop_time_ns { - assert!(stop_time_ns > now_ns, "stop_time was < now_ns"); - assert!( - start_time_ns + interval_ns <= stop_time_ns, - "start_time + interval was > stop_time" - ); - }; - let mut timer = tokio::time::interval_at(start, Duration::from_nanos(interval_ns)); loop { @@ -335,7 +319,7 @@ impl LiveTimer { // Check if expired if let Some(stop_time_ns) = stop_time_ns { - if next_time_ns >= stop_time_ns { + if std::cmp::max(next_time_ns, now_ns) >= stop_time_ns { break; // Timer expired } } @@ -542,4 +526,31 @@ mod tests { wait_until(|| timer.is_expired(), Duration::from_secs(2)); assert!(timer.next_time_ns() > next_time_ns); } + + #[tokio::test] + async fn test_live_timer_with_zero_interval_and_immediate_stop_time() { + pyo3::prepare_freethreaded_python(); + + let handler = Python::with_gil(|py| { + let callable = wrap_pyfunction!(receive_event, py).unwrap(); + EventHandler::new(callable.into_py(py)) + }); + + // Create a new LiveTimer with a stop time + let clock = get_atomic_clock_realtime(); + let start_time = UnixNanos::default(); + let interval_ns = 0; + let stop_time = clock.get_time_ns(); + let mut timer = LiveTimer::new( + "TEST_TIMER", + interval_ns, + start_time, + Some(stop_time), + handler, + ) + .unwrap(); + timer.start(); + + wait_until(|| timer.is_expired(), Duration::from_secs(2)); + } } diff --git a/nautilus_trader/common/component.pyx b/nautilus_trader/common/component.pyx index f0c8ed59781d..b3ac00f5ae9c 100644 --- a/nautilus_trader/common/component.pyx +++ b/nautilus_trader/common/component.pyx @@ -596,7 +596,6 @@ cdef class TestClock(Clock): ): Condition.valid_string(name, "name") Condition.not_in(name, self.timer_names, "name", "self.timer_names") - Condition.positive_int(interval_ns, "interval_ns") cdef uint64_t ts_now = self.timestamp_ns() @@ -764,9 +763,8 @@ cdef class LiveClock(Clock): uint64_t stop_time_ns, callback: Callable[[TimeEvent], None] | None = None, ): + Condition.valid_string(name, "name") Condition.not_in(name, self.timer_names, "name", "self.timer_names") - Condition.not_in(name, self.timer_names, "name", "self.timer_names") - Condition.positive_int(interval_ns, "interval_ns") if callback is not None: callback = create_pyo3_conversion_wrapper(callback) diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index 66c2b48ea08b..e6881f4e03e0 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -415,6 +415,11 @@ uintptr_t live_clock_timer_count(struct LiveClock_API *clock); * * - Assumes `name_ptr` is a valid C string pointer. * - Assumes `callback_ptr` is a valid `PyCallable` pointer. + * + * # Panics + * + * - Panics if `name` is not a valid string. + * - Panics if `callback_ptr` is NULL and no default callback has been assigned on the clock. */ void live_clock_set_time_alert(struct LiveClock_API *clock, const char *name_ptr, @@ -426,6 +431,11 @@ void live_clock_set_time_alert(struct LiveClock_API *clock, * * - Assumes `name_ptr` is a valid C string pointer. * - Assumes `callback_ptr` is a valid `PyCallable` pointer. + * + * # Panics + * + * - Panics if `name` is not a valid string. + * - Panics if `callback_ptr` is NULL and no default callback has been assigned on the clock. */ void live_clock_set_timer(struct LiveClock_API *clock, const char *name_ptr, diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index 76d979b38bd4..899a9273f402 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -270,6 +270,11 @@ cdef extern from "../includes/common.h": # # - Assumes `name_ptr` is a valid C string pointer. # - Assumes `callback_ptr` is a valid `PyCallable` pointer. + # + # # Panics + # + # - Panics if `name` is not a valid string. + # - Panics if `callback_ptr` is NULL and no default callback has been assigned on the clock. void live_clock_set_time_alert(LiveClock_API *clock, const char *name_ptr, uint64_t alert_time_ns, @@ -279,6 +284,11 @@ cdef extern from "../includes/common.h": # # - Assumes `name_ptr` is a valid C string pointer. # - Assumes `callback_ptr` is a valid `PyCallable` pointer. + # + # # Panics + # + # - Panics if `name` is not a valid string. + # - Panics if `callback_ptr` is NULL and no default callback has been assigned on the clock. void live_clock_set_timer(LiveClock_API *clock, const char *name_ptr, uint64_t interval_ns, From 27b7abb9ac0bc7206833b4845c28346e4c9031ff Mon Sep 17 00:00:00 2001 From: David Blom Date: Tue, 14 May 2024 19:31:34 +0200 Subject: [PATCH 169/193] Fix matching engine cached filled quantity applying partial fills (#1642) --- nautilus_trader/backtest/matching_engine.pyx | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 901ffa4b5912..e2eccf7f24d6 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -1837,25 +1837,30 @@ cdef class OrderMatchingEngine: order.liquidity_side = liquidity_side + cdef Quantity cached_filled_qty = self._cached_filled_qty.get(order.client_order_id) + cdef Quantity corrected_last_qty = None + cdef Quantity leaves_qty = None + if cached_filled_qty is None: + self._cached_filled_qty[order.client_order_id] = Quantity.from_raw_c(last_qty._mem.raw, last_qty._mem.precision) + corrected_last_qty = Quantity.from_raw_c(last_qty._mem.raw, last_qty._mem.precision) + else: + leaves_qty = Quantity.from_raw_c(order.quantity._mem.raw - cached_filled_qty._mem.raw, last_qty._mem.precision) + corrected_last_qty = Quantity.from_raw_c(min(leaves_qty._mem.raw, last_qty._mem.raw), last_qty._mem.precision) + cached_filled_qty._mem.raw += corrected_last_qty._mem.raw + # Calculate commission cdef Money commission = self._fee_model.get_commission( order=order, - fill_qty=last_qty, + fill_qty=corrected_last_qty, fill_px=last_px, instrument=self.instrument, ) - cdef Quantity cached_filled_qty = self._cached_filled_qty.get(order.client_order_id) - if cached_filled_qty is None: - self._cached_filled_qty[order.client_order_id] = Quantity.from_raw_c(last_qty._mem.raw, last_qty._mem.precision) - else: - cached_filled_qty._mem.raw += last_qty._mem.raw - self._generate_order_filled( order=order, venue_order_id=self._get_venue_order_id(order), venue_position_id=venue_position_id, - last_qty=last_qty, + last_qty=corrected_last_qty, last_px=last_px, quote_currency=self.instrument.quote_currency, commission=commission, From c7170c130548fc58351e199a225cea66cf02975c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 15 May 2024 03:42:38 +1000 Subject: [PATCH 170/193] Simplify cached_filled_qty calculations --- RELEASES.md | 2 +- nautilus_trader/backtest/matching_engine.pyx | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 9d8b1b642127..f6aaea1b800d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,7 +10,7 @@ Released on TBD (UTC). - Added `ParquetDataCatalog` S3 support (#1620), thanks benjaminsingleton - Added `Bar.from_raw_arrays_to_list` (#1623), thanks rsmb7z - Improved venue order ID generation and assignment (it was previously possible for the `OrderMatchingEngine` to generate multiple IDs for the same order) -- Improved `LiveTimer` robustness and flexibility by not requiring positive intervals or stop times in the futures (will immediately produce a time event), thanks for reporting @davidsblom +- Improved `LiveTimer` robustness and flexibility by not requiring positive intervals or stop times in the future (will immediately produce a time event), thanks for reporting @davidsblom ### Breaking Changes - Removed `allow_cash_positions` config (simplify to the most common use case, spot trading should track positions) diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index e2eccf7f24d6..8db257cb0f7b 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -1838,20 +1838,18 @@ cdef class OrderMatchingEngine: order.liquidity_side = liquidity_side cdef Quantity cached_filled_qty = self._cached_filled_qty.get(order.client_order_id) - cdef Quantity corrected_last_qty = None cdef Quantity leaves_qty = None if cached_filled_qty is None: self._cached_filled_qty[order.client_order_id] = Quantity.from_raw_c(last_qty._mem.raw, last_qty._mem.precision) - corrected_last_qty = Quantity.from_raw_c(last_qty._mem.raw, last_qty._mem.precision) else: leaves_qty = Quantity.from_raw_c(order.quantity._mem.raw - cached_filled_qty._mem.raw, last_qty._mem.precision) - corrected_last_qty = Quantity.from_raw_c(min(leaves_qty._mem.raw, last_qty._mem.raw), last_qty._mem.precision) - cached_filled_qty._mem.raw += corrected_last_qty._mem.raw + last_qty = Quantity.from_raw_c(min(leaves_qty._mem.raw, last_qty._mem.raw), last_qty._mem.precision) + cached_filled_qty._mem.raw += last_qty._mem.raw # Calculate commission cdef Money commission = self._fee_model.get_commission( order=order, - fill_qty=corrected_last_qty, + fill_qty=last_qty, fill_px=last_px, instrument=self.instrument, ) @@ -1860,7 +1858,7 @@ cdef class OrderMatchingEngine: order=order, venue_order_id=self._get_venue_order_id(order), venue_position_id=venue_position_id, - last_qty=corrected_last_qty, + last_qty=last_qty, last_px=last_px, quote_currency=self.instrument.quote_currency, commission=commission, From ac95a9695c7c2777332e6aa208420dea76cedcf0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 15 May 2024 03:57:51 +1000 Subject: [PATCH 171/193] Fix clippy lints --- nautilus_core/core/src/deserialization.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_core/core/src/deserialization.rs b/nautilus_core/core/src/deserialization.rs index b636ef336bff..0658b551db5b 100644 --- a/nautilus_core/core/src/deserialization.rs +++ b/nautilus_core/core/src/deserialization.rs @@ -33,14 +33,14 @@ impl<'de> Visitor<'de> for BoolVisitor { where E: serde::de::Error, { - Ok(if value { 1 } else { 0 }) + Ok(u8::from(value)) } fn visit_u64(self, value: u64) -> Result where E: serde::de::Error, { - if value > u8::MAX as u64 { + if value > u64::from(u8::MAX) { Err(E::invalid_value(Unexpected::Unsigned(value), &self)) } else { Ok(value as u8) From 03ceff2c7e5ed357f3c6941f686b8d4278a4e4d3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 15 May 2024 04:26:37 +1000 Subject: [PATCH 172/193] Use NonZeroU64 for timer interval fields --- nautilus_core/common/src/timer.rs | 39 ++++++++++++------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 98e9bfb981a8..a69a3fb34892 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -19,6 +19,7 @@ use std::{ cmp::Ordering, ffi::c_char, fmt::{Display, Formatter}, + num::NonZeroU64, sync::{ atomic::{self, AtomicBool, AtomicU64}, Arc, @@ -26,11 +27,8 @@ use std::{ }; use nautilus_core::{ - correctness::check_valid_string, - datetime::floor_to_nearest_microsecond, - nanos::{DurationNanos, UnixNanos}, - time::get_atomic_clock_realtime, - uuid::UUID4, + correctness::check_valid_string, datetime::floor_to_nearest_microsecond, nanos::UnixNanos, + time::get_atomic_clock_realtime, uuid::UUID4, }; #[cfg(feature = "python")] use pyo3::{types::PyCapsule, IntoPy, PyObject, Python}; @@ -120,23 +118,11 @@ impl Ord for TimeEventHandler { } } -pub trait Timer { - fn new( - name: Ustr, - interval_ns: DurationNanos, - start_time_ns: UnixNanos, - stop_time_ns: Option, - ) -> Self; - fn pop_event(&self, event_id: UUID4, ts_init: UnixNanos) -> TimeEvent; - fn iterate_next_time(&mut self, ts_now: UnixNanos); - fn cancel(&mut self); -} - /// Provides a test timer for user with a `TestClock`. #[derive(Clone, Copy, Debug)] pub struct TestTimer { pub name: Ustr, - pub interval_ns: u64, + pub interval_ns: NonZeroU64, pub start_time_ns: UnixNanos, pub stop_time_ns: Option, next_time_ns: UnixNanos, @@ -151,13 +137,15 @@ impl TestTimer { stop_time_ns: Option, ) -> anyhow::Result { check_valid_string(name, stringify!(name))?; + // SAFETY: Guaranteed to be non-zero + let interval_ns = NonZeroU64::new(std::cmp::max(interval_ns, 1) as u64).unwrap(); Ok(Self { name: Ustr::from(name), interval_ns, start_time_ns, stop_time_ns, - next_time_ns: start_time_ns + interval_ns, + next_time_ns: start_time_ns + interval_ns.get(), is_expired: false, }) } @@ -186,8 +174,9 @@ impl TestTimer { /// of events. A [`TimeEvent`] is appended for each time a next event is /// <= the given `to_time_ns`. pub fn advance(&mut self, to_time_ns: UnixNanos) -> impl Iterator + '_ { - let advances = to_time_ns.saturating_sub(self.next_time_ns.as_u64() - self.interval_ns) - / self.interval_ns; + let advances = to_time_ns + .saturating_sub(self.next_time_ns.as_u64() - self.interval_ns.get()) + / self.interval_ns.get(); self.take(advances as usize).map(|(event, _)| event) } @@ -231,7 +220,7 @@ impl Iterator for TestTimer { /// Provides a live timer for use with a `LiveClock`. pub struct LiveTimer { pub name: Ustr, - pub interval_ns: u64, + pub interval_ns: NonZeroU64, pub start_time_ns: UnixNanos, pub stop_time_ns: Option, next_time_ns: Arc, @@ -249,6 +238,8 @@ impl LiveTimer { callback: EventHandler, ) -> anyhow::Result { check_valid_string(name, stringify!(name))?; + // SAFETY: Guaranteed to be non-zero + let interval_ns = NonZeroU64::new(std::cmp::max(interval_ns, 1) as u64).unwrap(); debug!("Creating timer '{}'", name); Ok(Self { @@ -256,7 +247,7 @@ impl LiveTimer { interval_ns, start_time_ns, stop_time_ns, - next_time_ns: Arc::new(AtomicU64::new(start_time_ns.as_u64() + interval_ns)), + next_time_ns: Arc::new(AtomicU64::new(start_time_ns.as_u64() + interval_ns.get())), is_expired: Arc::new(AtomicBool::new(false)), callback, canceler: None, @@ -278,7 +269,7 @@ impl LiveTimer { let stop_time_ns = self.stop_time_ns; let next_time_ns = self.next_time_ns.load(atomic::Ordering::SeqCst); let next_time_atomic = self.next_time_ns.clone(); - let interval_ns = std::cmp::max(self.interval_ns, 1); // Must be positive for tokio timer + let interval_ns = self.interval_ns.get(); let is_expired = self.is_expired.clone(); let callback = self.callback.clone(); From 5108b57d8c63c4c6d1c160ed29c841d6859490b3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 15 May 2024 07:18:08 +1000 Subject: [PATCH 173/193] Fix clippy lint --- nautilus_core/common/src/timer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index a69a3fb34892..5f0cb54dd145 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -138,7 +138,7 @@ impl TestTimer { ) -> anyhow::Result { check_valid_string(name, stringify!(name))?; // SAFETY: Guaranteed to be non-zero - let interval_ns = NonZeroU64::new(std::cmp::max(interval_ns, 1) as u64).unwrap(); + let interval_ns = NonZeroU64::new(std::cmp::max(interval_ns, 1)).unwrap(); Ok(Self { name: Ustr::from(name), @@ -239,7 +239,7 @@ impl LiveTimer { ) -> anyhow::Result { check_valid_string(name, stringify!(name))?; // SAFETY: Guaranteed to be non-zero - let interval_ns = NonZeroU64::new(std::cmp::max(interval_ns, 1) as u64).unwrap(); + let interval_ns = NonZeroU64::new(std::cmp::max(interval_ns, 1)).unwrap(); debug!("Creating timer '{}'", name); Ok(Self { From 3373cba44f8216f089ade14ccdb014491e4137c0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 15 May 2024 18:29:59 +1000 Subject: [PATCH 174/193] Update dependencies including pyarrow --- nautilus_core/Cargo.lock | 12 +++--- nautilus_core/Cargo.toml | 2 +- poetry.lock | 88 ++++++++++++++++++++-------------------- pyproject.toml | 4 +- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 479e1eb89e49..43e994ead053 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4076,9 +4076,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -4168,18 +4168,18 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 006b2a0fc0d5..c24ea0ce2de7 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -41,7 +41,7 @@ rand = "0.8.5" rmp-serde = "1.3.0" rust_decimal = "1.35.0" rust_decimal_macros = "1.34.2" -serde = { version = "1.0.201", features = ["derive"] } +serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.60" diff --git a/poetry.lock b/poetry.lock index dc564e286614..704cdbdcb5c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1545,13 +1545,13 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pandas-stubs" -version = "2.2.1.240316" +version = "2.2.2.240514" description = "Type annotations for pandas" optional = false python-versions = ">=3.9" files = [ - {file = "pandas_stubs-2.2.1.240316-py3-none-any.whl", hash = "sha256:0126a26451a37cb893ea62357ca87ba3d181bd999ec8ba2ca5602e20207d6682"}, - {file = "pandas_stubs-2.2.1.240316.tar.gz", hash = "sha256:236a4f812fb6b1922e9607ff09e427f6d8540c421c9e5a40e3e4ddf7adac7f05"}, + {file = "pandas_stubs-2.2.2.240514-py3-none-any.whl", hash = "sha256:5d6f64d45a98bc94152a0f76fa648e598cd2b9ba72302fd34602479f0c391a53"}, + {file = "pandas_stubs-2.2.2.240514.tar.gz", hash = "sha256:85b20da44a62c80eb8389bcf4cbfe31cce1cafa8cca4bf1fc75ec45892e72ce8"}, ] [package.dependencies] @@ -1571,13 +1571,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -1659,47 +1659,47 @@ files = [ [[package]] name = "pyarrow" -version = "16.0.0" +version = "16.1.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-16.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:22a1fdb1254e5095d629e29cd1ea98ed04b4bbfd8e42cc670a6b639ccc208b60"}, - {file = "pyarrow-16.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:574a00260a4ed9d118a14770edbd440b848fcae5a3024128be9d0274dbcaf858"}, - {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0815d0ddb733b8c1b53a05827a91f1b8bde6240f3b20bf9ba5d650eb9b89cdf"}, - {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df0080339387b5d30de31e0a149c0c11a827a10c82f0c67d9afae3981d1aabb7"}, - {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:edf38cce0bf0dcf726e074159c60516447e4474904c0033f018c1f33d7dac6c5"}, - {file = "pyarrow-16.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91d28f9a40f1264eab2af7905a4d95320ac2f287891e9c8b0035f264fe3c3a4b"}, - {file = "pyarrow-16.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:99af421ee451a78884d7faea23816c429e263bd3618b22d38e7992c9ce2a7ad9"}, - {file = "pyarrow-16.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d22d0941e6c7bafddf5f4c0662e46f2075850f1c044bf1a03150dd9e189427ce"}, - {file = "pyarrow-16.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:266ddb7e823f03733c15adc8b5078db2df6980f9aa93d6bb57ece615df4e0ba7"}, - {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cc23090224b6594f5a92d26ad47465af47c1d9c079dd4a0061ae39551889efe"}, - {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56850a0afe9ef37249d5387355449c0f94d12ff7994af88f16803a26d38f2016"}, - {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:705db70d3e2293c2f6f8e84874b5b775f690465798f66e94bb2c07bab0a6bb55"}, - {file = "pyarrow-16.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5448564754c154997bc09e95a44b81b9e31ae918a86c0fcb35c4aa4922756f55"}, - {file = "pyarrow-16.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:729f7b262aa620c9df8b9967db96c1575e4cfc8c25d078a06968e527b8d6ec05"}, - {file = "pyarrow-16.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fb8065dbc0d051bf2ae2453af0484d99a43135cadabacf0af588a3be81fbbb9b"}, - {file = "pyarrow-16.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:20ce707d9aa390593ea93218b19d0eadab56390311cb87aad32c9a869b0e958c"}, - {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5823275c8addbbb50cd4e6a6839952682a33255b447277e37a6f518d6972f4e1"}, - {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ab8b9050752b16a8b53fcd9853bf07d8daf19093533e990085168f40c64d978"}, - {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:42e56557bc7c5c10d3e42c3b32f6cff649a29d637e8f4e8b311d334cc4326730"}, - {file = "pyarrow-16.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2a7abdee4a4a7cfa239e2e8d721224c4b34ffe69a0ca7981354fe03c1328789b"}, - {file = "pyarrow-16.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ef2f309b68396bcc5a354106741d333494d6a0d3e1951271849787109f0229a6"}, - {file = "pyarrow-16.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:ed66e5217b4526fa3585b5e39b0b82f501b88a10d36bd0d2a4d8aa7b5a48e2df"}, - {file = "pyarrow-16.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc8814310486f2a73c661ba8354540f17eef51e1b6dd090b93e3419d3a097b3a"}, - {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c2f5e239db7ed43e0ad2baf46a6465f89c824cc703f38ef0fde927d8e0955f7"}, - {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f293e92d1db251447cb028ae12f7bc47526e4649c3a9924c8376cab4ad6b98bd"}, - {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:dd9334a07b6dc21afe0857aa31842365a62eca664e415a3f9536e3a8bb832c07"}, - {file = "pyarrow-16.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:d91073d1e2fef2c121154680e2ba7e35ecf8d4969cc0af1fa6f14a8675858159"}, - {file = "pyarrow-16.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:71d52561cd7aefd22cf52538f262850b0cc9e4ec50af2aaa601da3a16ef48877"}, - {file = "pyarrow-16.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b93c9a50b965ee0bf4fef65e53b758a7e8dcc0c2d86cebcc037aaaf1b306ecc0"}, - {file = "pyarrow-16.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d831690844706e374c455fba2fb8cfcb7b797bfe53ceda4b54334316e1ac4fa4"}, - {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35692ce8ad0b8c666aa60f83950957096d92f2a9d8d7deda93fb835e6053307e"}, - {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dd3151d098e56f16a8389c1247137f9e4c22720b01c6f3aa6dec29a99b74d80"}, - {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:bd40467bdb3cbaf2044ed7a6f7f251c8f941c8b31275aaaf88e746c4f3ca4a7a"}, - {file = "pyarrow-16.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:00a1dcb22ad4ceb8af87f7bd30cc3354788776c417f493089e0a0af981bc8d80"}, - {file = "pyarrow-16.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:fda9a7cebd1b1d46c97b511f60f73a5b766a6de4c5236f144f41a5d5afec1f35"}, - {file = "pyarrow-16.0.0.tar.gz", hash = "sha256:59bb1f1edbbf4114c72415f039f1359f1a57d166a331c3229788ccbfbb31689a"}, + {file = "pyarrow-16.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:17e23b9a65a70cc733d8b738baa6ad3722298fa0c81d88f63ff94bf25eaa77b9"}, + {file = "pyarrow-16.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4740cc41e2ba5d641071d0ab5e9ef9b5e6e8c7611351a5cb7c1d175eaf43674a"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98100e0268d04e0eec47b73f20b39c45b4006f3c4233719c3848aa27a03c1aef"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68f409e7b283c085f2da014f9ef81e885d90dcd733bd648cfba3ef265961848"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a8914cd176f448e09746037b0c6b3a9d7688cef451ec5735094055116857580c"}, + {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48be160782c0556156d91adbdd5a4a7e719f8d407cb46ae3bb4eaee09b3111bd"}, + {file = "pyarrow-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cf389d444b0f41d9fe1444b70650fea31e9d52cfcb5f818b7888b91b586efff"}, + {file = "pyarrow-16.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d0ebea336b535b37eee9eee31761813086d33ed06de9ab6fc6aaa0bace7b250c"}, + {file = "pyarrow-16.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e73cfc4a99e796727919c5541c65bb88b973377501e39b9842ea71401ca6c1c"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9251264247ecfe93e5f5a0cd43b8ae834f1e61d1abca22da55b20c788417f6"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf5aace92d520d3d2a20031d8b0ec27b4395cab9f74e07cc95edf42a5cc0147"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:25233642583bf658f629eb230b9bb79d9af4d9f9229890b3c878699c82f7d11e"}, + {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a33a64576fddfbec0a44112eaf844c20853647ca833e9a647bfae0582b2ff94b"}, + {file = "pyarrow-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:185d121b50836379fe012753cf15c4ba9638bda9645183ab36246923875f8d1b"}, + {file = "pyarrow-16.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:2e51ca1d6ed7f2e9d5c3c83decf27b0d17bb207a7dea986e8dc3e24f80ff7d6f"}, + {file = "pyarrow-16.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06ebccb6f8cb7357de85f60d5da50e83507954af617d7b05f48af1621d331c9a"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04707f1979815f5e49824ce52d1dceb46e2f12909a48a6a753fe7cafbc44a0c"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d32000693deff8dc5df444b032b5985a48592c0697cb6e3071a5d59888714e2"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8785bb10d5d6fd5e15d718ee1d1f914fe768bf8b4d1e5e9bf253de8a26cb1628"}, + {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e1369af39587b794873b8a307cc6623a3b1194e69399af0efd05bb202195a5a7"}, + {file = "pyarrow-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:febde33305f1498f6df85e8020bca496d0e9ebf2093bab9e0f65e2b4ae2b3444"}, + {file = "pyarrow-16.1.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b5f5705ab977947a43ac83b52ade3b881eb6e95fcc02d76f501d549a210ba77f"}, + {file = "pyarrow-16.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d27bf89dfc2576f6206e9cd6cf7a107c9c06dc13d53bbc25b0bd4556f19cf5f"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d07de3ee730647a600037bc1d7b7994067ed64d0eba797ac74b2bc77384f4c2"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbef391b63f708e103df99fbaa3acf9f671d77a183a07546ba2f2c297b361e83"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19741c4dbbbc986d38856ee7ddfdd6a00fc3b0fc2d928795b95410d38bb97d15"}, + {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f2c5fb249caa17b94e2b9278b36a05ce03d3180e6da0c4c3b3ce5b2788f30eed"}, + {file = "pyarrow-16.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e6b6d3cd35fbb93b70ade1336022cc1147b95ec6af7d36906ca7fe432eb09710"}, + {file = "pyarrow-16.1.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:18da9b76a36a954665ccca8aa6bd9f46c1145f79c0bb8f4f244f5f8e799bca55"}, + {file = "pyarrow-16.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99f7549779b6e434467d2aa43ab2b7224dd9e41bdde486020bae198978c9e05e"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f07fdffe4fd5b15f5ec15c8b64584868d063bc22b86b46c9695624ca3505b7b4"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfe389a08ea374972bd4065d5f25d14e36b43ebc22fc75f7b951f24378bf0b5"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b20bd67c94b3a2ea0a749d2a5712fc845a69cb5d52e78e6449bbd295611f3aa"}, + {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ba8ac20693c0bb0bf4b238751d4409e62852004a8cf031c73b0e0962b03e45e3"}, + {file = "pyarrow-16.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:31a1851751433d89a986616015841977e0a188662fcffd1a5677453f1df2de0a"}, + {file = "pyarrow-16.1.0.tar.gz", hash = "sha256:15fbb22ea96d11f0b5768504a3f961edab25eaf4197c341720c4a387f6c60315"}, ] [package.dependencies] @@ -2669,4 +2669,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "434f862de93395f24c47c15ac385f0d27c200af07e733fd7253e39ecbc21ea86" +content-hash = "8209ca6f538d76ddc77943a96677a1b0dab8bc977509a60197f043974f33d322" diff --git a/pyproject.toml b/pyproject.toml index 4dc4cdf15d9f..fb6166eb9451 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ click = "^8.1.7" fsspec = "==2023.6.0" # Pinned due breaking changes msgspec = "^0.18.6" pandas = "^2.2.2" -pyarrow = ">=16.0.0" +pyarrow = ">=16.1.0" pytz = ">=2023.4.0" tqdm = "^4.66.4" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} @@ -81,7 +81,7 @@ optional = true black = "^24.4.2" docformatter = "^1.7.5" mypy = "^1.10.0" -pandas-stubs = "^2.2.1" +pandas-stubs = "^2.2.2" pre-commit = "^3.7.1" ruff = "^0.4.4" types-pytz = "^2023.3" From 3046583810824c44c8271e34a7c30aa06cffcec2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 15 May 2024 18:35:07 +1000 Subject: [PATCH 175/193] Upgrade databento --- nautilus_core/Cargo.lock | 4 ++-- nautilus_core/adapters/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 43e994ead053..8bd66fafbaad 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1142,9 +1142,9 @@ checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "databento" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0429639ce27e07a088b53b9e89dea7519c6e1871df5508a7ae33fc2c61b6cdf" +checksum = "b867144e234bd2db892e77568f09c6c39b1466e460ef4806d32eaa339a078d3c" dependencies = [ "dbn", "futures", diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 66a48147aa0a..8803ba4ebdd3 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -35,9 +35,9 @@ strum = { workspace = true } tokio = { workspace = true } thiserror = { workspace = true } ustr = { workspace = true } -databento = { version = "0.8.0", optional = true } +databento = { version = "0.9.0", optional = true } streaming-iterator = "0.1.9" -time = "0.3.35" +time = "0.3.36" [dev-dependencies] criterion = { workspace = true } From 95b449b4401c9ab40260797576e1bcb30d270022 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 16 May 2024 19:02:29 +1000 Subject: [PATCH 176/193] Update dependencies including databento --- nautilus_core/Cargo.lock | 22 +++++++++++----------- nautilus_core/adapters/Cargo.toml | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 8bd66fafbaad..e86f07cc04bb 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1088,9 +1088,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -1098,23 +1098,23 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", + "strsim 0.11.1", "syn 2.0.63", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", @@ -1142,9 +1142,9 @@ checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "databento" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867144e234bd2db892e77568f09c6c39b1466e460ef4806d32eaa339a078d3c" +checksum = "a338c81ee0781aa34b24423ca61c083620284e2a1a1198d20e73fb52e63ba2cd" dependencies = [ "dbn", "futures", @@ -5064,9 +5064,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" [[package]] name = "toml_edit" diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 8803ba4ebdd3..087576aa2472 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -35,7 +35,7 @@ strum = { workspace = true } tokio = { workspace = true } thiserror = { workspace = true } ustr = { workspace = true } -databento = { version = "0.9.0", optional = true } +databento = { version = "0.9.1", optional = true } streaming-iterator = "0.1.9" time = "0.3.36" From 923644f17c6e8414de4de3a683753e6ff721a881 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 16 May 2024 21:47:25 +1000 Subject: [PATCH 177/193] Add initial adapters development guide --- docs/developer_guide/adapters.md | 258 +++++++++++++++++++++++++++++++ docs/developer_guide/index.md | 2 + 2 files changed, 260 insertions(+) create mode 100644 docs/developer_guide/adapters.md diff --git a/docs/developer_guide/adapters.md b/docs/developer_guide/adapters.md new file mode 100644 index 000000000000..c1e72b831b56 --- /dev/null +++ b/docs/developer_guide/adapters.md @@ -0,0 +1,258 @@ +# Adapter Development Guide + +## Introduction + +This developer guide provides instructions on how to develop an integration adapter for the NautilusTrader platform. +Adapters provide connectivity to trading venues and data providers - converting their raw API +into a unified interface. + +## Structure of an Adapter + +An adapter typically consists of several components: +1. **Instrument Provider**: Supplies instrument definitions +2. **Data Client**: Handles live market data feeds and historical data requests +3. **Execution Client**: Handles order execution and management +5. **Configuration**: Configures the client settings + +## Steps to Implement a New Adapter + +1. Create a new Python subpackage for your adapter +2. Implement the Instrument Provider by inheriting from `InstrumentProvider` and implementing the necessary methods to load instruments +3. Implement the Data Client by inheriting from either the `LiveDataClient` and `LiveMarketDataClient` class as applicable, providing implementations for the required methods +4. Implement the Execution Client by inheriting from `LiveExecutionClient` and providing implementations for the required methods +5. Create configuration classes to hold your adapter’s settings +6. Test your adapter thoroughly to ensure all methods are correctly implemented and the adapter works as expected + +## Template for Building an Adapter + +Below is a step-by-step guide to building an adapter for a new data provider using the provided template. + +### Instrument Provider + +The `InstrumentProvider` supplies instrument definitions available on the venue. This +includes loading all available instruments, specific instruments by ID, and applying filters to the +instrument list. + +```{python} +from nautilus_trader.common.providers import InstrumentProvider +from nautilus_trader.model.identifiers import InstrumentId + +class TemplateInstrumentProvider(InstrumentProvider): + """ + An example template of an ``InstrumentProvider`` showing the minimal methods which must be implemented for an integration to be complete. + """ + + async def load_all_async(self, filters: dict | None = None) -> None: + raise NotImplementedError("method `load_all_async` must be implemented in the subclass") + + async def load_ids_async(self, instrument_ids: list[InstrumentId], filters: dict | None = None) -> None: + raise NotImplementedError("method `load_ids_async` must be implemented in the subclass") + + async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None) -> None: + raise NotImplementedError("method `load_async` must be implemented in the subclass") +``` + +**Key Methods:** +- `load_all_async`: Loads all instruments asynchronously, optionally applying filters +- `load_ids_async`: Loads specific instruments by their IDs +- `load_async`: Loads a single instrument by its ID + +### Data Client + +The `LiveDataClient` handles the subscription and management of data feeds that are not specifically +related to market data. This might include news feeds, custom data streams, or other data sources +that enhance trading strategies but do not directly represent market activity. + +```{python} +from nautilus_trader.live.data_client import LiveDataClient +from nautilus_trader.model.data import DataType +from nautilus_trader.core.uuid import UUID4 + +class TemplateLiveDataClient(LiveDataClient): + """ + An example of a ``LiveDataClient`` highlighting the overridable abstract methods. + """ + + async def _connect(self) -> None: + raise NotImplementedError("method `_connect` must be implemented in the subclass") + + async def _disconnect(self) -> None: + raise NotImplementedError("method `_disconnect` must be implemented in the subclass") + + def reset(self) -> None: + raise NotImplementedError("method `reset` must be implemented in the subclass") + + def dispose(self) -> None: + raise NotImplementedError("method `dispose` must be implemented in the subclass") + + async def _subscribe(self, data_type: DataType) -> None: + raise NotImplementedError("method `_subscribe` must be implemented in the subclass") + + async def _unsubscribe(self, data_type: DataType) -> None: + raise NotImplementedError("method `_unsubscribe` must be implemented in the subclass") + + async def _request(self, data_type: DataType, correlation_id: UUID4) -> None: + raise NotImplementedError("method `_request` must be implemented in the subclass") +``` + +**Key Methods:** +- `_connect`: Establishes a connection to the data provider +- `_disconnect`: Closes the connection to the data provider +- `reset`: Resets the state of the client +- `dispose`: Disposes of any resources held by the client +- `_subscribe`: Subscribes to a specific data type +- `_unsubscribe`: Unsubscribes from a specific data type +- `_request`: Requests data from the provider + +### Market Data Client + +The `MarketDataClient` handles market-specific data such as order books, top-of-book quotes and trade ticks, +and instrument status updates. It focuses on providing historical and real-time market data that is essential for +trading operations. + +```{python} +from nautilus_trader.live.data_client import LiveMarketDataClient +from nautilus_trader.model.data import BarType, DataType +from nautilus_trader.model.enums import BookType +from nautilus_trader.model.identifiers import InstrumentId + +class TemplateLiveMarketDataClient(LiveMarketDataClient): + """ + An example of a ``LiveMarketDataClient`` highlighting the overridable abstract methods. + """ + + async def _connect(self) -> None: + raise NotImplementedError("method `_connect` must be implemented in the subclass") + + async def _disconnect(self) -> None: + raise NotImplementedError("method `_disconnect` must be implemented in the subclass") + + def reset(self) -> None: + raise NotImplementedError("method `reset` must be implemented in the subclass") + + def dispose(self) -> None: + raise NotImplementedError("method `dispose` must be implemented in the subclass") + + async def _subscribe_instruments(self) -> None: + raise NotImplementedError("method `_subscribe_instruments` must be implemented in the subclass") + + async def _unsubscribe_instruments(self) -> None: + raise NotImplementedError("method `_unsubscribe_instruments` must be implemented in the subclass") + + async def _subscribe_order_book_deltas(self, instrument_id: InstrumentId, book_type: BookType, depth: int | None = None, kwargs: dict | None = None) -> None: + raise NotImplementedError("method `_subscribe_order_book_deltas` must be implemented in the subclass") + + async def _unsubscribe_order_book_deltas(self, instrument_id: InstrumentId) -> None: + raise NotImplementedError("method `_unsubscribe_order_book_deltas` must be implemented in the subclass") +``` + +**Key Methods:** +- `_connect`: Establishes a connection to the venues APIs +- `_disconnect`: Closes the connection to the venues APIs +- `reset`: Resets the state of the client +- `dispose`: Disposes of any resources held by the client +- `_subscribe_instruments`: Subscribes to market data for multiple instruments +- `_unsubscribe_instruments`: Unsubscribes from market data for multiple instruments +- `_subscribe_order_book_deltas`: Subscribes to order book delta updates +- `_unsubscribe_order_book_deltas`: Unsubscribes from order book delta updates + +### Execution Client + +The `ExecutionClient` is responsible for order management, including submission, modification, and +cancellation of orders. It is a crucial component of the adapter that interacts with the venues +trading system to manage and execute trades. + +```{python} +from nautilus_trader.execution.messages import CancelAllOrders, CancelOrder, ModifyOrder, SubmitOrder +from nautilus_trader.execution.reports import FillReport, OrderStatusReport, PositionStatusReport +from nautilus_trader.live.execution_client import LiveExecutionClient +from nautilus_trader.model.identifiers import ClientOrderId, InstrumentId, VenueOrderId + +class TemplateLiveExecutionClient(LiveExecutionClient): + """ + An example of a ``LiveExecutionClient`` highlighting the method requirements. + """ + + async def _connect(self) -> None: + raise NotImplementedError("method `_connect` must be implemented in the subclass") + + async def _disconnect(self) -> None: + raise NotImplementedError("method `_disconnect` must be implemented in the subclass") + + async def _submit_order(self, command: SubmitOrder) -> None: + raise NotImplementedError("method `_submit_order` must be implemented in the subclass") + + async def _modify_order(self, command: ModifyOrder) -> None: + raise NotImplementedError("method `_modify_order` must be implemented in the subclass") + + async def _cancel_order(self, command: CancelOrder) -> None: + raise NotImplementedError("method `_cancel_order` must be implemented in the subclass") + + async def _cancel_all_orders(self, command: CancelAllOrders) -> None: + raise NotImplementedError("method `_cancel_all_orders` must be implemented in the subclass") + + async def generate_order_status_report( + self, instrument_id: InstrumentId, client_order_id: ClientOrderId | None = None, venue_order_id: VenueOrderId | None = None + ) -> OrderStatusReport | None: + raise NotImplementedError("method `generate_order_status_report` must be implemented in the subclass") + + async def generate_order_status_reports( + self, instrument_id: InstrumentId | None = None, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, open_only: bool = False + ) -> list[OrderStatusReport]: + raise NotImplementedError("method `generate_order_status_reports` must be implemented in the subclass") + + async def generate_fill_reports( + self, instrument_id: InstrumentId | None = None, venue_order_id: VenueOrderId | None = None, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None + ) -> list[FillReport]: + raise NotImplementedError("method `generate_fill_reports` must be implemented in the subclass") + + async def generate_position_status_reports( + self, instrument_id: InstrumentId | None = None, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None + ) -> list[PositionStatusReport]: + raise NotImplementedError("method `generate_position_status_reports` must be implemented in the subclass") +``` + +**Key Methods:** +- `_connect`: Establishes a connection to the venues APIs +- `_disconnect`: Closes the connection to the venues APIs +- `_submit_order`: Submits a new order to the venue +- `_modify_order`: Modifies an existing order on the venue +- `_cancel_order`: Cancels a specific order on the venue +- `_cancel_all_orders`: Cancels all orders on the venue +- `generate_order_status_report`: Generates a report for a specific order on the venue +- `generate_order_status_reports`: Generates reports for all orders on the venue +- `generate_fill_reports`: Generates reports for filled orders on the venue +- `generate_position_status_reports`: Generates reports for position status on the venue + +### Configuration + +The configuration file defines settings specific to the adapter, such as API keys and connection +details. These settings are essential for initializing and managing the adapter’s connection to the +data provider. + +```{python} +from nautilus_trader.config import LiveDataClientConfig, LiveExecClientConfig + +class TemplateDataClientConfig(LiveDataClientConfig): + """ + Configuration for ``TemplateDataClient`` instances. + """ + + api_key: str + api_secret: str + base_url: str + +class TemplateExecClientConfig(LiveExecClientConfig): + """ + Configuration for ``TemplateExecClient`` instances. + """ + + api_key: str + api_secret: str + base_url: str +``` + +**Key Attributes:** +- `api_key`: The API key for authenticating with the data provider +- `api_secret`: The API secret for authenticating with the data provider +- `base_url`: The base URL for connecting to the data provider’s API diff --git a/docs/developer_guide/index.md b/docs/developer_guide/index.md index 06641f27dc4c..d43c0e162f81 100644 --- a/docs/developer_guide/index.md +++ b/docs/developer_guide/index.md @@ -10,6 +10,7 @@ cython.md rust.md testing.md + adapters.md packaged_data.md ``` @@ -51,4 +52,5 @@ types and how these map to their corresponding `PyObject` types. - [Cython](cython.md) - [Rust](rust.md) - [Testing](testing.md) +- [Adapters](adapters.md) - [Packaged Data](packaged_data.md) From 85f8e08e10352ceb236f7062af87629d0009f086 Mon Sep 17 00:00:00 2001 From: imemo88 <167953507+imemo88@users.noreply.github.com> Date: Thu, 16 May 2024 14:13:32 +0200 Subject: [PATCH 178/193] Improve Betfair session renewal (#1644) --- nautilus_trader/adapters/betfair/execution.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/nautilus_trader/adapters/betfair/execution.py b/nautilus_trader/adapters/betfair/execution.py index 64a10ecf5069..79ec3ce8e3f3 100644 --- a/nautilus_trader/adapters/betfair/execution.py +++ b/nautilus_trader/adapters/betfair/execution.py @@ -149,6 +149,7 @@ def __init__( self._strategy_hashes: dict[str, str] = {} self._set_account_id(AccountId(f"{BETFAIR_VENUE}-001")) AccountFactory.register_calculated_account(BETFAIR_VENUE.value) + self._reconnect_in_progress = False @property def instrument_provider(self) -> BetfairInstrumentProvider: @@ -184,11 +185,25 @@ async def _disconnect(self) -> None: # -- ERROR HANDLING --------------------------------------------------------------------------- async def on_api_exception(self, error: BetfairError) -> None: if "INVALID_SESSION_INFORMATION" in error.args[0]: - # Session is invalid, need to reconnect - self._log.warning("Invalid session error, reconnecting...") - await self._client.disconnect() - await self._connect() - self._log.info("Reconnected") + if self._reconnect_in_progress: + self._log.info("Reconnect already in progress.") + return + + # Avoid multiple reconnection attempts when multiple INVALID_SESSION_INFORMATION errors + # are received at "the same time" from the BF API. Simulaneous reconnection attempts + # will result in MAX_CONNECTION_LIMIT_EXCEEDED errors. + self._reconnect_in_progress = True + + try: + # Session is invalid, need to reconnect + self._log.warning("Invalid session error, reconnecting..") + await self._disconnect() + await self._connect() + self._log.info("Reconnected.") + except Exception: + self._log.error("Reconnection failed.", exc_info=True) + + self._reconnect_in_progress = False # -- ACCOUNT HANDLERS ------------------------------------------------------------------------- From a6ba752196be20249393968bd4bd098bfdf0923c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 16 May 2024 22:09:31 +1000 Subject: [PATCH 179/193] Refine core UUID4 --- nautilus_core/core/src/uuid.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index e1bd3c5c526b..e2ebfc1ab6ca 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -43,6 +43,7 @@ pub struct UUID4 { } impl UUID4 { + /// Creates a new `UUID4`. #[must_use] pub fn new() -> Self { let uuid = Uuid::new_v4(); @@ -54,6 +55,7 @@ impl UUID4 { Self { value } } + /// Converts the `UUID4` to a C string reference. #[must_use] pub fn to_cstr(&self) -> &CStr { // SAFETY: We always store valid C strings @@ -62,10 +64,10 @@ impl UUID4 { } impl FromStr for UUID4 { - type Err = &'static str; + type Err = uuid::Error; fn from_str(s: &str) -> Result { - let uuid = Uuid::parse_str(s).map_err(|_| "Invalid UUID string")?; + let uuid = Uuid::try_parse(s)?; let c_string = CString::new(uuid.to_string()).expect("`CString` conversion failed"); let bytes = c_string.as_bytes_with_nul(); let mut value = [0; UUID4_LEN]; @@ -138,6 +140,12 @@ mod tests { assert_eq!(uuid_parsed.to_string().len(), 36); } + #[rstest] + fn test_invalid_uuid() { + let invalid_uuid = "invalid-uuid-string"; + assert!(UUID4::from_str(invalid_uuid).is_err()); + } + #[rstest] fn test_default() { let uuid: UUID4 = UUID4::default(); From 88da572afbd159ad522e7b27f12d5729a747cc41 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 16 May 2024 22:19:52 +1000 Subject: [PATCH 180/193] Add additional timer docs in Rust --- nautilus_core/common/src/timer.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 5f0cb54dd145..fdea2c94c8c4 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -130,6 +130,7 @@ pub struct TestTimer { } impl TestTimer { + /// Creates a new `TestTimer`. pub fn new( name: &str, interval_ns: u64, @@ -150,11 +151,13 @@ impl TestTimer { }) } + /// Returns the next time in UNIX nanoseconds when the timer will fire. #[must_use] pub fn next_time_ns(&self) -> UnixNanos { self.next_time_ns } + /// Returns whether the timer is expired. #[must_use] pub fn is_expired(&self) -> bool { self.is_expired @@ -230,6 +233,7 @@ pub struct LiveTimer { } impl LiveTimer { + /// Creates a new `LiveTimer`. pub fn new( name: &str, interval_ns: u64, @@ -254,16 +258,19 @@ impl LiveTimer { }) } + /// Returns the next time in UNIX nanoseconds when the timer will fire. #[must_use] pub fn next_time_ns(&self) -> UnixNanos { UnixNanos::from(self.next_time_ns.load(atomic::Ordering::SeqCst)) } + /// Returns whether the timer is expired. #[must_use] pub fn is_expired(&self) -> bool { self.is_expired.load(atomic::Ordering::SeqCst) } + /// Starts the timer. pub fn start(&mut self) { let event_name = self.name; let stop_time_ns = self.stop_time_ns; From fab5a0ff675e5af63057347d1a40f68d68395659 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 16 May 2024 22:23:24 +1000 Subject: [PATCH 181/193] Update release notes --- RELEASES.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index f6aaea1b800d..271897d209f7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -18,18 +18,19 @@ Released on TBD (UTC). - Changed `Order.to_dict()` `commission` and `linked_order_id` fields to lists of strings rather than comma separated strings ### Fixes +- Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` - Fixed `TimeEvent` equality (now based on then event `id` rather than the event `name`) - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed venue order ID generation and application in sandbox mode (was previously generating additional venue order IDs), thanks for reporting @rsmb7z and @davidsblom - Fixed multiple fills causing overfills in sandbox mode (`OrderMatchingEngine` now cached filled quantity to prevent this), thanks @davidsblom +- Fixed `leaves_qty` exception message underflow (now correctly displays the projected negative leaves quantity) - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z - Fixed Interactive Brokers `IBOrder` attributes assignment (#1634), thanks @rsmb7z - Fixed IBKR reconnection after gateway/TWS disconnection (#1622), thanks @benjaminsingleton -- Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) -- Fixed `leaves_qty` exception message underflow (now correctly displays the projected negative leaves quantity) +- Fixed Betfair stream reconnection and avoid multiple reconnect attempts, thanks @imemo88 --- From 0d09adf0aeccd3f13c865f6f8e30c922fa0c3eea Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 16 May 2024 22:30:58 +1000 Subject: [PATCH 182/193] Upgrade betfair_parser --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 704cdbdcb5c3..13eabb26439c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -188,13 +188,13 @@ lxml = ["lxml"] [[package]] name = "betfair-parser" -version = "0.11.1" +version = "0.12.0" description = "A betfair parser" optional = true python-versions = "<4.0,>=3.9" files = [ - {file = "betfair_parser-0.11.1-py3-none-any.whl", hash = "sha256:df2be01ab95840878e5ac472153062f9b6debfbd76022512f75e578d74bad05c"}, - {file = "betfair_parser-0.11.1.tar.gz", hash = "sha256:9c3246dee0a82bdd90e3eb9ee4df5bc38a8cd65f763333e1e5018b59a1c49bfb"}, + {file = "betfair_parser-0.12.0-py3-none-any.whl", hash = "sha256:a2cf327647beb95dc40ee770c411ed91c67bdfb3ae7ed1714f1096375c2b055c"}, + {file = "betfair_parser-0.12.0.tar.gz", hash = "sha256:452680e8d670a114745f298543b53ffd92e7b3fb71255454dc14c6ca980d7362"}, ] [package.dependencies] @@ -2669,4 +2669,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "8209ca6f538d76ddc77943a96677a1b0dab8bc977509a60197f043974f33d322" +content-hash = "5b1edf888e0728a59228819f4a2629dee0925bc7505df18513aea7a1d81ba138" diff --git a/pyproject.toml b/pyproject.toml index fb6166eb9451..7d9c61d321bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ tqdm = "^4.66.4" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} async-timeout = {version = "^4.0.3", optional = true} -betfair_parser = {version = "==0.11.1", optional = true} # Pinned for stability +betfair_parser = {version = "==0.12.0", optional = true} # Pinned for stability defusedxml = {version = "^0.7.1", optional = true} docker = {version = "^7.0.0", optional = true} nautilus_ibapi = {version = "==10.19.2", optional = true} # Pinned for stability From d5d06b296c0fba071058b7b5672e271b26e9832a Mon Sep 17 00:00:00 2001 From: David Blom Date: Fri, 17 May 2024 09:50:53 +0200 Subject: [PATCH 183/193] Add bar_execution option to SandboxExecutionClient (#1646) fixes #1645 --- nautilus_trader/adapters/sandbox/config.py | 3 +++ nautilus_trader/adapters/sandbox/execution.py | 2 ++ nautilus_trader/adapters/sandbox/factory.py | 1 + 3 files changed, 6 insertions(+) diff --git a/nautilus_trader/adapters/sandbox/config.py b/nautilus_trader/adapters/sandbox/config.py index 41f6ce4e0aa2..6b1f17fb561b 100644 --- a/nautilus_trader/adapters/sandbox/config.py +++ b/nautilus_trader/adapters/sandbox/config.py @@ -28,9 +28,12 @@ class SandboxExecutionClientConfig(LiveExecClientConfig, frozen=True, kw_only=Tr The currency for this venue balance : int The starting balance for this venue + bar_execution: bool + If bars should be processed by the matching engine(s) (and move the market). """ venue: str currency: str balance: int + bar_execution: bool = True diff --git a/nautilus_trader/adapters/sandbox/execution.py b/nautilus_trader/adapters/sandbox/execution.py index 18d112a52329..e0c2ccdea5fe 100644 --- a/nautilus_trader/adapters/sandbox/execution.py +++ b/nautilus_trader/adapters/sandbox/execution.py @@ -87,6 +87,7 @@ def __init__( oms_type: OmsType = OmsType.NETTING, account_type: AccountType = AccountType.MARGIN, default_leverage: Decimal = Decimal(10), + bar_execution: bool = True, ) -> None: self._currency = Currency.from_str(currency) money = Money(value=balance, currency=self._currency) @@ -124,6 +125,7 @@ def __init__( fee_model=MakerTakerFeeModel(), latency_model=LatencyModel(0), clock=self.test_clock, + bar_execution=bar_execution, frozen_account=True, # <-- Freezing account ) self._client = BacktestExecClient( diff --git a/nautilus_trader/adapters/sandbox/factory.py b/nautilus_trader/adapters/sandbox/factory.py index 95f0479a373f..213e8c12271c 100644 --- a/nautilus_trader/adapters/sandbox/factory.py +++ b/nautilus_trader/adapters/sandbox/factory.py @@ -73,5 +73,6 @@ def create( # type: ignore venue=name or config.venue, balance=config.balance, currency=config.currency, + bar_execution=config.bar_execution, ) return exec_client From 0660436d05befe0e55fc9b8e02dfc4141ac5873c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 17 May 2024 17:55:47 +1000 Subject: [PATCH 184/193] Fix adapters guide title and code blocks --- docs/developer_guide/adapters.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/developer_guide/adapters.md b/docs/developer_guide/adapters.md index c1e72b831b56..335ff2c9595b 100644 --- a/docs/developer_guide/adapters.md +++ b/docs/developer_guide/adapters.md @@ -1,4 +1,4 @@ -# Adapter Development Guide +# Adapters ## Introduction @@ -33,7 +33,7 @@ The `InstrumentProvider` supplies instrument definitions available on the venue. includes loading all available instruments, specific instruments by ID, and applying filters to the instrument list. -```{python} +```python from nautilus_trader.common.providers import InstrumentProvider from nautilus_trader.model.identifiers import InstrumentId @@ -63,7 +63,7 @@ The `LiveDataClient` handles the subscription and management of data feeds that related to market data. This might include news feeds, custom data streams, or other data sources that enhance trading strategies but do not directly represent market activity. -```{python} +```python from nautilus_trader.live.data_client import LiveDataClient from nautilus_trader.model.data import DataType from nautilus_trader.core.uuid import UUID4 @@ -110,7 +110,7 @@ The `MarketDataClient` handles market-specific data such as order books, top-of- and instrument status updates. It focuses on providing historical and real-time market data that is essential for trading operations. -```{python} +```python from nautilus_trader.live.data_client import LiveMarketDataClient from nautilus_trader.model.data import BarType, DataType from nautilus_trader.model.enums import BookType @@ -162,7 +162,7 @@ The `ExecutionClient` is responsible for order management, including submission, cancellation of orders. It is a crucial component of the adapter that interacts with the venues trading system to manage and execute trades. -```{python} +```python from nautilus_trader.execution.messages import CancelAllOrders, CancelOrder, ModifyOrder, SubmitOrder from nautilus_trader.execution.reports import FillReport, OrderStatusReport, PositionStatusReport from nautilus_trader.live.execution_client import LiveExecutionClient @@ -230,7 +230,7 @@ The configuration file defines settings specific to the adapter, such as API key details. These settings are essential for initializing and managing the adapter’s connection to the data provider. -```{python} +```python from nautilus_trader.config import LiveDataClientConfig, LiveExecClientConfig class TemplateDataClientConfig(LiveDataClientConfig): From 876912de13d9bfb9a3c0a9d16de812519e1ebdaf Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 17 May 2024 18:16:00 +1000 Subject: [PATCH 185/193] Change OrderMatchingEngine bar processing conditions --- RELEASES.md | 6 ++++-- nautilus_trader/backtest/matching_engine.pyx | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 271897d209f7..3fa61121c874 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,8 +7,9 @@ Released on TBD (UTC). - Added `Cfd` and `Commodity` instruments with Interactive Brokers support (#1604), thanks @DracheShiki - Added `OrderMatchingEngine` futures and options contract activation and expiration simulation - Added Sandbox example with Interactive Brokers (#1618), thanks @rsmb7z -- Added `ParquetDataCatalog` S3 support (#1620), thanks benjaminsingleton -- Added `Bar.from_raw_arrays_to_list` (#1623), thanks rsmb7z +- Added `ParquetDataCatalog` S3 support (#1620), thanks @benjaminsingleton +- Added `Bar.from_raw_arrays_to_list` (#1623), thanks @rsmb7z +- Added `SandboxExecutionClientConfig.bar_execution` option, thanks @davidsblom - Improved venue order ID generation and assignment (it was previously possible for the `OrderMatchingEngine` to generate multiple IDs for the same order) - Improved `LiveTimer` robustness and flexibility by not requiring positive intervals or stop times in the future (will immediately produce a time event), thanks for reporting @davidsblom @@ -16,6 +17,7 @@ Released on TBD (UTC). - Removed `allow_cash_positions` config (simplify to the most common use case, spot trading should track positions) - Changed `tags` param and return type from `str` to `list[str]` (more naturally expresses multiple tags) - Changed `Order.to_dict()` `commission` and `linked_order_id` fields to lists of strings rather than comma separated strings +- Changed `OrderMatchingEngine` to no longer process internally aggregated bars for execution (no tests failed, but still classifying as a behavior change), thanks for reporting @davidsblom ### Fixes - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 8db257cb0f7b..972fec2a11c3 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -34,6 +34,7 @@ from nautilus_trader.core.data cimport Data from nautilus_trader.core.datetime cimport format_iso8601 from nautilus_trader.core.datetime cimport unix_nanos_to_dt from nautilus_trader.core.rust.model cimport AccountType +from nautilus_trader.core.rust.model cimport AggregationSource from nautilus_trader.core.rust.model cimport AggressorSide from nautilus_trader.core.rust.model cimport BookType from nautilus_trader.core.rust.model cimport ContingencyType @@ -478,6 +479,9 @@ cdef class OrderMatchingEngine: return # Can only process an L1 book with bars cdef BarType bar_type = bar.bar_type + if bar_type._mem.aggregation_source == AggregationSource.INTERNAL: + return # Do not process internally aggregated bars + cdef InstrumentId instrument_id = bar_type.instrument_id cdef BarType execution_bar_type = self._execution_bar_types.get(instrument_id) From 0abd58ae0b59b25b7cccdbb873ab6f0739fdb829 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 17 May 2024 21:59:50 +1000 Subject: [PATCH 186/193] Improve Cache logging consistency --- nautilus_core/common/src/cache/core.rs | 34 ++++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/nautilus_core/common/src/cache/core.rs b/nautilus_core/common/src/cache/core.rs index f51ab3412520..143995e25706 100644 --- a/nautilus_core/common/src/cache/core.rs +++ b/nautilus_core/common/src/cache/core.rs @@ -957,7 +957,7 @@ impl Cache { check_valid_string(key, stringify!(key))?; check_slice_not_empty(value.as_slice(), stringify!(value))?; - debug!("Add general {key}"); + debug!("Adding general {key}"); self.general.insert(key.to_string(), value.clone()); if let Some(database) = &self.database { @@ -968,14 +968,14 @@ impl Cache { /// Add the given order `book` to the cache. pub fn add_order_book(&mut self, book: OrderBook) -> anyhow::Result<()> { - debug!("Add `OrderBook` {}", book.instrument_id); + debug!("Adding `OrderBook` {}", book.instrument_id); self.books.insert(book.instrument_id, book); Ok(()) } /// Add the given `quote` tick to the cache. pub fn add_quote(&mut self, quote: QuoteTick) -> anyhow::Result<()> { - debug!("Add `QuoteTick` {}", quote.instrument_id); + debug!("Adding `QuoteTick` {}", quote.instrument_id); let quotes_deque = self .quotes .entry(quote.instrument_id) @@ -989,7 +989,7 @@ impl Cache { check_slice_not_empty(quotes, stringify!(quotes))?; let instrument_id = quotes[0].instrument_id; - debug!("Add `QuoteTick`[{}] {}", quotes.len(), instrument_id); + debug!("Adding `QuoteTick`[{}] {}", quotes.len(), instrument_id); let quotes_deque = self .quotes .entry(instrument_id) @@ -1003,7 +1003,7 @@ impl Cache { /// Add the given `trade` tick to the cache. pub fn add_trade(&mut self, trade: TradeTick) -> anyhow::Result<()> { - debug!("Add `TradeTick` {}", trade.instrument_id); + debug!("Adding `TradeTick` {}", trade.instrument_id); let trades_deque = self .trades .entry(trade.instrument_id) @@ -1017,7 +1017,7 @@ impl Cache { check_slice_not_empty(trades, stringify!(trades))?; let instrument_id = trades[0].instrument_id; - debug!("Add `TradeTick`[{}] {}", trades.len(), instrument_id); + debug!("Adding `TradeTick`[{}] {}", trades.len(), instrument_id); let trades_deque = self .trades .entry(instrument_id) @@ -1031,7 +1031,7 @@ impl Cache { /// Add the given `bar` to the cache. pub fn add_bar(&mut self, bar: Bar) -> anyhow::Result<()> { - debug!("Add `Bar` {}", bar.bar_type); + debug!("Adding `Bar` {}", bar.bar_type); let bars = self .bars .entry(bar.bar_type) @@ -1045,7 +1045,7 @@ impl Cache { check_slice_not_empty(bars, stringify!(bars))?; let bar_type = bars[0].bar_type; - debug!("Add `Bar`[{}] {}", bars.len(), bar_type); + debug!("Adding `Bar`[{}] {}", bars.len(), bar_type); let bars_deque = self .bars .entry(bar_type) @@ -1059,7 +1059,7 @@ impl Cache { /// Add the given `currency` to the cache. pub fn add_currency(&mut self, currency: Currency) -> anyhow::Result<()> { - debug!("Add `Currency` {}", currency.code); + debug!("Adding `Currency` {}", currency.code); if let Some(database) = &self.database { database.add_currency(¤cy)?; @@ -1071,7 +1071,7 @@ impl Cache { /// Add the given `instrument` to the cache. pub fn add_instrument(&mut self, instrument: InstrumentAny) -> anyhow::Result<()> { - debug!("Add `Instrument` {}", instrument.id()); + debug!("Adding `Instrument` {}", instrument.id()); if let Some(database) = &self.database { database.add_instrument(&instrument)?; @@ -1083,7 +1083,7 @@ impl Cache { /// Add the given `synthetic` instrument to the cache. pub fn add_synthetic(&mut self, synthetic: SyntheticInstrument) -> anyhow::Result<()> { - debug!("Add `SyntheticInstrument` {}", synthetic.id); + debug!("Adding `SyntheticInstrument` {}", synthetic.id); if let Some(database) = &self.database { database.add_synthetic(&synthetic)?; @@ -1095,7 +1095,7 @@ impl Cache { /// Add the given `account` to the cache. pub fn add_account(&mut self, account: Box) -> anyhow::Result<()> { - debug!("Add `Account` {}", account.id()); + debug!("Adding `Account` {}", account.id()); if let Some(database) = &self.database { database.add_account(account.as_ref())?; @@ -1185,7 +1185,7 @@ impl Cache { )?; }; - debug!("Added {:?}", order); + debug!("Adding {:?}", order); self.index.orders.insert(client_order_id); self.index @@ -1315,6 +1315,8 @@ impl Cache { self.index.positions.insert(position.id); self.index.positions_open.insert(position.id); + log::debug!("Adding {position}"); + self.add_position_id( &position.id, &position.instrument_id.venue, @@ -1335,12 +1337,6 @@ impl Cache { .or_default(); instrument_positions.insert(position.id); - log::debug!( - "Added Position(id={}, strategy_id={})", - position.id, - position.strategy_id, - ); - if let Some(database) = &mut self.database { database.add_position(&position)?; // TODO: Implement position snapshots From 95702c03064f44e00c9a592c6b77bd4a72f5298e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 18 May 2024 07:56:41 +1000 Subject: [PATCH 187/193] Update Rust dependencies --- nautilus_core/Cargo.lock | 114 ++++++++++++++++++++------------------- nautilus_core/Cargo.toml | 4 +- 2 files changed, 62 insertions(+), 56 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index e86f07cc04bb..c31b1d69a93e 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -144,9 +144,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "18b8795de6d09abb2b178fa5a9e3bb10da935750f33449a132b328b9391b2c6a" [[package]] name = "arc-swap" @@ -408,7 +408,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -420,6 +420,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -590,7 +596,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", "syn_derive", ] @@ -839,7 +845,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1107,7 +1113,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1118,7 +1124,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1461,7 +1467,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1503,7 +1509,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1513,7 +1519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1554,9 +1560,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" dependencies = [ "serde", ] @@ -1780,7 +1786,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -1831,9 +1837,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -1873,15 +1879,15 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http 1.1.0", "indexmap 2.2.6", "slab", @@ -2099,7 +2105,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.4", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -2234,9 +2240,9 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", "js-sys", @@ -2400,9 +2406,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -2423,9 +2429,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -2521,9 +2527,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -2996,7 +3002,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -3073,7 +3079,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -3289,7 +3295,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -3548,7 +3554,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -3561,7 +3567,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -3811,7 +3817,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.4", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "http-body-util", @@ -3953,7 +3959,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.63", + "syn 2.0.64", "unicode-ident", ] @@ -4065,9 +4071,9 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring", "rustls-pki-types", @@ -4183,7 +4189,7 @@ checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -4241,7 +4247,7 @@ checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -4431,7 +4437,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -4682,7 +4688,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -4704,9 +4710,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" dependencies = [ "proc-macro2", "quote", @@ -4722,7 +4728,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -4838,22 +4844,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -4977,7 +4983,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -5127,7 +5133,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -5247,7 +5253,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] @@ -5434,7 +5440,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", "wasm-bindgen-shared", ] @@ -5468,7 +5474,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5780,7 +5786,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.64", ] [[package]] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index c24ea0ce2de7..91be304fe3e1 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -26,7 +26,7 @@ description = "A high-performance algorithmic trading platform and event-driven documentation = "https://docs.nautilustrader.io" [workspace.dependencies] -anyhow = "1.0.83" +anyhow = "1.0.84" chrono = "0.4.38" derive_builder = "0.20.0" futures = "0.3.30" @@ -44,7 +44,7 @@ rust_decimal_macros = "1.34.2" serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" strum = { version = "0.26.2", features = ["derive"] } -thiserror = "1.0.60" +thiserror = "1.0.61" thousands = "0.2.0" tracing = "0.1.40" tokio = { version = "1.37.0", features = ["full"] } From 326ebf19c62f736315be07f8bac02ed0ea0a5f26 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 18 May 2024 08:51:37 +1000 Subject: [PATCH 188/193] Check redis version on connection --- docs/concepts/message_bus.md | 3 ++ nautilus_core/Cargo.lock | 2 + nautilus_core/Cargo.toml | 1 + nautilus_core/infrastructure/Cargo.toml | 2 + nautilus_core/infrastructure/src/redis/mod.rs | 49 ++++++++++++++++--- .../infrastructure/src/redis/msgbus.rs | 4 +- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/docs/concepts/message_bus.md b/docs/concepts/message_bus.md index 62a1a99ae8ba..60dc5cd9e205 100644 --- a/docs/concepts/message_bus.md +++ b/docs/concepts/message_bus.md @@ -38,6 +38,7 @@ integration written for it, this then allows external publishing of messages. ```{note} Currently Redis is supported for all serializable messages which are published. +The minimum supported Redis version is 6.2.0. ``` Under the hood, when a backing database (or any other compatible technology) is configured, @@ -169,4 +170,6 @@ Automatic stream trimming helps manage the size of your message streams by remov ```{note} The current Redis implementation will maintain the `autotrim_mins` as a maximum width (plus roughly a minute, as streams are trimmed no more than once per minute). Rather than for instance a maximum lookback window based on the current wall clock time. + +The minimum supported Redis version is 6.2.0. ``` diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index c31b1d69a93e..2bedd5d2b222 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2734,6 +2734,7 @@ name = "nautilus-infrastructure" version = "0.22.0" dependencies = [ "anyhow", + "log", "nautilus-common", "nautilus-core", "nautilus-model", @@ -2742,6 +2743,7 @@ dependencies = [ "rmp-serde", "rstest", "rust_decimal", + "semver", "serde_json", "serial_test", "sqlx", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 91be304fe3e1..253345f20588 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -41,6 +41,7 @@ rand = "0.8.5" rmp-serde = "1.3.0" rust_decimal = "1.35.0" rust_decimal_macros = "1.34.2" +semver = "1.0.23" serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" strum = { version = "0.26.2", features = ["derive"] } diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 5d2a7ae3be37..b523ae138214 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -16,8 +16,10 @@ nautilus-core = { path = "../core" , features = ["python"] } nautilus-model = { path = "../model" , features = ["python", "stubs"] } anyhow = { workspace = true } pyo3 = { workspace = true, optional = true } +log = { workspace = true } rmp-serde = { workspace = true } rust_decimal = { workspace = true } +semver = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tracing = {workspace = true } diff --git a/nautilus_core/infrastructure/src/redis/mod.rs b/nautilus_core/infrastructure/src/redis/mod.rs index 24b945aed94b..0f7a49b3fd0b 100644 --- a/nautilus_core/infrastructure/src/redis/mod.rs +++ b/nautilus_core/infrastructure/src/redis/mod.rs @@ -23,10 +23,12 @@ use std::{collections::HashMap, time::Duration}; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; use redis::*; +use semver::Version; use serde_json::{json, Value}; -use tracing::debug; +use tracing::{debug, info}; -const DELIMITER: char = ':'; +const REDIS_MIN_VERSION: &str = "6.2.0"; +const REDIS_DELIMITER: char = ':'; pub fn get_redis_url(database_config: &serde_json::Value) -> (String, String) { let host = database_config @@ -90,14 +92,27 @@ pub fn get_redis_url(database_config: &serde_json::Value) -> (String, String) { (url, redacted_url) } -pub fn create_redis_connection(database_config: &serde_json::Value) -> RedisResult { +pub fn create_redis_connection(database_config: &serde_json::Value) -> anyhow::Result { let (redis_url, redacted_url) = get_redis_url(database_config); debug!("Connecting to {redacted_url}"); let default_timeout = 20; let timeout = get_timeout_duration(database_config, default_timeout); let client = redis::Client::open(redis_url)?; - let conn = client.get_connection_with_timeout(timeout)?; - debug!("Connected"); + let mut conn = client.get_connection_with_timeout(timeout)?; + + let redis_version = get_redis_version(&mut conn)?; + let conn_msg = format!("Connected to redis v{redis_version}"); + let version = Version::parse(&redis_version)?; + let min_version = Version::parse(REDIS_MIN_VERSION)?; + + if version >= min_version { + info!(conn_msg); + } else { + // TODO: Using `log` error here so that the message is displayed regardless of whether + // the logging config has pyo3 enabled. Later we can standardize this to `tracing`. + log::error!("{conn_msg}, but minimum supported verson {REDIS_MIN_VERSION}"); + }; + Ok(conn) } @@ -129,12 +144,12 @@ fn get_stream_name( if let Some(json!(true)) = config.get("use_trader_id") { stream_name.push_str(trader_id.as_str()); - stream_name.push(DELIMITER); + stream_name.push(REDIS_DELIMITER); } if let Some(json!(true)) = config.get("use_instance_id") { stream_name.push_str(&format!("{instance_id}")); - stream_name.push(DELIMITER); + stream_name.push(REDIS_DELIMITER); } let stream_prefix = config @@ -143,10 +158,28 @@ fn get_stream_name( .as_str() .expect("Invalid configuration: `streams_prefix` is not a string"); stream_name.push_str(stream_prefix); - stream_name.push(DELIMITER); + stream_name.push(REDIS_DELIMITER); stream_name } +pub fn get_redis_version(conn: &mut Connection) -> anyhow::Result { + let info: String = redis::cmd("INFO").query(conn)?; + parse_redis_version(&info) +} + +fn parse_redis_version(info: &str) -> anyhow::Result { + for line in info.lines() { + if line.starts_with("redis_version:") { + let version = line + .split(':') + .nth(1) + .ok_or(anyhow::anyhow!("Version not found"))?; + return Ok(version.trim().to_string()); + } + } + Err(anyhow::anyhow!("Redis version not found in info")) +} + #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/nautilus_core/infrastructure/src/redis/msgbus.rs b/nautilus_core/infrastructure/src/redis/msgbus.rs index a3590116d064..6bbe0b4f5a65 100644 --- a/nautilus_core/infrastructure/src/redis/msgbus.rs +++ b/nautilus_core/infrastructure/src/redis/msgbus.rs @@ -31,6 +31,7 @@ use crate::redis::{create_redis_connection, get_buffer_interval, get_stream_name const XTRIM: &str = "XTRIM"; const MINID: &str = "MINID"; +const TRIM_BUFFER_SECONDS: u64 = 60; #[cfg_attr( feature = "python", @@ -184,9 +185,10 @@ fn drain_buffer( // Autotrim stream let last_trim_ms = last_trim_index.entry(key.clone()).or_insert(0); // Remove clone let unix_duration_now = duration_since_unix_epoch(); + let trim_buffer = Duration::from_secs(TRIM_BUFFER_SECONDS); // Improve efficiency of this by batching - if *last_trim_ms < (unix_duration_now - Duration::from_secs(60)).as_millis() as usize { + if *last_trim_ms < (unix_duration_now - trim_buffer).as_millis() as usize { let min_timestamp_ms = (unix_duration_now - autotrim_duration.unwrap()).as_millis() as usize; let result: Result<(), redis::RedisError> = redis::cmd(XTRIM) From dcc5677bba005bd2136d331496dcb791286cdd8e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 18 May 2024 09:53:52 +1000 Subject: [PATCH 189/193] Remove redundant Portfolio calculations Open positions will not have been adjusted at this point. --- nautilus_trader/portfolio/portfolio.pyx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/nautilus_trader/portfolio/portfolio.pyx b/nautilus_trader/portfolio/portfolio.pyx index 7552461f1445..9e87970cc200 100644 --- a/nautilus_trader/portfolio/portfolio.pyx +++ b/nautilus_trader/portfolio/portfolio.pyx @@ -450,28 +450,12 @@ cdef class Portfolio(PortfolioFacade): cdef list[Position] positions_open cdef AccountState account_state = None if isinstance(event, OrderFilled): - positions_open = self._cache.positions_open( - venue=None, # Faster query filtering - instrument_id=instrument.id, - ) - self._update_net_position( - instrument_id=instrument.id, - positions_open=positions_open - ) - self._accounts.update_balances( account=account, instrument=instrument, fill=event, ) - if account.type == AccountType.MARGIN and account.calculate_account_state: - self._accounts.update_positions( - account=account, - instrument=instrument, - positions_open=positions_open, - ts_event=event.ts_event, - ) self._unrealized_pnls[event.instrument_id] = self._calculate_unrealized_pnl( instrument_id=event.instrument_id, From ae9677a559c4be36fbeaf017a7ad92e23a82de63 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 18 May 2024 09:56:55 +1000 Subject: [PATCH 190/193] Fix CashAccount PnL and balance calculations --- RELEASES.md | 1 + nautilus_trader/accounting/accounts/cash.pyx | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 3fa61121c874..13475a7ac4a6 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -20,6 +20,7 @@ Released on TBD (UTC). - Changed `OrderMatchingEngine` to no longer process internally aggregated bars for execution (no tests failed, but still classifying as a behavior change), thanks for reporting @davidsblom ### Fixes +- Fixed `CashAccount` PnL and balance calculations (was adjusting filled quantity based on open position quantity - causing a desync and incorrect balance values) - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` - Fixed `TimeEvent` equality (now based on then event `id` rather than the event `name`) diff --git a/nautilus_trader/accounting/accounts/cash.pyx b/nautilus_trader/accounting/accounts/cash.pyx index 13c61a4fa9e3..45f1d03b2c00 100644 --- a/nautilus_trader/accounting/accounts/cash.pyx +++ b/nautilus_trader/accounting/accounts/cash.pyx @@ -335,20 +335,25 @@ cdef class CashAccount(Account): cdef Currency quote_currency = instrument.quote_currency cdef Currency base_currency = instrument.get_base_currency() - cdef double fill_qty = fill.last_qty.as_f64_c() cdef double fill_px = fill.last_px.as_f64_c() + cdef double fill_qty = fill.last_qty.as_f64_c() + cdef double last_qty = fill_qty + # TODO: This adjustment is potentially problematic and causing other bugs, + # the intent is to only 'book' PnL when a position is being reduced - rather than entered. if position is not None and position.quantity._mem.raw != 0: # Only book open quantity towards realized PnL fill_qty = fmin(fill_qty, position.quantity.as_f64_c()) + # Below we are using the original `last_qty` to adjust the base currency, + # this is to avoid a desync in account balance vs filled quantities later. if fill.order_side == OrderSide.BUY: if base_currency and not self.base_currency: - pnls[base_currency] = Money(fill_qty, base_currency) + pnls[base_currency] = Money(last_qty, base_currency) pnls[quote_currency] = Money(-(fill_px * fill_qty), quote_currency) elif fill.order_side == OrderSide.SELL: if base_currency and not self.base_currency: - pnls[base_currency] = Money(-fill_qty, base_currency) + pnls[base_currency] = Money(-last_qty, base_currency) pnls[quote_currency] = Money(fill_px * fill_qty, quote_currency) else: # pragma: no cover (design-time error) raise RuntimeError(f"invalid `OrderSide`, was {fill.order_side}") # pragma: no cover (design-time error) From 06bafaba62999126123d942d59aed3dcf9284333 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 18 May 2024 10:15:30 +1000 Subject: [PATCH 191/193] Add Cfd instrument optional base_currency --- nautilus_trader/model/instruments/cfd.pxd | 3 +++ nautilus_trader/model/instruments/cfd.pyx | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/model/instruments/cfd.pxd b/nautilus_trader/model/instruments/cfd.pxd index 8c50fadcfb9c..c75e68162234 100644 --- a/nautilus_trader/model/instruments/cfd.pxd +++ b/nautilus_trader/model/instruments/cfd.pxd @@ -14,9 +14,12 @@ # ------------------------------------------------------------------------------------------------- from nautilus_trader.model.instruments.base cimport Instrument +from nautilus_trader.model.objects cimport Currency cdef class Cfd(Instrument): + cdef readonly Currency base_currency + """The base currency for the instrument.\n\n:returns: `Currency` or ``None``""" cdef readonly str isin """The instruments International Securities Identification Number (ISIN).\n\n:returns: `str` or ``None``""" diff --git a/nautilus_trader/model/instruments/cfd.pyx b/nautilus_trader/model/instruments/cfd.pyx index ade41ec57c03..96adf5f25f83 100644 --- a/nautilus_trader/model/instruments/cfd.pyx +++ b/nautilus_trader/model/instruments/cfd.pyx @@ -70,6 +70,8 @@ cdef class Cfd(Instrument): The UNIX timestamp (nanoseconds) when the data event occurred. ts_init : uint64_t The UNIX timestamp (nanoseconds) when the data object was initialized. + base_currency : Currency + The base currency. lot_size : Quantity, optional The rounded lot unit size. max_quantity : Quantity, optional @@ -153,7 +155,6 @@ cdef class Cfd(Instrument): str tick_scheme_name = None, dict info = None, ): - super().__init__( instrument_id=instrument_id, raw_symbol=raw_symbol, @@ -183,6 +184,8 @@ cdef class Cfd(Instrument): info=info, ) + self.base_currency = base_currency + @staticmethod cdef Cfd from_dict_c(dict values): Condition.not_none(values, "values") @@ -234,6 +237,7 @@ cdef class Cfd(Instrument): "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "lot_size": str(obj.lot_size) if obj.lot_size is not None else None, + "base_currency": obj.base_currency.code if obj.base_currency is not None else None, "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, "max_notional": str(obj.max_notional) if obj.max_notional is not None else None, From da5451db30f4166780b8cd8a341eb2c607423a81 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 18 May 2024 10:15:43 +1000 Subject: [PATCH 192/193] Update release notes --- RELEASES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 13475a7ac4a6..e9f5ff0acc07 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,6 @@ # NautilusTrader 1.192.0 Beta -Released on TBD (UTC). +Released on 18th May 2024 (UTC). ### Enhancements - Added Nautilus CLI (see [docs](https://docs.nautilustrader.io/nightly/developer_guide/index.html)) (#1602), many thanks @filipmacek From 380169ce19f6609d9d16404ad58338a4b69ccef5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 18 May 2024 10:22:02 +1000 Subject: [PATCH 193/193] Update release notes --- RELEASES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index e9f5ff0acc07..d3e32439bb00 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,7 +9,7 @@ Released on 18th May 2024 (UTC). - Added Sandbox example with Interactive Brokers (#1618), thanks @rsmb7z - Added `ParquetDataCatalog` S3 support (#1620), thanks @benjaminsingleton - Added `Bar.from_raw_arrays_to_list` (#1623), thanks @rsmb7z -- Added `SandboxExecutionClientConfig.bar_execution` option, thanks @davidsblom +- Added `SandboxExecutionClientConfig.bar_execution` option (#1646), thanks @davidsblom - Improved venue order ID generation and assignment (it was previously possible for the `OrderMatchingEngine` to generate multiple IDs for the same order) - Improved `LiveTimer` robustness and flexibility by not requiring positive intervals or stop times in the future (will immediately produce a time event), thanks for reporting @davidsblom @@ -23,17 +23,17 @@ Released on 18th May 2024 (UTC). - Fixed `CashAccount` PnL and balance calculations (was adjusting filled quantity based on open position quantity - causing a desync and incorrect balance values) - Fixed `from_str` for `Price`, `Quantity` and `Money` when input string contains underscores in Rust, thanks for reporting @filipmacek - Fixed `Money` string parsing where the value from `str(money)` can now be passed to `Money.from_str` -- Fixed `TimeEvent` equality (now based on then event `id` rather than the event `name`) +- Fixed `TimeEvent` equality (now based on the event `id` rather than the event `name`) - Fixed `ParquetDataCatalog` bar queries by `instrument_id` which were no longer returning data (the intent is to use `bar_type`, however using `instrument_id` now returns all matching bars) - Fixed venue order ID generation and application in sandbox mode (was previously generating additional venue order IDs), thanks for reporting @rsmb7z and @davidsblom -- Fixed multiple fills causing overfills in sandbox mode (`OrderMatchingEngine` now cached filled quantity to prevent this), thanks @davidsblom +- Fixed multiple fills causing overfills in sandbox mode (`OrderMatchingEngine` now caching filled quantity to prevent this) (#1642), thanks @davidsblom - Fixed `leaves_qty` exception message underflow (now correctly displays the projected negative leaves quantity) - Fixed Interactive Brokers contract details parsing (#1615), thanks @rsmb7z - Fixed Interactive Brokers portfolio registration (#1616), thanks @rsmb7z - Fixed Interactive Brokers `IBOrder` attributes assignment (#1634), thanks @rsmb7z - Fixed IBKR reconnection after gateway/TWS disconnection (#1622), thanks @benjaminsingleton - Fixed Binance Futures account balance calculation (was over stating `free` balance with margin collateral, which could result in a negative `locked` balance) -- Fixed Betfair stream reconnection and avoid multiple reconnect attempts, thanks @imemo88 +- Fixed Betfair stream reconnection and avoid multiple reconnect attempts (#1644), thanks @imemo88 ---