use crate::rti::ResponseReferenceData;
#[derive(Debug, Clone, Default)]
pub struct InstrumentInfo {
pub symbol: String,
pub exchange: String,
pub exchange_symbol: Option<String>,
pub name: Option<String>,
pub product_code: Option<String>,
pub instrument_type: Option<String>,
pub underlying: Option<String>,
pub currency: Option<String>,
pub expiration_date: Option<String>,
pub tick_size: Option<f64>,
pub point_value: Option<f64>,
pub is_tradable: bool,
}
impl InstrumentInfo {
pub fn price_precision(&self) -> u8 {
match self.tick_size {
Some(tick) if tick > 0.0 => {
let mut precision = 0u8;
let mut value = tick;
while value < 1.0 && precision < 10 {
value *= 10.0;
precision += 1;
}
let fractional = value - value.floor();
if fractional > 0.0001 && precision < 10 {
let mut frac = fractional;
while frac > 0.0001 && precision < 10 {
frac *= 10.0;
frac -= frac.floor();
precision += 1;
}
}
precision
}
_ => 2,
}
}
pub fn size_precision(&self) -> u8 {
0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstrumentInfoError {
pub message: String,
}
impl std::fmt::Display for InstrumentInfoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for InstrumentInfoError {}
impl TryFrom<&ResponseReferenceData> for InstrumentInfo {
type Error = InstrumentInfoError;
fn try_from(data: &ResponseReferenceData) -> Result<Self, Self::Error> {
let symbol = data.symbol.clone().ok_or_else(|| InstrumentInfoError {
message: "missing symbol".to_string(),
})?;
let exchange = data.exchange.clone().ok_or_else(|| InstrumentInfoError {
message: "missing exchange".to_string(),
})?;
let is_tradable = data
.is_tradable
.as_ref()
.map(|s| s.eq_ignore_ascii_case("true") || s == "1")
.unwrap_or(false);
Ok(InstrumentInfo {
symbol,
exchange,
exchange_symbol: data.exchange_symbol.clone(),
name: data.symbol_name.clone(),
product_code: data.product_code.clone(),
instrument_type: data.instrument_type.clone(),
underlying: data.underlying_symbol.clone(),
currency: data.currency.clone(),
expiration_date: data.expiration_date.clone(),
tick_size: data.min_qprice_change,
point_value: data.single_point_value,
is_tradable,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_price_precision() {
let mut info = InstrumentInfo::default();
info.tick_size = Some(0.25); assert_eq!(info.price_precision(), 2);
info.tick_size = Some(0.01); assert_eq!(info.price_precision(), 2);
info.tick_size = Some(0.03125); assert_eq!(info.price_precision(), 5);
info.tick_size = Some(1.0); assert_eq!(info.price_precision(), 0);
info.tick_size = None; assert_eq!(info.price_precision(), 2);
}
#[test]
fn test_try_from_missing_symbol() {
let data = ResponseReferenceData {
template_id: 15,
symbol: None,
exchange: Some("CME".to_string()),
..Default::default()
};
let result = InstrumentInfo::try_from(&data);
assert!(result.is_err());
assert_eq!(result.unwrap_err().message, "missing symbol");
}
#[test]
fn test_try_from_missing_exchange() {
let data = ResponseReferenceData {
template_id: 15,
symbol: Some("ESH4".to_string()),
exchange: None,
..Default::default()
};
let result = InstrumentInfo::try_from(&data);
assert!(result.is_err());
assert_eq!(result.unwrap_err().message, "missing exchange");
}
#[test]
fn test_try_from_success() {
let data = ResponseReferenceData {
template_id: 15,
symbol: Some("ESH4".to_string()),
exchange: Some("CME".to_string()),
symbol_name: Some("E-mini S&P 500".to_string()),
product_code: Some("ES".to_string()),
instrument_type: Some("Future".to_string()),
currency: Some("USD".to_string()),
min_qprice_change: Some(0.25),
single_point_value: Some(50.0),
is_tradable: Some("true".to_string()),
..Default::default()
};
let info = InstrumentInfo::try_from(&data).unwrap();
assert_eq!(info.symbol, "ESH4");
assert_eq!(info.exchange, "CME");
assert_eq!(info.name, Some("E-mini S&P 500".to_string()));
assert_eq!(info.product_code, Some("ES".to_string()));
assert_eq!(info.tick_size, Some(0.25));
assert_eq!(info.point_value, Some(50.0));
assert!(info.is_tradable);
}
}