Skip to main content

barter_data_sniper/subscription/
mod.rs

1use crate::{exchange::Connector, instrument::InstrumentData};
2use barter_instrument_copy::{
3    asset::name::AssetNameInternal,
4    exchange::ExchangeId,
5    instrument::market_data::{kind::MarketDataInstrumentKind, MarketDataInstrument},
6    Keyed,
7};
8use barter_integration_copy::{
9    error::SocketError, protocol::websocket::WsMessage, subscription::SubscriptionId, Validator,
10};
11use derive_more::Display;
12use fnv::FnvHashMap;
13use serde::{Deserialize, Serialize};
14use smol_str::ToSmolStr;
15use std::{borrow::Borrow, fmt::Debug, hash::Hash};
16
17/// OrderBook [`SubscriptionKind`]s and the associated Barter output data models.
18pub mod book;
19
20/// Candle [`SubscriptionKind`] and the associated Barter output data model.
21pub mod candle;
22
23/// Liquidation [`SubscriptionKind`] and the associated Barter output data model.
24pub mod liquidation;
25
26/// Public trade [`SubscriptionKind`] and the associated Barter output data model.
27pub mod trade;
28
29/// Defines kind of a [`Subscription`], and the output [`Self::Event`] that it yields.
30pub trait SubscriptionKind
31where
32    Self: Debug + Clone,
33{
34    type Event: Debug;
35    fn as_str(&self) -> &'static str;
36}
37
38/// Barter [`Subscription`] used to subscribe to a [`SubscriptionKind`] for a particular exchange
39/// [`MarketDataInstrument`].
40#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
41pub struct Subscription<Exchange = ExchangeId, Inst = MarketDataInstrument, Kind = SubKind> {
42    pub exchange: Exchange,
43    #[serde(flatten)]
44    pub instrument: Inst,
45    #[serde(alias = "type")]
46    pub kind: Kind,
47}
48
49#[derive(
50    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize, Display,
51)]
52pub enum SubKind {
53    PublicTrades,
54    OrderBooksL1,
55    OrderBooksL2,
56    OrderBooksL3,
57    Liquidations,
58    Candles,
59}
60
61impl<Exchange, Instrument, Kind> std::fmt::Display for Subscription<Exchange, Instrument, Kind>
62where
63    Exchange: Display,
64    Instrument: Display,
65    Kind: Display,
66{
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        write!(f, "{}_{}{}", self.exchange, self.kind, self.instrument)
69    }
70}
71
72impl<Exchange, S, Kind> From<(Exchange, S, S, MarketDataInstrumentKind, Kind)>
73    for Subscription<Exchange, MarketDataInstrument, Kind>
74where
75    S: Into<AssetNameInternal>,
76{
77    fn from(
78        (exchange, base, quote, instrument_kind, kind): (
79            Exchange,
80            S,
81            S,
82            MarketDataInstrumentKind,
83            Kind,
84        ),
85    ) -> Self {
86        Self::new(exchange, (base, quote, instrument_kind), kind)
87    }
88}
89
90impl<InstrumentKey, Exchange, S, Kind>
91    From<(
92        InstrumentKey,
93        Exchange,
94        S,
95        S,
96        MarketDataInstrumentKind,
97        Kind,
98    )> for Subscription<Exchange, Keyed<InstrumentKey, MarketDataInstrument>, Kind>
99where
100    S: Into<AssetNameInternal>,
101{
102    fn from(
103        (instrument_id, exchange, base, quote, instrument_kind, kind): (
104            InstrumentKey,
105            Exchange,
106            S,
107            S,
108            MarketDataInstrumentKind,
109            Kind,
110        ),
111    ) -> Self {
112        let instrument = Keyed::new(instrument_id, (base, quote, instrument_kind).into());
113
114        Self::new(exchange, instrument, kind)
115    }
116}
117
118impl<Exchange, I, Instrument, Kind> From<(Exchange, I, Kind)>
119    for Subscription<Exchange, Instrument, Kind>
120where
121    I: Into<Instrument>,
122{
123    fn from((exchange, instrument, kind): (Exchange, I, Kind)) -> Self {
124        Self::new(exchange, instrument, kind)
125    }
126}
127
128impl<Instrument, Exchange, Kind> Subscription<Exchange, Instrument, Kind> {
129    /// Constructs a new [`Subscription`] using the provided configuration.
130    pub fn new<I>(exchange: Exchange, instrument: I, kind: Kind) -> Self
131    where
132        I: Into<Instrument>,
133    {
134        Self {
135            exchange,
136            instrument: instrument.into(),
137            kind,
138        }
139    }
140}
141
142impl<Exchange, Instrument, Kind> Validator for Subscription<Exchange, Instrument, Kind>
143where
144    Exchange: Connector,
145    Instrument: InstrumentData,
146{
147    fn validate(self) -> Result<Self, SocketError>
148    where
149        Self: Sized,
150    {
151        // Validate the Exchange supports the Subscription InstrumentKind
152        if exchange_supports_instrument_kind(Exchange::ID, self.instrument.kind()) {
153            Ok(self)
154        } else {
155            Err(SocketError::Unsupported {
156                entity: Exchange::ID.to_string(),
157                item: self.instrument.kind().to_string(),
158            })
159        }
160    }
161}
162
163/// Determines whether the [`Connector`] associated with this [`ExchangeId`] supports the
164/// ingestion of market data for the provided [`MarketDataInstrumentKind`].
165#[allow(clippy::match_like_matches_macro)]
166pub fn exchange_supports_instrument_kind(
167    exchange: ExchangeId,
168    instrument_kind: &MarketDataInstrumentKind,
169) -> bool {
170    use barter_instrument_copy::{
171        exchange::ExchangeId::*, instrument::market_data::kind::MarketDataInstrumentKind::*,
172    };
173
174    match (exchange, instrument_kind) {
175        // Spot
176        (
177            BinanceFuturesUsd | Bitmex | BybitPerpetualsUsd | GateioPerpetualsUsd
178            | GateioPerpetualsBtc,
179            Spot,
180        ) => false,
181        (_, Spot) => true,
182
183        // Future
184        (GateioFuturesUsd | GateioFuturesBtc | Okx, Future(_)) => true,
185        (_, Future(_)) => false,
186
187        // Perpetual
188        (
189            BinanceFuturesUsd | Bitmex | Okx | BybitPerpetualsUsd | GateioPerpetualsUsd
190            | GateioPerpetualsBtc,
191            Perpetual,
192        ) => true,
193        (_, Perpetual) => false,
194
195        // Option
196        (GateioOptions | Okx, Option(_)) => true,
197        (_, Option(_)) => false,
198    }
199}
200
201impl<Instrument> Validator for Subscription<ExchangeId, Instrument, SubKind>
202where
203    Instrument: InstrumentData,
204{
205    fn validate(self) -> Result<Self, SocketError>
206    where
207        Self: Sized,
208    {
209        // Validate the Exchange supports the Subscription InstrumentKind
210        if exchange_supports_instrument_kind_sub_kind(
211            &self.exchange,
212            self.instrument.kind(),
213            self.kind,
214        ) {
215            Ok(self)
216        } else {
217            Err(SocketError::Unsupported {
218                entity: self.exchange.to_string(),
219                item: self.instrument.kind().to_string(),
220            })
221        }
222    }
223}
224
225/// Determines whether the [`Connector`] associated with this [`ExchangeId`] supports the
226/// ingestion of market data for the provided [`MarketDataInstrumentKind`] and [`SubKind`] combination.
227pub fn exchange_supports_instrument_kind_sub_kind(
228    exchange_id: &ExchangeId,
229    instrument_kind: &MarketDataInstrumentKind,
230    sub_kind: SubKind,
231) -> bool {
232    use ExchangeId::*;
233    use MarketDataInstrumentKind::*;
234    use SubKind::*;
235
236    match (exchange_id, instrument_kind, sub_kind) {
237        (BinanceSpot, Spot, PublicTrades | OrderBooksL1) => true,
238        (BinanceFuturesUsd, Perpetual, PublicTrades | OrderBooksL1 | Liquidations) => true,
239        (Bitfinex, Spot, PublicTrades) => true,
240        (Bitmex, Perpetual, PublicTrades) => true,
241        (BybitSpot, Spot, PublicTrades) => true,
242        (BybitPerpetualsUsd, Perpetual, PublicTrades) => true,
243        (Coinbase, Spot, PublicTrades) => true,
244        (GateioSpot, Spot, PublicTrades) => true,
245        (GateioFuturesUsd, Future(_), PublicTrades) => true,
246        (GateioFuturesBtc, Future(_), PublicTrades) => true,
247        (GateioPerpetualsUsd, Perpetual, PublicTrades) => true,
248        (GateioPerpetualsBtc, Perpetual, PublicTrades) => true,
249        (GateioOptions, Option(_), PublicTrades) => true,
250        (Kraken, Spot, PublicTrades | OrderBooksL1) => true,
251        (Okx, Spot | Future(_) | Perpetual | Option(_), PublicTrades) => true,
252
253        (_, _, _) => false,
254    }
255}
256
257/// Metadata generated from a collection of Barter [`Subscription`]s, including the exchange
258/// specific subscription payloads that are sent to the exchange.
259#[derive(Clone, Eq, PartialEq, Debug)]
260pub struct SubscriptionMeta<InstrumentKey> {
261    /// `HashMap` containing the mapping between a [`SubscriptionId`] and
262    /// it's associated Barter [`MarketDataInstrument`].
263    pub instrument_map: Map<InstrumentKey>,
264    /// Collection of [`WsMessage`]s containing exchange specific subscription payloads to be sent.
265    pub ws_subscriptions: Vec<WsMessage>,
266}
267
268/// New type`HashMap` that maps a [`SubscriptionId`] to some associated type `T`.
269///
270/// Used by [`ExchangeTransformer`](crate::transformer::ExchangeTransformer)s to identify the
271/// Barter [`MarketDataInstrument`] associated with incoming exchange messages.
272#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
273pub struct Map<T>(pub FnvHashMap<SubscriptionId, T>);
274
275impl<T> FromIterator<(SubscriptionId, T)> for Map<T> {
276    fn from_iter<Iter>(iter: Iter) -> Self
277    where
278        Iter: IntoIterator<Item = (SubscriptionId, T)>,
279    {
280        Self(iter.into_iter().collect::<FnvHashMap<SubscriptionId, T>>())
281    }
282}
283
284impl<T> Map<T> {
285    /// Find the `InstrumentKey` associated with the provided [`SubscriptionId`].
286    pub fn find<SubId>(&self, id: &SubId) -> Result<&T, SocketError>
287    where
288        SubscriptionId: Borrow<SubId>,
289        SubId: AsRef<str> + Hash + Eq + ?Sized,
290    {
291        self.0
292            .get(id)
293            .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
294    }
295
296    /// Find the mutable reference to `T` associated with the provided [`SubscriptionId`].
297    pub fn find_mut<SubId>(&mut self, id: &SubId) -> Result<&mut T, SocketError>
298    where
299        SubscriptionId: Borrow<SubId>,
300        SubId: AsRef<str> + Hash + Eq + ?Sized,
301    {
302        self.0
303            .get_mut(id)
304            .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    mod subscription {
313        use super::*;
314        use crate::{
315            exchange::{coinbase::Coinbase, okx::Okx},
316            subscription::trade::PublicTrades,
317        };
318        use barter_instrument_copy::instrument::market_data::MarketDataInstrument;
319
320        mod de {
321            use super::*;
322            use crate::{
323                exchange::{
324                    binance::{futures::BinanceFuturesUsd, spot::BinanceSpot},
325                    gateio::perpetual::GateioPerpetualsUsd,
326                    okx::Okx,
327                },
328                subscription::{book::OrderBooksL2, trade::PublicTrades},
329            };
330            use barter_instrument_copy::instrument::market_data::MarketDataInstrument;
331
332            #[test]
333            fn test_subscription_okx_spot_public_trades() {
334                let input = r#"
335                {
336                    "exchange": "okx",
337                    "base": "btc",
338                    "quote": "usdt",
339                    "instrument_kind": "spot",
340                    "kind": "public_trades"
341                }
342                "#;
343
344                serde_json::from_str::<Subscription<Okx, MarketDataInstrument, PublicTrades>>(
345                    input,
346                )
347                .unwrap();
348            }
349
350            #[test]
351            fn test_subscription_binance_spot_public_trades() {
352                let input = r#"
353                {
354                    "exchange": "binance_spot",
355                    "base": "btc",
356                    "quote": "usdt",
357                    "instrument_kind": "spot",
358                    "kind": "public_trades"
359                }
360                "#;
361
362                serde_json::from_str::<Subscription<BinanceSpot, MarketDataInstrument, PublicTrades>>(input)
363                    .unwrap();
364            }
365
366            #[test]
367            fn test_subscription_binance_futures_usd_order_books_l2() {
368                let input = r#"
369                {
370                    "exchange": "binance_futures_usd",
371                    "base": "btc",
372                    "quote": "usdt",
373                    "instrument_kind": "perpetual",
374                    "kind": "order_books_l2"
375                }
376                "#;
377
378                serde_json::from_str::<
379                    Subscription<BinanceFuturesUsd, MarketDataInstrument, OrderBooksL2>,
380                >(input)
381                .unwrap();
382            }
383
384            #[test]
385            fn subscription_gateio_futures_usd_public_trades() {
386                let input = r#"
387                {
388                    "exchange": "gateio_perpetuals_usd",
389                    "base": "btc",
390                    "quote": "usdt",
391                    "instrument_kind": "perpetual",
392                    "kind": "public_trades"
393                }
394                "#;
395
396                serde_json::from_str::<
397                    Subscription<GateioPerpetualsUsd, MarketDataInstrument, PublicTrades>,
398                >(input)
399                .unwrap();
400            }
401        }
402
403        #[test]
404        fn test_validate_bitfinex_public_trades() {
405            struct TestCase {
406                input: Subscription<Coinbase, MarketDataInstrument, PublicTrades>,
407                expected:
408                    Result<Subscription<Coinbase, MarketDataInstrument, PublicTrades>, SocketError>,
409            }
410
411            let tests = vec![
412                TestCase {
413                    // TC0: Valid Coinbase Spot PublicTrades subscription
414                    input: Subscription::from((
415                        Coinbase,
416                        "base",
417                        "quote",
418                        MarketDataInstrumentKind::Spot,
419                        PublicTrades,
420                    )),
421                    expected: Ok(Subscription::from((
422                        Coinbase,
423                        "base",
424                        "quote",
425                        MarketDataInstrumentKind::Spot,
426                        PublicTrades,
427                    ))),
428                },
429                TestCase {
430                    // TC1: Invalid Coinbase FuturePerpetual PublicTrades subscription
431                    input: Subscription::from((
432                        Coinbase,
433                        "base",
434                        "quote",
435                        MarketDataInstrumentKind::Perpetual,
436                        PublicTrades,
437                    )),
438                    expected: Err(SocketError::Unsupported {
439                        entity: "".to_string(),
440                        item: "".to_string(),
441                    }),
442                },
443            ];
444
445            for (index, test) in tests.into_iter().enumerate() {
446                let actual = test.input.validate();
447                match (actual, test.expected) {
448                    (Ok(actual), Ok(expected)) => {
449                        assert_eq!(actual, expected, "TC{} failed", index)
450                    }
451                    (Err(_), Err(_)) => {
452                        // Test passed
453                    }
454                    (actual, expected) => {
455                        // Test failed
456                        panic!("TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n");
457                    }
458                }
459            }
460        }
461
462        #[test]
463        fn test_validate_okx_public_trades() {
464            struct TestCase {
465                input: Subscription<Okx, MarketDataInstrument, PublicTrades>,
466                expected:
467                    Result<Subscription<Okx, MarketDataInstrument, PublicTrades>, SocketError>,
468            }
469
470            let tests = vec![
471                TestCase {
472                    // TC0: Valid Okx Spot PublicTrades subscription
473                    input: Subscription::from((
474                        Okx,
475                        "base",
476                        "quote",
477                        MarketDataInstrumentKind::Spot,
478                        PublicTrades,
479                    )),
480                    expected: Ok(Subscription::from((
481                        Okx,
482                        "base",
483                        "quote",
484                        MarketDataInstrumentKind::Spot,
485                        PublicTrades,
486                    ))),
487                },
488                TestCase {
489                    // TC1: Valid Okx FuturePerpetual PublicTrades subscription
490                    input: Subscription::from((
491                        Okx,
492                        "base",
493                        "quote",
494                        MarketDataInstrumentKind::Perpetual,
495                        PublicTrades,
496                    )),
497                    expected: Ok(Subscription::from((
498                        Okx,
499                        "base",
500                        "quote",
501                        MarketDataInstrumentKind::Perpetual,
502                        PublicTrades,
503                    ))),
504                },
505            ];
506
507            for (index, test) in tests.into_iter().enumerate() {
508                let actual = test.input.validate();
509                match (actual, test.expected) {
510                    (Ok(actual), Ok(expected)) => {
511                        assert_eq!(actual, expected, "TC{} failed", index)
512                    }
513                    (Err(_), Err(_)) => {
514                        // Test passed
515                    }
516                    (actual, expected) => {
517                        // Test failed
518                        panic!("TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n");
519                    }
520                }
521            }
522        }
523    }
524
525    mod instrument_map {
526        use super::*;
527        use barter_instrument_copy::instrument::market_data::MarketDataInstrument;
528
529        #[test]
530        fn test_find_instrument() {
531            // Initialise SubscriptionId-InstrumentKey HashMap
532            let ids = Map(FnvHashMap::from_iter([(
533                SubscriptionId::from("present"),
534                MarketDataInstrument::from(("base", "quote", MarketDataInstrumentKind::Spot)),
535            )]));
536
537            struct TestCase {
538                input: SubscriptionId,
539                expected: Result<MarketDataInstrument, SocketError>,
540            }
541
542            let cases = vec![
543                TestCase {
544                    // TC0: SubscriptionId (channel) is present in the HashMap
545                    input: SubscriptionId::from("present"),
546                    expected: Ok(MarketDataInstrument::from((
547                        "base",
548                        "quote",
549                        MarketDataInstrumentKind::Spot,
550                    ))),
551                },
552                TestCase {
553                    // TC1: SubscriptionId (channel) is not present in the HashMap
554                    input: SubscriptionId::from("not present"),
555                    expected: Err(SocketError::Unidentifiable(SubscriptionId::from(
556                        "not present",
557                    ))),
558                },
559            ];
560
561            for (index, test) in cases.into_iter().enumerate() {
562                let actual = ids.find(&test.input);
563                match (actual, test.expected) {
564                    (Ok(actual), Ok(expected)) => {
565                        assert_eq!(*actual, expected, "TC{} failed", index)
566                    }
567                    (Err(_), Err(_)) => {
568                        // Test passed
569                    }
570                    (actual, expected) => {
571                        // Test failed
572                        panic!("TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n");
573                    }
574                }
575            }
576        }
577    }
578}