use std::ffi::c_char;
use databento::dbn;
use nautilus_core::UnixNanos;
use nautilus_model::{
enums::{AggressorSide, AssetClass, BookAction, InstrumentClass, OptionKind, OrderSide},
identifiers::Symbol,
types::{
Currency, Price, Quantity,
price::{PRICE_UNDEF, decode_raw_price_i64},
},
};
use ustr::Ustr;
#[must_use]
pub const fn parse_optional_bool(c: c_char) -> Option<bool> {
match c as u8 as char {
'Y' => Some(true),
'N' => Some(false),
_ => None,
}
}
#[must_use]
pub const fn parse_order_side(c: c_char) -> OrderSide {
match c as u8 as char {
'A' => OrderSide::Sell,
'B' => OrderSide::Buy,
_ => OrderSide::NoOrderSide,
}
}
#[must_use]
pub const fn parse_aggressor_side(c: c_char) -> AggressorSide {
match c as u8 as char {
'A' => AggressorSide::Seller,
'B' => AggressorSide::Buyer,
_ => AggressorSide::NoAggressor,
}
}
pub fn parse_book_action(c: c_char) -> anyhow::Result<BookAction> {
match c as u8 as char {
'A' => Ok(BookAction::Add),
'C' => Ok(BookAction::Delete),
'F' => Ok(BookAction::Update),
'M' => Ok(BookAction::Update),
'R' => Ok(BookAction::Clear),
invalid => anyhow::bail!("Invalid `BookAction`, was '{invalid}'"),
}
}
pub fn parse_option_kind(c: c_char) -> anyhow::Result<OptionKind> {
match c as u8 as char {
'C' => Ok(OptionKind::Call),
'P' => Ok(OptionKind::Put),
invalid => anyhow::bail!("Invalid `OptionKind`, was '{invalid}'"),
}
}
pub(super) fn parse_currency_or_usd_default(
value: Result<&str, impl std::error::Error>,
) -> Currency {
match value {
Ok(value) if !value.is_empty() => Currency::try_from_str(value).unwrap_or_else(|| {
log::warn!("Unknown currency code '{value}', defaulting to USD");
Currency::USD()
}),
Ok(_) => Currency::USD(),
Err(e) => {
log::warn!("Error parsing currency: {e}");
Currency::USD()
}
}
}
#[must_use]
pub fn parse_cfi_iso10926(value: &str) -> (Option<AssetClass>, Option<InstrumentClass>) {
let chars: Vec<char> = value.chars().collect();
if chars.len() < 3 {
return (None, None);
}
let cfi_category = chars[0];
let cfi_group = chars[1];
let cfi_attribute1 = chars[2];
let mut asset_class = match cfi_category {
'D' => Some(AssetClass::Debt),
'E' => Some(AssetClass::Equity),
'S' => None,
_ => None,
};
let instrument_class = match cfi_group {
'I' => Some(InstrumentClass::Future),
_ => None,
};
if cfi_attribute1 == 'I' {
asset_class = Some(AssetClass::Index);
}
(asset_class, instrument_class)
}
pub(super) fn decode_underlying(underlying_str: &str, symbol: &Symbol) -> Ustr {
if underlying_str.is_empty() {
symbol
.as_str()
.split_whitespace()
.next()
.map_or_else(|| symbol.inner(), Ustr::from)
} else {
Ustr::from(underlying_str)
}
}
pub fn parse_status_reason(value: u16) -> anyhow::Result<Option<Ustr>> {
let value_str = match value {
0 => return Ok(None),
1 => "Scheduled",
2 => "Surveillance intervention",
3 => "Market event",
4 => "Instrument activation",
5 => "Instrument expiration",
6 => "Recovery in process",
10 => "Regulatory",
11 => "Administrative",
12 => "Non-compliance",
13 => "Filings not current",
14 => "SEC trading suspension",
15 => "New issue",
16 => "Issue available",
17 => "Issues reviewed",
18 => "Filing requirements satisfied",
30 => "News pending",
31 => "News released",
32 => "News and resumption times",
33 => "News not forthcoming",
40 => "Order imbalance",
50 => "LULD pause",
60 => "Operational",
70 => "Additional information requested",
80 => "Merger effective",
90 => "ETF",
100 => "Corporate action",
110 => "New Security offering",
120 => "Market wide halt level 1",
121 => "Market wide halt level 2",
122 => "Market wide halt level 3",
123 => "Market wide halt carryover",
124 => "Market wide halt resumption",
130 => "Quotation not available",
invalid => anyhow::bail!("Invalid `StatusMsg` reason, was '{invalid}'"),
};
Ok(Some(Ustr::from(value_str)))
}
pub fn parse_status_trading_event(value: u16) -> anyhow::Result<Option<Ustr>> {
let value_str = match value {
0 => return Ok(None),
1 => "No cancel",
2 => "Change trading session",
3 => "Implied matching on",
4 => "Implied matching off",
_ => anyhow::bail!("Invalid `StatusMsg` trading_event, was '{value}'"),
};
Ok(Some(Ustr::from(value_str)))
}
#[inline(always)]
pub fn decode_price(value: i64, precision: u8, field_name: &str) -> anyhow::Result<Price> {
if value == i64::MAX {
anyhow::bail!("Missing required price for `{field_name}`")
} else {
Ok(Price::from_raw(decode_raw_price_i64(value), precision))
}
}
#[inline(always)]
#[must_use]
pub fn decode_optional_price(value: i64, precision: u8) -> Option<Price> {
if value == i64::MAX {
None
} else {
Some(Price::from_raw(decode_raw_price_i64(value), precision))
}
}
#[inline(always)]
#[must_use]
pub fn decode_price_or_undef(value: i64, precision: u8) -> Price {
if value == i64::MAX {
Price::from_raw(PRICE_UNDEF, 0)
} else {
Price::from_raw(decode_raw_price_i64(value), precision)
}
}
#[inline(always)]
#[must_use]
pub fn precision_from_raw(value: i64) -> u8 {
let mut v = value.unsigned_abs();
if v == 0 {
return 0;
}
let mut trailing = 0u8;
while trailing < 9 && v.is_multiple_of(10) {
v /= 10;
trailing += 1;
}
9 - trailing
}
#[inline(always)]
#[must_use]
pub fn decode_price_increment(value: i64, precision: u8) -> Price {
match value {
0 | i64::MAX => Price::new(10f64.powi(-i32::from(precision)), precision),
_ => {
let derived = precision_from_raw(value).max(precision);
Price::from_raw(decode_raw_price_i64(value), derived)
}
}
}
#[inline(always)]
#[must_use]
pub fn decode_quantity(value: u64) -> Quantity {
Quantity::from(value)
}
#[inline(always)]
#[must_use]
pub fn decode_optional_quantity(value: i64) -> Option<Quantity> {
match value {
i64::MAX => None,
_ => Some(Quantity::from(value)),
}
}
#[inline(always)]
pub fn decode_timestamp(value: u64, field_name: &str) -> anyhow::Result<UnixNanos> {
if value == dbn::UNDEF_TIMESTAMP {
anyhow::bail!("Missing required timestamp for `{field_name}`")
} else {
Ok(UnixNanos::from(value))
}
}
#[inline(always)]
#[must_use]
pub fn decode_optional_timestamp(value: u64) -> Option<UnixNanos> {
if value == dbn::UNDEF_TIMESTAMP {
None
} else {
Some(UnixNanos::from(value))
}
}
pub fn decode_multiplier(value: i64) -> anyhow::Result<Quantity> {
const SCALE: u128 = 1_000_000_000;
match value {
0 | i64::MAX => Ok(Quantity::from(1)),
v if v < 0 => anyhow::bail!("Invalid negative multiplier: {v}"),
v => {
let abs = v as u128;
let int_part = abs / SCALE;
let frac_part = abs % SCALE;
if frac_part == 0 {
Ok(Quantity::from(int_part as u64))
} else {
let mut frac_str = format!("{frac_part:09}");
while frac_str.ends_with('0') {
frac_str.pop();
}
let s = format!("{int_part}.{frac_str}");
Ok(Quantity::from(s))
}
}
}
}
#[inline(always)]
#[must_use]
pub fn decode_lot_size(value: i32) -> Quantity {
match value {
0 | i32::MAX => Quantity::from(1),
value => Quantity::from(value),
}
}