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
17pub mod book;
19
20pub mod candle;
22
23pub mod liquidation;
25
26pub mod trade;
28
29pub trait SubscriptionKind
31where
32 Self: Debug + Clone,
33{
34 type Event: Debug;
35 fn as_str(&self) -> &'static str;
36}
37
38#[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 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 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#[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 (
177 BinanceFuturesUsd | Bitmex | BybitPerpetualsUsd | GateioPerpetualsUsd
178 | GateioPerpetualsBtc,
179 Spot,
180 ) => false,
181 (_, Spot) => true,
182
183 (GateioFuturesUsd | GateioFuturesBtc | Okx, Future(_)) => true,
185 (_, Future(_)) => false,
186
187 (
189 BinanceFuturesUsd | Bitmex | Okx | BybitPerpetualsUsd | GateioPerpetualsUsd
190 | GateioPerpetualsBtc,
191 Perpetual,
192 ) => true,
193 (_, Perpetual) => false,
194
195 (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 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
225pub 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#[derive(Clone, Eq, PartialEq, Debug)]
260pub struct SubscriptionMeta<InstrumentKey> {
261 pub instrument_map: Map<InstrumentKey>,
264 pub ws_subscriptions: Vec<WsMessage>,
266}
267
268#[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 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 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 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 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 }
454 (actual, expected) => {
455 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 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 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 }
516 (actual, expected) => {
517 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 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 input: SubscriptionId::from("present"),
546 expected: Ok(MarketDataInstrument::from((
547 "base",
548 "quote",
549 MarketDataInstrumentKind::Spot,
550 ))),
551 },
552 TestCase {
553 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 }
570 (actual, expected) => {
571 panic!("TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n");
573 }
574 }
575 }
576 }
577 }
578}