Skip to main content

nautilus_model/identifiers/
trade_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 trade match ID (assigned by a trading venue).
17
18use std::{
19    ffi::CStr,
20    fmt::{Debug, Display},
21    hash::Hash,
22};
23
24use nautilus_core::{StackStr, correctness::CorrectnessResult};
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26
27/// Represents a valid trade match ID (assigned by a trading venue).
28///
29/// The unique ID assigned to the trade entity once it is received or matched by
30/// the venue or central counterparty.
31///
32/// Can correspond to the `TradeID <1003> field` of the FIX protocol.
33///
34/// Maximum length is 36 characters.
35#[repr(C)]
36#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
37#[cfg_attr(
38    feature = "python",
39    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
40)]
41#[cfg_attr(
42    feature = "python",
43    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
44)]
45pub struct TradeId(StackStr);
46
47impl TradeId {
48    /// Creates a new [`TradeId`] instance with correctness checking.
49    ///
50    /// Maximum length is 36 characters.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if:
55    /// - `value` is an invalid string (e.g., is empty or contains non-ASCII characters).
56    /// - `value` length exceeds 36 characters.
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        Ok(Self(StackStr::new_checked(value.as_ref())?))
63    }
64
65    /// Creates a new [`TradeId`] instance.
66    ///
67    /// Maximum length is 36 characters.
68    ///
69    /// # Panics
70    ///
71    /// This function panics if:
72    /// - `value` is an invalid string (e.g., is empty or contains non-ASCII characters).
73    /// - `value` length exceeds 36 characters.
74    pub fn new<T: AsRef<str>>(value: T) -> Self {
75        Self(StackStr::new(value.as_ref()))
76    }
77
78    /// Creates a [`TradeId`] from a byte slice.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if `bytes` is empty, contains non-ASCII characters,
83    /// or exceeds 36 bytes (excluding trailing null terminator).
84    pub fn from_bytes(bytes: &[u8]) -> CorrectnessResult<Self> {
85        Ok(Self(StackStr::from_bytes(bytes)?))
86    }
87
88    /// Returns the inner string value.
89    #[inline]
90    #[must_use]
91    pub fn as_str(&self) -> &str {
92        self.0.as_str()
93    }
94
95    /// Returns a C string slice from the trade ID value.
96    #[inline]
97    #[must_use]
98    pub fn as_cstr(&self) -> &CStr {
99        self.0.as_cstr()
100    }
101}
102
103impl Debug for TradeId {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "{}('{}')", stringify!(TradeId), self)
106    }
107}
108
109impl Display for TradeId {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(f, "{}", self.0)
112    }
113}
114
115impl Serialize for TradeId {
116    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117    where
118        S: Serializer,
119    {
120        self.0.serialize(serializer)
121    }
122}
123
124impl<'de> Deserialize<'de> for TradeId {
125    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126    where
127        D: Deserializer<'de>,
128    {
129        let inner = StackStr::deserialize(deserializer)?;
130        Ok(Self(inner))
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use nautilus_core::correctness::CorrectnessError;
137    use rstest::rstest;
138
139    use crate::identifiers::{TradeId, stubs::*};
140
141    #[rstest]
142    fn test_trade_id_new_valid() {
143        let trade_id = TradeId::new("TRADE12345");
144        assert_eq!(trade_id.to_string(), "TRADE12345");
145    }
146
147    #[rstest]
148    fn test_trade_id_new_checked_returns_typed_error_with_stable_display() {
149        let error = TradeId::new_checked("").unwrap_err();
150
151        assert_eq!(
152            error,
153            CorrectnessError::PredicateViolation {
154                message: "String is empty".to_string(),
155            }
156        );
157        assert_eq!(error.to_string(), "String is empty");
158    }
159
160    #[rstest]
161    #[should_panic(expected = "exceeds maximum length")]
162    fn test_trade_id_new_invalid_length() {
163        let _ = TradeId::new("A".repeat(37).as_str());
164    }
165
166    #[rstest]
167    #[case(b"1234567890", "1234567890")]
168    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234", "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234")] // 30 chars
169    #[case(b"1234567890\0", "1234567890")]
170    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234\0", "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234")] // 30 chars with null
171    fn test_trade_id_from_valid_bytes(#[case] input: &[u8], #[case] expected: &str) {
172        let trade_id = TradeId::from_bytes(input).unwrap();
173        assert_eq!(trade_id.to_string(), expected);
174    }
175
176    #[rstest]
177    #[should_panic(expected = "String is empty")]
178    fn test_trade_id_from_bytes_empty() {
179        TradeId::from_bytes(&[] as &[u8]).unwrap();
180    }
181
182    #[rstest]
183    #[should_panic(expected = "String is empty")]
184    fn test_trade_id_single_null_byte() {
185        TradeId::from_bytes(&[0u8] as &[u8]).unwrap();
186    }
187
188    #[rstest]
189    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901")] // 37 bytes, no null
190    #[case(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678901\0")] // 38 bytes, with null
191    #[should_panic(expected = "exceeds maximum length")]
192    fn test_trade_id_exceeds_max_length(#[case] input: &[u8]) {
193        TradeId::from_bytes(input).unwrap();
194    }
195
196    #[rstest]
197    fn test_trade_id_with_null_terminator_at_max_length() {
198        let input = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\0" as &[u8];
199        let trade_id = TradeId::from_bytes(input).unwrap();
200        assert_eq!(trade_id.to_string(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"); // 36 chars
201    }
202
203    #[rstest]
204    fn test_trade_id_as_cstr() {
205        let trade_id = TradeId::new("TRADE12345");
206        assert_eq!(trade_id.as_cstr().to_str().unwrap(), "TRADE12345");
207    }
208
209    #[rstest]
210    fn test_trade_id_as_str() {
211        let trade_id = TradeId::new("TRADE12345");
212        assert_eq!(trade_id.as_str(), "TRADE12345");
213    }
214
215    #[rstest]
216    fn test_trade_id_equality() {
217        let trade_id1 = TradeId::new("TRADE12345");
218        let trade_id2 = TradeId::new("TRADE12345");
219        assert_eq!(trade_id1, trade_id2);
220    }
221
222    #[rstest]
223    fn test_string_reprs(trade_id: TradeId) {
224        assert_eq!(trade_id.to_string(), "1234567890");
225        assert_eq!(format!("{trade_id}"), "1234567890");
226        assert_eq!(format!("{trade_id:?}"), "TradeId('1234567890')");
227    }
228
229    #[rstest]
230    fn test_trade_id_ordering() {
231        let trade_id1 = TradeId::new("TRADE12345");
232        let trade_id2 = TradeId::new("TRADE12346");
233        assert!(trade_id1 < trade_id2);
234    }
235
236    #[rstest]
237    fn test_trade_id_serialization() {
238        let trade_id = TradeId::new("TRADE12345");
239        let json = serde_json::to_string(&trade_id).unwrap();
240        assert_eq!(json, "\"TRADE12345\"");
241
242        let deserialized: TradeId = serde_json::from_str(&json).unwrap();
243        assert_eq!(trade_id, deserialized);
244    }
245}