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" diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 4ed2bcfa8f15..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] @@ -1196,7 +1401,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 { @@ -1701,14 +1906,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"; 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, 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"); + } } 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/orders/list.rs b/nautilus_core/model/src/orders/list.rs index 5c6f25e7f8c5..1593aa5ebb99 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_new_and_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=" + )); + } +} 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; +} 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(