rithmic_rs/util/
instrument.rs1use crate::rti::ResponseReferenceData;
4
5#[derive(Debug, Clone, Default)]
13pub struct InstrumentInfo {
14 pub symbol: String,
16 pub exchange: String,
18 pub exchange_symbol: Option<String>,
20 pub name: Option<String>,
22 pub product_code: Option<String>,
24 pub instrument_type: Option<String>,
26 pub underlying: Option<String>,
28 pub currency: Option<String>,
30 pub expiration_date: Option<String>,
32 pub tick_size: Option<f64>,
34 pub point_value: Option<f64>,
36 pub is_tradable: bool,
38}
39
40impl InstrumentInfo {
41 pub fn price_precision(&self) -> u8 {
57 match self.tick_size {
58 Some(tick) if tick > 0.0 => {
59 let mut precision = 0u8;
60 let mut value = tick;
61
62 while value < 1.0 && precision < 10 {
63 value *= 10.0;
64 precision += 1;
65 }
66
67 let fractional = value - value.floor();
68
69 if fractional > 0.0001 && precision < 10 {
70 let mut frac = fractional;
71
72 while frac > 0.0001 && precision < 10 {
73 frac *= 10.0;
74 frac -= frac.floor();
75 precision += 1;
76 }
77 }
78 precision
79 }
80 _ => 2,
81 }
82 }
83
84 pub fn size_precision(&self) -> u8 {
86 0
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct InstrumentInfoError {
93 pub message: String,
95}
96
97impl std::fmt::Display for InstrumentInfoError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(f, "{}", self.message)
100 }
101}
102
103impl std::error::Error for InstrumentInfoError {}
104
105impl TryFrom<&ResponseReferenceData> for InstrumentInfo {
106 type Error = InstrumentInfoError;
107
108 fn try_from(data: &ResponseReferenceData) -> Result<Self, Self::Error> {
109 let symbol = data.symbol.clone().ok_or_else(|| InstrumentInfoError {
110 message: "missing symbol".to_string(),
111 })?;
112
113 let exchange = data.exchange.clone().ok_or_else(|| InstrumentInfoError {
114 message: "missing exchange".to_string(),
115 })?;
116
117 let is_tradable = data
118 .is_tradable
119 .as_ref()
120 .map(|s| s.eq_ignore_ascii_case("true") || s == "1")
121 .unwrap_or(false);
122
123 Ok(InstrumentInfo {
124 symbol,
125 exchange,
126 exchange_symbol: data.exchange_symbol.clone(),
127 name: data.symbol_name.clone(),
128 product_code: data.product_code.clone(),
129 instrument_type: data.instrument_type.clone(),
130 underlying: data.underlying_symbol.clone(),
131 currency: data.currency.clone(),
132 expiration_date: data.expiration_date.clone(),
133 tick_size: data.min_qprice_change,
134 point_value: data.single_point_value,
135 is_tradable,
136 })
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 #[allow(clippy::field_reassign_with_default)]
146 fn test_price_precision() {
147 let mut info = InstrumentInfo::default();
148
149 info.tick_size = Some(0.25); assert_eq!(info.price_precision(), 2);
151
152 info.tick_size = Some(0.01); assert_eq!(info.price_precision(), 2);
154
155 info.tick_size = Some(0.03125); assert_eq!(info.price_precision(), 5);
157
158 info.tick_size = Some(1.0); assert_eq!(info.price_precision(), 0);
160
161 info.tick_size = None; assert_eq!(info.price_precision(), 2);
163 }
164
165 #[test]
166 fn test_try_from_missing_symbol() {
167 let data = ResponseReferenceData {
168 template_id: 15,
169 symbol: None,
170 exchange: Some("CME".to_string()),
171 ..Default::default()
172 };
173
174 let result = InstrumentInfo::try_from(&data);
175 assert!(result.is_err());
176 assert_eq!(result.unwrap_err().message, "missing symbol");
177 }
178
179 #[test]
180 fn test_try_from_missing_exchange() {
181 let data = ResponseReferenceData {
182 template_id: 15,
183 symbol: Some("ESH4".to_string()),
184 exchange: None,
185 ..Default::default()
186 };
187
188 let result = InstrumentInfo::try_from(&data);
189 assert!(result.is_err());
190 assert_eq!(result.unwrap_err().message, "missing exchange");
191 }
192
193 #[test]
194 fn test_try_from_success() {
195 let data = ResponseReferenceData {
196 template_id: 15,
197 symbol: Some("ESH4".to_string()),
198 exchange: Some("CME".to_string()),
199 symbol_name: Some("E-mini S&P 500".to_string()),
200 product_code: Some("ES".to_string()),
201 instrument_type: Some("Future".to_string()),
202 currency: Some("USD".to_string()),
203 min_qprice_change: Some(0.25),
204 single_point_value: Some(50.0),
205 is_tradable: Some("true".to_string()),
206 ..Default::default()
207 };
208
209 let info = InstrumentInfo::try_from(&data).unwrap();
210 assert_eq!(info.symbol, "ESH4");
211 assert_eq!(info.exchange, "CME");
212 assert_eq!(info.name, Some("E-mini S&P 500".to_string()));
213 assert_eq!(info.product_code, Some("ES".to_string()));
214 assert_eq!(info.tick_size, Some(0.25));
215 assert_eq!(info.point_value, Some(50.0));
216 assert!(info.is_tradable);
217 }
218}