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;
25use serde::{Deserialize, Deserializer, Serialize};
26
27use crate::identifiers::{Symbol, Venue};
28
29#[repr(C)]
33#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
34#[cfg_attr(
35 feature = "python",
36 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
37)]
38pub struct InstrumentId {
39 pub symbol: Symbol,
41 pub venue: Venue,
43}
44
45impl InstrumentId {
46 #[must_use]
48 pub fn new(symbol: Symbol, venue: Venue) -> Self {
49 Self { symbol, venue }
50 }
51
52 #[must_use]
53 pub fn is_synthetic(&self) -> bool {
54 self.venue.is_synthetic()
55 }
56}
57
58impl InstrumentId {
59 pub fn from_as_ref<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
60 Self::from_str(value.as_ref())
61 }
62}
63
64impl FromStr for InstrumentId {
65 type Err = anyhow::Error;
66
67 fn from_str(s: &str) -> anyhow::Result<Self> {
68 match s.rsplit_once('.') {
69 Some((symbol_part, venue_part)) => {
70 check_valid_string(symbol_part, stringify!(value))?;
71 check_valid_string(venue_part, stringify!(value))?;
72 Ok(Self {
73 symbol: Symbol::new(symbol_part),
74 venue: Venue::new(venue_part),
75 })
76 }
77 None => {
78 anyhow::bail!(err_message(
79 s,
80 "missing '.' separator between symbol and venue components".to_string()
81 ))
82 }
83 }
84 }
85}
86
87impl From<&str> for InstrumentId {
88 fn from(value: &str) -> Self {
95 Self::from_str(value).unwrap()
96 }
97}
98
99impl From<String> for InstrumentId {
100 fn from(value: String) -> Self {
107 Self::from(value.as_str())
108 }
109}
110
111impl Debug for InstrumentId {
112 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
113 write!(f, "\"{}.{}\"", self.symbol, self.venue)
114 }
115}
116
117impl Display for InstrumentId {
118 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
119 write!(f, "{}.{}", self.symbol, self.venue)
120 }
121}
122
123impl Serialize for InstrumentId {
124 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
125 where
126 S: serde::Serializer,
127 {
128 serializer.serialize_str(&self.to_string())
129 }
130}
131
132impl<'de> Deserialize<'de> for InstrumentId {
133 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134 where
135 D: Deserializer<'de>,
136 {
137 let instrument_id_str = String::deserialize(deserializer)?;
138 Ok(Self::from(instrument_id_str.as_str()))
139 }
140}
141
142fn err_message(s: &str, e: String) -> String {
143 format!("Error parsing `InstrumentId` from '{s}': {e}")
144}
145
146#[cfg(test)]
150mod tests {
151
152 use rstest::rstest;
153
154 use super::InstrumentId;
155 use crate::identifiers::stubs::*;
156
157 #[rstest]
158 fn test_instrument_id_parse_success(instrument_id_eth_usdt_binance: InstrumentId) {
159 assert_eq!(instrument_id_eth_usdt_binance.symbol.to_string(), "ETHUSDT");
160 assert_eq!(instrument_id_eth_usdt_binance.venue.to_string(), "BINANCE");
161 }
162
163 #[rstest]
164 #[should_panic(
165 expected = "Error parsing `InstrumentId` from 'ETHUSDT-BINANCE': missing '.' separator between symbol and venue components"
166 )]
167 fn test_instrument_id_parse_failure_no_dot() {
168 let _ = InstrumentId::from("ETHUSDT-BINANCE");
169 }
170
171 #[rstest]
172 fn test_string_reprs() {
173 let id = InstrumentId::from("ETH/USDT.BINANCE");
174 assert_eq!(id.to_string(), "ETH/USDT.BINANCE");
175 assert_eq!(format!("{id}"), "ETH/USDT.BINANCE");
176 }
177}