use crate::contracts::{
ComboLeg, ComboLegOpenClose, Contract, Currency, DeltaNeutralContract, Exchange, LegAction, SecurityIdType, SecurityType, Symbol, TagValue,
};
use crate::orders::SoftDollarTier;
use crate::proto;
use crate::Error;
pub(crate) trait DecodeProto: prost::Message + Default {
fn decode_proto(buf: &[u8]) -> Result<Self, Error> {
Self::decode(buf).map_err(Error::protobuf_decode)
}
}
impl<M: prost::Message + Default> DecodeProto for M {}
pub(crate) fn s(opt: &Option<String>) -> String {
opt.clone().unwrap_or_default()
}
pub(crate) fn optional_f64(val: Option<f64>) -> Option<f64> {
val.filter(|&v| v != f64::MAX)
}
const UNSET_DECIMAL_WIRE: [&str; 3] = [
"9223372036854775807", "2147483647", "-9223372036854775808", ];
pub(crate) fn parse_optional_decimal(opt: Option<&str>) -> Result<Option<f64>, Error> {
let Some(text) = opt.filter(|t| !t.is_empty()) else {
return Ok(None);
};
if UNSET_DECIMAL_WIRE.contains(&text) {
return Ok(None);
}
let value = text
.parse::<f64>()
.map_err(|e| Error::parse_field(text, format!("invalid decimal wire value: {e}")))?;
if !value.is_finite() || value == f64::MAX {
return Ok(None);
}
Ok(Some(value))
}
pub(crate) fn parse_decimal_or_zero(opt: Option<&str>) -> Result<f64, Error> {
Ok(parse_optional_decimal(opt)?.unwrap_or(0.0))
}
pub(crate) fn required_decimal(opt: Option<&str>, field: &str, message: &str) -> Result<f64, Error> {
required(parse_optional_decimal(opt)?, field, message)
}
pub(crate) fn parse_required<T>(opt: Option<&str>, label: &str) -> Result<T, Error>
where
T: std::str::FromStr<Err = Error>,
{
match opt {
Some(s) if !s.is_empty() => s.parse(),
_ => Err(Error::Parse(0, String::new(), format!("missing {label}"))),
}
}
pub(crate) fn parse_optional<T>(opt: Option<&str>) -> Result<Option<T>, Error>
where
T: std::str::FromStr<Err = Error>,
{
opt.filter(|s| !s.is_empty()).map(|s| s.parse()).transpose()
}
pub(crate) fn required<T>(field: Option<T>, name: &str, message: &str) -> Result<T, Error> {
field.ok_or_else(|| Error::parse_proto(name, format!("missing in {message}")))
}
pub(crate) fn ts(secs: i64) -> time::OffsetDateTime {
time::OffsetDateTime::from_unix_timestamp(secs).unwrap_or(time::OffsetDateTime::UNIX_EPOCH)
}
pub(crate) fn tag_values(map: &std::collections::HashMap<String, String>) -> Vec<TagValue> {
map.iter()
.map(|(k, v)| TagValue {
tag: k.clone(),
value: v.clone(),
})
.collect()
}
pub fn decode_contract(proto: &proto::Contract) -> Result<Contract, Error> {
Ok(Contract {
contract_id: proto.con_id.unwrap_or_default(),
symbol: Symbol::from(s(&proto.symbol)),
security_type: parse_optional(proto.sec_type.as_deref())?.unwrap_or_else(|| SecurityType::Other(String::new())),
last_trade_date_or_contract_month: s(&proto.last_trade_date_or_contract_month),
strike: proto.strike.unwrap_or_default(),
right: parse_optional(proto.right.as_deref())?,
multiplier: proto.multiplier.map(|m| m.to_string()).unwrap_or_default(),
exchange: Exchange::from(s(&proto.exchange)),
primary_exchange: Exchange::from(s(&proto.primary_exch)),
currency: Currency::from(s(&proto.currency)),
local_symbol: s(&proto.local_symbol),
trading_class: s(&proto.trading_class),
include_expired: proto.include_expired.unwrap_or_default(),
security_id_type: parse_optional::<SecurityIdType>(proto.sec_id_type.as_deref())?,
security_id: s(&proto.sec_id),
description: s(&proto.description),
issuer_id: s(&proto.issuer_id),
combo_legs_description: s(&proto.combo_legs_descrip),
combo_legs: proto.combo_legs.iter().map(decode_combo_leg).collect::<Result<Vec<_>, _>>()?,
delta_neutral_contract: proto.delta_neutral_contract.as_ref().map(decode_delta_neutral_contract),
last_trade_date: proto.last_trade_date.as_deref().and_then(parse_last_trade_date),
})
}
fn parse_last_trade_date(raw: &str) -> Option<time::Date> {
if raw.is_empty() {
return None;
}
let date = time::Date::parse(raw, time::macros::format_description!("[year][month][day]")).ok();
if date.is_none() {
log::warn!("unparseable contract lastTradeDate {raw:?}; expected YYYYMMDD");
}
date
}
pub fn decode_combo_leg(proto: &proto::ComboLeg) -> Result<ComboLeg, Error> {
Ok(ComboLeg {
contract_id: proto.con_id.unwrap_or_default(),
ratio: proto.ratio.unwrap_or_default(),
action: parse_required::<LegAction>(proto.action.as_deref(), "LegAction")?,
exchange: s(&proto.exchange),
open_close: ComboLegOpenClose::from(proto.open_close.unwrap_or_default()),
short_sale_slot: proto.short_sales_slot.unwrap_or_default(),
designated_location: s(&proto.designated_location),
exempt_code: proto.exempt_code.unwrap_or_default(),
})
}
pub fn decode_delta_neutral_contract(proto: &proto::DeltaNeutralContract) -> DeltaNeutralContract {
DeltaNeutralContract {
contract_id: proto.con_id.unwrap_or_default(),
delta: proto.delta.unwrap_or_default(),
price: proto.price.unwrap_or_default(),
}
}
pub fn decode_soft_dollar_tier(proto: &proto::SoftDollarTier) -> SoftDollarTier {
SoftDollarTier {
name: s(&proto.name),
value: s(&proto.value),
display_name: s(&proto.display_name),
}
}
#[cfg(test)]
#[path = "decoders_tests.rs"]
mod tests;