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 = "posei_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> {
63 Self::from_str(value.as_ref())
64 }
65}
66
67impl FromStr for InstrumentId {
68 type Err = anyhow::Error;
69
70 fn from_str(s: &str) -> anyhow::Result<Self> {
71 match s.rsplit_once('.') {
72 Some((symbol_part, venue_part)) => {
73 check_valid_string(symbol_part, stringify!(value))?;
74 check_valid_string(venue_part, stringify!(value))?;
75 Ok(Self {
76 symbol: Symbol::new(symbol_part),
77 venue: Venue::new(venue_part),
78 })
79 }
80 None => {
81 anyhow::bail!(err_message(
82 s,
83 "missing '.' separator between symbol and venue components".to_string()
84 ))
85 }
86 }
87 }
88}
89
90impl From<&str> for InstrumentId {
91 fn from(value: &str) -> Self {
97 Self::from_str(value).unwrap()
98 }
99}
100
101impl From<String> for InstrumentId {
102 fn from(value: String) -> Self {
108 Self::from(value.as_str())
109 }
110}
111
112impl Debug for InstrumentId {
113 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
114 write!(f, "\"{}.{}\"", self.symbol, self.venue)
115 }
116}
117
118impl Display for InstrumentId {
119 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
120 write!(f, "{}.{}", self.symbol, self.venue)
121 }
122}
123
124impl Serialize for InstrumentId {
125 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
126 where
127 S: serde::Serializer,
128 {
129 serializer.serialize_str(&self.to_string())
130 }
131}
132
133impl<'de> Deserialize<'de> for InstrumentId {
134 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135 where
136 D: Deserializer<'de>,
137 {
138 let instrument_id_str = String::deserialize(deserializer)?;
139 Ok(Self::from(instrument_id_str.as_str()))
140 }
141}
142
143fn err_message(s: &str, e: String) -> String {
144 format!("Error parsing `InstrumentId` from '{s}': {e}")
145}
146
147#[cfg(test)]
151mod tests {
152
153 use rstest::rstest;
154
155 use super::InstrumentId;
156 use crate::identifiers::stubs::*;
157
158 #[rstest]
159 fn test_instrument_id_parse_success(instrument_id_eth_usdt_binance: InstrumentId) {
160 assert_eq!(instrument_id_eth_usdt_binance.symbol.to_string(), "ETHUSDT");
161 assert_eq!(instrument_id_eth_usdt_binance.venue.to_string(), "BINANCE");
162 }
163
164 #[rstest]
165 #[should_panic(
166 expected = "Error parsing `InstrumentId` from 'ETHUSDT-BINANCE': missing '.' separator between symbol and venue components"
167 )]
168 fn test_instrument_id_parse_failure_no_dot() {
169 let _ = InstrumentId::from("ETHUSDT-BINANCE");
170 }
171
172 #[rstest]
173 fn test_string_reprs() {
174 let id = InstrumentId::from("ETH/USDT.BINANCE");
175 assert_eq!(id.to_string(), "ETH/USDT.BINANCE");
176 assert_eq!(format!("{id}"), "ETH/USDT.BINANCE");
177 }
178}