nautilus_model/identifiers/
instrument_id.rs1use std::{
19 fmt::{Debug, Display, Formatter},
20 hash::Hash,
21 str::FromStr,
22};
23
24use nautilus_core::correctness::{check_valid_string_ascii, check_valid_string_utf8};
25use serde::{Deserialize, Deserializer, Serialize};
26
27#[cfg(feature = "defi")]
28use crate::defi::{Blockchain, validation::validate_address};
29use crate::identifiers::{Symbol, Venue};
30
31#[repr(C)]
35#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
36#[cfg_attr(
37 feature = "python",
38 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
39)]
40pub struct InstrumentId {
41 pub symbol: Symbol,
43 pub venue: Venue,
45}
46
47impl InstrumentId {
48 #[must_use]
50 pub fn new(symbol: Symbol, venue: Venue) -> Self {
51 Self { symbol, venue }
52 }
53
54 #[must_use]
55 pub fn is_synthetic(&self) -> bool {
56 self.venue.is_synthetic()
57 }
58}
59
60impl InstrumentId {
61 pub fn from_as_ref<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
65 Self::from_str(value.as_ref())
66 }
67
68 #[cfg(feature = "defi")]
70 #[must_use]
71 pub fn blockchain(&self) -> Option<Blockchain> {
72 self.venue
73 .parse_dex()
74 .map(|(blockchain, _)| blockchain)
75 .ok()
76 }
77}
78
79impl FromStr for InstrumentId {
80 type Err = anyhow::Error;
81
82 fn from_str(s: &str) -> anyhow::Result<Self> {
83 match s.rsplit_once('.') {
84 Some((symbol_part, venue_part)) => {
85 check_valid_string_utf8(symbol_part, stringify!(value))?;
86 check_valid_string_ascii(venue_part, stringify!(value))?;
87
88 let venue = Venue::new_checked(venue_part)?;
89
90 let symbol = {
91 #[cfg(feature = "defi")]
92 if venue.is_dex() {
93 let validated_address = validate_address(symbol_part)
94 .map_err(|e| anyhow::anyhow!(err_message(s, e.to_string())))?;
95 Symbol::new(validated_address.to_string())
96 } else {
97 Symbol::new(symbol_part)
98 }
99
100 #[cfg(not(feature = "defi"))]
101 Symbol::new(symbol_part)
102 };
103
104 Ok(Self { symbol, venue })
105 }
106 None => {
107 anyhow::bail!(err_message(
108 s,
109 "missing '.' separator between symbol and venue components".to_string()
110 ))
111 }
112 }
113 }
114}
115
116impl From<&str> for InstrumentId {
117 fn from(value: &str) -> Self {
123 Self::from_str(value).unwrap()
124 }
125}
126
127impl From<String> for InstrumentId {
128 fn from(value: String) -> Self {
134 Self::from(value.as_str())
135 }
136}
137
138impl Debug for InstrumentId {
139 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140 write!(f, "\"{}.{}\"", self.symbol, self.venue)
141 }
142}
143
144impl Display for InstrumentId {
145 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{}.{}", self.symbol, self.venue)
147 }
148}
149
150impl Serialize for InstrumentId {
151 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
152 where
153 S: serde::Serializer,
154 {
155 serializer.serialize_str(&self.to_string())
156 }
157}
158
159impl<'de> Deserialize<'de> for InstrumentId {
160 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161 where
162 D: Deserializer<'de>,
163 {
164 let instrument_id_str = String::deserialize(deserializer)?;
165 Ok(Self::from(instrument_id_str.as_str()))
166 }
167}
168
169fn err_message(s: &str, e: String) -> String {
170 format!("Error parsing `InstrumentId` from '{s}': {e}")
171}
172
173#[cfg(test)]
174mod tests {
175 use std::str::FromStr;
176
177 use rstest::rstest;
178
179 use super::InstrumentId;
180 use crate::identifiers::stubs::*;
181
182 #[rstest]
183 fn test_instrument_id_parse_success(instrument_id_eth_usdt_binance: InstrumentId) {
184 assert_eq!(instrument_id_eth_usdt_binance.symbol.to_string(), "ETHUSDT");
185 assert_eq!(instrument_id_eth_usdt_binance.venue.to_string(), "BINANCE");
186 }
187
188 #[rstest]
189 #[should_panic(
190 expected = "Error parsing `InstrumentId` from 'ETHUSDT-BINANCE': missing '.' separator between symbol and venue components"
191 )]
192 fn test_instrument_id_parse_failure_no_dot() {
193 let _ = InstrumentId::from("ETHUSDT-BINANCE");
194 }
195
196 #[rstest]
197 fn test_string_reprs() {
198 let id = InstrumentId::from("ETH/USDT.BINANCE");
199 assert_eq!(id.to_string(), "ETH/USDT.BINANCE");
200 assert_eq!(format!("{id}"), "ETH/USDT.BINANCE");
201 }
202
203 #[rstest]
204 fn test_instrument_id_from_str_with_utf8_symbol() {
205 let non_ascii_symbol = "TËST-PÉRP";
206 let non_ascii_instrument = "TËST-PÉRP.BINANCE";
207
208 let id = InstrumentId::from_str(non_ascii_instrument).unwrap();
209 assert_eq!(id.symbol.to_string(), non_ascii_symbol);
210 assert_eq!(id.venue.to_string(), "BINANCE");
211 assert_eq!(id.to_string(), non_ascii_instrument);
212 }
213
214 #[cfg(feature = "defi")]
215 #[rstest]
216 fn test_blockchain_instrument_id_valid() {
217 let id =
218 InstrumentId::from("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Arbitrum:UniswapV3");
219 assert_eq!(
220 id.symbol.to_string(),
221 "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"
222 );
223 assert_eq!(id.venue.to_string(), "Arbitrum:UniswapV3");
224 }
225
226 #[cfg(feature = "defi")]
227 #[rstest]
228 #[should_panic(
229 expected = "Error creating `Venue` from 'InvalidChain:UniswapV3': invalid blockchain venue 'InvalidChain:UniswapV3': chain 'InvalidChain' not recognized"
230 )]
231 fn test_blockchain_instrument_id_invalid_chain() {
232 let _ =
233 InstrumentId::from("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.InvalidChain:UniswapV3");
234 }
235
236 #[cfg(feature = "defi")]
237 #[rstest]
238 #[should_panic(
239 expected = "Error creating `Venue` from 'Arbitrum:': invalid blockchain venue 'Arbitrum:': expected format 'Chain:DexId'"
240 )]
241 fn test_blockchain_instrument_id_empty_dex() {
242 let _ = InstrumentId::from("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Arbitrum:");
243 }
244
245 #[cfg(feature = "defi")]
246 #[rstest]
247 fn test_regular_venue_with_blockchain_like_name_but_without_dex() {
248 let id = InstrumentId::from("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Ethereum");
250 assert_eq!(
251 id.symbol.to_string(),
252 "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"
253 );
254 assert_eq!(id.venue.to_string(), "Ethereum");
255 }
256
257 #[cfg(feature = "defi")]
258 #[rstest]
259 #[should_panic(
260 expected = "Error parsing `InstrumentId` from 'invalidaddress.Ethereum:UniswapV3': Ethereum address must start with '0x': invalidaddress"
261 )]
262 fn test_blockchain_instrument_id_invalid_address_no_prefix() {
263 let _ = InstrumentId::from("invalidaddress.Ethereum:UniswapV3");
264 }
265
266 #[cfg(feature = "defi")]
267 #[rstest]
268 #[should_panic(
269 expected = "Error parsing `InstrumentId` from '0x123.Ethereum:UniswapV3': Blockchain address '0x123' is incorrect: odd number of digits"
270 )]
271 fn test_blockchain_instrument_id_invalid_address_short() {
272 let _ = InstrumentId::from("0x123.Ethereum:UniswapV3");
273 }
274
275 #[cfg(feature = "defi")]
276 #[rstest]
277 #[should_panic(
278 expected = "Error parsing `InstrumentId` from '0xC31E54c7a869B9FcBEcc14363CF510d1c41fa44G.Ethereum:UniswapV3': Blockchain address '0xC31E54c7a869B9FcBEcc14363CF510d1c41fa44G' is incorrect: invalid character 'G' at position 39"
279 )]
280 fn test_blockchain_instrument_id_invalid_address_non_hex() {
281 let _ = InstrumentId::from("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa44G.Ethereum:UniswapV3");
282 }
283
284 #[cfg(feature = "defi")]
285 #[rstest]
286 #[should_panic(
287 expected = "Error parsing `InstrumentId` from '0xc31e54c7a869b9fcbecc14363cf510d1c41fa443.Ethereum:UniswapV3': Blockchain address '0xc31e54c7a869b9fcbecc14363cf510d1c41fa443' has incorrect checksum"
288 )]
289 fn test_blockchain_instrument_id_invalid_address_checksum() {
290 let _ = InstrumentId::from("0xc31e54c7a869b9fcbecc14363cf510d1c41fa443.Ethereum:UniswapV3");
291 }
292
293 #[cfg(feature = "defi")]
294 #[rstest]
295 fn test_blockchain_extraction_valid_dex() {
296 let id =
297 InstrumentId::from("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443.Arbitrum:UniswapV3");
298 let blockchain = id.blockchain();
299 assert!(blockchain.is_some());
300 assert_eq!(blockchain.unwrap(), crate::defi::Blockchain::Arbitrum);
301 }
302
303 #[cfg(feature = "defi")]
304 #[rstest]
305 fn test_blockchain_extraction_tradifi_venue() {
306 let id = InstrumentId::from("ETH/USDT.BINANCE");
307 let blockchain = id.blockchain();
308 assert!(blockchain.is_none());
309 }
310}