nautilus_model/identifiers/
symbol.rs1use std::{
19 fmt::{Debug, Display, Formatter},
20 hash::Hash,
21};
22
23use nautilus_core::correctness::{check_valid_string, FAILED};
24use ustr::Ustr;
25
26#[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 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 pub fn new<T: AsRef<str>>(value: T) -> Self {
57 Self::new_checked(value).expect(FAILED)
58 }
59
60 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 #[must_use]
77 pub fn inner(&self) -> Ustr {
78 self.0
79 }
80
81 #[must_use]
83 pub fn as_str(&self) -> &str {
84 self.0.as_str()
85 }
86
87 #[must_use]
89 pub fn is_composite(&self) -> bool {
90 self.as_str().contains('.')
91 }
92
93 #[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 #[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#[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}