nautilus_model/identifiers/
symbol.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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 ticker symbol ID for a tradable instrument.
17
18use std::{
19    fmt::{Debug, Display, Formatter},
20    hash::Hash,
21};
22
23use nautilus_core::correctness::{FAILED, check_valid_string};
24use ustr::Ustr;
25
26/// Represents a valid ticker symbol ID for a tradable instrument.
27#[repr(C)]
28#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
29#[cfg_attr(
30    feature = "python",
31    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
32)]
33pub struct Symbol(Ustr);
34
35impl Symbol {
36    /// Creates a new [`Symbol`] instance with correctness checking.
37    ///
38    /// # Error
39    ///
40    /// Returns an error if `value` is not a valid string.
41    ///
42    /// # Notes
43    ///
44    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
45    pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
46        let value = value.as_ref();
47        check_valid_string(value, stringify!(value))?;
48        Ok(Self(Ustr::from(value)))
49    }
50
51    /// Creates a new [`Symbol`] instance.
52    ///
53    /// # Panic
54    ///
55    /// - If `value` is not a valid string.
56    pub fn new<T: AsRef<str>>(value: T) -> Self {
57        Self::new_checked(value).expect(FAILED)
58    }
59
60    /// Sets the inner identifier value.
61    #[allow(dead_code)]
62    pub(crate) fn set_inner(&mut self, value: &str) {
63        self.0 = Ustr::from(value);
64    }
65
66    #[must_use]
67    pub fn from_str_unchecked<T: AsRef<str>>(s: T) -> Self {
68        Self(Ustr::from(s.as_ref()))
69    }
70
71    #[must_use]
72    pub const fn from_ustr_unchecked(s: Ustr) -> Self {
73        Self(s)
74    }
75
76    /// Returns the inner identifier value.
77    #[must_use]
78    pub fn inner(&self) -> Ustr {
79        self.0
80    }
81
82    /// Returns the inner identifier value as a string slice.
83    #[must_use]
84    pub fn as_str(&self) -> &str {
85        self.0.as_str()
86    }
87
88    /// Returns true if the symbol string contains a period (`.`).
89    #[must_use]
90    pub fn is_composite(&self) -> bool {
91        self.as_str().contains('.')
92    }
93
94    /// Returns the symbol root.
95    ///
96    /// The symbol root is the substring that appears before the first period (`.`)
97    /// in the full symbol string. It typically represents the underlying asset for
98    /// futures and options contracts. If no period is found, the entire symbol
99    /// string is considered the root.
100    #[must_use]
101    pub fn root(&self) -> &str {
102        let symbol_str = self.as_str();
103        if let Some(index) = symbol_str.find('.') {
104            &symbol_str[..index]
105        } else {
106            symbol_str
107        }
108    }
109
110    /// Returns the symbol topic.
111    ///
112    /// The symbol topic is the root symbol with a wildcard (`*`) appended if the symbol has a root,
113    /// otherwise returns the full symbol string.
114    #[must_use]
115    pub fn topic(&self) -> String {
116        let root_str = self.root();
117        if root_str == self.as_str() {
118            root_str.to_string()
119        } else {
120            format!("{}*", root_str)
121        }
122    }
123}
124
125impl Debug for Symbol {
126    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127        write!(f, "{:?}", self.0)
128    }
129}
130
131impl Display for Symbol {
132    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133        write!(f, "{}", self.0)
134    }
135}
136
137impl From<Ustr> for Symbol {
138    fn from(value: Ustr) -> Self {
139        Self(value)
140    }
141}
142
143////////////////////////////////////////////////////////////////////////////////
144// Tests
145////////////////////////////////////////////////////////////////////////////////
146#[cfg(test)]
147mod tests {
148    use rstest::rstest;
149
150    use crate::identifiers::{Symbol, stubs::*};
151
152    #[rstest]
153    fn test_string_reprs(symbol_eth_perp: Symbol) {
154        assert_eq!(symbol_eth_perp.as_str(), "ETH-PERP");
155        assert_eq!(format!("{symbol_eth_perp}"), "ETH-PERP");
156    }
157
158    #[rstest]
159    #[case("AUDUSD", false)]
160    #[case("AUD/USD", false)]
161    #[case("CL.FUT", true)]
162    #[case("LO.OPT", true)]
163    #[case("ES.c.0", true)]
164    fn test_symbol_is_composite(#[case] input: &str, #[case] expected: bool) {
165        let symbol = Symbol::new(input);
166        assert_eq!(symbol.is_composite(), expected);
167    }
168
169    #[rstest]
170    #[case("AUDUSD", "AUDUSD")]
171    #[case("AUD/USD", "AUD/USD")]
172    #[case("CL.FUT", "CL")]
173    #[case("LO.OPT", "LO")]
174    #[case("ES.c.0", "ES")]
175    fn test_symbol_root(#[case] input: &str, #[case] expected_root: &str) {
176        let symbol = Symbol::new(input);
177        assert_eq!(symbol.root(), expected_root);
178    }
179
180    #[rstest]
181    #[case("AUDUSD", "AUDUSD")]
182    #[case("AUD/USD", "AUD/USD")]
183    #[case("CL.FUT", "CL*")]
184    #[case("LO.OPT", "LO*")]
185    #[case("ES.c.0", "ES*")]
186    fn test_symbol_topic(#[case] input: &str, #[case] expected_topic: &str) {
187        let symbol = Symbol::new(input);
188        assert_eq!(symbol.topic(), expected_topic);
189    }
190}