nautilus_model/identifiers/
instrument_id.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Represents a valid instrument ID.
17
18use 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/// Represents a valid instrument ID.
30///
31/// The symbol and venue combination should uniquely identify the instrument.
32#[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    /// The instruments ticker symbol.
40    pub symbol: Symbol,
41    /// The instruments trading venue.
42    pub venue: Venue,
43}
44
45impl InstrumentId {
46    /// Creates a new [`InstrumentId`] instance.
47    #[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    /// # Errors
60    ///
61    /// Returns an error if parsing the string fails or string is invalid.
62    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    /// Creates a [`InstrumentId`] from a string slice.
92    ///
93    /// # Panics
94    ///
95    /// Panics if the `value` string is not valid.
96    fn from(value: &str) -> Self {
97        Self::from_str(value).unwrap()
98    }
99}
100
101impl From<String> for InstrumentId {
102    /// Creates a [`InstrumentId`] from a string.
103    ///
104    /// # Panics
105    ///
106    /// Panics if the `value` string is not valid.
107    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////////////////////////////////////////////////////////////////////////////////
148// Tests
149////////////////////////////////////////////////////////////////////////////////
150#[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}