nautilus_databento/decode/
primitives.rs1use std::ffi::c_char;
17
18use databento::dbn;
19use nautilus_core::UnixNanos;
20use nautilus_model::{
21 enums::{AggressorSide, AssetClass, BookAction, InstrumentClass, OptionKind, OrderSide},
22 identifiers::Symbol,
23 types::{
24 Currency, Price, Quantity,
25 price::{PRICE_UNDEF, decode_raw_price_i64},
26 },
27};
28use ustr::Ustr;
29
30#[must_use]
31pub const fn parse_optional_bool(c: c_char) -> Option<bool> {
32 match c as u8 as char {
33 'Y' => Some(true),
34 'N' => Some(false),
35 _ => None,
36 }
37}
38
39#[must_use]
40pub const fn parse_order_side(c: c_char) -> OrderSide {
41 match c as u8 as char {
42 'A' => OrderSide::Sell,
43 'B' => OrderSide::Buy,
44 _ => OrderSide::NoOrderSide,
45 }
46}
47
48#[must_use]
49pub const fn parse_aggressor_side(c: c_char) -> AggressorSide {
50 match c as u8 as char {
51 'A' => AggressorSide::Seller,
52 'B' => AggressorSide::Buyer,
53 _ => AggressorSide::NoAggressor,
54 }
55}
56
57pub fn parse_book_action(c: c_char) -> anyhow::Result<BookAction> {
63 match c as u8 as char {
64 'A' => Ok(BookAction::Add),
65 'C' => Ok(BookAction::Delete),
66 'F' => Ok(BookAction::Update),
67 'M' => Ok(BookAction::Update),
68 'R' => Ok(BookAction::Clear),
69 invalid => anyhow::bail!("Invalid `BookAction`, was '{invalid}'"),
70 }
71}
72
73pub fn parse_option_kind(c: c_char) -> anyhow::Result<OptionKind> {
79 match c as u8 as char {
80 'C' => Ok(OptionKind::Call),
81 'P' => Ok(OptionKind::Put),
82 invalid => anyhow::bail!("Invalid `OptionKind`, was '{invalid}'"),
83 }
84}
85
86pub(super) fn parse_currency_or_usd_default(
87 value: Result<&str, impl std::error::Error>,
88) -> Currency {
89 match value {
90 Ok(value) if !value.is_empty() => Currency::try_from_str(value).unwrap_or_else(|| {
91 log::warn!("Unknown currency code '{value}', defaulting to USD");
92 Currency::USD()
93 }),
94 Ok(_) => Currency::USD(),
95 Err(e) => {
96 log::warn!("Error parsing currency: {e}");
97 Currency::USD()
98 }
99 }
100}
101
102#[must_use]
106pub fn parse_cfi_iso10926(value: &str) -> (Option<AssetClass>, Option<InstrumentClass>) {
107 let chars: Vec<char> = value.chars().collect();
108 if chars.len() < 3 {
109 return (None, None);
110 }
111
112 let cfi_category = chars[0];
114 let cfi_group = chars[1];
115 let cfi_attribute1 = chars[2];
116 let mut asset_class = match cfi_category {
121 'D' => Some(AssetClass::Debt),
122 'E' => Some(AssetClass::Equity),
123 'S' => None,
124 _ => None,
125 };
126
127 let instrument_class = match cfi_group {
128 'I' => Some(InstrumentClass::Future),
129 _ => None,
130 };
131
132 if cfi_attribute1 == 'I' {
133 asset_class = Some(AssetClass::Index);
134 }
135
136 (asset_class, instrument_class)
137}
138
139pub(super) fn decode_underlying(underlying_str: &str, symbol: &Symbol) -> Ustr {
140 if underlying_str.is_empty() {
141 symbol
143 .as_str()
144 .split_whitespace()
145 .next()
146 .map_or_else(|| symbol.inner(), Ustr::from)
147 } else {
148 Ustr::from(underlying_str)
149 }
150}
151
152pub fn parse_status_reason(value: u16) -> anyhow::Result<Option<Ustr>> {
160 let value_str = match value {
161 0 => return Ok(None),
162 1 => "Scheduled",
163 2 => "Surveillance intervention",
164 3 => "Market event",
165 4 => "Instrument activation",
166 5 => "Instrument expiration",
167 6 => "Recovery in process",
168 10 => "Regulatory",
169 11 => "Administrative",
170 12 => "Non-compliance",
171 13 => "Filings not current",
172 14 => "SEC trading suspension",
173 15 => "New issue",
174 16 => "Issue available",
175 17 => "Issues reviewed",
176 18 => "Filing requirements satisfied",
177 30 => "News pending",
178 31 => "News released",
179 32 => "News and resumption times",
180 33 => "News not forthcoming",
181 40 => "Order imbalance",
182 50 => "LULD pause",
183 60 => "Operational",
184 70 => "Additional information requested",
185 80 => "Merger effective",
186 90 => "ETF",
187 100 => "Corporate action",
188 110 => "New Security offering",
189 120 => "Market wide halt level 1",
190 121 => "Market wide halt level 2",
191 122 => "Market wide halt level 3",
192 123 => "Market wide halt carryover",
193 124 => "Market wide halt resumption",
194 130 => "Quotation not available",
195 invalid => anyhow::bail!("Invalid `StatusMsg` reason, was '{invalid}'"),
196 };
197
198 Ok(Some(Ustr::from(value_str)))
199}
200
201pub fn parse_status_trading_event(value: u16) -> anyhow::Result<Option<Ustr>> {
207 let value_str = match value {
208 0 => return Ok(None),
209 1 => "No cancel",
210 2 => "Change trading session",
211 3 => "Implied matching on",
212 4 => "Implied matching off",
213 _ => anyhow::bail!("Invalid `StatusMsg` trading_event, was '{value}'"),
214 };
215
216 Ok(Some(Ustr::from(value_str)))
217}
218
219#[inline(always)]
228pub fn decode_price(value: i64, precision: u8, field_name: &str) -> anyhow::Result<Price> {
229 if value == i64::MAX {
230 anyhow::bail!("Missing required price for `{field_name}`")
231 } else {
232 Ok(Price::from_raw(decode_raw_price_i64(value), precision))
233 }
234}
235
236#[inline(always)]
241#[must_use]
242pub fn decode_optional_price(value: i64, precision: u8) -> Option<Price> {
243 if value == i64::MAX {
244 None
245 } else {
246 Some(Price::from_raw(decode_raw_price_i64(value), precision))
247 }
248}
249
250#[inline(always)]
255#[must_use]
256pub fn decode_price_or_undef(value: i64, precision: u8) -> Price {
257 if value == i64::MAX {
258 Price::from_raw(PRICE_UNDEF, 0)
259 } else {
260 Price::from_raw(decode_raw_price_i64(value), precision)
261 }
262}
263
264#[inline(always)]
270#[must_use]
271pub fn precision_from_raw(value: i64) -> u8 {
272 let mut v = value.unsigned_abs();
273 if v == 0 {
274 return 0;
275 }
276 let mut trailing = 0u8;
277 while trailing < 9 && v.is_multiple_of(10) {
278 v /= 10;
279 trailing += 1;
280 }
281 9 - trailing
282}
283
284#[inline(always)]
290#[must_use]
291pub fn decode_price_increment(value: i64, precision: u8) -> Price {
292 match value {
293 0 | i64::MAX => Price::new(10f64.powi(-i32::from(precision)), precision),
294 _ => {
295 let derived = precision_from_raw(value).max(precision);
296 Price::from_raw(decode_raw_price_i64(value), derived)
297 }
298 }
299}
300
301#[inline(always)]
303#[must_use]
304pub fn decode_quantity(value: u64) -> Quantity {
305 Quantity::from(value)
306}
307
308#[inline(always)]
314pub fn decode_optional_quantity(value: i64) -> anyhow::Result<Option<Quantity>> {
315 match value {
316 i64::MAX => Ok(None),
317 value if value >= 0 => Ok(Some(Quantity::from(value))),
318 value => anyhow::bail!("Invalid negative quantity: {value}"),
319 }
320}
321
322#[inline(always)]
330pub fn decode_timestamp(value: u64, field_name: &str) -> anyhow::Result<UnixNanos> {
331 if value == dbn::UNDEF_TIMESTAMP {
332 anyhow::bail!("Missing required timestamp for `{field_name}`")
333 } else {
334 Ok(UnixNanos::from(value))
335 }
336}
337
338#[inline(always)]
342#[must_use]
343pub fn decode_optional_timestamp(value: u64) -> Option<UnixNanos> {
344 if value == dbn::UNDEF_TIMESTAMP {
345 None
346 } else {
347 Some(UnixNanos::from(value))
348 }
349}
350
351pub fn decode_multiplier(value: i64) -> anyhow::Result<Quantity> {
358 const SCALE: u128 = 1_000_000_000;
359
360 match value {
361 0 | i64::MAX => Ok(Quantity::from(1)),
362 v if v < 0 => anyhow::bail!("Invalid negative multiplier: {v}"),
363 v => {
364 let abs = v as u128;
367 let int_part = abs / SCALE;
368 let frac_part = abs % SCALE;
369
370 if frac_part == 0 {
373 Ok(Quantity::from(int_part as u64))
375 } else {
376 let mut frac_str = format!("{frac_part:09}");
377 while frac_str.ends_with('0') {
378 frac_str.pop();
379 }
380 let s = format!("{int_part}.{frac_str}");
381 Ok(Quantity::from(s))
382 }
383 }
384 }
385}
386
387#[inline(always)]
389#[must_use]
390pub fn decode_lot_size(value: i32) -> Quantity {
391 match value {
392 0 | i32::MAX => Quantity::from(1),
393 value => Quantity::from(value),
394 }
395}