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// -------------------------------------------------------------------------------------------------
1516//! Represents a valid ticker symbol ID for a tradable instrument.
1718use std::{
19 fmt::{Debug, Display, Formatter},
20 hash::Hash,
21};
2223use nautilus_core::correctness::{FAILED, check_valid_string};
24use ustr::Ustr;
2526/// 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);
3435impl 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.
45pub fn new_checked<T: AsRef<str>>(value: T) -> anyhow::Result<Self> {
46let value = value.as_ref();
47 check_valid_string(value, stringify!(value))?;
48Ok(Self(Ustr::from(value)))
49 }
5051/// Creates a new [`Symbol`] instance.
52 ///
53 /// # Panic
54 ///
55 /// - If `value` is not a valid string.
56pub fn new<T: AsRef<str>>(value: T) -> Self {
57Self::new_checked(value).expect(FAILED)
58 }
5960/// Sets the inner identifier value.
61#[allow(dead_code)]
62pub(crate) fn set_inner(&mut self, value: &str) {
63self.0 = Ustr::from(value);
64 }
6566#[must_use]
67pub fn from_str_unchecked<T: AsRef<str>>(s: T) -> Self {
68Self(Ustr::from(s.as_ref()))
69 }
7071#[must_use]
72pub const fn from_ustr_unchecked(s: Ustr) -> Self {
73Self(s)
74 }
7576/// Returns the inner identifier value.
77#[must_use]
78pub fn inner(&self) -> Ustr {
79self.0
80}
8182/// Returns the inner identifier value as a string slice.
83#[must_use]
84pub fn as_str(&self) -> &str {
85self.0.as_str()
86 }
8788/// Returns true if the symbol string contains a period (`.`).
89#[must_use]
90pub fn is_composite(&self) -> bool {
91self.as_str().contains('.')
92 }
9394/// 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]
101pub fn root(&self) -> &str {
102let symbol_str = self.as_str();
103if let Some(index) = symbol_str.find('.') {
104&symbol_str[..index]
105 } else {
106 symbol_str
107 }
108 }
109110/// 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]
115pub fn topic(&self) -> String {
116let root_str = self.root();
117if root_str == self.as_str() {
118 root_str.to_string()
119 } else {
120format!("{}*", root_str)
121 }
122 }
123}
124125impl Debug for Symbol {
126fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127write!(f, "{:?}", self.0)
128 }
129}
130131impl Display for Symbol {
132fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133write!(f, "{}", self.0)
134 }
135}
136137impl From<Ustr> for Symbol {
138fn from(value: Ustr) -> Self {
139Self(value)
140 }
141}
142143////////////////////////////////////////////////////////////////////////////////
144// Tests
145////////////////////////////////////////////////////////////////////////////////
146#[cfg(test)]
147mod tests {
148use rstest::rstest;
149150use crate::identifiers::{Symbol, stubs::*};
151152#[rstest]
153fn test_string_reprs(symbol_eth_perp: Symbol) {
154assert_eq!(symbol_eth_perp.as_str(), "ETH-PERP");
155assert_eq!(format!("{symbol_eth_perp}"), "ETH-PERP");
156 }
157158#[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)]
164fn test_symbol_is_composite(#[case] input: &str, #[case] expected: bool) {
165let symbol = Symbol::new(input);
166assert_eq!(symbol.is_composite(), expected);
167 }
168169#[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")]
175fn test_symbol_root(#[case] input: &str, #[case] expected_root: &str) {
176let symbol = Symbol::new(input);
177assert_eq!(symbol.root(), expected_root);
178 }
179180#[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*")]
186fn test_symbol_topic(#[case] input: &str, #[case] expected_topic: &str) {
187let symbol = Symbol::new(input);
188assert_eq!(symbol.topic(), expected_topic);
189 }
190}