From ceedd7fe9c495dce4de8e5b8b2497cf0d077a8d0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 3 Mar 2024 17:44:59 +1100 Subject: [PATCH] Add Databento statistics and imbalance schemas --- nautilus_core/Cargo.lock | 1 + nautilus_core/adapters/Cargo.toml | 1 + .../adapters/src/databento/decode.rs | 93 ++++++-- nautilus_core/adapters/src/databento/enums.rs | 123 ++++++++++ nautilus_core/adapters/src/databento/mod.rs | 1 + .../adapters/src/databento/python/enums.rs | 210 ++++++++++++++++++ .../adapters/src/databento/python/mod.rs | 6 + .../adapters/src/databento/python/types.rs | 115 ++++++++++ nautilus_core/adapters/src/databento/types.rs | 153 +++++++++++++ nautilus_trader/core/nautilus_pyo3.pyi | 69 ++++++ 10 files changed, 759 insertions(+), 13 deletions(-) create mode 100644 nautilus_core/adapters/src/databento/enums.rs create mode 100644 nautilus_core/adapters/src/databento/python/enums.rs create mode 100644 nautilus_core/adapters/src/databento/python/types.rs diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index daacd8c64256..64dda2ce515f 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2460,6 +2460,7 @@ dependencies = [ "serde", "serde_json", "streaming-iterator", + "strum 0.26.1", "thiserror", "time", "tokio", diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 7e25044885d9..4e6db2fb313a 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -27,6 +27,7 @@ rust_decimal_macros = { workspace = true } tracing = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +strum = { workspace = true } tokio = { workspace = true } thiserror = { workspace = true } ustr = { workspace = true } diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index 96c23ad36233..6d080c0b3184 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -34,8 +34,8 @@ use nautilus_model::{ Data, }, enums::{ - AggregationSource, AggressorSide, AssetClass, BarAggregation, BookAction, InstrumentClass, - OptionKind, OrderSide, PriceType, + AggregationSource, AggressorSide, AssetClass, BarAggregation, BookAction, FromU8, + InstrumentClass, OptionKind, OrderSide, PriceType, }, identifiers::{instrument_id::InstrumentId, trade_id::TradeId}, instruments::{ @@ -46,6 +46,11 @@ use nautilus_model::{ }; use ustr::Ustr; +use super::{ + enums::{DatabentoStatisticType, DatabentoStatisticUpdateAction}, + types::{DatabentoImbalance, DatabentoStatistics}, +}; + const BAR_SPEC_1S: BarSpecification = BarSpecification { step: 1, aggregation: BarAggregation::Second, @@ -141,7 +146,7 @@ pub fn parse_cfi_iso10926(value: &str) -> Result<(Option, Option Result { +pub fn decode_price(value: i64, currency: Currency) -> Result { match value { 0 | i64::MAX => Price::new( 10f64.powi(-i32::from(currency.precision)), @@ -151,6 +156,20 @@ pub fn decode_min_price_increment(value: i64, currency: Currency) -> Result Result> { + match value { + i64::MAX => Ok(None), + _ => Ok(Some(Price::from_raw(value, currency.precision)?)), + } +} + +pub fn decode_optional_quantity_i32(value: i32, currency: Currency) -> Result> { + match value { + i32::MAX => Ok(None), + _ => Ok(Some(Quantity::new(value as f64, currency.precision)?)), + } +} + /// # Safety /// /// - Assumes `ptr` is a valid C string pointer. @@ -182,7 +201,7 @@ pub fn decode_equity_v1( None, // No ISIN available yet currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Some(Quantity::new(msg.min_lot_size_round_lot.into(), 0)?), None, // TBD None, // TBD @@ -212,7 +231,7 @@ pub fn decode_futures_contract_v1( msg.expiration, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -245,7 +264,7 @@ pub fn decode_futures_spread_v1( msg.expiration, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -285,7 +304,7 @@ pub fn decode_options_contract_v1( Price::from_raw(msg.strike_price, currency.precision)?, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -325,7 +344,7 @@ pub fn decode_options_spread_v1( msg.expiration, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -736,7 +755,7 @@ pub fn decode_equity( None, // No ISIN available yet currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Some(Quantity::new(msg.min_lot_size_round_lot.into(), 0)?), None, // TBD None, // TBD @@ -766,7 +785,7 @@ pub fn decode_futures_contract( msg.expiration, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -799,7 +818,7 @@ pub fn decode_futures_spread( msg.expiration, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -839,7 +858,7 @@ pub fn decode_options_contract( Price::from_raw(msg.strike_price, currency.precision)?, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -879,7 +898,7 @@ pub fn decode_options_spread( msg.expiration, currency, currency.precision, - decode_min_price_increment(msg.min_price_increment, currency)?, + decode_price(msg.min_price_increment, currency)?, Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD @@ -890,3 +909,51 @@ pub fn decode_options_spread( ts_init, ) } + +pub fn decode_imbalance_msg( + msg: &dbn::ImbalanceMsg, + instrument_id: InstrumentId, + ts_init: UnixNanos, +) -> anyhow::Result { + DatabentoImbalance::new( + instrument_id, + Price::from_raw(msg.ref_price, 2)?, + Price::from_raw(msg.cont_book_clr_price, 2)?, + Price::from_raw(msg.auct_interest_clr_price, 2)?, + Quantity::new(msg.paired_qty as f64, 0)?, + Quantity::new(msg.total_imbalance_qty as f64, 0)?, + parse_order_side(msg.side), + msg.significant_imbalance as c_char, + msg.hd.ts_event, + msg.ts_recv, + ts_init, + ) +} + +pub fn decode_statistics_msg( + msg: &dbn::StatMsg, + instrument_id: InstrumentId, + ts_init: UnixNanos, +) -> anyhow::Result { + let currency = Currency::USD(); // Hard coded for now + let stat_type = DatabentoStatisticType::from_u8(msg.stat_type as u8) + .expect("Invalid value for `stat_type`"); + let update_action = DatabentoStatisticUpdateAction::from_u8(msg.stat_type as u8) + .expect("Invalid value for `update_action`"); + + DatabentoStatistics::new( + instrument_id, + stat_type, + update_action, + decode_optional_price(msg.price, currency)?, + decode_optional_quantity_i32(msg.quantity, currency)?, + msg.channel_id, + msg.stat_flags, + msg.sequence, + msg.ts_ref, + msg.ts_in_delta, + msg.hd.ts_event, + msg.ts_recv, + ts_init, + ) +} diff --git a/nautilus_core/adapters/src/databento/enums.rs b/nautilus_core/adapters/src/databento/enums.rs new file mode 100644 index 000000000000..0e560533f2c4 --- /dev/null +++ b/nautilus_core/adapters/src/databento/enums.rs @@ -0,0 +1,123 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +//! Defines enumerations for the Databento integration. + +use std::str::FromStr; + +use nautilus_model::{enum_strum_serde, enums::FromU8}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use strum::{AsRefStr, Display, EnumIter, EnumString, FromRepr}; + +/// Represents a Databento statistic type. +#[repr(C)] +#[derive( + Copy, + Clone, + Debug, + Display, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + AsRefStr, + FromRepr, + EnumIter, + EnumString, +)] +#[strum(ascii_case_insensitive)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento") +)] +pub enum DatabentoStatisticType { + OpeningPrice = 1, + IndicativeOpeningPrice = 2, + SettlementPrice = 3, + TradingSessionLowPrice = 4, + TradingSessionHighPrice = 5, + ClearedVolume = 6, + LowestOffer = 7, + HighestBid = 8, + OpenInterest = 9, + FixingPrice = 10, + ClosePrice = 11, + NetChange = 12, + Vwap = 13, +} + +impl FromU8 for DatabentoStatisticType { + fn from_u8(value: u8) -> Option { + match value { + 1 => Some(Self::OpeningPrice), + 2 => Some(Self::IndicativeOpeningPrice), + 3 => Some(Self::SettlementPrice), + 4 => Some(Self::TradingSessionLowPrice), + 5 => Some(Self::TradingSessionHighPrice), + 6 => Some(Self::ClearedVolume), + 7 => Some(Self::LowestOffer), + 8 => Some(Self::HighestBid), + 9 => Some(Self::OpenInterest), + 10 => Some(Self::FixingPrice), + 11 => Some(Self::ClosePrice), + 12 => Some(Self::NetChange), + 13 => Some(Self::Vwap), + _ => None, + } + } +} + +/// Represents a Databento statistic update action. +#[repr(C)] +#[derive( + Copy, + Clone, + Debug, + Display, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + AsRefStr, + FromRepr, + EnumIter, + EnumString, +)] +#[strum(ascii_case_insensitive)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento") +)] +pub enum DatabentoStatisticUpdateAction { + Added = 1, + Deleted = 2, +} + +impl FromU8 for DatabentoStatisticUpdateAction { + fn from_u8(value: u8) -> Option { + match value { + 1 => Some(Self::Added), + 2 => Some(Self::Deleted), + _ => None, + } + } +} + +enum_strum_serde!(DatabentoStatisticType); +enum_strum_serde!(DatabentoStatisticUpdateAction); diff --git a/nautilus_core/adapters/src/databento/mod.rs b/nautilus_core/adapters/src/databento/mod.rs index 7dd3cb80a161..8b45d33b78f2 100644 --- a/nautilus_core/adapters/src/databento/mod.rs +++ b/nautilus_core/adapters/src/databento/mod.rs @@ -15,6 +15,7 @@ pub mod common; pub mod decode; +pub mod enums; pub mod live; pub mod loader; pub mod symbology; diff --git a/nautilus_core/adapters/src/databento/python/enums.rs b/nautilus_core/adapters/src/databento/python/enums.rs new file mode 100644 index 000000000000..db045cca0f5d --- /dev/null +++ b/nautilus_core/adapters/src/databento/python/enums.rs @@ -0,0 +1,210 @@ +// ------------------------------------------------------------------------------------------------- +// 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 pyo3::{prelude::*, types::PyType, PyTypeInfo}; + +use crate::databento::enums::{DatabentoStatisticType, DatabentoStatisticUpdateAction}; + +#[pymethods] +impl DatabentoStatisticType { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> anyhow::Result { + let t = Self::type_object(py); + Self::py_from_str(t, value) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + self.to_string() + } + + fn __repr__(&self) -> String { + format!( + "<{}.{}: '{}'>", + stringify!(DatabentoStatisticType), + self.name(), + self.value(), + ) + } + + #[getter] + #[must_use] + pub fn name(&self) -> String { + self.to_string() + } + + #[getter] + #[must_use] + pub fn value(&self) -> u8 { + *self as u8 + } + + // #[classmethod] + // fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + // EnumIterator::new::(py) + // } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, data: &PyAny) -> anyhow::Result { + let data_str: &str = data.str().and_then(|s| s.extract())?; + let tokenized = data_str.to_uppercase(); + Self::from_str(&tokenized).map_err(anyhow::Error::new) + } + #[classattr] + #[pyo3(name = "OPENING_PRICE")] + fn py_opening_price() -> Self { + Self::OpeningPrice + } + + #[classattr] + #[pyo3(name = "INDICATIVE_OPENING_PRICE")] + fn py_indicative_opening_price() -> Self { + Self::IndicativeOpeningPrice + } + + #[classattr] + #[pyo3(name = "SETTLEMENT_PRICE")] + fn py_settlement_price() -> Self { + Self::SettlementPrice + } + + #[classattr] + #[pyo3(name = "TRADING_SESSION_LOW_PRICE")] + fn py_trading_session_low_price() -> Self { + Self::TradingSessionLowPrice + } + + #[classattr] + #[pyo3(name = "TRADING_SESSION_HIGH_PRICE")] + fn py_trading_session_high_price() -> Self { + Self::TradingSessionHighPrice + } + + #[classattr] + #[pyo3(name = "CLEARED_VOLUME")] + fn py_cleared_volume() -> Self { + Self::ClearedVolume + } + + #[classattr] + #[pyo3(name = "LOWEST_OFFER")] + fn py_lowest_offer() -> Self { + Self::LowestOffer + } + + #[classattr] + #[pyo3(name = "HIGHEST_BID")] + fn py_highest_bid() -> Self { + Self::HighestBid + } + + #[classattr] + #[pyo3(name = "OPEN_INTEREST")] + fn py_open_interest() -> Self { + Self::OpenInterest + } + + #[classattr] + #[pyo3(name = "FIXING_PRICE")] + fn py_fixing_price() -> Self { + Self::FixingPrice + } + + #[classattr] + #[pyo3(name = "CLOSE_PRICE")] + fn py_close_price() -> Self { + Self::ClosePrice + } + + #[classattr] + #[pyo3(name = "NET_CHANGE")] + fn py_net_change() -> Self { + Self::NetChange + } + + #[classattr] + #[pyo3(name = "VWAP")] + fn py_vwap() -> Self { + Self::Vwap + } +} + +#[pymethods] +impl DatabentoStatisticUpdateAction { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> anyhow::Result { + let t = Self::type_object(py); + Self::py_from_str(t, value) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + self.to_string() + } + + fn __repr__(&self) -> String { + format!( + "<{}.{}: '{}'>", + stringify!(DatabentoStatisticUpdateAction), + self.name(), + self.value(), + ) + } + + #[getter] + #[must_use] + pub fn name(&self) -> String { + self.to_string() + } + + #[getter] + #[must_use] + pub fn value(&self) -> u8 { + *self as u8 + } + + // #[classmethod] + // fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + // EnumIterator::new::(py) + // } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, data: &PyAny) -> anyhow::Result { + let data_str: &str = data.str().and_then(|s| s.extract())?; + let tokenized = data_str.to_uppercase(); + Self::from_str(&tokenized).map_err(anyhow::Error::new) + } + #[classattr] + #[pyo3(name = "ADDED")] + fn py_added() -> Self { + Self::Added + } + + #[classattr] + #[pyo3(name = "DELETED")] + fn py_deleted() -> Self { + Self::Deleted + } +} diff --git a/nautilus_core/adapters/src/databento/python/mod.rs b/nautilus_core/adapters/src/databento/python/mod.rs index 1f0ba5cded4e..5da249685115 100644 --- a/nautilus_core/adapters/src/databento/python/mod.rs +++ b/nautilus_core/adapters/src/databento/python/mod.rs @@ -14,16 +14,22 @@ // ------------------------------------------------------------------------------------------------- pub mod decode; +pub mod enums; pub mod historical; pub mod live; pub mod loader; +pub mod types; use pyo3::prelude::*; /// Loaded as nautilus_pyo3.databento #[pymodule] pub fn databento(_: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/nautilus_core/adapters/src/databento/python/types.rs b/nautilus_core/adapters/src/databento/python/types.rs new file mode 100644 index 000000000000..ba7a0e89f8f3 --- /dev/null +++ b/nautilus_core/adapters/src/databento/python/types.rs @@ -0,0 +1,115 @@ +// ------------------------------------------------------------------------------------------------- +// 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::python::serialization::from_dict_pyo3; +use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; + +use crate::databento::types::{DatabentoImbalance, DatabentoStatistics}; + +#[pymethods] +impl DatabentoImbalance { + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } + } + + fn __repr__(&self) -> String { + format!( + "{}(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={})", + stringify!(DatabentoImbalance), + self.instrument_id, + self.ref_price, + self.cont_book_clr_price, + self.auct_interest_clr_price, + self.paired_qty, + self.total_imbalance_qty, + self.side, + self.significant_imbalance, + self.ts_event, + self.ts_recv, + self.ts_init, + ) + } + + fn __str__(&self) -> String { + self.__repr__() + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + // TODO + #[pyo3(name = "to_dict")] + pub fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("type", stringify!(DatabentoImbalance))?; + Ok(dict.into()) + } +} + +#[pymethods] +impl DatabentoStatistics { + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } + } + + fn __repr__(&self) -> String { + format!( + "{}(instrument_id={}, stat_type={}, update_action={}, price=TBD, quantity=TBD, channel_id={}, stat_flags={}, sequence={}, ts_ref={}, ts_in_delta={}, ts_event={}, ts_recv={}, ts_init={})", + stringify!(DatabentoStatistics), + self.instrument_id, + self.stat_type, + self.update_action, + // self.price, // TODO: Implement display for Option + // self.quantity, // TODO: Implement display for Option + self.channel_id, + self.stat_flags, + self.sequence, + self.ts_ref, + self.ts_in_delta, + self.ts_event, + self.ts_recv, + self.ts_init, + ) + } + + fn __str__(&self) -> String { + self.__repr__() + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + // TODO + #[pyo3(name = "to_dict")] + pub fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("type", stringify!(DatabentoStatistics))?; + Ok(dict.into()) + } +} diff --git a/nautilus_core/adapters/src/databento/types.rs b/nautilus_core/adapters/src/databento/types.rs index 2ead53c59c10..21057d6970b3 100644 --- a/nautilus_core/adapters/src/databento/types.rs +++ b/nautilus_core/adapters/src/databento/types.rs @@ -13,23 +13,176 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +use std::ffi::c_char; + +use nautilus_core::time::UnixNanos; +use nautilus_model::{ + enums::OrderSide, + identifiers::instrument_id::InstrumentId, + types::{price::Price, quantity::Quantity}, +}; use serde::Deserialize; use ustr::Ustr; +use super::enums::{DatabentoStatisticType, DatabentoStatisticUpdateAction}; + /// Represents a Databento publisher ID. pub type PublisherId = u16; /// Represents a Databento dataset code. pub type Dataset = Ustr; +/// Represents a Databento publisher. #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento") )] #[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] pub struct DatabentoPublisher { + /// The publisher ID assigned by Databento, which denotes the dataset and venue. pub publisher_id: PublisherId, + /// The Databento dataset code for the publisher. pub dataset: dbn::Dataset, + /// The venue for the publisher. pub venue: dbn::Venue, + /// The publisher description. pub description: String, } + +/// Represents an auction imbalance. +/// +/// This data type includes the populated data fields provided by `Databento`, +/// except for the `publisher_id` and `instrument_id` integers. +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento") +)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +pub struct DatabentoImbalance { + // The instrument ID for the imbalance data. + pub instrument_id: InstrumentId, + // The reference price at which the imbalance shares are calculated. + pub ref_price: Price, + // The hypothetical auction-clearing price for both cross and continuous orders. + pub cont_book_clr_price: Price, + // The hypothetical auction-clearing price for cross orders only. + pub auct_interest_clr_price: Price, + // The quantity of shares which are eligible to be matched at `ref_price`. + pub paired_qty: Quantity, + // The quantity of shares which are not paired at `ref_price`. + pub total_imbalance_qty: Quantity, + // The market side of the `total_imbalance_qty` (can be `NO_ORDER_SIDE`). + pub side: OrderSide, + // A venue-specific character code. For Nasdaq, contains the raw Price Variation Indicator. + pub significant_imbalance: c_char, + // The UNIX timestamp (nanoseconds) when the data event occurred. + pub ts_event: UnixNanos, + // The UNIX timestamp (nanoseconds) when the data object was received by Databento. + pub ts_recv: UnixNanos, + // The UNIX timestamp (nanoseconds) when the data object was initialized. + pub ts_init: UnixNanos, +} + +impl DatabentoImbalance { + #[allow(clippy::too_many_arguments)] + pub fn new( + instrument_id: InstrumentId, + ref_price: Price, + cont_book_clr_price: Price, + auct_interest_clr_price: Price, + paired_qty: Quantity, + total_imbalance_qty: Quantity, + side: OrderSide, + significant_imbalance: c_char, + ts_event: UnixNanos, + ts_recv: UnixNanos, + ts_init: UnixNanos, + ) -> anyhow::Result { + Ok(Self { + 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, + }) + } +} + +/// Represents a statistics. +/// +/// This data type includes the populated data fields provided by `Databento`, +/// except for the `publisher_id` and `instrument_id` integers. +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento") +)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +pub struct DatabentoStatistics { + // The instrument ID for the statistics message. + pub instrument_id: InstrumentId, + // The type of statistic value contained in the message. + pub stat_type: DatabentoStatisticType, + // Indicates if the statistic is newly added (1) or deleted (2). (Deleted is only used with some stat_types). + pub update_action: DatabentoStatisticUpdateAction, + // The statistics price. + pub price: Option, + // The value for non-price statistics. + pub quantity: Option, + // The channel ID within the venue. + pub channel_id: u16, + // Additional flags associated with certain stat types. + pub stat_flags: u8, + // The message sequence number assigned at the venue. + pub sequence: u32, + // The UNIX timestamp (nanoseconds) Databento `ts_ref` reference timestamp). + pub ts_ref: UnixNanos, + // The matching-engine-sending timestamp expressed as the number of nanoseconds before the Databento `ts_recv`. + pub ts_in_delta: i32, + // The UNIX timestamp (nanoseconds) when the data event occurred. + pub ts_event: UnixNanos, + // The UNIX timestamp (nanoseconds) when the data object was received by Databento. + pub ts_recv: UnixNanos, + // The UNIX timestamp (nanoseconds) when the data object was initialized. + pub ts_init: UnixNanos, +} + +impl DatabentoStatistics { + #[allow(clippy::too_many_arguments)] + pub fn new( + instrument_id: InstrumentId, + stat_type: DatabentoStatisticType, + update_action: DatabentoStatisticUpdateAction, + price: Option, + quantity: Option, + channel_id: u16, + stat_flags: u8, + sequence: u32, + ts_ref: UnixNanos, + ts_in_delta: i32, + ts_event: UnixNanos, + ts_recv: UnixNanos, + ts_init: UnixNanos, + ) -> anyhow::Result { + Ok(Self { + instrument_id, + stat_type, + update_action, + price, + quantity, + channel_id, + stat_flags, + sequence, + ts_ref, + ts_in_delta, + ts_event, + ts_recv, + ts_init, + }) + } +} diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 8a99b2e068f8..689f07f01eff 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -2226,6 +2226,25 @@ class BookImbalanceRatio: # Databento +class DatabentoStatisticType(Enum): + OPENING_PRICE = "OPENING_PRICE" + INDICATIVE_OPENING_PRICE = "INDICATIVE_OPENING_PRICE" + SETTLEMENT_PRICE = "SETTLEMENT_PRICE" + TRADING_SESSION_LOW_PRICE = "TRADING_SESSION_LOW_PRICE" + TRADING_SESSION_HIGH_PRICE = "TRADING_SESSION_HIGH_PRICE" + CLEARED_VOLUME = "CLEARED_VOLUME" + LOWEST_OFFER = "LOWEST_OFFER" + HIGHEST_BID = "HIGHEST_BID" + OPEN_INTEREST = "OPEN_INTEREST" + FIXING_PRICE = "FIXING_PRICE" + CLOSE_PRICE = "CLOSE_PRICE" + NET_CHANGE = "NET_CHANGE" + VWAP = "VWAP" + +class DatabentoStatisticUpdateAction(Enum): + ADDED = "ADDED" + DELETED = "DELETED" + class DatabentoPublisher: @property def publisher_id(self) -> int: ... @@ -2236,6 +2255,56 @@ class DatabentoPublisher: @property def description(self) -> str: ... +class DatabentoImbalance: + @property + def instrument_id(self) -> InstrumentId: ... + @property + def ref_price(self) -> Price: ... + @property + def cont_book_clr_price(self) -> Price: ... + @property + def auct_interest_clr_price(self) -> Price: ... + @property + def paired_qty(self) -> Quantity: ... + @property + def total_imbalance_qty(self) -> Quantity: ... + @property + def side(self) -> OrderSide: ... + @property + def significant_imbalance(self) -> str: ... + @property + def ts_event(self) -> int: ... + @property + def ts_init(self) -> int: ... + +class DatabentoStatistics: + @property + def instrument_id(self) -> InstrumentId: ... + @property + def stat_type(self) -> DatabentoStatisticType: ... + @property + def update_action(self) -> DatabentoStatisticUpdateAction: ... + @property + def price(self) -> Price | None: ... + @property + def quantity(self) -> Quantity | None: ... + @property + def channel_id(self) -> int: ... + @property + def stat_flags(self) -> int: ... + @property + def sequence(self) -> int: ... + @property + def ts_ref(self) -> int: ... + @property + def ts_in_delta(self) -> int: ... + @property + def ts_event(self) -> int: ... + @property + def ts_recv(self) -> int: ... + @property + def ts_init(self) -> int: ... + class DatabentoDataLoader: def __init__( self,