Skip to main content

nautilus_model/identifiers/
trader_id.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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 trader ID.
17
18use std::fmt::{Debug, Display};
19
20use nautilus_core::correctness::{
21    CorrectnessResult, CorrectnessResultExt, FAILED, check_predicate_false, check_string_contains,
22    check_valid_string_ascii,
23};
24use ustr::Ustr;
25
26/// Represents a valid trader ID.
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", from_py_object)
32)]
33#[cfg_attr(
34    feature = "python",
35    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
36)]
37pub struct TraderId(Ustr);
38
39impl TraderId {
40    /// Creates a new [`TraderId`] instance.
41    ///
42    /// Must be correctly formatted with two valid strings either side of a hyphen.
43    /// It is expected a trader ID is the abbreviated name of the trader
44    /// with an order ID tag number separated by a hyphen.
45    ///
46    /// Example: "TESTER-001".
47    ///
48    /// The reason for the numerical component of the ID is so that order and position IDs
49    /// do not collide with those from another node instance.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if:
54    /// - `value` is not a valid ASCII string.
55    /// - `value` does not contain a hyphen '-' separator.
56    /// - Either the name or tag part (before/after the hyphen) is empty.
57    ///
58    /// # Notes
59    ///
60    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
61    pub fn new_checked<T: AsRef<str>>(value: T) -> CorrectnessResult<Self> {
62        let value = value.as_ref();
63        check_valid_string_ascii(value, stringify!(value))?;
64        check_string_contains(value, "-", stringify!(value))?;
65
66        if let Some((name, tag)) = value.rsplit_once('-') {
67            check_predicate_false(
68                name.is_empty(),
69                "`value` name part (before '-') cannot be empty",
70            )?;
71            check_predicate_false(
72                tag.is_empty(),
73                "`value` tag part (after '-') cannot be empty",
74            )?;
75        }
76
77        Ok(Self(Ustr::from(value)))
78    }
79
80    /// Creates a new [`TraderId`] instance.
81    ///
82    /// # Panics
83    ///
84    /// Panics if `value` is not a valid string, or does not contain a hyphen '-' separator.
85    pub fn new<T: AsRef<str>>(value: T) -> Self {
86        Self::new_checked(value).expect_display(FAILED)
87    }
88
89    /// Sets the inner identifier value.
90    #[cfg_attr(not(feature = "python"), allow(dead_code))]
91    pub(crate) fn set_inner(&mut self, value: &str) {
92        self.0 = Ustr::from(value);
93    }
94
95    /// Returns the inner identifier value.
96    #[must_use]
97    pub fn inner(&self) -> Ustr {
98        self.0
99    }
100
101    /// Returns the inner identifier value as a string slice.
102    #[must_use]
103    pub fn as_str(&self) -> &str {
104        self.0.as_str()
105    }
106
107    /// Returns the numerical tag portion of the trader ID.
108    ///
109    /// # Panics
110    ///
111    /// Panics if the internal ID string does not contain a '-' separator.
112    #[must_use]
113    pub fn get_tag(&self) -> &str {
114        self.0.split('-').next_back().unwrap()
115    }
116
117    /// Creates an external trader ID used for orders from external sources.
118    #[must_use]
119    pub fn external() -> Self {
120        Self::new("EXTERNAL-0")
121    }
122
123    /// Returns whether this trader ID is external.
124    #[must_use]
125    pub fn is_external(&self) -> bool {
126        self.0.as_str() == "EXTERNAL-0"
127    }
128}
129
130impl Default for TraderId {
131    /// Returns the default trader ID "TRADER-001".
132    fn default() -> Self {
133        Self::from("TRADER-001")
134    }
135}
136
137impl Debug for TraderId {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(f, "\"{}\"", self.0)
140    }
141}
142
143impl Display for TraderId {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "{}", self.0)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use nautilus_core::correctness::CorrectnessError;
152    use rstest::rstest;
153
154    use crate::identifiers::{stubs::*, trader_id::TraderId};
155
156    #[rstest]
157    fn test_string_reprs(trader_id: TraderId) {
158        assert_eq!(trader_id.as_str(), "TRADER-001");
159        assert_eq!(format!("{trader_id}"), "TRADER-001");
160    }
161
162    #[rstest]
163    fn test_get_tag(trader_id: TraderId) {
164        assert_eq!(trader_id.get_tag(), "001");
165    }
166
167    #[rstest]
168    #[should_panic(expected = "name part (before '-') cannot be empty")]
169    fn test_new_with_empty_name_panics() {
170        let _ = TraderId::new("-001");
171    }
172
173    #[rstest]
174    #[should_panic(expected = "tag part (after '-') cannot be empty")]
175    fn test_new_with_empty_tag_panics() {
176        let _ = TraderId::new("TRADER-");
177    }
178
179    #[rstest]
180    fn test_new_checked_with_empty_name_returns_error() {
181        assert!(TraderId::new_checked("-001").is_err());
182    }
183
184    #[rstest]
185    fn test_new_checked_with_empty_tag_returns_error() {
186        assert!(TraderId::new_checked("TRADER-").is_err());
187    }
188
189    #[rstest]
190    fn test_new_checked_with_empty_name_returns_typed_error_with_stable_display() {
191        let error = TraderId::new_checked("-001").unwrap_err();
192
193        match error {
194            CorrectnessError::PredicateViolation { ref message } => {
195                assert_eq!(message, "`value` name part (before '-') cannot be empty");
196            }
197            other => panic!("Expected typed predicate violation, was: {other:?}"),
198        }
199
200        assert_eq!(
201            error.to_string(),
202            "`value` name part (before '-') cannot be empty"
203        );
204    }
205
206    #[rstest]
207    fn test_new_checked_with_empty_tag_returns_typed_error_with_stable_display() {
208        let error = TraderId::new_checked("TRADER-").unwrap_err();
209
210        match error {
211            CorrectnessError::PredicateViolation { ref message } => {
212                assert_eq!(message, "`value` tag part (after '-') cannot be empty");
213            }
214            other => panic!("Expected typed predicate violation, was: {other:?}"),
215        }
216
217        assert_eq!(
218            error.to_string(),
219            "`value` tag part (after '-') cannot be empty"
220        );
221    }
222}