dbn/
compat.rs

1//! Compatibility shims for different DBN versions.
2
3/// The length of symbol fields in DBN version 1 (prior version being phased out).
4pub const SYMBOL_CSTR_LEN_V1: usize = 22;
5/// The length of symbol fields in DBN version 2 (current version).
6pub const SYMBOL_CSTR_LEN_V2: usize = 71;
7/// The length of symbol fields in DBN version 3 (future version).
8pub const SYMBOL_CSTR_LEN_V3: usize = SYMBOL_CSTR_LEN_V2;
9pub(crate) const METADATA_RESERVED_LEN_V1: usize = 47;
10
11/// Returns the length of symbol fields in the given DBN version
12pub const fn version_symbol_cstr_len(version: u8) -> usize {
13    if version < 2 {
14        SYMBOL_CSTR_LEN_V1
15    } else {
16        SYMBOL_CSTR_LEN_V2
17    }
18}
19pub use crate::record::ErrorMsg as ErrorMsgV2;
20pub use crate::record::InstrumentDefMsg as InstrumentDefMsgV2;
21pub use crate::record::SymbolMappingMsg as SymbolMappingMsgV2;
22pub use crate::record::SystemMsg as SystemMsgV2;
23
24use std::os::raw::c_char;
25
26// Dummy derive macro to get around `cfg_attr` incompatibility of several
27// of pyo3's attribute macros. See https://github.com/PyO3/pyo3/issues/780
28#[cfg(not(feature = "python"))]
29use dbn_macros::MockPyo3;
30
31use crate::{
32    macros::{dbn_record, CsvSerialize, JsonSerialize},
33    record::{transmute_header_bytes, transmute_record_bytes},
34    rtype, HasRType, RecordHeader, RecordRef, SecurityUpdateAction, UserDefinedInstrument,
35    VersionUpgradePolicy, WithTsOut, DBN_VERSION,
36};
37
38/// Decodes bytes into a [`RecordRef`], optionally applying conversion from structs
39/// of a prior DBN version to the current DBN version, according to the `version` and
40/// `upgrade_policy`.
41///
42/// # Preconditions
43/// This function assumes `version` is valid (not greater than [`DBN_VERSION`]).
44///
45/// # Panics
46/// This function will panic if it's passed only a single partial record in `input` and
47/// an upgrade is attempted. It will also panic if `version` is greater than [`DBN_VERSION`].
48///
49/// # Safety
50/// Assumes `input` contains a full record.
51pub unsafe fn decode_record_ref<'a>(
52    version: u8,
53    upgrade_policy: VersionUpgradePolicy,
54    ts_out: bool,
55    compat_buffer: &'a mut [u8; crate::MAX_RECORD_LEN],
56    input: &'a [u8],
57) -> RecordRef<'a> {
58    match (version, upgrade_policy) {
59        (1, VersionUpgradePolicy::UpgradeToV2) => {
60            let header = transmute_header_bytes(input).unwrap();
61            match header.rtype {
62                rtype::INSTRUMENT_DEF => {
63                    return upgrade_record::<InstrumentDefMsgV1, InstrumentDefMsgV2>(
64                        ts_out,
65                        compat_buffer,
66                        input,
67                    );
68                }
69                rtype::SYMBOL_MAPPING => {
70                    return upgrade_record::<SymbolMappingMsgV1, SymbolMappingMsgV2>(
71                        ts_out,
72                        compat_buffer,
73                        input,
74                    );
75                }
76                rtype::ERROR => {
77                    return upgrade_record::<ErrorMsgV1, ErrorMsgV2>(ts_out, compat_buffer, input);
78                }
79                rtype::SYSTEM => {
80                    return upgrade_record::<SystemMsgV1, SystemMsgV2>(
81                        ts_out,
82                        compat_buffer,
83                        input,
84                    );
85                }
86                _ => (),
87            }
88        }
89        (2, VersionUpgradePolicy::UpgradeToV2) => {}
90        (..=DBN_VERSION, VersionUpgradePolicy::AsIs) => {}
91        _ => panic!("Unsupported version {version}"),
92    }
93    RecordRef::new(input)
94}
95
96pub(crate) unsafe fn choose_record_ref<'a>(
97    version: u8,
98    upgrade_policy: VersionUpgradePolicy,
99    buffer: &'a [u8],
100    compat_buffer: &'a [u8],
101) -> RecordRef<'a> {
102    if version == 1 && upgrade_policy == VersionUpgradePolicy::UpgradeToV2 {
103        let header = transmute_header_bytes(buffer).unwrap();
104        match header.rtype {
105            rtype::INSTRUMENT_DEF | rtype::SYMBOL_MAPPING | rtype::ERROR | rtype::SYSTEM => {
106                return RecordRef::new(compat_buffer);
107            }
108            _ => (),
109        }
110    }
111    RecordRef::new(buffer)
112}
113
114unsafe fn upgrade_record<'a, T, U>(
115    ts_out: bool,
116    compat_buffer: &'a mut [u8; crate::MAX_RECORD_LEN],
117    input: &'a [u8],
118) -> RecordRef<'a>
119where
120    T: HasRType,
121    U: AsRef<[u8]> + HasRType + for<'b> From<&'b T>,
122{
123    if ts_out {
124        let rec = transmute_record_bytes::<WithTsOut<T>>(input).unwrap();
125        let upgraded = WithTsOut::new(U::from(&rec.rec), rec.ts_out);
126        compat_buffer[..upgraded.as_ref().len()].copy_from_slice(upgraded.as_ref());
127    } else {
128        let upgraded = U::from(transmute_record_bytes::<T>(input).unwrap());
129        compat_buffer[..upgraded.as_ref().len()].copy_from_slice(upgraded.as_ref());
130    }
131    RecordRef::new(compat_buffer)
132}
133
134/// A trait for compatibility between different versions of symbol mapping records.
135pub trait SymbolMappingRec: HasRType {
136    /// Returns the input symbol as a `&str`.
137    ///
138    /// # Errors
139    /// This function returns an error if `stype_in_symbol` contains invalid UTF-8.
140    fn stype_in_symbol(&self) -> crate::Result<&str>;
141
142    /// Returns the output symbol as a `&str`.
143    ///
144    /// # Errors
145    /// This function returns an error if `stype_out_symbol` contains invalid UTF-8.
146    fn stype_out_symbol(&self) -> crate::Result<&str>;
147
148    /// Parses the raw start of the mapping interval into a datetime. Returns `None` if
149    /// `start_ts` contains the sentinel for a null timestamp.
150    fn start_ts(&self) -> Option<time::OffsetDateTime>;
151
152    /// Parses the raw end of the mapping interval into a datetime. Returns `None` if
153    /// `end_ts` contains the sentinel for a null timestamp.
154    fn end_ts(&self) -> Option<time::OffsetDateTime>;
155}
156
157/// A trait for compatibility between different versions of definition records.
158pub trait InstrumentDefRec: HasRType {
159    /// Returns the instrument raw symbol assigned by the publisher as a `&str`.
160    ///
161    /// # Errors
162    /// This function returns an error if `raw_symbol` contains invalid UTF-8.
163    fn raw_symbol(&self) -> crate::Result<&str>;
164
165    /// Returns the underlying asset code (product code) of the instrument as a `&str`.
166    ///
167    /// # Errors
168    /// This function returns an error if `asset` contains invalid UTF-8.
169    fn asset(&self) -> crate::Result<&str>;
170
171    /// Returns the type of the strument, e.g. FUT for future or future spread as
172    /// a `&str`.
173    ///
174    /// # Errors
175    /// This function returns an error if `security_type` contains invalid UTF-8.
176    fn security_type(&self) -> crate::Result<&str>;
177
178    /// Returns the action indicating whether the instrument definition has been added,
179    /// modified, or deleted.
180    ///
181    /// # Errors
182    /// This function returns an error if the `security_update_action` field does not
183    /// contain a valid [`SecurityUpdateAction`].
184    fn security_update_action(&self) -> crate::Result<SecurityUpdateAction>;
185
186    /// The channel ID assigned by Databento as an incrementing integer starting at
187    /// zero.
188    fn channel_id(&self) -> u16;
189}
190
191// NOTE: Versioned records need to be defined in this file to work with cbindgen.
192
193/// Definition of an instrument in DBN version 1. The record of the
194/// [`Definition`](crate::enums::Schema::Definition) schema.
195#[repr(C)]
196#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
197#[cfg_attr(feature = "trivial_copy", derive(Copy))]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199#[cfg_attr(
200    feature = "python",
201    pyo3::pyclass(dict, module = "databento_dbn"),
202    derive(crate::macros::PyFieldDesc)
203)]
204#[cfg_attr(not(feature = "python"), derive(MockPyo3))] // bring `pyo3` attribute into scope
205#[cfg_attr(test, derive(type_layout::TypeLayout))]
206#[dbn_record(rtype::INSTRUMENT_DEF)]
207pub struct InstrumentDefMsgV1 {
208    /// The common header.
209    #[pyo3(get, set)]
210    pub hd: RecordHeader,
211    /// The capture-server-received timestamp expressed as the number of nanoseconds
212    /// since the UNIX epoch.
213    #[dbn(encode_order(0), index_ts, unix_nanos)]
214    #[pyo3(get, set)]
215    pub ts_recv: u64,
216    /// The minimum constant tick for the instrument in units of 1e-9, i.e.
217    /// 1/1,000,000,000 or 0.000000001.
218    #[dbn(fixed_price)]
219    #[pyo3(get, set)]
220    pub min_price_increment: i64,
221    /// The multiplier to convert the venue’s display price to the conventional price.
222    #[dbn(fixed_price)]
223    #[pyo3(get, set)]
224    pub display_factor: i64,
225    /// The last eligible trade time expressed as a number of nanoseconds since the
226    /// UNIX epoch.
227    ///
228    /// Will be [`crate::UNDEF_TIMESTAMP`] when null, such as for equities. Some publishers
229    /// only provide date-level granularity.
230    #[dbn(unix_nanos)]
231    #[pyo3(get, set)]
232    pub expiration: u64,
233    /// The time of instrument activation expressed as a number of nanoseconds since the
234    /// UNIX epoch.
235    ///
236    /// Will be [`crate::UNDEF_TIMESTAMP`] when null, such as for equities. Some publishers
237    /// only provide date-level granularity.
238    #[dbn(unix_nanos)]
239    #[pyo3(get, set)]
240    pub activation: u64,
241    /// The allowable high limit price for the trading day in units of 1e-9, i.e.
242    /// 1/1,000,000,000 or 0.000000001.
243    #[dbn(fixed_price)]
244    #[pyo3(get, set)]
245    pub high_limit_price: i64,
246    /// The allowable low limit price for the trading day in units of 1e-9, i.e.
247    /// 1/1,000,000,000 or 0.000000001.
248    #[dbn(fixed_price)]
249    #[pyo3(get, set)]
250    pub low_limit_price: i64,
251    /// The differential value for price banding in units of 1e-9, i.e. 1/1,000,000,000
252    /// or 0.000000001.
253    #[dbn(fixed_price)]
254    #[pyo3(get, set)]
255    pub max_price_variation: i64,
256    /// The trading session settlement price on `trading_reference_date`.
257    #[dbn(fixed_price)]
258    #[pyo3(get, set)]
259    pub trading_reference_price: i64,
260    /// The contract size for each instrument, in combination with `unit_of_measure`.
261    #[dbn(fixed_price)]
262    #[pyo3(get, set)]
263    pub unit_of_measure_qty: i64,
264    /// The value currently under development by the venue. Converted to units of 1e-9, i.e.
265    /// 1/1,000,000,000 or 0.000000001.
266    #[dbn(fixed_price)]
267    #[pyo3(get, set)]
268    pub min_price_increment_amount: i64,
269    /// The value used for price calculation in spread and leg pricing in units of 1e-9,
270    /// i.e. 1/1,000,000,000 or 0.000000001.
271    #[dbn(fixed_price)]
272    #[pyo3(get, set)]
273    pub price_ratio: i64,
274    /// A bitmap of instrument eligibility attributes.
275    #[pyo3(get, set)]
276    pub inst_attrib_value: i32,
277    /// The `instrument_id` of the first underlying instrument.
278    #[pyo3(get, set)]
279    pub underlying_id: u32,
280    /// The instrument ID assigned by the publisher. May be the same as `instrument_id`.
281    #[pyo3(get, set)]
282    pub raw_instrument_id: u32,
283    /// The implied book depth on the price level data feed.
284    #[pyo3(get, set)]
285    pub market_depth_implied: i32,
286    /// The (outright) book depth on the price level data feed.
287    #[pyo3(get, set)]
288    pub market_depth: i32,
289    /// The market segment of the instrument.
290    #[pyo3(get, set)]
291    pub market_segment_id: u32,
292    /// The maximum trading volume for the instrument.
293    #[pyo3(get, set)]
294    pub max_trade_vol: u32,
295    /// The minimum order entry quantity for the instrument.
296    #[pyo3(get, set)]
297    pub min_lot_size: i32,
298    /// The minimum quantity required for a block trade of the instrument.
299    #[pyo3(get, set)]
300    pub min_lot_size_block: i32,
301    /// The minimum quantity required for a round lot of the instrument. Multiples of
302    /// this quantity are also round lots.
303    #[pyo3(get, set)]
304    pub min_lot_size_round_lot: i32,
305    /// The minimum trading volume for the instrument.
306    #[pyo3(get, set)]
307    pub min_trade_vol: u32,
308    #[doc(hidden)]
309    pub _reserved2: [u8; 4],
310    /// The number of deliverables per instrument, i.e. peak days.
311    #[pyo3(get, set)]
312    pub contract_multiplier: i32,
313    /// The quantity that a contract will decay daily, after `decay_start_date` has
314    /// been reached.
315    #[pyo3(get, set)]
316    pub decay_quantity: i32,
317    /// The fixed contract value assigned to each instrument.
318    #[pyo3(get, set)]
319    pub original_contract_size: i32,
320    #[doc(hidden)]
321    pub _reserved3: [u8; 4],
322    /// The trading session date corresponding to the settlement price in
323    /// `trading_reference_price`, in number of days since the UNIX epoch.
324    #[pyo3(get, set)]
325    pub trading_reference_date: u16,
326    /// The channel ID assigned at the venue.
327    #[pyo3(get, set)]
328    pub appl_id: i16,
329    /// The calendar year reflected in the instrument symbol.
330    #[pyo3(get, set)]
331    pub maturity_year: u16,
332    /// The date at which a contract will begin to decay.
333    #[pyo3(get, set)]
334    pub decay_start_date: u16,
335    /// The channel ID assigned by Databento as an incrementing integer starting at zero.
336    #[pyo3(get, set)]
337    pub channel_id: u16,
338    /// The currency used for price fields.
339    #[dbn(fmt_method)]
340    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
341    pub currency: [c_char; 4],
342    /// The currency used for settlement, if different from `currency`.
343    #[dbn(fmt_method)]
344    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
345    pub settl_currency: [c_char; 4],
346    /// The strategy type of the spread.
347    #[dbn(fmt_method)]
348    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
349    pub secsubtype: [c_char; 6],
350    /// The instrument raw symbol assigned by the publisher.
351    #[dbn(encode_order(2), fmt_method)]
352    pub raw_symbol: [c_char; SYMBOL_CSTR_LEN_V1],
353    /// The security group code of the instrument.
354    #[dbn(fmt_method)]
355    pub group: [c_char; 21],
356    /// The exchange used to identify the instrument.
357    #[dbn(fmt_method)]
358    pub exchange: [c_char; 5],
359    /// The underlying asset code (product code) of the instrument.
360    #[dbn(fmt_method)]
361    pub asset: [c_char; 7],
362    /// The ISO standard instrument categorization code.
363    #[dbn(fmt_method)]
364    pub cfi: [c_char; 7],
365    /// The type of the instrument, e.g. FUT for future or future spread.
366    #[dbn(fmt_method)]
367    pub security_type: [c_char; 7],
368    /// The unit of measure for the instrument’s original contract size, e.g. USD or LBS.
369    #[dbn(fmt_method)]
370    pub unit_of_measure: [c_char; 31],
371    /// The symbol of the first underlying instrument.
372    #[dbn(fmt_method)]
373    pub underlying: [c_char; 21],
374    /// The currency of [`strike_price`](Self::strike_price).
375    #[dbn(fmt_method)]
376    pub strike_price_currency: [c_char; 4],
377    /// The classification of the instrument.
378    #[dbn(c_char, encode_order(4))]
379    #[pyo3(set)]
380    pub instrument_class: c_char,
381    #[doc(hidden)]
382    pub _reserved4: [u8; 2],
383    /// The strike price of the option. Converted to units of 1e-9, i.e. 1/1,000,000,000
384    /// or 0.000000001.
385    #[dbn(fixed_price)]
386    #[pyo3(get, set)]
387    pub strike_price: i64,
388    #[doc(hidden)]
389    pub _reserved5: [u8; 6],
390    /// The matching algorithm used for the instrument, typically **F**IFO.
391    #[dbn(c_char)]
392    #[pyo3(set)]
393    pub match_algorithm: c_char,
394    /// The current trading state of the instrument.
395    #[pyo3(get, set)]
396    pub md_security_trading_status: u8,
397    /// The price denominator of the main fraction.
398    #[pyo3(get, set)]
399    pub main_fraction: u8,
400    ///  The number of digits to the right of the tick mark, to display fractional prices.
401    #[pyo3(get, set)]
402    pub price_display_format: u8,
403    /// The type indicators for the settlement price, as a bitmap.
404    #[pyo3(get, set)]
405    pub settl_price_type: u8,
406    /// The price denominator of the sub fraction.
407    #[pyo3(get, set)]
408    pub sub_fraction: u8,
409    /// The product complex of the instrument.
410    #[pyo3(get, set)]
411    pub underlying_product: u8,
412    /// Indicates if the instrument definition has been added, modified, or deleted.
413    #[dbn(encode_order(3))]
414    #[pyo3(set)]
415    pub security_update_action: SecurityUpdateAction,
416    /// The calendar month reflected in the instrument symbol.
417    #[pyo3(get, set)]
418    pub maturity_month: u8,
419    /// The calendar day reflected in the instrument symbol, or 0.
420    #[pyo3(get, set)]
421    pub maturity_day: u8,
422    /// The calendar week reflected in the instrument symbol, or 0.
423    #[pyo3(get, set)]
424    pub maturity_week: u8,
425    /// Indicates if the instrument is user defined: **Y**es or **N**o.
426    #[pyo3(set)]
427    pub user_defined_instrument: UserDefinedInstrument,
428    /// The type of `contract_multiplier`. Either `1` for hours, or `2` for days.
429    #[pyo3(get, set)]
430    pub contract_multiplier_unit: i8,
431    /// The schedule for delivering electricity.
432    #[pyo3(get, set)]
433    pub flow_schedule_type: i8,
434    /// The tick rule of the spread.
435    #[pyo3(get, set)]
436    pub tick_rule: u8,
437    // Filler for alignment.
438    #[doc(hidden)]
439    pub _dummy: [u8; 3],
440}
441
442/// An error message from the Databento Live Subscription Gateway (LSG) in DBN version
443/// 1.
444#[repr(C)]
445#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
446#[cfg_attr(feature = "trivial_copy", derive(Copy))]
447#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
448#[cfg_attr(
449    feature = "python",
450    pyo3::pyclass(dict, module = "databento_dbn"),
451    derive(crate::macros::PyFieldDesc)
452)]
453#[cfg_attr(not(feature = "python"), derive(MockPyo3))] // bring `pyo3` attribute into scope
454#[cfg_attr(test, derive(type_layout::TypeLayout))]
455#[dbn_record(rtype::ERROR)]
456pub struct ErrorMsgV1 {
457    /// The common header.
458    #[pyo3(get, set)]
459    pub hd: RecordHeader,
460    /// The error message.
461    #[dbn(fmt_method)]
462    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
463    pub err: [c_char; 64],
464}
465
466/// Definition of an instrument in DBN version 3. The record of the
467/// [`Definition`](crate::enums::Schema::Definition) schema.
468#[repr(C)]
469#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
470#[cfg_attr(feature = "trivial_copy", derive(Copy))]
471#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
472#[cfg_attr(
473    feature = "python",
474    pyo3::pyclass(dict, module = "databento_dbn"),
475    derive(crate::macros::PyFieldDesc)
476)]
477#[cfg_attr(not(feature = "python"), derive(MockPyo3))] // bring `pyo3` attribute into scope
478#[cfg_attr(test, derive(type_layout::TypeLayout))]
479#[dbn_record(rtype::SYMBOL_MAPPING)]
480pub struct SymbolMappingMsgV1 {
481    /// The common header.
482    #[pyo3(get, set)]
483    pub hd: RecordHeader,
484    /// The input symbol.
485    #[dbn(fmt_method)]
486    pub stype_in_symbol: [c_char; SYMBOL_CSTR_LEN_V1],
487    /// The output symbol.
488    #[dbn(fmt_method)]
489    pub stype_out_symbol: [c_char; SYMBOL_CSTR_LEN_V1],
490    // Filler for alignment.
491    #[doc(hidden)]
492    pub _dummy: [u8; 4],
493    /// The start of the mapping interval expressed as the number of nanoseconds since
494    /// the UNIX epoch.
495    #[dbn(unix_nanos)]
496    #[pyo3(get, set)]
497    pub start_ts: u64,
498    /// The end of the mapping interval expressed as the number of nanoseconds since
499    /// the UNIX epoch.
500    #[dbn(unix_nanos)]
501    #[pyo3(get, set)]
502    pub end_ts: u64,
503}
504
505/// A non-error message from the Databento Live Subscription Gateway (LSG) in DBN
506/// version 1. Also used for heartbeating.
507#[repr(C)]
508#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
509#[cfg_attr(feature = "trivial_copy", derive(Copy))]
510#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
511#[cfg_attr(
512    feature = "python",
513    pyo3::pyclass(dict, module = "databento_dbn"),
514    derive(crate::macros::PyFieldDesc)
515)]
516#[cfg_attr(not(feature = "python"), derive(MockPyo3))] // bring `pyo3` attribute into scope
517#[cfg_attr(test, derive(type_layout::TypeLayout))]
518#[dbn_record(rtype::SYSTEM)]
519pub struct SystemMsgV1 {
520    /// The common header.
521    #[pyo3(get, set)]
522    pub hd: RecordHeader,
523    /// The message from the Databento Live Subscription Gateway (LSG).
524    #[dbn(fmt_method)]
525    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
526    pub msg: [c_char; 64],
527}
528
529/// Definition of an instrument. The record of the
530/// [`Definition`](crate::enums::Schema::Definition) schema.
531#[repr(C)]
532#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)]
533#[cfg_attr(feature = "trivial_copy", derive(Copy))]
534#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
535#[cfg_attr(
536    feature = "python",
537    pyo3::pyclass(dict, module = "databento_dbn"),
538    derive(crate::macros::PyFieldDesc)
539)]
540#[cfg_attr(not(feature = "python"), derive(MockPyo3))] // bring `pyo3` attribute into scope
541#[cfg_attr(test, derive(type_layout::TypeLayout))]
542#[dbn_record(rtype::INSTRUMENT_DEF)]
543pub struct InstrumentDefMsgV3 {
544    /// The common header.
545    #[pyo3(get, set)]
546    pub hd: RecordHeader,
547    /// The capture-server-received timestamp expressed as the number of nanoseconds
548    /// since the UNIX epoch.
549    #[dbn(encode_order(0), index_ts, unix_nanos)]
550    #[pyo3(get, set)]
551    pub ts_recv: u64,
552    /// The minimum constant tick for the instrument in units of 1e-9, i.e.
553    /// 1/1,000,000,000 or 0.000000001.
554    #[dbn(fixed_price)]
555    #[pyo3(get, set)]
556    pub min_price_increment: i64,
557    /// The multiplier to convert the venue’s display price to the conventional price,
558    /// in units of 1e-9, i.e. 1/1,000,000,000 or 0.000000001.
559    #[dbn(fixed_price)]
560    #[pyo3(get, set)]
561    pub display_factor: i64,
562    /// The last eligible trade time expressed as a number of nanoseconds since the
563    /// UNIX epoch.
564    ///
565    /// Will be [`crate::UNDEF_TIMESTAMP`] when null, such as for equities. Some publishers
566    /// only provide date-level granularity.
567    #[dbn(unix_nanos)]
568    #[pyo3(get, set)]
569    pub expiration: u64,
570    /// The time of instrument activation expressed as a number of nanoseconds since the
571    /// UNIX epoch.
572    ///
573    /// Will be [`crate::UNDEF_TIMESTAMP`] when null, such as for equities. Some publishers
574    /// only provide date-level granularity.
575    #[dbn(unix_nanos)]
576    #[pyo3(get, set)]
577    pub activation: u64,
578    /// The allowable high limit price for the trading day in units of 1e-9, i.e.
579    /// 1/1,000,000,000 or 0.000000001.
580    #[dbn(fixed_price)]
581    #[pyo3(get, set)]
582    pub high_limit_price: i64,
583    /// The allowable low limit price for the trading day in units of 1e-9, i.e.
584    /// 1/1,000,000,000 or 0.000000001.
585    #[dbn(fixed_price)]
586    #[pyo3(get, set)]
587    pub low_limit_price: i64,
588    /// The differential value for price banding in units of 1e-9, i.e. 1/1,000,000,000
589    /// or 0.000000001.
590    #[dbn(fixed_price)]
591    #[pyo3(get, set)]
592    pub max_price_variation: i64,
593    /// The contract size for each instrument, in combination with `unit_of_measure`, in units
594    /// of 1e-9, i.e. 1/1,000,000,000 or 0.000000001.
595    #[dbn(fixed_price)]
596    #[pyo3(get, set)]
597    pub unit_of_measure_qty: i64,
598    /// The value currently under development by the venue. Converted to units of 1e-9, i.e.
599    /// 1/1,000,000,000 or 0.000000001.
600    #[dbn(fixed_price)]
601    #[pyo3(get, set)]
602    pub min_price_increment_amount: i64,
603    /// The value used for price calculation in spread and leg pricing in units of 1e-9,
604    /// i.e. 1/1,000,000,000 or 0.000000001.
605    #[dbn(fixed_price)]
606    #[pyo3(get, set)]
607    pub price_ratio: i64,
608    /// The strike price of the option. Converted to units of 1e-9, i.e. 1/1,000,000,000
609    /// or 0.000000001.
610    #[dbn(fixed_price, encode_order(54))]
611    #[pyo3(get, set)]
612    pub strike_price: i64,
613    /// The instrument ID assigned by the publisher. May be the same as `instrument_id`.
614    #[dbn(encode_order(20))]
615    #[pyo3(get, set)]
616    pub raw_instrument_id: u64,
617    /// The tied price (if any) of the leg.
618    #[dbn(fixed_price, encode_order(165))]
619    #[pyo3(get, set)]
620    pub leg_price: i64,
621    /// The associated delta (if any) of the leg.
622    #[dbn(fixed_price, encode_order(166))]
623    #[pyo3(get, set)]
624    pub leg_delta: i64,
625    /// A bitmap of instrument eligibility attributes.
626    #[dbn(fmt_binary)]
627    #[pyo3(get, set)]
628    pub inst_attrib_value: i32,
629    /// The `instrument_id` of the first underlying instrument.
630    #[pyo3(get, set)]
631    pub underlying_id: u32,
632    /// The implied book depth on the price level data feed.
633    #[pyo3(get, set)]
634    pub market_depth_implied: i32,
635    /// The (outright) book depth on the price level data feed.
636    #[pyo3(get, set)]
637    pub market_depth: i32,
638    /// The market segment of the instrument.
639    #[pyo3(get, set)]
640    pub market_segment_id: u32,
641    /// The maximum trading volume for the instrument.
642    #[pyo3(get, set)]
643    pub max_trade_vol: u32,
644    /// The minimum order entry quantity for the instrument.
645    #[pyo3(get, set)]
646    pub min_lot_size: i32,
647    /// The minimum quantity required for a block trade of the instrument.
648    #[pyo3(get, set)]
649    pub min_lot_size_block: i32,
650    /// The minimum quantity required for a round lot of the instrument. Multiples of
651    /// this quantity are also round lots.
652    #[pyo3(get, set)]
653    pub min_lot_size_round_lot: i32,
654    /// The minimum trading volume for the instrument.
655    #[pyo3(get, set)]
656    pub min_trade_vol: u32,
657    /// The number of deliverables per instrument, i.e. peak days.
658    #[pyo3(get, set)]
659    pub contract_multiplier: i32,
660    /// The quantity that a contract will decay daily, after `decay_start_date` has
661    /// been reached.
662    #[pyo3(get, set)]
663    pub decay_quantity: i32,
664    /// The fixed contract value assigned to each instrument.
665    #[pyo3(get, set)]
666    pub original_contract_size: i32,
667    /// The numeric ID assigned to the leg instrument.
668    #[dbn(encode_order(160))]
669    #[pyo3(get, set)]
670    pub leg_instrument_id: u32,
671    /// The numerator of the quantity ratio of the leg within the spread.
672    #[dbn(encode_order(167))]
673    #[pyo3(get, set)]
674    pub leg_ratio_price_numerator: i32,
675    /// The denominator of the quantity ratio of the leg within the spread.
676    #[dbn(encode_order(168))]
677    #[pyo3(get, set)]
678    pub leg_ratio_price_denominator: i32,
679    /// The numerator of the price ratio of the leg within the spread.
680    #[dbn(encode_order(169))]
681    #[pyo3(get, set)]
682    pub leg_ratio_qty_numerator: i32,
683    /// The denominator of the price ratio of the leg within the spread.
684    #[dbn(encode_order(170))]
685    #[pyo3(get, set)]
686    pub leg_ratio_qty_denominator: i32,
687    /// The numeric ID of the leg instrument's underlying instrument.
688    #[dbn(encode_order(171))]
689    #[pyo3(get, set)]
690    pub leg_underlying_id: u32,
691    /// The channel ID assigned at the venue.
692    #[pyo3(get, set)]
693    pub appl_id: i16,
694    /// The calendar year reflected in the instrument symbol.
695    #[pyo3(get, set)]
696    pub maturity_year: u16,
697    /// The date at which a contract will begin to decay.
698    #[pyo3(get, set)]
699    pub decay_start_date: u16,
700    /// The channel ID assigned by Databento as an incrementing integer starting at zero.
701    #[pyo3(get, set)]
702    pub channel_id: u16,
703    /// The number of legs in the strategy or spread. Will be 0 for outrights.
704    #[dbn(encode_order(158))]
705    #[pyo3(get, set)]
706    pub leg_count: u16,
707    /// The 0-based index of the leg.
708    #[dbn(encode_order(159))]
709    #[pyo3(get, set)]
710    pub leg_index: u16,
711    /// The currency used for price fields.
712    #[dbn(fmt_method)]
713    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
714    pub currency: [c_char; 4],
715    /// The currency used for settlement, if different from `currency`.
716    #[dbn(fmt_method)]
717    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
718    pub settl_currency: [c_char; 4],
719    /// The strategy type of the spread.
720    #[dbn(fmt_method)]
721    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
722    pub secsubtype: [c_char; 6],
723    /// The instrument raw symbol assigned by the publisher.
724    #[dbn(encode_order(2), fmt_method)]
725    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
726    pub raw_symbol: [c_char; SYMBOL_CSTR_LEN_V3],
727    /// The security group code of the instrument.
728    #[dbn(fmt_method)]
729    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
730    pub group: [c_char; 21],
731    /// The exchange used to identify the instrument.
732    #[dbn(fmt_method)]
733    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
734    pub exchange: [c_char; 5],
735    /// The underlying asset code (product code) of the instrument.
736    #[dbn(fmt_method)]
737    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
738    pub asset: [c_char; 7],
739    /// The ISO standard instrument categorization code.
740    #[dbn(fmt_method)]
741    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
742    pub cfi: [c_char; 7],
743    /// The type of the instrument, e.g. FUT for future or future spread.
744    #[dbn(fmt_method)]
745    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
746    pub security_type: [c_char; 7],
747    /// The unit of measure for the instrument’s original contract size, e.g. USD or LBS.
748    #[dbn(fmt_method)]
749    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
750    pub unit_of_measure: [c_char; 31],
751    /// The symbol of the first underlying instrument.
752    #[dbn(fmt_method)]
753    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
754    pub underlying: [c_char; 21],
755    /// The currency of [`strike_price`](Self::strike_price).
756    #[dbn(fmt_method)]
757    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
758    pub strike_price_currency: [c_char; 4],
759    /// The leg instrument's raw symbol assigned by the publisher.
760    #[dbn(encode_order(161), fmt_method)]
761    #[cfg_attr(feature = "serde", serde(with = "crate::record::cstr_serde"))]
762    #[pyo3(get)]
763    pub leg_raw_symbol: [c_char; SYMBOL_CSTR_LEN_V3],
764    /// The classification of the instrument.
765    #[dbn(c_char, encode_order(4))]
766    #[pyo3(set)]
767    pub instrument_class: c_char,
768    /// The matching algorithm used for the instrument, typically **F**IFO.
769    #[dbn(c_char)]
770    #[pyo3(set)]
771    pub match_algorithm: c_char,
772    /// The price denominator of the main fraction.
773    #[pyo3(get, set)]
774    pub main_fraction: u8,
775    ///  The number of digits to the right of the tick mark, to display fractional prices.
776    #[pyo3(get, set)]
777    pub price_display_format: u8,
778    /// The price denominator of the sub fraction.
779    #[pyo3(get, set)]
780    pub sub_fraction: u8,
781    /// The product complex of the instrument.
782    #[pyo3(get, set)]
783    pub underlying_product: u8,
784    /// Indicates if the instrument definition has been added, modified, or deleted.
785    #[dbn(c_char, encode_order(3))]
786    pub security_update_action: c_char,
787    /// The calendar month reflected in the instrument symbol.
788    #[pyo3(get, set)]
789    pub maturity_month: u8,
790    /// The calendar day reflected in the instrument symbol, or 0.
791    #[pyo3(get, set)]
792    pub maturity_day: u8,
793    /// The calendar week reflected in the instrument symbol, or 0.
794    #[pyo3(get, set)]
795    pub maturity_week: u8,
796    /// Indicates if the instrument is user defined: **Y**es or **N**o.
797    #[dbn(c_char)]
798    pub user_defined_instrument: c_char,
799    /// The type of `contract_multiplier`. Either `1` for hours, or `2` for days.
800    #[pyo3(get, set)]
801    pub contract_multiplier_unit: i8,
802    /// The schedule for delivering electricity.
803    #[pyo3(get, set)]
804    pub flow_schedule_type: i8,
805    /// The tick rule of the spread.
806    #[pyo3(get, set)]
807    pub tick_rule: u8,
808    /// The classification of the leg instrument.
809    #[dbn(c_char, encode_order(163))]
810    pub leg_instrument_class: c_char,
811    /// The side taken for the leg when purchasing the spread.
812    #[dbn(c_char, encode_order(164))]
813    pub leg_side: c_char,
814    // Filler for alignment.
815    #[doc(hidden)]
816    #[cfg_attr(feature = "serde", serde(skip))]
817    pub _reserved: [u8; 21],
818}
819
820#[cfg(test)]
821mod tests {
822    use std::ffi::c_char;
823
824    use time::OffsetDateTime;
825
826    use crate::{Mbp1Msg, Record, MAX_RECORD_LEN};
827
828    use super::*;
829
830    #[cfg(feature = "python")]
831    #[test]
832    fn test_strike_price_order_didnt_change() {
833        use crate::python::PyFieldDesc;
834
835        assert_eq!(
836            InstrumentDefMsgV1::ordered_fields(""),
837            InstrumentDefMsgV2::ordered_fields("")
838        );
839    }
840
841    #[test]
842    fn upgrade_symbol_mapping_ts_out() -> crate::Result<()> {
843        let orig = WithTsOut::new(
844            SymbolMappingMsgV1::new(1, 2, "ES.c.0", "ESH4", 0, 0)?,
845            OffsetDateTime::now_utc().unix_timestamp_nanos() as u64,
846        );
847        let mut compat_buffer = [0; MAX_RECORD_LEN];
848        let res = unsafe {
849            decode_record_ref(
850                1,
851                VersionUpgradePolicy::UpgradeToV2,
852                true,
853                &mut compat_buffer,
854                orig.as_ref(),
855            )
856        };
857        let upgraded = res.get::<WithTsOut<SymbolMappingMsgV2>>().unwrap();
858        assert_eq!(orig.ts_out, upgraded.ts_out);
859        assert_eq!(orig.rec.stype_in_symbol()?, upgraded.rec.stype_in_symbol()?);
860        assert_eq!(
861            orig.rec.stype_out_symbol()?,
862            upgraded.rec.stype_out_symbol()?
863        );
864        assert_eq!(upgraded.record_size(), std::mem::size_of_val(upgraded));
865        // used compat buffer
866        assert!(std::ptr::addr_eq(upgraded.header(), compat_buffer.as_ptr()));
867        Ok(())
868    }
869
870    #[test]
871    fn upgrade_mbp1_ts_out() -> crate::Result<()> {
872        let rec = Mbp1Msg {
873            price: 1_250_000_000,
874            side: b'A' as c_char,
875            ..Mbp1Msg::default()
876        };
877        let orig = WithTsOut::new(rec, OffsetDateTime::now_utc().unix_timestamp_nanos() as u64);
878        let mut compat_buffer = [0; MAX_RECORD_LEN];
879        let res = unsafe {
880            decode_record_ref(
881                1,
882                VersionUpgradePolicy::UpgradeToV2,
883                true,
884                &mut compat_buffer,
885                orig.as_ref(),
886            )
887        };
888        let upgraded = res.get::<WithTsOut<Mbp1Msg>>().unwrap();
889        // compat buffer unused and pointer unchanged
890        assert!(std::ptr::eq(orig.header(), upgraded.header()));
891        Ok(())
892    }
893}