nautilus_model/identifiers/
symbol.rs1use std::{
19 fmt::{Debug, Display, Formatter},
20 hash::Hash,
21};
22
23use nautilus_core::correctness::{FAILED, check_valid_string};
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 = "posei_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 #[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 #[must_use]
78 pub fn inner(&self) -> Ustr {
79 self.0
80 }
81
82 #[must_use]
84 pub fn as_str(&self) -> &str {
85 self.0.as_str()
86 }
87
88 #[must_use]
90 pub fn is_composite(&self) -> bool {
91 self.as_str().contains('.')
92 }
93
94 #[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 #[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#[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}