nautilus_model/identifiers/
mod.rs1use serde::{Deserialize, Deserializer, Serialize, Serializer};
29
30#[macro_use]
31mod macros;
32
33pub mod account_id;
34pub mod actor_id;
35pub mod client_id;
36pub mod client_order_id;
37pub mod component_id;
38pub mod exec_algorithm_id;
39pub mod instrument_id;
40pub mod option_instrument_id;
41pub mod option_series_id;
42pub mod order_list_id;
43pub mod position_id;
44pub mod strategy_id;
45pub mod symbol;
46pub mod trade_id;
47pub mod trader_id;
48pub mod venue;
49pub mod venue_order_id;
50
51#[cfg(any(test, feature = "test-support"))]
52pub mod stubs;
53
54pub use crate::identifiers::{
56 account_id::AccountId,
57 actor_id::ActorId,
58 client_id::ClientId,
59 client_order_id::ClientOrderId,
60 component_id::ComponentId,
61 exec_algorithm_id::ExecAlgorithmId,
62 instrument_id::{GENERIC_SPREAD_ID_SEPARATOR, InstrumentId, InstrumentIdError},
63 option_instrument_id::{
64 OptionContractSpec, OptionInstrumentIdError, OptionSymbologyScheme,
65 cme_globex_option_symbol, compute_option_instrument_id, osi_option_symbol,
66 },
67 option_series_id::{OptionSeriesId, OptionSeriesIdError},
68 order_list_id::OrderListId,
69 position_id::PositionId,
70 strategy_id::{
71 StrategyId, UNASSIGNED_ORDER_ID_TAG, check_order_id_tag, normalize_order_id_tag,
72 },
73 symbol::Symbol,
74 trade_id::TradeId,
75 trader_id::TraderId,
76 venue::Venue,
77 venue_order_id::VenueOrderId,
78};
79
80pub fn new_generic_spread_id(
97 instrument_ratios: &[(InstrumentId, i64)],
98) -> anyhow::Result<InstrumentId> {
99 anyhow::ensure!(
100 instrument_ratios.len() > 1,
101 "instrument_ratios list needs to have at least 2 legs"
102 );
103
104 let first_venue = instrument_ratios[0].0.venue;
105 for (instrument_id, ratio) in instrument_ratios {
106 anyhow::ensure!(*ratio != 0, "ratio cannot be zero");
107 anyhow::ensure!(*ratio != i64::MIN, "ratio cannot be i64::MIN");
108 anyhow::ensure!(
109 instrument_id.venue == first_venue,
110 "All venues must match. Expected {first_venue}, was {}",
111 instrument_id.venue
112 );
113 }
114
115 let mut sorted_ratios = instrument_ratios.to_vec();
116 sorted_ratios.sort_by_key(|(instrument_id, _)| instrument_id.symbol);
117
118 let symbol_parts: Vec<String> = sorted_ratios
119 .into_iter()
120 .map(|(instrument_id, ratio)| {
121 if ratio > 0 {
122 format!("({ratio}){}", instrument_id.symbol)
123 } else {
124 format!("(({})){}", ratio.abs(), instrument_id.symbol)
125 }
126 })
127 .collect();
128
129 Ok(InstrumentId::new(
130 Symbol::new(symbol_parts.join(GENERIC_SPREAD_ID_SEPARATOR)),
131 first_venue,
132 ))
133}
134
135pub fn parse_generic_spread_id_legs(
147 instrument_id: &InstrumentId,
148) -> anyhow::Result<Vec<(InstrumentId, i64)>> {
149 let symbol = instrument_id.symbol.as_str();
150 anyhow::ensure!(
151 symbol.contains(GENERIC_SPREAD_ID_SEPARATOR),
152 "Invalid generic spread instrument ID: {instrument_id}"
153 );
154
155 symbol
156 .split(GENERIC_SPREAD_ID_SEPARATOR)
157 .map(|component| {
158 parse_generic_spread_leg(component, instrument_id.venue)
159 .ok_or_else(|| anyhow::anyhow!("Invalid generic spread leg component: {component}"))
160 })
161 .collect()
162}
163
164fn parse_generic_spread_leg(component: &str, venue: Venue) -> Option<(InstrumentId, i64)> {
165 if let Some(rest) = component.strip_prefix("((") {
166 let (ratio, symbol) = rest.split_once("))")?;
167 return parse_generic_spread_leg_parts(ratio, symbol, venue, -1);
168 }
169
170 let rest = component.strip_prefix('(')?;
171 let (ratio, symbol) = rest.split_once(')')?;
172 parse_generic_spread_leg_parts(ratio, symbol, venue, 1)
173}
174
175fn parse_generic_spread_leg_parts(
176 ratio: &str,
177 symbol: &str,
178 venue: Venue,
179 sign: i64,
180) -> Option<(InstrumentId, i64)> {
181 if !ratio.bytes().all(|b| b.is_ascii_digit()) {
184 return None;
185 }
186
187 let ratio = ratio.parse::<i64>().ok()?.checked_mul(sign)?;
188 if ratio == 0 {
189 return None;
190 }
191
192 let symbol = Symbol::new_checked(symbol).ok()?;
193 Some((InstrumentId::new(symbol, venue), ratio))
194}
195
196impl_from_str_for_identifier!(account_id::AccountId);
197impl_from_str_for_identifier!(actor_id::ActorId);
198impl_from_str_for_identifier!(client_id::ClientId);
199impl_from_str_for_identifier!(client_order_id::ClientOrderId);
200impl_from_str_for_identifier!(component_id::ComponentId);
201impl_from_str_for_identifier!(exec_algorithm_id::ExecAlgorithmId);
202impl_from_str_for_identifier!(order_list_id::OrderListId);
203impl_from_str_for_identifier!(position_id::PositionId);
204impl_from_str_for_identifier!(strategy_id::StrategyId);
205impl_from_str_for_identifier!(symbol::Symbol);
206impl_from_str_for_identifier!(trade_id::TradeId);
207impl_from_str_for_identifier!(trader_id::TraderId);
208impl_from_str_for_identifier!(venue::Venue);
209impl_from_str_for_identifier!(venue_order_id::VenueOrderId);
210
211impl_serialization_for_identifier!(account_id::AccountId);
212impl_serialization_for_identifier!(actor_id::ActorId);
213impl_serialization_for_identifier!(client_id::ClientId);
214impl_serialization_for_identifier!(client_order_id::ClientOrderId);
215impl_serialization_for_identifier!(component_id::ComponentId);
216impl_serialization_for_identifier!(exec_algorithm_id::ExecAlgorithmId);
217impl_serialization_for_identifier!(order_list_id::OrderListId);
218impl_serialization_for_identifier!(position_id::PositionId);
219impl_serialization_for_identifier!(strategy_id::StrategyId);
220impl_serialization_for_identifier!(symbol::Symbol);
221impl_serialization_for_identifier!(trader_id::TraderId);
222impl_serialization_for_identifier!(venue::Venue);
223impl_serialization_for_identifier!(venue_order_id::VenueOrderId);
224
225impl_as_ref_for_identifier!(account_id::AccountId);
226impl_as_ref_for_identifier!(actor_id::ActorId);
227impl_as_ref_for_identifier!(client_id::ClientId);
228impl_as_ref_for_identifier!(client_order_id::ClientOrderId);
229impl_as_ref_for_identifier!(component_id::ComponentId);
230impl_as_ref_for_identifier!(exec_algorithm_id::ExecAlgorithmId);
231impl_as_ref_for_identifier!(order_list_id::OrderListId);
232impl_as_ref_for_identifier!(position_id::PositionId);
233impl_as_ref_for_identifier!(strategy_id::StrategyId);
234impl_as_ref_for_identifier!(symbol::Symbol);
235impl_as_ref_for_identifier!(trader_id::TraderId);
236impl_as_ref_for_identifier!(venue::Venue);
237impl_as_ref_for_identifier!(venue_order_id::VenueOrderId);
238
239pub fn interned_string_stats() {
241 ustr::string_cache_iter().for_each(|s| println!("{s}"));
242}
243
244#[cfg(test)]
245mod tests {
246 use rstest::rstest;
247
248 use super::{InstrumentId, new_generic_spread_id, parse_generic_spread_id_legs};
249
250 #[rstest]
251 fn test_new_generic_spread_id_sorts_and_formats_legs() {
252 let msft = InstrumentId::from("MSFT.NASDAQ");
253 let aapl = InstrumentId::from("AAPL.NASDAQ");
254
255 let spread = new_generic_spread_id(&[(msft, 1), (aapl, -2)]).unwrap();
256
257 assert_eq!(spread, InstrumentId::from("((2))AAPL___(1)MSFT.NASDAQ"));
258 }
259
260 #[rstest]
261 #[case(
262 vec![("MSFT.NASDAQ", 1)],
263 "instrument_ratios list needs to have at least 2 legs"
264 )]
265 #[case(vec![("MSFT.NASDAQ", 0), ("AAPL.NASDAQ", 1)], "ratio cannot be zero")]
266 #[case(
267 vec![("MSFT.NASDAQ", 1), ("AAPL.XNAS", 1)],
268 "All venues must match. Expected NASDAQ, was XNAS"
269 )]
270 #[case(vec![("MSFT.NASDAQ", i64::MIN), ("AAPL.NASDAQ", 1)], "ratio cannot be i64::MIN")]
271 fn test_new_generic_spread_id_rejects_invalid_legs(
272 #[case] legs: Vec<(&str, i64)>,
273 #[case] expected: &str,
274 ) {
275 let legs: Vec<(InstrumentId, i64)> = legs
276 .into_iter()
277 .map(|(instrument_id, ratio)| (InstrumentId::from(instrument_id), ratio))
278 .collect();
279
280 let result = new_generic_spread_id(&legs);
281
282 assert_eq!(result.unwrap_err().to_string(), expected);
283 }
284
285 #[rstest]
286 #[case("((-2))AAPL___(1)MSFT.NASDAQ", "((-2))AAPL")] #[case("(+1)MSFT___(2)AAPL.NASDAQ", "(+1)MSFT")] #[case("(-1)MSFT___(2)AAPL.NASDAQ", "(-1)MSFT")] #[case("()MSFT___(2)AAPL.NASDAQ", "()MSFT")] #[case("(1a)MSFT___(2)AAPL.NASDAQ", "(1a)MSFT")] fn test_parse_generic_spread_id_legs_rejects_non_digit_ratios(
292 #[case] value: &str,
293 #[case] component: &str,
294 ) {
295 let result = parse_generic_spread_id_legs(&InstrumentId::from(value));
296
297 assert_eq!(
298 result.unwrap_err().to_string(),
299 format!("Invalid generic spread leg component: {component}")
300 );
301 }
302
303 #[rstest]
304 #[case("(0)MSFT___(2)AAPL.NASDAQ", "(0)MSFT")] #[case("(1)___(2)AAPL.NASDAQ", "(1)")] #[case("(1) ___(2)AAPL.NASDAQ", "(1) ")] #[case("MSFT___(2)AAPL.NASDAQ", "MSFT")] #[case(
309 "(9223372036854775808)MSFT___(2)AAPL.NASDAQ",
310 "(9223372036854775808)MSFT"
311 )] fn test_parse_generic_spread_id_legs_rejects_malformed_legs(
313 #[case] value: &str,
314 #[case] component: &str,
315 ) {
316 let result = parse_generic_spread_id_legs(&InstrumentId::from(value));
317
318 assert_eq!(
319 result.unwrap_err().to_string(),
320 format!("Invalid generic spread leg component: {component}")
321 );
322 }
323
324 #[rstest]
325 fn test_parse_generic_spread_id_legs_rejects_id_without_separator() {
326 let result = parse_generic_spread_id_legs(&InstrumentId::from("(1)MSFT.NASDAQ"));
327
328 assert_eq!(
329 result.unwrap_err().to_string(),
330 "Invalid generic spread instrument ID: (1)MSFT.NASDAQ"
331 );
332 }
333
334 #[rstest]
335 fn test_generic_spread_id_round_trip() {
336 let spread = new_generic_spread_id(&[
337 (InstrumentId::from("ESM4 P5230.XCME"), -1),
338 (InstrumentId::from("ESM4 P5250.XCME"), 1),
339 ])
340 .unwrap();
341
342 assert_eq!(
343 parse_generic_spread_id_legs(&spread).unwrap(),
344 vec![
345 (InstrumentId::from("ESM4 P5230.XCME"), -1),
346 (InstrumentId::from("ESM4 P5250.XCME"), 1),
347 ]
348 );
349 }
350}