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) -> Option<OrderSide> {
41 match c as u8 as char {
42 'A' => Some(OrderSide::Sell),
43 'B' => Some(OrderSide::Buy),
44 _ => None,
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::Sell,
52 'B' => AggressorSide::Buy,
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 'M' => Ok(BookAction::Update),
67 'R' => Ok(BookAction::Clear),
68 invalid => anyhow::bail!("Invalid `BookAction`, was '{invalid}'"),
73 }
74}
75
76pub fn parse_option_kind(c: c_char) -> anyhow::Result<OptionKind> {
82 match c as u8 as char {
83 'C' => Ok(OptionKind::Call),
84 'P' => Ok(OptionKind::Put),
85 invalid => anyhow::bail!("Invalid `OptionKind`, was '{invalid}'"),
86 }
87}
88
89pub(super) fn parse_currency_or_usd_default(
90 value: Result<&str, impl std::error::Error>,
91) -> Currency {
92 match value {
93 Ok(value) if !value.is_empty() => Currency::try_from_str(value).unwrap_or_else(|| {
94 log::warn!("Unknown currency code '{value}', defaulting to USD");
95 Currency::USD()
96 }),
97 Ok(_) => Currency::USD(),
98 Err(e) => {
99 log::warn!("Error parsing currency: {e}");
100 Currency::USD()
101 }
102 }
103}
104
105#[must_use]
109pub fn parse_cfi_iso10926(value: &str) -> (Option<AssetClass>, Option<InstrumentClass>) {
110 let chars: Vec<char> = value.chars().collect();
111 if chars.len() < 3 {
112 return (None, None);
113 }
114
115 let cfi_category = chars[0];
117 let cfi_group = chars[1];
118 let cfi_attribute1 = chars[2];
119 let mut asset_class = match cfi_category {
124 'D' => Some(AssetClass::Debt),
125 'E' => Some(AssetClass::Equity),
126 'S' => None,
127 _ => None,
128 };
129
130 let instrument_class = match cfi_group {
131 'I' => Some(InstrumentClass::Future),
132 _ => None,
133 };
134
135 if cfi_attribute1 == 'I' {
136 asset_class = Some(AssetClass::Index);
137 }
138
139 (asset_class, instrument_class)
140}
141
142pub(super) fn decode_underlying(underlying_str: &str, symbol: &Symbol) -> Ustr {
143 if underlying_str.is_empty() {
144 symbol
146 .as_str()
147 .split_whitespace()
148 .next()
149 .map_or_else(|| symbol.inner(), Ustr::from)
150 } else {
151 Ustr::from(underlying_str)
152 }
153}
154
155pub fn parse_status_reason(value: u16) -> anyhow::Result<Option<Ustr>> {
163 let value_str = match value {
164 0 => return Ok(None),
165 1 => "Scheduled",
166 2 => "Surveillance intervention",
167 3 => "Market event",
168 4 => "Instrument activation",
169 5 => "Instrument expiration",
170 6 => "Recovery in process",
171 10 => "Regulatory",
172 11 => "Administrative",
173 12 => "Non-compliance",
174 13 => "Filings not current",
175 14 => "SEC trading suspension",
176 15 => "New issue",
177 16 => "Issue available",
178 17 => "Issues reviewed",
179 18 => "Filing requirements satisfied",
180 30 => "News pending",
181 31 => "News released",
182 32 => "News and resumption times",
183 33 => "News not forthcoming",
184 40 => "Order imbalance",
185 50 => "LULD pause",
186 60 => "Operational",
187 70 => "Additional information requested",
188 80 => "Merger effective",
189 90 => "ETF",
190 100 => "Corporate action",
191 110 => "New Security offering",
192 120 => "Market wide halt level 1",
193 121 => "Market wide halt level 2",
194 122 => "Market wide halt level 3",
195 123 => "Market wide halt carryover",
196 124 => "Market wide halt resumption",
197 130 => "Quotation not available",
198 invalid => anyhow::bail!("Invalid `StatusMsg` reason, was '{invalid}'"),
199 };
200
201 Ok(Some(Ustr::from(value_str)))
202}
203
204pub fn parse_status_trading_event(value: u16) -> anyhow::Result<Option<Ustr>> {
210 let value_str = match value {
211 0 => return Ok(None),
212 1 => "No cancel",
213 2 => "Change trading session",
214 3 => "Implied matching on",
215 4 => "Implied matching off",
216 _ => anyhow::bail!("Invalid `StatusMsg` trading_event, was '{value}'"),
217 };
218
219 Ok(Some(Ustr::from(value_str)))
220}
221
222#[inline(always)]
231pub fn decode_price(value: i64, precision: u8, field_name: &str) -> anyhow::Result<Price> {
232 if value == i64::MAX {
233 anyhow::bail!("Missing required price for `{field_name}`")
234 } else {
235 Ok(Price::from_raw(decode_raw_price_i64(value), precision))
236 }
237}
238
239#[inline(always)]
244#[must_use]
245pub fn decode_optional_price(value: i64, precision: u8) -> Option<Price> {
246 if value == i64::MAX {
247 None
248 } else {
249 Some(Price::from_raw(decode_raw_price_i64(value), precision))
250 }
251}
252
253#[inline(always)]
258#[must_use]
259pub fn decode_price_or_undef(value: i64, precision: u8) -> Price {
260 if value == i64::MAX {
261 Price::from_raw(PRICE_UNDEF, 0)
262 } else {
263 Price::from_raw(decode_raw_price_i64(value), precision)
264 }
265}
266
267#[inline(always)]
273#[must_use]
274pub fn precision_from_raw(value: i64) -> u8 {
275 let mut v = value.unsigned_abs();
276 if v == 0 {
277 return 0;
278 }
279 let mut trailing = 0u8;
280 while trailing < 9 && v.is_multiple_of(10) {
281 v /= 10;
282 trailing += 1;
283 }
284 9 - trailing
285}
286
287#[inline(always)]
297#[must_use]
298pub fn decode_price_increment(value: i64, precision: u8) -> Price {
299 match value {
300 0 | i64::MAX => {
301 let exponent = i8::try_from(precision).expect("precision exceeded i8 range");
302 Price::from_mantissa_exponent(1, -exponent, precision)
303 }
304 _ => {
305 let derived = precision_from_raw(value).max(precision);
306 Price::from_raw(decode_raw_price_i64(value), derived)
307 }
308 }
309}
310
311#[inline(always)]
313#[must_use]
314pub fn decode_quantity(value: u64) -> Quantity {
315 quantity_from_whole(value)
316}
317
318#[inline(always)]
324pub fn decode_optional_quantity(value: i64) -> anyhow::Result<Option<Quantity>> {
325 match value {
326 i64::MAX => Ok(None),
327 value if value >= 0 => Ok(Some(quantity_from_whole(value as u64))),
328 value => anyhow::bail!("Invalid negative quantity: {value}"),
329 }
330}
331
332#[inline(always)]
340pub fn decode_timestamp(value: u64, field_name: &str) -> anyhow::Result<UnixNanos> {
341 if value == dbn::UNDEF_TIMESTAMP {
342 anyhow::bail!("Missing required timestamp for `{field_name}`")
343 } else {
344 Ok(UnixNanos::from(value))
345 }
346}
347
348#[inline(always)]
352#[must_use]
353pub fn decode_optional_timestamp(value: u64) -> Option<UnixNanos> {
354 if value == dbn::UNDEF_TIMESTAMP {
355 None
356 } else {
357 Some(UnixNanos::from(value))
358 }
359}
360
361pub fn decode_multiplier(value: i64) -> anyhow::Result<Quantity> {
368 const SCALE: u64 = 1_000_000_000;
369
370 match value {
371 0 | i64::MAX => Ok(quantity_from_whole(1)),
372 v if v < 0 => anyhow::bail!("Invalid negative multiplier: {v}"),
373 v => {
374 let mantissa = v as u64;
375 let mut frac_part = mantissa % SCALE;
376 let mut precision = 9u8;
377 while precision > 0 && frac_part.is_multiple_of(10) {
378 frac_part /= 10;
379 precision -= 1;
380 }
381
382 Ok(Quantity::from_mantissa_exponent_checked(
383 mantissa, -9, precision,
384 )?)
385 }
386 }
387}
388
389#[inline(always)]
395#[must_use]
396pub fn decode_lot_size(value: i32) -> Quantity {
397 match value {
398 0 | i32::MAX => quantity_from_whole(1),
399 value => {
400 assert!(value >= 0, "Invalid negative lot size: {value}");
401 quantity_from_whole(value as u64)
402 }
403 }
404}
405
406#[inline(always)]
407#[must_use]
408fn quantity_from_whole(value: u64) -> Quantity {
409 Quantity::from_mantissa_exponent(value, 0, 0)
410}