1use crate::{
2 exchange::Connector, instrument::InstrumentData, subscription::candle::CandleInterval,
3};
4use derive_more::Display;
5use fnv::FnvHashMap;
6use rustrade_instrument::{
7 Keyed,
8 asset::name::AssetNameInternal,
9 exchange::ExchangeId,
10 instrument::market_data::{MarketDataInstrument, kind::MarketDataInstrumentKind},
11};
12use rustrade_integration::{
13 Validator, error::SocketError, protocol::websocket::WsMessage, subscription::SubscriptionId,
14};
15use serde::{Deserialize, Serialize};
16use smol_str::{ToSmolStr, format_smolstr};
17use std::{borrow::Borrow, fmt::Debug, hash::Hash};
18
19pub mod book;
21
22pub mod candle;
24
25pub mod greeks;
27
28pub mod liquidation;
30
31pub mod quote;
33
34pub mod trade;
36
37pub trait SubscriptionKind
39where
40 Self: Debug + Clone,
41{
42 type Event: Debug;
43 fn as_str(&self) -> &'static str;
44}
45
46#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
49pub struct Subscription<Exchange = ExchangeId, Inst = MarketDataInstrument, Kind = SubKind> {
50 pub exchange: Exchange,
51 #[serde(flatten)]
52 pub instrument: Inst,
53 #[serde(alias = "type")]
54 pub kind: Kind,
55}
56
57pub fn display_subscriptions_without_exchange<Exchange, Instrument, Kind>(
58 subscriptions: &[Subscription<Exchange, Instrument, Kind>],
59) -> String
60where
61 Instrument: std::fmt::Display,
62 Kind: std::fmt::Display,
63{
64 subscriptions
65 .iter()
66 .map(
67 |Subscription {
68 exchange: _,
69 instrument,
70 kind,
71 }| { format_smolstr!("({instrument}, {kind})") },
72 )
73 .collect::<Vec<_>>()
74 .join(",")
75}
76
77impl<Exchange, Instrument, Kind> std::fmt::Display for Subscription<Exchange, Instrument, Kind>
78where
79 Exchange: std::fmt::Display,
80 Instrument: std::fmt::Display,
81 Kind: std::fmt::Display,
82{
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(f, "({}|{}|{})", self.exchange, self.kind, self.instrument)
85 }
86}
87
88#[derive(
89 Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Deserialize, Serialize,
90)]
91pub enum SubKind {
92 PublicTrades,
93 OrderBooksL1,
94 OrderBooksL2,
95 OrderBooksL3,
96 Liquidations,
97 #[display("candles")]
105 Candles {
106 interval: CandleInterval,
107 },
108 Quotes,
111}
112
113impl<Exchange, S, Kind> From<(Exchange, S, S, MarketDataInstrumentKind, Kind)>
114 for Subscription<Exchange, MarketDataInstrument, Kind>
115where
116 S: Into<AssetNameInternal>,
117{
118 fn from(
119 (exchange, base, quote, instrument_kind, kind): (
120 Exchange,
121 S,
122 S,
123 MarketDataInstrumentKind,
124 Kind,
125 ),
126 ) -> Self {
127 Self::new(exchange, (base, quote, instrument_kind), kind)
128 }
129}
130
131impl<InstrumentKey, Exchange, S, Kind>
132 From<(
133 InstrumentKey,
134 Exchange,
135 S,
136 S,
137 MarketDataInstrumentKind,
138 Kind,
139 )> for Subscription<Exchange, Keyed<InstrumentKey, MarketDataInstrument>, Kind>
140where
141 S: Into<AssetNameInternal>,
142{
143 fn from(
144 (instrument_id, exchange, base, quote, instrument_kind, kind): (
145 InstrumentKey,
146 Exchange,
147 S,
148 S,
149 MarketDataInstrumentKind,
150 Kind,
151 ),
152 ) -> Self {
153 let instrument = Keyed::new(instrument_id, (base, quote, instrument_kind).into());
154
155 Self::new(exchange, instrument, kind)
156 }
157}
158
159impl<Exchange, I, Instrument, Kind> From<(Exchange, I, Kind)>
160 for Subscription<Exchange, Instrument, Kind>
161where
162 I: Into<Instrument>,
163{
164 fn from((exchange, instrument, kind): (Exchange, I, Kind)) -> Self {
165 Self::new(exchange, instrument, kind)
166 }
167}
168
169impl<Instrument, Exchange, Kind> Subscription<Exchange, Instrument, Kind> {
170 pub fn new<I>(exchange: Exchange, instrument: I, kind: Kind) -> Self
172 where
173 I: Into<Instrument>,
174 {
175 Self {
176 exchange,
177 instrument: instrument.into(),
178 kind,
179 }
180 }
181}
182
183impl<Exchange, Instrument, Kind> Validator for Subscription<Exchange, Instrument, Kind>
184where
185 Exchange: Connector,
186 Instrument: InstrumentData,
187{
188 type Error = SocketError;
189
190 fn validate(self) -> Result<Self, Self::Error>
191 where
192 Self: Sized,
193 {
194 if exchange_supports_instrument_kind(Exchange::ID, self.instrument.kind()) {
196 Ok(self)
197 } else {
198 Err(SocketError::Unsupported {
199 entity: Exchange::ID.to_string(),
200 item: self.instrument.kind().to_string(),
201 })
202 }
203 }
204}
205
206#[allow(clippy::match_like_matches_macro)]
209pub fn exchange_supports_instrument_kind(
210 exchange: ExchangeId,
211 instrument_kind: &MarketDataInstrumentKind,
212) -> bool {
213 use rustrade_instrument::{
214 exchange::ExchangeId::*, instrument::market_data::kind::MarketDataInstrumentKind::*,
215 };
216
217 match (exchange, instrument_kind) {
218 (
220 BinanceFuturesUsd | Bitmex | BybitPerpetualsUsd | GateioPerpetualsUsd
221 | GateioPerpetualsBtc,
222 Spot,
223 ) => false,
224 (_, Spot) => true,
225
226 (GateioFuturesUsd | GateioFuturesBtc | Okx, Future { .. }) => true,
228 (_, Future { .. }) => false,
229
230 (
232 BinanceFuturesUsd | Bitmex | Okx | BybitPerpetualsUsd | GateioPerpetualsUsd
233 | GateioPerpetualsBtc | HyperliquidPerp,
234 Perpetual,
235 ) => true,
236 (_, Perpetual) => false,
237
238 (GateioOptions | Okx, Option { .. }) => true,
240 (_, Option { .. }) => false,
241 }
242}
243
244impl<Instrument> Validator for Subscription<ExchangeId, Instrument, SubKind>
245where
246 Instrument: InstrumentData,
247{
248 type Error = SocketError;
249
250 fn validate(self) -> Result<Self, Self::Error>
251 where
252 Self: Sized,
253 {
254 if exchange_supports_instrument_kind_sub_kind(
256 &self.exchange,
257 self.instrument.kind(),
258 self.kind,
259 ) {
260 Ok(self)
261 } else {
262 Err(SocketError::Unsupported {
263 entity: self.exchange.to_string(),
264 item: format!("({}, {})", self.instrument.kind(), self.kind),
265 })
266 }
267 }
268}
269
270pub fn exchange_supports_instrument_kind_sub_kind(
273 exchange_id: &ExchangeId,
274 instrument_kind: &MarketDataInstrumentKind,
275 sub_kind: SubKind,
276) -> bool {
277 use ExchangeId::*;
278 use MarketDataInstrumentKind::*;
279 use SubKind::*;
280
281 match (exchange_id, instrument_kind, sub_kind) {
282 (BinanceSpot, Spot, PublicTrades | OrderBooksL1 | OrderBooksL2 | Candles { .. }) => true,
283 (
284 BinanceFuturesUsd,
285 Perpetual,
286 PublicTrades | OrderBooksL1 | OrderBooksL2 | Liquidations | Candles { .. },
287 ) => true,
288 (Bitfinex, Spot, PublicTrades) => true,
289 (Bitmex, Perpetual, PublicTrades) => true,
290 (BybitSpot, Spot, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
291 (BybitPerpetualsUsd, Perpetual, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
292 (Coinbase, Spot, PublicTrades) => true,
293 (GateioSpot, Spot, PublicTrades) => true,
294 (GateioFuturesUsd, Future { .. }, PublicTrades) => true,
295 (GateioFuturesBtc, Future { .. }, PublicTrades) => true,
296 (GateioPerpetualsUsd, Perpetual, PublicTrades) => true,
297 (GateioPerpetualsBtc, Perpetual, PublicTrades) => true,
298 (GateioOptions, Option { .. }, PublicTrades) => true,
299 (Kraken, Spot, PublicTrades | OrderBooksL1) => true,
300 (Okx, Spot | Future { .. } | Perpetual | Option { .. }, PublicTrades) => true,
301 (HyperliquidPerp, Perpetual, PublicTrades | OrderBooksL2) => true,
302
303 (_, _, _) => false,
304 }
305}
306
307#[derive(Clone, Eq, PartialEq, Debug)]
310pub struct SubscriptionMeta<InstrumentKey> {
311 pub instrument_map: Map<InstrumentKey>,
314 pub ws_subscriptions: Vec<WsMessage>,
316}
317
318#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
323pub struct Map<T>(pub FnvHashMap<SubscriptionId, T>);
324
325impl<T> FromIterator<(SubscriptionId, T)> for Map<T> {
326 fn from_iter<Iter>(iter: Iter) -> Self
327 where
328 Iter: IntoIterator<Item = (SubscriptionId, T)>,
329 {
330 Self(iter.into_iter().collect::<FnvHashMap<SubscriptionId, T>>())
331 }
332}
333
334impl<T> Map<T> {
335 pub fn find<SubId>(&self, id: &SubId) -> Result<&T, SocketError>
337 where
338 SubscriptionId: Borrow<SubId>,
339 SubId: AsRef<str> + Hash + Eq + ?Sized,
340 {
341 self.0
342 .get(id)
343 .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
344 }
345
346 pub fn find_mut<SubId>(&mut self, id: &SubId) -> Result<&mut T, SocketError>
348 where
349 SubscriptionId: Borrow<SubId>,
350 SubId: AsRef<str> + Hash + Eq + ?Sized,
351 {
352 self.0
353 .get_mut(id)
354 .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
355 }
356}
357
358#[cfg(test)]
359#[allow(clippy::unwrap_used)] mod tests {
361 use super::*;
362
363 mod subscription {
364 use super::*;
365 use crate::{
366 exchange::{coinbase::Coinbase, okx::Okx},
367 subscription::trade::PublicTrades,
368 };
369 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
370
371 mod de {
372 use super::*;
373 use crate::{
374 exchange::{
375 binance::{futures::BinanceFuturesUsd, spot::BinanceSpot},
376 gateio::perpetual::GateioPerpetualsUsd,
377 okx::Okx,
378 },
379 subscription::{book::OrderBooksL2, trade::PublicTrades},
380 };
381 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
382
383 #[test]
384 fn test_subscription_okx_spot_public_trades() {
385 let input = r#"
386 {
387 "exchange": "okx",
388 "base": "btc",
389 "quote": "usdt",
390 "instrument_kind": "spot",
391 "kind": "public_trades"
392 }
393 "#;
394
395 serde_json::from_str::<Subscription<Okx, MarketDataInstrument, PublicTrades>>(
396 input,
397 )
398 .unwrap();
399 }
400
401 #[test]
402 fn test_subscription_binance_spot_public_trades() {
403 let input = r#"
404 {
405 "exchange": "binance_spot",
406 "base": "btc",
407 "quote": "usdt",
408 "instrument_kind": "spot",
409 "kind": "public_trades"
410 }
411 "#;
412
413 serde_json::from_str::<Subscription<BinanceSpot, MarketDataInstrument, PublicTrades>>(input)
414 .unwrap();
415 }
416
417 #[test]
418 fn test_subscription_binance_futures_usd_order_books_l2() {
419 let input = r#"
420 {
421 "exchange": "binance_futures_usd",
422 "base": "btc",
423 "quote": "usdt",
424 "instrument_kind": "perpetual",
425 "kind": "order_books_l2"
426 }
427 "#;
428
429 serde_json::from_str::<
430 Subscription<BinanceFuturesUsd, MarketDataInstrument, OrderBooksL2>,
431 >(input)
432 .unwrap();
433 }
434
435 #[test]
436 fn subscription_gateio_futures_usd_public_trades() {
437 let input = r#"
438 {
439 "exchange": "gateio_perpetuals_usd",
440 "base": "btc",
441 "quote": "usdt",
442 "instrument_kind": "perpetual",
443 "kind": "public_trades"
444 }
445 "#;
446
447 serde_json::from_str::<
448 Subscription<GateioPerpetualsUsd, MarketDataInstrument, PublicTrades>,
449 >(input)
450 .unwrap();
451 }
452 }
453
454 #[test]
455 fn test_validate_bitfinex_public_trades() {
456 struct TestCase {
457 input: Subscription<Coinbase, MarketDataInstrument, PublicTrades>,
458 expected:
459 Result<Subscription<Coinbase, MarketDataInstrument, PublicTrades>, SocketError>,
460 }
461
462 let tests = vec![
463 TestCase {
464 input: Subscription::from((
466 Coinbase,
467 "base",
468 "quote",
469 MarketDataInstrumentKind::Spot,
470 PublicTrades,
471 )),
472 expected: Ok(Subscription::from((
473 Coinbase,
474 "base",
475 "quote",
476 MarketDataInstrumentKind::Spot,
477 PublicTrades,
478 ))),
479 },
480 TestCase {
481 input: Subscription::from((
483 Coinbase,
484 "base",
485 "quote",
486 MarketDataInstrumentKind::Perpetual,
487 PublicTrades,
488 )),
489 expected: Err(SocketError::Unsupported {
490 entity: "".to_string(),
491 item: "".to_string(),
492 }),
493 },
494 ];
495
496 for (index, test) in tests.into_iter().enumerate() {
497 let actual = test.input.validate();
498 match (actual, test.expected) {
499 (Ok(actual), Ok(expected)) => {
500 assert_eq!(actual, expected, "TC{} failed", index)
501 }
502 (Err(_), Err(_)) => {
503 }
505 (actual, expected) => {
506 panic!(
508 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
509 );
510 }
511 }
512 }
513 }
514
515 #[test]
516 fn test_validate_okx_public_trades() {
517 struct TestCase {
518 input: Subscription<Okx, MarketDataInstrument, PublicTrades>,
519 expected:
520 Result<Subscription<Okx, MarketDataInstrument, PublicTrades>, SocketError>,
521 }
522
523 let tests = vec![
524 TestCase {
525 input: Subscription::from((
527 Okx,
528 "base",
529 "quote",
530 MarketDataInstrumentKind::Spot,
531 PublicTrades,
532 )),
533 expected: Ok(Subscription::from((
534 Okx,
535 "base",
536 "quote",
537 MarketDataInstrumentKind::Spot,
538 PublicTrades,
539 ))),
540 },
541 TestCase {
542 input: Subscription::from((
544 Okx,
545 "base",
546 "quote",
547 MarketDataInstrumentKind::Perpetual,
548 PublicTrades,
549 )),
550 expected: Ok(Subscription::from((
551 Okx,
552 "base",
553 "quote",
554 MarketDataInstrumentKind::Perpetual,
555 PublicTrades,
556 ))),
557 },
558 ];
559
560 for (index, test) in tests.into_iter().enumerate() {
561 let actual = test.input.validate();
562 match (actual, test.expected) {
563 (Ok(actual), Ok(expected)) => {
564 assert_eq!(actual, expected, "TC{} failed", index)
565 }
566 (Err(_), Err(_)) => {
567 }
569 (actual, expected) => {
570 panic!(
572 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
573 );
574 }
575 }
576 }
577 }
578 }
579
580 mod sub_kind {
581 use super::*;
582
583 #[test]
584 fn candles_variant_serde_round_trips() {
585 let kind = SubKind::Candles {
586 interval: CandleInterval::Min1,
587 };
588
589 let json = serde_json::to_string(&kind).unwrap();
590 let back = serde_json::from_str::<SubKind>(&json).unwrap();
591
592 assert_eq!(kind, back, "SubKind::Candles must serde round-trip");
593 }
594
595 #[test]
596 fn candles_variant_serde_shape_is_externally_tagged() {
597 let json = serde_json::to_string(&SubKind::Candles {
602 interval: CandleInterval::Min1,
603 })
604 .unwrap();
605 assert_eq!(json, r#"{"Candles":{"interval":"1m"}}"#);
606 }
607
608 #[test]
609 fn candles_display_tag_is_interval_independent() {
610 assert_eq!(
612 SubKind::Candles {
613 interval: CandleInterval::Sec1
614 }
615 .to_string(),
616 "candles"
617 );
618 assert_eq!(
619 SubKind::Candles {
620 interval: CandleInterval::Month1
621 }
622 .to_string(),
623 "candles"
624 );
625 }
626
627 #[test]
628 fn candles_supported_on_both_binance_venues_for_every_interval() {
629 for interval in CandleInterval::ALL {
632 assert!(
633 exchange_supports_instrument_kind_sub_kind(
634 &ExchangeId::BinanceSpot,
635 &MarketDataInstrumentKind::Spot,
636 SubKind::Candles { interval },
637 ),
638 "BinanceSpot/Spot must support Candles {interval:?}"
639 );
640 assert!(
641 exchange_supports_instrument_kind_sub_kind(
642 &ExchangeId::BinanceFuturesUsd,
643 &MarketDataInstrumentKind::Perpetual,
644 SubKind::Candles { interval },
645 ),
646 "BinanceFuturesUsd/Perpetual must support Candles {interval:?}"
647 );
648 }
649 }
650
651 #[test]
652 fn candles_rejected_for_unsupported_venue_kind_pairing() {
653 assert!(
655 !exchange_supports_instrument_kind_sub_kind(
656 &ExchangeId::BinanceSpot,
657 &MarketDataInstrumentKind::Perpetual,
658 SubKind::Candles {
659 interval: CandleInterval::Min1,
660 },
661 ),
662 "BinanceSpot does not serve Perpetual candles"
663 );
664 }
665 }
666
667 mod instrument_map {
668 use super::*;
669 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
670
671 #[test]
672 fn test_find_instrument() {
673 let ids = Map(FnvHashMap::from_iter([(
675 SubscriptionId::from("present"),
676 MarketDataInstrument::from(("base", "quote", MarketDataInstrumentKind::Spot)),
677 )]));
678
679 struct TestCase {
680 input: SubscriptionId,
681 expected: Result<MarketDataInstrument, SocketError>,
682 }
683
684 let cases = vec![
685 TestCase {
686 input: SubscriptionId::from("present"),
688 expected: Ok(MarketDataInstrument::from((
689 "base",
690 "quote",
691 MarketDataInstrumentKind::Spot,
692 ))),
693 },
694 TestCase {
695 input: SubscriptionId::from("not present"),
697 expected: Err(SocketError::Unidentifiable(SubscriptionId::from(
698 "not present",
699 ))),
700 },
701 ];
702
703 for (index, test) in cases.into_iter().enumerate() {
704 let actual = ids.find(&test.input);
705 match (actual, test.expected) {
706 (Ok(actual), Ok(expected)) => {
707 assert_eq!(*actual, expected, "TC{} failed", index)
708 }
709 (Err(_), Err(_)) => {
710 }
712 (actual, expected) => {
713 panic!(
715 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
716 );
717 }
718 }
719 }
720 }
721 }
722}