diff --git a/CHANGELOG.md b/CHANGELOG.md index e15f297..9b903fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 0.15.0 - TBD ### Enhancements +- Improved `Debug` implementation for all record types + - Prices are formatted as decimals + - Fixed-length strings are formatted as strings + - Bit flag fields are formatted as binary + - Several fields are formatted as enums instead of their raw representations - Added `--schema` option to `dbn` CLI tool to filter a DBN to a particular schema. This allows outputting saved live data to CSV - Allowed passing `--limit` option to `dbn` CLI tool with `--metadata` flag diff --git a/rust/dbn-macros/src/dbn_attr.rs b/rust/dbn-macros/src/dbn_attr.rs index 5f41bdf..3127836 100644 --- a/rust/dbn-macros/src/dbn_attr.rs +++ b/rust/dbn-macros/src/dbn_attr.rs @@ -8,6 +8,8 @@ use syn::{parenthesized, spanned::Spanned, token, Field, FieldsNamed, Meta}; pub const C_CHAR_ATTR: &str = "c_char"; pub const FIXED_PRICE_ATTR: &str = "fixed_price"; +pub const FMT_BINARY: &str = "fmt_binary"; +pub const FMT_METHOD: &str = "fmt_method"; pub const INDEX_TS_ATTR: &str = "index_ts"; pub const SKIP_ATTR: &str = "skip"; pub const UNIX_NANOS_ATTR: &str = "unix_nanos"; @@ -62,6 +64,8 @@ pub fn find_dbn_attr_args(field: &Field) -> syn::Result> { } else if let Some(i) = meta.path.get_ident() { if i == C_CHAR_ATTR || i == FIXED_PRICE_ATTR + || i == FMT_BINARY + || i == FMT_METHOD || i == INDEX_TS_ATTR || i == SKIP_ATTR || i == UNIX_NANOS_ATTR @@ -119,6 +123,23 @@ pub fn is_hidden(field: &Field) -> bool { .any(|id| id == SKIP_ATTR) } +pub fn find_dbn_debug_attr(field: &Field) -> syn::Result> { + let mut args: Vec<_> = find_dbn_attr_args(field)? + .into_iter() + .filter(|id| { + id == C_CHAR_ATTR || id == FIXED_PRICE_ATTR || id == FMT_BINARY || id == FMT_METHOD + }) + .collect(); + match args.len() { + 0 => Ok(None), + 1 => Ok(Some(args.pop().unwrap())), + _ => Err(syn::Error::new( + field.span(), + "Passed incompatible format arguments to dbn attr", + )), + } +} + pub fn find_dbn_serialize_attr(field: &Field) -> syn::Result> { let mut args: Vec<_> = find_dbn_attr_args(field)? .into_iter() diff --git a/rust/dbn-macros/src/debug.rs b/rust/dbn-macros/src/debug.rs new file mode 100644 index 0000000..eb8c6f7 --- /dev/null +++ b/rust/dbn-macros/src/debug.rs @@ -0,0 +1,77 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Field, ItemStruct}; + +use crate::{ + dbn_attr::{ + find_dbn_debug_attr, is_hidden, C_CHAR_ATTR, FIXED_PRICE_ATTR, FMT_BINARY, FMT_METHOD, + }, + utils::crate_name, +}; + +pub fn record_debug_impl(input_struct: &ItemStruct) -> TokenStream { + let record_type = &input_struct.ident; + let field_iter = input_struct + .fields + .iter() + .map(|f| format_field(f).unwrap_or_else(|e| e.into_compile_error())); + quote! { + impl ::std::fmt::Debug for #record_type { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + let mut debug_struct = f.debug_struct(stringify!(#record_type)); + #(#field_iter)* + debug_struct.finish() + } + } + } +} + +pub fn derive_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + // let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput); + let input_struct = parse_macro_input!(input as ItemStruct); + let record_type = &input_struct.ident; + let field_iter = input_struct + .fields + .iter() + .map(|f| format_field(f).unwrap_or_else(|e| e.into_compile_error())); + quote! { + impl ::std::fmt::Debug for #record_type { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + let mut debug_struct = f.debug_struct(stringify!(#record_type)); + #(#field_iter)* + debug_struct.finish() + } + } + } + .into() +} + +fn format_field(field: &Field) -> syn::Result { + let ident = field.ident.as_ref().unwrap(); + if is_hidden(field) { + return Ok(quote!()); + } + Ok(match find_dbn_debug_attr(field)? { + Some(id) if id == C_CHAR_ATTR => { + quote! { debug_struct.field(stringify!(#ident), &(self.#ident as u8 as char)); } + } + Some(id) if id == FIXED_PRICE_ATTR => { + let crate_name = crate_name(); + quote! { debug_struct.field(stringify!(#ident), &#crate_name::pretty::Px(self.#ident)); } + } + Some(id) if id == FMT_BINARY => { + // format as `0b00101010` + quote! { debug_struct.field(stringify!(#ident), &format_args!("{:#010b}", &self.#ident)); } + } + Some(id) if id == FMT_METHOD => { + // Try to use method to format, otherwise fallback on raw value + return Ok(quote! { + match self.#ident() { + Ok(s) => debug_struct.field(stringify!(#ident), &s), + Err(_) => debug_struct.field(stringify!(#ident), &self.#ident), + }; + }); + } + _ => quote! { debug_struct.field(stringify!(#ident), &self.#ident); }, + }) +} diff --git a/rust/dbn-macros/src/has_rtype.rs b/rust/dbn-macros/src/has_rtype.rs index 4824b6a..df34473 100644 --- a/rust/dbn-macros/src/has_rtype.rs +++ b/rust/dbn-macros/src/has_rtype.rs @@ -28,6 +28,7 @@ pub fn attribute_macro_impl( let raw_index_ts = get_raw_index_ts(&input_struct).unwrap_or_else(|e| e.into_compile_error()); let rtypes = args.args.iter(); let crate_name = crate::utils::crate_name(); + let impl_debug = crate::debug::record_debug_impl(&input_struct); quote! ( #input_struct @@ -67,11 +68,13 @@ pub fn attribute_macro_impl( } } } + + #impl_debug ) .into() } -struct Args { +pub(crate) struct Args { args: Vec, span: Span, } diff --git a/rust/dbn-macros/src/lib.rs b/rust/dbn-macros/src/lib.rs index 3aa6844..13cc2c3 100644 --- a/rust/dbn-macros/src/lib.rs +++ b/rust/dbn-macros/src/lib.rs @@ -1,6 +1,7 @@ use proc_macro::TokenStream; mod dbn_attr; +mod debug; mod has_rtype; mod py_field_desc; mod serialize; @@ -15,6 +16,15 @@ pub fn derive_mock_pyo3(_item: TokenStream) -> TokenStream { TokenStream::new() } +/// Dummy derive macro to enable enable the `dbn` helper attribute for record types +/// using the `dbn_record` proc macro but neither `CsvSerialize` nor `JsonSerialize` as +/// helper attributes aren't supported for proc macros alone. See +/// . +#[proc_macro_derive(DbnAttr, attributes(dbn))] +pub fn dbn_attr(_item: TokenStream) -> TokenStream { + TokenStream::new() +} + /// Derive macro for CSV serialization. Supports the following `dbn` attributes: /// - `c_char`: serializes the field as a `char` /// - `encode_order`: overrides the position of the field in the CSV table @@ -31,7 +41,9 @@ pub fn derive_csv_serialize(input: TokenStream) -> TokenStream { serialize::derive_csv_macro_impl(input) } -/// Derive macro for JSON serialization. Supports the following `dbn` attributes: +/// Derive macro for JSON serialization. +/// +/// Supports the following `dbn` attributes: /// - `c_char`: serializes the field as a `char` /// - `fixed_price`: serializes the field as fixed-price, with the output format /// depending on `PRETTY_PX` @@ -46,8 +58,9 @@ pub fn derive_json_serialize(input: TokenStream) -> TokenStream { serialize::derive_json_macro_impl(input) } -/// Derive macro for field descriptions exposed to Python. Supports the following `dbn` -/// attributes: +/// Derive macro for field descriptions exposed to Python. +/// +/// Supports the following `dbn` attributes: /// - `c_char`: indicates the field dtype should be a single-character string rather /// than an integer /// - `encode_order`: overrides the position of the field in the ordered list @@ -59,19 +72,47 @@ pub fn derive_py_field_desc(input: TokenStream) -> TokenStream { py_field_desc::derive_impl(input) } -/// Attribute macro that acts like a derive macro for for `HasRType` and -/// `AsRef<[u8]>`. +/// Attribute macro that acts like a derive macro for for `Debug` (with customization), +/// `Record`, `RecordMut`, `HasRType`, `PartialOrd`, and `AsRef<[u8]>`. /// /// Expects 1 or more paths to `u8` constants that are the RTypes associated /// with this record. /// /// Supports the following `dbn` attributes: +/// - `c_char`: format the type as a `char` instead of as a numeric +/// - `fixed_price`: format the integer as a fixed-precision decimal +/// - `fmt_binary`: format as a binary +/// - `fmt_method`: try to format by calling the getter method with the same name as the /// - `index_ts`: indicates this field is the primary timestamp for the record +/// field. If the getter returns an error, the raw field value will be used +/// - `skip`: won't be included in the `Debug` output +/// +/// Note: attribute macros don't support helper attributes on their own. If not deriving +/// `CsvSerialize` or `JsonSerialize`, derive `DbnAttr` to use the `dbn` helper attribute +/// without a compiler error. #[proc_macro_attribute] pub fn dbn_record(attr: TokenStream, input: TokenStream) -> TokenStream { has_rtype::attribute_macro_impl(attr, input) } +/// Derive macro for Debug representations with the same extensions for DBN records +/// as `dbn_record`. +/// +/// Supports the following `dbn` attributes: +/// - `c_char`: format the type as a `char` instead of as a numeric +/// - `fixed_price`: format the integer as a fixed-precision decimal +/// - `fmt_binary`: format as a binary +/// - `fmt_method`: try to format by calling the getter method with the same name as the +/// field. If the getter returns an error, the raw field value will be used +/// - `skip`: won't be included in the `Debug` output +/// +/// Note: fields beginning with `_` will automatically be skipped, e.g. `_dummy` isn't +/// included in the `Debug` output. +#[proc_macro_derive(RecordDebug, attributes(dbn))] +pub fn derive_record_debug(input: TokenStream) -> TokenStream { + debug::derive_impl(input) +} + #[cfg(test)] mod tests { #[test] diff --git a/rust/dbn/src/compat.rs b/rust/dbn/src/compat.rs index 4eb90be..b0b0952 100644 --- a/rust/dbn/src/compat.rs +++ b/rust/dbn/src/compat.rs @@ -73,7 +73,7 @@ pub unsafe fn decode_record_ref<'a>( /// /// Note: This will be renamed to `InstrumentDefMsg` in DBN version 2. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -208,32 +208,43 @@ pub struct InstrumentDefMsgV1 { #[pyo3(get, set)] pub channel_id: u16, /// The currency used for price fields. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))] pub currency: [c_char; 4], /// The currency used for settlement, if different from `currency`. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))] pub settl_currency: [c_char; 4], /// The strategy type of the spread. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))] pub secsubtype: [c_char; 6], /// The instrument raw symbol assigned by the publisher. - #[dbn(encode_order(2))] + #[dbn(encode_order(2), fmt_method)] pub raw_symbol: [c_char; SYMBOL_CSTR_LEN_V1], /// The security group code of the instrument. + #[dbn(fmt_method)] pub group: [c_char; 21], /// The exchange used to identify the instrument. + #[dbn(fmt_method)] pub exchange: [c_char; 5], /// The underlying asset code (product code) of the instrument. + #[dbn(fmt_method)] pub asset: [c_char; 7], /// The ISO standard instrument categorization code. + #[dbn(fmt_method)] pub cfi: [c_char; 7], /// The type of the instrument, e.g. FUT for future or future spread. + #[dbn(fmt_method)] pub security_type: [c_char; 7], /// The unit of measure for the instrument’s original contract size, e.g. USD or LBS. + #[dbn(fmt_method)] pub unit_of_measure: [c_char; 31], /// The symbol of the first underlying instrument. + #[dbn(fmt_method)] pub underlying: [c_char; 21], /// The currency of [`strike_price`](Self::strike_price). + #[dbn(fmt_method)] pub strike_price_currency: [c_char; 4], /// The classification of the instrument. #[dbn(c_char, encode_order(4))] @@ -304,7 +315,7 @@ pub struct InstrumentDefMsgV1 { /// /// Note: This will be renamed to `SymbolMappingMsg` in DBN version 2. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -320,8 +331,10 @@ pub struct SymbolMappingMsgV1 { #[pyo3(get, set)] pub hd: RecordHeader, /// The input symbol. + #[dbn(fmt_method)] pub stype_in_symbol: [c_char; SYMBOL_CSTR_LEN_V1], /// The output symbol. + #[dbn(fmt_method)] pub stype_out_symbol: [c_char; SYMBOL_CSTR_LEN_V1], // Filler for alignment. #[doc(hidden)] diff --git a/rust/dbn/src/macros.rs b/rust/dbn/src/macros.rs index fb90942..a44dafd 100644 --- a/rust/dbn/src/macros.rs +++ b/rust/dbn/src/macros.rs @@ -1,7 +1,7 @@ //! Helper macros for working with multiple RTypes, Schemas, and types of records. // Re-export -pub use dbn_macros::{dbn_record, CsvSerialize, JsonSerialize, PyFieldDesc}; +pub use dbn_macros::{dbn_record, CsvSerialize, DbnAttr, JsonSerialize, PyFieldDesc, RecordDebug}; /// Base macro for type dispatch based on rtype. /// diff --git a/rust/dbn/src/pretty.rs b/rust/dbn/src/pretty.rs index 57acb6c..5139d35 100644 --- a/rust/dbn/src/pretty.rs +++ b/rust/dbn/src/pretty.rs @@ -37,7 +37,7 @@ impl fmt::Debug for Ts { impl fmt::Debug for Px { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + f.write_str(&fmt_px(self.0)) } } diff --git a/rust/dbn/src/record.rs b/rust/dbn/src/record.rs index 1702fbb..1f6cbf9 100644 --- a/rust/dbn/src/record.rs +++ b/rust/dbn/src/record.rs @@ -18,7 +18,7 @@ use crate::{ Action, InstrumentClass, MatchAlgorithm, SecurityUpdateAction, Side, StatType, StatUpdateAction, UserDefinedInstrument, }, - macros::{dbn_record, CsvSerialize, JsonSerialize}, + macros::{dbn_record, CsvSerialize, JsonSerialize, RecordDebug}, publishers::Publisher, Error, Result, SYMBOL_CSTR_LEN, }; @@ -33,7 +33,7 @@ pub use conv::{ /// Common data for all Databento records. Always found at the beginning of a record /// struct. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -64,7 +64,7 @@ pub struct RecordHeader { /// A market-by-order (MBO) tick message. The record of the /// [`Mbo`](crate::enums::Schema::Mbo) schema. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -93,6 +93,7 @@ pub struct MboMsg { pub size: u32, /// A combination of packet end with matching engine status. See /// [`enums::flags`](crate::enums::flags) for possible values. + #[dbn(fmt_binary)] #[pyo3(get)] pub flags: u8, /// A channel ID within the venue. @@ -121,7 +122,7 @@ pub struct MboMsg { /// A level. #[repr(C)] -#[derive(Clone, Debug, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, JsonSerialize, RecordDebug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -150,7 +151,7 @@ pub struct BidAskPair { /// Market by price implementation with a book depth of 0. Equivalent to /// MBP-0. The record of the [`Trades`](crate::enums::Schema::Trades) schema. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -181,6 +182,7 @@ pub struct TradeMsg { pub side: c_char, /// A combination of packet end with matching engine status. See /// [`enums::flags`](crate::enums::flags) for possible values. + #[dbn(fmt_binary)] #[pyo3(get)] pub flags: u8, /// The depth of actual book change. @@ -203,7 +205,7 @@ pub struct TradeMsg { /// Market by price implementation with a known book depth of 1. The record of the /// [`Mbp1`](crate::enums::Schema::Mbp1) schema. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -235,6 +237,7 @@ pub struct Mbp1Msg { pub side: c_char, /// A combination of packet end with matching engine status. See /// [`enums::flags`](crate::enums::flags) for possible values. + #[dbn(fmt_binary)] #[pyo3(get)] pub flags: u8, /// The depth of actual book change. @@ -260,7 +263,7 @@ pub struct Mbp1Msg { /// Market by price implementation with a known book depth of 10. The record of the /// [`Mbp10`](crate::enums::Schema::Mbp10) schema. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -292,6 +295,7 @@ pub struct Mbp10Msg { pub side: c_char, /// A combination of packet end with matching engine status. See /// [`enums::flags`](crate::enums::flags) for possible values. + #[dbn(fmt_binary)] #[pyo3(get)] pub flags: u8, /// The depth of actual book change. @@ -324,7 +328,7 @@ pub type TbboMsg = Mbp1Msg; /// - [`Ohlcv1D`](crate::enums::Schema::Ohlcv1D) /// - [`OhlcvEod`](crate::enums::Schema::OhlcvEod) #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -364,7 +368,7 @@ pub struct OhlcvMsg { /// [`Status`](crate::enums::Schema::Status) schema. #[doc(hidden)] #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -384,6 +388,7 @@ pub struct StatusMsg { #[dbn(unix_nanos)] #[pyo3(get, set)] pub ts_recv: u64, + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub group: [c_char; 21], #[pyo3(get, set)] @@ -397,7 +402,7 @@ pub struct StatusMsg { /// Definition of an instrument. The record of the /// [`Definition`](crate::enums::Schema::Definition) schema. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -533,40 +538,51 @@ pub struct InstrumentDefMsg { #[pyo3(get, set)] pub channel_id: u16, /// The currency used for price fields. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub currency: [c_char; 4], /// The currency used for settlement, if different from `currency`. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub settl_currency: [c_char; 4], /// The strategy type of the spread. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub secsubtype: [c_char; 6], /// The instrument raw symbol assigned by the publisher. - #[dbn(encode_order(2))] + #[dbn(encode_order(2), fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub raw_symbol: [c_char; SYMBOL_CSTR_LEN], /// The security group code of the instrument. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub group: [c_char; 21], /// The exchange used to identify the instrument. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub exchange: [c_char; 5], /// The underlying asset code (product code) of the instrument. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub asset: [c_char; 7], /// The ISO standard instrument categorization code. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub cfi: [c_char; 7], /// The type of the instrument, e.g. FUT for future or future spread. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub security_type: [c_char; 7], /// The unit of measure for the instrument’s original contract size, e.g. USD or LBS. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub unit_of_measure: [c_char; 31], /// The symbol of the first underlying instrument. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub underlying: [c_char; 21], /// The currency of [`strike_price`](Self::strike_price). + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub strike_price_currency: [c_char; 4], /// The classification of the instrument. @@ -628,7 +644,7 @@ pub struct InstrumentDefMsg { /// An auction imbalance message. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -722,7 +738,7 @@ pub struct ImbalanceMsg { /// A statistics message. A catchall for various data disseminated by publishers. /// The [`stat_type`](Self::stat_type) indicates the statistic contained in the message. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -759,13 +775,16 @@ pub struct StatMsg { pub ts_in_delta: i32, /// The type of statistic value contained in the message. Refer to the /// [`StatType`](crate::enums::StatType) for variants. + #[dbn(fmt_method)] pub stat_type: u16, /// A channel ID within the venue. pub channel_id: u16, /// Indicates if the statistic is newly added (1) or deleted (2). (Deleted is only used with /// some stat types) + #[dbn(fmt_method)] pub update_action: u8, /// Additional flags associate with certain stat types. + #[dbn(fmt_binary)] pub stat_flags: u8, // Filler for alignment #[doc(hidden)] @@ -775,7 +794,7 @@ pub struct StatMsg { /// An error message from the Databento Live Subscription Gateway (LSG). #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -791,6 +810,7 @@ pub struct ErrorMsg { #[pyo3(get, set)] pub hd: RecordHeader, /// The error message. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub err: [c_char; 64], } @@ -798,7 +818,7 @@ pub struct ErrorMsg { /// A symbol mapping message which maps a symbol of one [`SType`](crate::enums::SType) /// to another. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -815,15 +835,19 @@ pub struct SymbolMappingMsg { pub hd: RecordHeader, // TODO(carter): special serialization to string? /// The input symbology type of `stype_in_symbol`. + #[dbn(fmt_method)] #[pyo3(get, set)] pub stype_in: u8, /// The input symbol. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub stype_in_symbol: [c_char; SYMBOL_CSTR_LEN], /// The output symbology type of `stype_out_symbol`. + #[dbn(fmt_method)] #[pyo3(get, set)] pub stype_out: u8, /// The output symbol. + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub stype_out_symbol: [c_char; SYMBOL_CSTR_LEN], /// The start of the mapping interval expressed as the number of nanoseconds since @@ -841,7 +865,7 @@ pub struct SymbolMappingMsg { /// A non-error message from the Databento Live Subscription Gateway (LSG). Also used /// for heartbeating. #[repr(C)] -#[derive(Clone, Debug, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trivial_copy", derive(Copy))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -857,6 +881,7 @@ pub struct SystemMsg { #[pyo3(get, set)] pub hd: RecordHeader, /// The message from the Databento Live Subscription Gateway (LSG). + #[dbn(fmt_method)] #[cfg_attr(feature = "serde", serde(with = "conv::cstr_serde"))] pub msg: [c_char; 64], } diff --git a/rust/dbn/src/record/methods.rs b/rust/dbn/src/record/methods.rs index 043a035..17c0fba 100644 --- a/rust/dbn/src/record/methods.rs +++ b/rust/dbn/src/record/methods.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::{ compat::{InstrumentDefMsgV1, SymbolMappingMsgV1}, SType, @@ -64,6 +66,25 @@ impl RecordHeader { } } +impl Debug for RecordHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut debug_struct = f.debug_struct("RecordHeader"); + debug_struct.field("length", &self.length); + match self.rtype() { + Ok(rtype) => debug_struct.field("rtype", &format_args!("{rtype:?}")), + Err(_) => debug_struct.field("rtype", &format_args!("{:#04X}", &self.rtype)), + }; + match self.publisher() { + Ok(p) => debug_struct.field("publisher_id", &format_args!("{p:?}")), + Err(_) => debug_struct.field("publisher_id", &self.publisher_id), + }; + debug_struct + .field("instrument_id", &self.instrument_id) + .field("ts_event", &self.ts_event) + .finish() + } +} + impl MboMsg { /// Tries to convert the raw order side to an enum. /// @@ -786,6 +807,8 @@ impl WithTsOut { #[cfg(test)] mod tests { + use crate::flags; + use super::*; #[test] @@ -796,4 +819,90 @@ mod tests { "couldn't convert 0x0E to dbn::enums::rtype::RType" ); } + + #[test] + fn debug_mbo() { + let rec = MboMsg { + hd: RecordHeader::new::( + rtype::MBO, + Publisher::OpraPillarXcbo as u16, + 678, + 1704468548242628731, + ), + flags: flags::LAST | flags::BAD_TS_RECV, + price: 4_500_500_000_000, + side: b'B' as c_char, + action: b'A' as c_char, + ..Default::default() + }; + assert_eq!( + format!("{rec:?}"), + "MboMsg { hd: RecordHeader { length: 14, rtype: Mbo, publisher_id: OpraPillarXcbo, \ + instrument_id: 678, ts_event: 1704468548242628731 }, order_id: 0, \ + price: 4500.500000000, size: 4294967295, flags: 0b10001000, channel_id: 0, \ + action: 'A', side: 'B', ts_recv: 18446744073709551615, ts_in_delta: 0, sequence: 0 }" + ); + } + + #[test] + fn debug_stats() { + let rec = StatMsg { + stat_type: StatType::OpenInterest as u16, + update_action: StatUpdateAction::New as u8, + quantity: 5, + stat_flags: 0b00000010, + ..Default::default() + }; + assert_eq!( + format!("{rec:?}"), + "StatMsg { hd: RecordHeader { length: 16, rtype: Statistics, publisher_id: 0, \ + instrument_id: 0, ts_event: 18446744073709551615 }, ts_recv: 18446744073709551615, \ + ts_ref: 18446744073709551615, price: UNDEF_PRICE, quantity: 5, sequence: 0, ts_in_delta: 0, \ + stat_type: OpenInterest, channel_id: 0, update_action: New, stat_flags: 0b00000010 }" + ); + } + + #[test] + fn debug_instrument_err() { + let rec = ErrorMsg { + err: str_to_c_chars("Missing stype_in").unwrap(), + ..Default::default() + }; + assert_eq!( + format!("{rec:?}"), + "ErrorMsg { hd: RecordHeader { length: 20, rtype: Error, publisher_id: 0, \ + instrument_id: 0, ts_event: 18446744073709551615 }, err: \"Missing stype_in\" }" + ); + } + + #[test] + fn debug_instrument_sys() { + let rec = SystemMsg::heartbeat(123); + assert_eq!( + format!("{rec:?}"), + "SystemMsg { hd: RecordHeader { length: 20, rtype: System, publisher_id: 0, \ + instrument_id: 0, ts_event: 123 }, msg: \"Heartbeat\" }" + ); + } + + #[test] + fn debug_instrument_symbol_mapping() { + let rec = SymbolMappingMsg { + hd: RecordHeader::new::( + rtype::SYMBOL_MAPPING, + 0, + 5602, + 1704466940331347283, + ), + stype_in: SType::RawSymbol as u8, + stype_in_symbol: str_to_c_chars("ESM4").unwrap(), + stype_out: SType::RawSymbol as u8, + stype_out_symbol: str_to_c_chars("ESM4").unwrap(), + ..Default::default() + }; + assert_eq!( + format!("{rec:?}"), + "SymbolMappingMsg { hd: RecordHeader { length: 44, rtype: SymbolMapping, publisher_id: 0, instrument_id: 5602, ts_event: 1704466940331347283 }, stype_in: RawSymbol, stype_in_symbol: \"ESM4\", stype_out: RawSymbol, stype_out_symbol: \"ESM4\", start_ts: 18446744073709551615, end_ts: 18446744073709551615 }" + ); + } }