Skip to main content

nautilus_common/messages/data/
identifiers.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
16use 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/// A rejected identifier count for a single-member data path.
30///
31/// The message contains the operation, identifier kind, and count without request parameters.
32#[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
40/// Converts a single instrument ID or collection to a non-empty list.
41///
42/// Duplicate IDs are removed in first-occurrence order.
43pub trait IntoInstrumentIds: sealed::Sealed {
44    /// Returns the normalized instrument IDs.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the collection is empty.
49    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
109/// Checks that a list can route through one venue or an explicit client.
110///
111/// # Errors
112///
113/// Returns an error if:
114/// - The list is empty.
115/// - The list contains duplicate IDs.
116/// - The IDs span venues without an explicit client.
117pub 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
136/// Checks identifier-list routing for instrument metadata, books, status, close, and option greeks.
137///
138/// External data clients handle these lists themselves.
139///
140/// # Errors
141///
142/// Returns an error if the client is not external and the list contains multiple IDs
143/// with a synthetic member.
144pub 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/// The engine route shared by every member of a quote subscription.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum QuoteRouting {
161    Client,
162    Synthetic,
163    Spread,
164}
165
166/// Returns the common quote route using the current instrument cache and spread parameters.
167///
168/// External data clients route the full list. A spread absent from the cache routes to the client.
169///
170/// # Errors
171///
172/// Returns an error if the list is empty, or if a non-external client receives mixed
173/// client, synthetic, and spread routes.
174pub 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
225/// Converts a single bar type or collection to a non-empty list.
226///
227/// Duplicate types are removed in first-occurrence order.
228pub trait IntoBarTypes: sealed::Sealed {
229    /// Returns the normalized bar types.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the collection is empty.
234    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
292/// Checks that a list can route through one venue or an explicit client.
293///
294/// # Errors
295///
296/// Returns an error if:
297/// - The list is empty.
298/// - The list contains duplicate types.
299/// - The types span venues without an explicit client.
300pub 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}