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::{check_valid_string, FAILED};
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    pub(crate) fn set_inner(&mut self, value: &str) {
62        self.0 = Ustr::from(value);
63    }
64
65    #[must_use]
66    pub fn from_str_unchecked<T: AsRef<str>>(s: T) -> Self {
67        Self(Ustr::from(s.as_ref()))
68    }
69
70    #[must_use]
71    pub const fn from_ustr_unchecked(s: Ustr) -> Self {
72        Self(s)
73    }
74
75    /// Returns the inner identifier value.
76    #[must_use]
77    pub fn inner(&self) -> Ustr {
78        self.0
79    }
80
81    /// Returns the inner identifier value as a string slice.
82    #[must_use]
83    pub fn as_str(&self) -> &str {
84        self.0.as_str()
85    }
86
87    /// Returns true if the symbol string contains a period (`.`).
88    #[must_use]
89    pub fn is_composite(&self) -> bool {
90        self.as_str().contains('.')
91    }
92
93    /// Returns the symbol root.
94    ///
95    /// The symbol root is the substring that appears before the first period (`.`)
96    /// in the full symbol string. It typically represents the underlying asset for
97    /// futures and options contracts. If no period is found, the entire symbol
98    /// string is considered the root.
99    #[must_use]
100    pub fn root(&self) -> &str {
101        let symbol_str = self.as_str();
102        if let Some(index) = symbol_str.find('.') {
103            &symbol_str[..index]
104        } else {
105            symbol_str
106        }
107    }
108
109    /// Returns the symbol topic.
110    ///
111    /// The symbol topic is the root symbol with a wildcard (`*`) appended if the symbol has a root,
112    /// otherwise returns the full symbol string.
113    #[must_use]
114    pub fn topic(&self) -> String {
115        let root_str = self.root();
116        if root_str == self.as_str() {
117            root_str.to_string()
118        } else {
119            format!("{}*", root_str)
120        }
121    }
122}
123
124impl Debug for Symbol {
125    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
126        write!(f, "{:?}", self.0)
127    }
128}
129
130impl Display for Symbol {
131    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
132        write!(f, "{}", self.0)
133    }
134}
135
136impl From<Ustr> for Symbol {
137    fn from(value: Ustr) -> Self {
138        Self(value)
139    }
140}
141
142////////////////////////////////////////////////////////////////////////////////
143// Tests
144////////////////////////////////////////////////////////////////////////////////
145#[cfg(test)]
146mod tests {
147    use rstest::rstest;
148
149    use crate::identifiers::{stubs::*, Symbol};
150
151    #[rstest]
152    fn test_string_reprs(symbol_eth_perp: Symbol) {
153        assert_eq!(symbol_eth_perp.as_str(), "ETH-PERP");
154        assert_eq!(format!("{symbol_eth_perp}"), "ETH-PERP");
155    }
156
157    #[rstest]
158    #[case("AUDUSD", false)]
159    #[case("AUD/USD", false)]
160    #[case("CL.FUT", true)]
161    #[case("LO.OPT", true)]
162    #[case("ES.c.0", true)]
163    fn test_symbol_is_composite(#[case] input: &str, #[case] expected: bool) {
164        let symbol = Symbol::new(input);
165        assert_eq!(symbol.is_composite(), expected);
166    }
167
168    #[rstest]
169    #[case("AUDUSD", "AUDUSD")]
170    #[case("AUD/USD", "AUD/USD")]
171    #[case("CL.FUT", "CL")]
172    #[case("LO.OPT", "LO")]
173    #[case("ES.c.0", "ES")]
174    fn test_symbol_root(#[case] input: &str, #[case] expected_root: &str) {
175        let symbol = Symbol::new(input);
176        assert_eq!(symbol.root(), expected_root);
177    }
178
179    #[rstest]
180    #[case("AUDUSD", "AUDUSD")]
181    #[case("AUD/USD", "AUD/USD")]
182    #[case("CL.FUT", "CL*")]
183    #[case("LO.OPT", "LO*")]
184    #[case("ES.c.0", "ES*")]
185    fn test_symbol_topic(#[case] input: &str, #[case] expected_topic: &str) {
186        let symbol = Symbol::new(input);
187        assert_eq!(symbol.topic(), expected_topic);
188    }
189}