nautilus_common/messages/data/
identifiers.rs1use std::hash::Hash;
17
18use ahash::AHashSet;
19use nautilus_core::Params;
20use nautilus_model::{
21 data::BarType,
22 identifiers::{ClientId, InstrumentId},
23 instruments::InstrumentAny,
24};
25use serde::{Deserialize, de::Error};
26
27use crate::cache::Cache;
28
29#[derive(Debug, thiserror::Error)]
33#[error("{context} requires one {kind} on this adapter path, received {count}")]
34pub struct IdentifierCountError {
35 context: &'static str,
36 kind: &'static str,
37 count: usize,
38}
39
40pub trait IntoInstrumentIds: sealed::Sealed {
44 fn into_instrument_ids(self) -> anyhow::Result<Vec<InstrumentId>>;
50}
51
52impl IntoInstrumentIds for InstrumentId {
53 fn into_instrument_ids(self) -> anyhow::Result<Vec<InstrumentId>> {
54 Ok(vec![self])
55 }
56}
57
58impl IntoInstrumentIds for &InstrumentId {
59 fn into_instrument_ids(self) -> anyhow::Result<Vec<InstrumentId>> {
60 (*self).into_instrument_ids()
61 }
62}
63
64impl IntoInstrumentIds for Vec<InstrumentId> {
65 fn into_instrument_ids(mut self) -> anyhow::Result<Vec<InstrumentId>> {
66 anyhow::ensure!(!self.is_empty(), "instrument IDs cannot be empty");
67
68 deduplicate(&mut self);
69
70 Ok(self)
71 }
72}
73
74impl IntoInstrumentIds for &Vec<InstrumentId> {
75 fn into_instrument_ids(self) -> anyhow::Result<Vec<InstrumentId>> {
76 self.as_slice().into_instrument_ids()
77 }
78}
79
80impl IntoInstrumentIds for &[InstrumentId] {
81 fn into_instrument_ids(self) -> anyhow::Result<Vec<InstrumentId>> {
82 self.to_vec().into_instrument_ids()
83 }
84}
85
86impl<const N: usize> IntoInstrumentIds for [InstrumentId; N] {
87 fn into_instrument_ids(self) -> anyhow::Result<Vec<InstrumentId>> {
88 Vec::from(self).into_instrument_ids()
89 }
90}
91
92impl<const N: usize> IntoInstrumentIds for &[InstrumentId; N] {
93 fn into_instrument_ids(self) -> anyhow::Result<Vec<InstrumentId>> {
94 self.as_slice().into_instrument_ids()
95 }
96}
97
98pub(super) fn deserialize_instrument_ids<'de, D>(
99 deserializer: D,
100) -> Result<Vec<InstrumentId>, D::Error>
101where
102 D: serde::Deserializer<'de>,
103{
104 Vec::<InstrumentId>::deserialize(deserializer)?
105 .into_instrument_ids()
106 .map_err(D::Error::custom)
107}
108
109pub fn ensure_routable_instrument_ids(
118 instrument_ids: &[InstrumentId],
119 has_client_id: bool,
120) -> anyhow::Result<()> {
121 let first = instrument_ids
122 .first()
123 .ok_or_else(|| anyhow::anyhow!("instrument IDs cannot be empty"))?;
124 let mut seen = AHashSet::new();
125 anyhow::ensure!(
126 instrument_ids.iter().all(|id| seen.insert(*id)),
127 "instrument IDs must be unique"
128 );
129 anyhow::ensure!(
130 has_client_id || instrument_ids.iter().all(|id| id.venue == first.venue),
131 "multiple instrument venues require an explicit client ID"
132 );
133 Ok(())
134}
135
136pub fn ensure_subscription_routes(
145 ids: &[InstrumentId],
146 client_id: Option<&ClientId>,
147 cache: &Cache,
148) -> anyhow::Result<()> {
149 anyhow::ensure!(
150 client_id.is_some_and(|id| cache.is_external_data_client(id))
151 || ids.len() <= 1
152 || !ids.iter().any(InstrumentId::is_synthetic),
153 "Cannot subscribe synthetic instruments for this data type"
154 );
155 Ok(())
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum QuoteRouting {
161 Client,
162 Synthetic,
163 Spread,
164}
165
166pub fn quote_routing(
175 ids: &[InstrumentId],
176 client_id: Option<&ClientId>,
177 params: Option<&Params>,
178 cache: &Cache,
179) -> anyhow::Result<QuoteRouting> {
180 let first = ids
181 .first()
182 .ok_or_else(|| anyhow::anyhow!("instrument IDs cannot be empty"))?;
183
184 if client_id.is_some_and(|id| cache.is_external_data_client(id)) {
185 return Ok(QuoteRouting::Client);
186 }
187
188 let aggregate_spreads = params
189 .and_then(|params| params.get_bool("aggregate_spread_quotes"))
190 .unwrap_or(false);
191
192 let classify = |id: &InstrumentId| {
193 if id.is_synthetic() {
194 QuoteRouting::Synthetic
195 } else if aggregate_spreads && cache.instrument(id).is_some_and(InstrumentAny::is_spread) {
196 QuoteRouting::Spread
197 } else {
198 QuoteRouting::Client
199 }
200 };
201
202 let routing = classify(first);
203 anyhow::ensure!(
204 ids.iter().all(|id| classify(id) == routing),
205 "quote identifier lists cannot mix client, synthetic, and spread routes"
206 );
207 Ok(routing)
208}
209
210pub(super) fn single_instrument_id(
211 instrument_ids: &[InstrumentId],
212 context: &'static str,
213) -> anyhow::Result<InstrumentId> {
214 match instrument_ids {
215 [instrument_id] => Ok(*instrument_id),
216 _ => Err(IdentifierCountError {
217 context,
218 kind: "instrument ID",
219 count: instrument_ids.len(),
220 }
221 .into()),
222 }
223}
224
225pub trait IntoBarTypes: sealed::Sealed {
229 fn into_bar_types(self) -> anyhow::Result<Vec<BarType>>;
235}
236
237impl IntoBarTypes for BarType {
238 fn into_bar_types(self) -> anyhow::Result<Vec<BarType>> {
239 Ok(vec![self])
240 }
241}
242
243impl IntoBarTypes for &BarType {
244 fn into_bar_types(self) -> anyhow::Result<Vec<BarType>> {
245 (*self).into_bar_types()
246 }
247}
248
249impl IntoBarTypes for Vec<BarType> {
250 fn into_bar_types(mut self) -> anyhow::Result<Vec<BarType>> {
251 anyhow::ensure!(!self.is_empty(), "bar types cannot be empty");
252
253 deduplicate(&mut self);
254
255 Ok(self)
256 }
257}
258
259impl IntoBarTypes for &Vec<BarType> {
260 fn into_bar_types(self) -> anyhow::Result<Vec<BarType>> {
261 self.as_slice().into_bar_types()
262 }
263}
264
265impl IntoBarTypes for &[BarType] {
266 fn into_bar_types(self) -> anyhow::Result<Vec<BarType>> {
267 self.to_vec().into_bar_types()
268 }
269}
270
271impl<const N: usize> IntoBarTypes for [BarType; N] {
272 fn into_bar_types(self) -> anyhow::Result<Vec<BarType>> {
273 Vec::from(self).into_bar_types()
274 }
275}
276
277impl<const N: usize> IntoBarTypes for &[BarType; N] {
278 fn into_bar_types(self) -> anyhow::Result<Vec<BarType>> {
279 self.as_slice().into_bar_types()
280 }
281}
282
283pub(super) fn deserialize_bar_types<'de, D>(deserializer: D) -> Result<Vec<BarType>, D::Error>
284where
285 D: serde::Deserializer<'de>,
286{
287 Vec::<BarType>::deserialize(deserializer)?
288 .into_bar_types()
289 .map_err(D::Error::custom)
290}
291
292pub fn ensure_routable_bar_types(bar_types: &[BarType], has_client_id: bool) -> anyhow::Result<()> {
301 let first = bar_types
302 .first()
303 .ok_or_else(|| anyhow::anyhow!("bar types cannot be empty"))?;
304 let mut seen = AHashSet::new();
305 anyhow::ensure!(
306 bar_types.iter().all(|bar_type| seen.insert(*bar_type)),
307 "bar types must be unique"
308 );
309 anyhow::ensure!(
310 has_client_id
311 || bar_types
312 .iter()
313 .all(|bar_type| bar_type.instrument_id().venue == first.instrument_id().venue),
314 "multiple bar type venues require an explicit client ID"
315 );
316 Ok(())
317}
318
319pub(super) fn single_bar_type(
320 bar_types: &[BarType],
321 context: &'static str,
322) -> anyhow::Result<BarType> {
323 match bar_types {
324 [bar_type] => Ok(*bar_type),
325 _ => Err(IdentifierCountError {
326 context,
327 kind: "bar type",
328 count: bar_types.len(),
329 }
330 .into()),
331 }
332}
333
334fn deduplicate<T: Copy + Eq + Hash>(values: &mut Vec<T>) {
335 if values.len() > 1 {
336 let mut seen = AHashSet::new();
337 values.retain(|value| seen.insert(*value));
338 }
339}
340
341mod sealed {
342 use nautilus_model::{data::BarType, identifiers::InstrumentId};
343
344 pub trait Sealed {}
345
346 impl Sealed for InstrumentId {}
347 impl Sealed for &InstrumentId {}
348 impl Sealed for Vec<InstrumentId> {}
349 impl Sealed for &Vec<InstrumentId> {}
350 impl Sealed for &[InstrumentId] {}
351 impl<const N: usize> Sealed for [InstrumentId; N] {}
352 impl<const N: usize> Sealed for &[InstrumentId; N] {}
353 impl Sealed for BarType {}
354 impl Sealed for &BarType {}
355 impl Sealed for Vec<BarType> {}
356 impl Sealed for &Vec<BarType> {}
357 impl Sealed for &[BarType] {}
358 impl<const N: usize> Sealed for [BarType; N] {}
359 impl<const N: usize> Sealed for &[BarType; N] {}
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[rstest::rstest]
367 fn converts_single_inputs() {
368 let instrument_id = InstrumentId::from("BTC/USD.SIM");
369
370 assert_eq!(
371 instrument_id.into_instrument_ids().unwrap(),
372 vec![instrument_id]
373 );
374 assert_eq!(
375 (&instrument_id).into_instrument_ids().unwrap(),
376 vec![instrument_id]
377 );
378 }
379
380 #[rstest::rstest]
381 #[case::singleton(["BTC/USD.SIM"], &["BTC/USD.SIM"])]
382 #[case::repeated_single_id(
383 ["ETH/USD.SIM", "ETH/USD.SIM", "ETH/USD.SIM"],
384 &["ETH/USD.SIM"]
385 )]
386 #[case::distinct_ids(
387 ["SOL/USD.SIM", "BTC/USD.SIM", "ETH/USD.SIM"],
388 &["SOL/USD.SIM", "BTC/USD.SIM", "ETH/USD.SIM"]
389 )]
390 #[case::interleaved_duplicates_two_ids(
391 ["ETH/USD.SIM", "BTC/USD.SIM", "ETH/USD.SIM"],
392 &["ETH/USD.SIM", "BTC/USD.SIM"]
393 )]
394 #[case::interleaved_duplicates_three_ids(
395 [
396 "SOL/USD.SIM", "ETH/USD.SIM", "SOL/USD.SIM", "BTC/USD.SIM",
397 "ETH/USD.SIM", "BTC/USD.SIM", "SOL/USD.SIM"
398 ],
399 &["SOL/USD.SIM", "ETH/USD.SIM", "BTC/USD.SIM"]
400 )]
401 #[case::same_symbol_different_venues(
402 ["BTC/USD.OTHER", "BTC/USD.SIM", "BTC/USD.OTHER"],
403 &["BTC/USD.OTHER", "BTC/USD.SIM"]
404 )]
405 fn converts_collection_inputs_in_first_occurrence_order<const N: usize>(
406 #[case] input: [&str; N],
407 #[case] expected: &[&str],
408 ) {
409 let input = input.map(InstrumentId::from);
410 let expected: Vec<_> = expected.iter().copied().map(InstrumentId::from).collect();
411 let vector = input.to_vec();
412
413 assert_eq!(input.into_instrument_ids().unwrap(), expected);
414 assert_eq!((&input).into_instrument_ids().unwrap(), expected);
415 assert_eq!(input.as_slice().into_instrument_ids().unwrap(), expected);
416 assert_eq!((&vector).into_instrument_ids().unwrap(), expected);
417 assert_eq!(vector.into_instrument_ids().unwrap(), expected);
418 }
419
420 #[rstest::rstest]
421 fn rejects_empty_collection_inputs() {
422 let input: [InstrumentId; 0] = [];
423 let vector = Vec::<InstrumentId>::new();
424
425 assert_eq!(
426 input.into_instrument_ids().unwrap_err().to_string(),
427 "instrument IDs cannot be empty"
428 );
429 assert_eq!(
430 (&input).into_instrument_ids().unwrap_err().to_string(),
431 "instrument IDs cannot be empty"
432 );
433 assert_eq!(
434 input
435 .as_slice()
436 .into_instrument_ids()
437 .unwrap_err()
438 .to_string(),
439 "instrument IDs cannot be empty"
440 );
441 assert_eq!(
442 (&vector).into_instrument_ids().unwrap_err().to_string(),
443 "instrument IDs cannot be empty"
444 );
445 assert_eq!(
446 vector.into_instrument_ids().unwrap_err().to_string(),
447 "instrument IDs cannot be empty"
448 );
449 }
450
451 #[rstest::rstest]
452 fn normalizes_full_bar_types_in_first_occurrence_order() {
453 let first = BarType::from("BTC/USD.SIM-1-MINUTE-LAST-EXTERNAL");
454 let second = BarType::from("BTC/USD.SIM-5-MINUTE-LAST-EXTERNAL");
455 let composite = BarType::from("BTC/USD.SIM-5-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL");
456 let input = [second, first, composite, first, second];
457 let expected = vec![second, first, composite];
458
459 assert_eq!(first.into_bar_types().unwrap(), vec![first]);
460 assert_eq!((&first).into_bar_types().unwrap(), vec![first]);
461 assert_eq!(input.into_bar_types().unwrap(), expected);
462 assert_eq!((&input).into_bar_types().unwrap(), expected);
463 assert_eq!(input.as_slice().into_bar_types().unwrap(), expected);
464 assert_eq!(input.to_vec().into_bar_types().unwrap(), expected);
465 assert_eq!((&input.to_vec()).into_bar_types().unwrap(), expected);
466 }
467
468 #[rstest::rstest]
469 fn rejects_empty_bar_type_inputs() {
470 let empty: [BarType; 0] = [];
471 let vector = Vec::<BarType>::new();
472 assert_eq!(
473 empty.into_bar_types().unwrap_err().to_string(),
474 "bar types cannot be empty"
475 );
476 assert!((&empty).into_bar_types().is_err());
477 assert!(empty.as_slice().into_bar_types().is_err());
478 assert!((&vector).into_bar_types().is_err());
479 assert!(vector.into_bar_types().is_err());
480 }
481}