From fbf67eb7905f49bf86e3c0b2c7e06181225a05c4 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sat, 20 Apr 2024 23:13:24 +0200 Subject: [PATCH] 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 }