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)]
213pub fn exchange_supports_instrument_kind(
214 exchange: ExchangeId,
215 instrument_kind: &MarketDataInstrumentKind,
216) -> bool {
217 use rustrade_instrument::{
218 exchange::ExchangeId::*, instrument::market_data::kind::MarketDataInstrumentKind::*,
219 };
220
221 match (exchange, instrument_kind) {
222 (
224 BinanceFuturesUsd | Bitmex | BybitPerpetualsUsd | GateioPerpetualsUsd
225 | GateioPerpetualsBtc,
226 Spot,
227 ) => false,
228 (LseFx | LseCrypto | LseEquities, Spot) => true,
229 (LseCfd | LseFutures | LseOptions, Spot) => false,
230 (MassiveStocks | MassiveCrypto | MassiveForex, Spot) => true,
231 (MassiveOptions, Spot) => false,
232 (_, Spot) => true,
238
239 (GateioFuturesUsd | GateioFuturesBtc | Okx, Future { .. }) => true,
241 (_, Future { .. }) => false,
242
243 (
245 BinanceFuturesUsd | Bitmex | Okx | BybitPerpetualsUsd | GateioPerpetualsUsd
246 | GateioPerpetualsBtc | HyperliquidPerp,
247 Perpetual,
248 ) => true,
249 (_, Perpetual) => false,
250
251 (GateioOptions | Okx | LseOptions | MassiveOptions, Option { .. }) => true,
253 (_, Option { .. }) => false,
254
255 (LseCfd | LseFutures, Cfd) => true,
257 (_, Cfd) => false,
258 }
259}
260
261impl<Instrument> Validator for Subscription<ExchangeId, Instrument, SubKind>
262where
263 Instrument: InstrumentData,
264{
265 type Error = SocketError;
266
267 fn validate(self) -> Result<Self, Self::Error>
268 where
269 Self: Sized,
270 {
271 if exchange_supports_instrument_kind_sub_kind(
273 &self.exchange,
274 self.instrument.kind(),
275 self.kind,
276 ) {
277 Ok(self)
278 } else {
279 Err(SocketError::Unsupported {
280 entity: self.exchange.to_string(),
281 item: format!("({}, {})", self.instrument.kind(), self.kind),
282 })
283 }
284 }
285}
286
287pub fn exchange_supports_instrument_kind_sub_kind(
294 exchange_id: &ExchangeId,
295 instrument_kind: &MarketDataInstrumentKind,
296 sub_kind: SubKind,
297) -> bool {
298 use ExchangeId::*;
299 use MarketDataInstrumentKind::*;
300 use SubKind::*;
301
302 match (exchange_id, instrument_kind, sub_kind) {
303 (BinanceSpot, Spot, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
304 (BinanceSpot, Spot, Candles { interval }) => {
305 crate::exchange::binance::supports_candle_interval(interval)
306 }
307 (
308 BinanceFuturesUsd,
309 Perpetual,
310 PublicTrades | OrderBooksL1 | OrderBooksL2 | Liquidations,
311 ) => true,
312 (BinanceFuturesUsd, Perpetual, Candles { interval }) => {
313 crate::exchange::binance::supports_candle_interval(interval)
314 }
315 (Bitfinex, Spot, PublicTrades) => true,
316 (Bitmex, Perpetual, PublicTrades) => true,
317 (BybitSpot, Spot, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
318 (BybitPerpetualsUsd, Perpetual, PublicTrades | OrderBooksL1 | OrderBooksL2) => true,
319 (Coinbase, Spot, PublicTrades) => true,
320 (GateioSpot, Spot, PublicTrades) => true,
321 (GateioFuturesUsd, Future { .. }, PublicTrades) => true,
322 (GateioFuturesBtc, Future { .. }, PublicTrades) => true,
323 (GateioPerpetualsUsd, Perpetual, PublicTrades) => true,
324 (GateioPerpetualsBtc, Perpetual, PublicTrades) => true,
325 (GateioOptions, Option { .. }, PublicTrades) => true,
326 (Kraken, Spot, PublicTrades | OrderBooksL1) => true,
327 (Okx, Spot | Future { .. } | Perpetual | Option { .. }, PublicTrades) => true,
328 (HyperliquidPerp, Perpetual, PublicTrades | OrderBooksL2) => true,
329
330 (AlpacaCrypto | AlpacaIex | AlpacaSip, Spot, PublicTrades | OrderBooksL1) => true,
339
340 (LseFx | LseCrypto | LseEquities, Spot, PublicTrades | OrderBooksL1) => true,
362 (LseCfd | LseFutures, Cfd, PublicTrades | OrderBooksL1) => true,
363 (LseOptions, Option { .. }, PublicTrades) => true,
368 (MassiveStocks | MassiveCrypto, Spot, PublicTrades | OrderBooksL1) => true,
382 (MassiveForex, Spot, OrderBooksL1) => true,
383 (MassiveOptions, Option { .. }, PublicTrades | OrderBooksL1) => true,
384 (
385 MassiveStocks | MassiveCrypto | MassiveForex,
386 Spot,
387 Candles {
388 interval: CandleInterval::Sec1 | CandleInterval::Min1,
389 },
390 ) => true,
391 (
392 MassiveOptions,
393 Option { .. },
394 Candles {
395 interval: CandleInterval::Sec1 | CandleInterval::Min1,
396 },
397 ) => true,
398
399 (_, _, _) => false,
400 }
401}
402
403#[derive(Clone, Eq, PartialEq, Debug)]
406pub struct SubscriptionMeta<InstrumentKey> {
407 pub instrument_map: Map<InstrumentKey>,
410 pub ws_subscriptions: Vec<WsMessage>,
412}
413
414#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
419pub struct Map<T>(pub FnvHashMap<SubscriptionId, T>);
420
421impl<T> FromIterator<(SubscriptionId, T)> for Map<T> {
422 fn from_iter<Iter>(iter: Iter) -> Self
423 where
424 Iter: IntoIterator<Item = (SubscriptionId, T)>,
425 {
426 Self(iter.into_iter().collect::<FnvHashMap<SubscriptionId, T>>())
427 }
428}
429
430impl<T> Map<T> {
431 pub fn find<SubId>(&self, id: &SubId) -> Result<&T, SocketError>
433 where
434 SubscriptionId: Borrow<SubId>,
435 SubId: AsRef<str> + Hash + Eq + ?Sized,
436 {
437 self.0
438 .get(id)
439 .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
440 }
441
442 pub fn find_mut<SubId>(&mut self, id: &SubId) -> Result<&mut T, SocketError>
444 where
445 SubscriptionId: Borrow<SubId>,
446 SubId: AsRef<str> + Hash + Eq + ?Sized,
447 {
448 self.0
449 .get_mut(id)
450 .ok_or_else(|| SocketError::Unidentifiable(SubscriptionId(id.as_ref().to_smolstr())))
451 }
452}
453
454#[cfg(test)]
455#[allow(clippy::unwrap_used)] mod tests {
457 use super::*;
458
459 mod supports {
460 use super::*;
461 use chrono::{DateTime, Utc};
462 use rust_decimal::Decimal;
463 use rustrade_instrument::{
464 exchange::ExchangeId,
465 instrument::{
466 kind::option::{OptionExercise, OptionKind},
467 market_data::kind::MarketDataOptionContract,
468 },
469 };
470
471 const LSE: [(ExchangeId, MarketDataInstrumentKind); 5] = [
474 (ExchangeId::LseFx, MarketDataInstrumentKind::Spot),
475 (ExchangeId::LseCrypto, MarketDataInstrumentKind::Spot),
476 (ExchangeId::LseEquities, MarketDataInstrumentKind::Spot),
477 (ExchangeId::LseFutures, MarketDataInstrumentKind::Cfd),
478 (ExchangeId::LseCfd, MarketDataInstrumentKind::Cfd),
479 ];
480
481 #[test]
482 fn test_lse_supports_exactly_its_own_kind() {
483 for (exchange, supported) in LSE {
487 assert!(
488 exchange_supports_instrument_kind(exchange, &supported),
489 "{exchange:?} should support {supported:?}"
490 );
491
492 for denied in [
493 MarketDataInstrumentKind::Spot,
494 MarketDataInstrumentKind::Cfd,
495 MarketDataInstrumentKind::Perpetual,
496 ] {
497 if denied == supported {
498 continue;
499 }
500 assert!(
501 !exchange_supports_instrument_kind(exchange, &denied),
502 "{exchange:?} should not support {denied:?}"
503 );
504 }
505 }
506 }
507
508 fn option_kind() -> MarketDataInstrumentKind {
509 MarketDataInstrumentKind::Option(MarketDataOptionContract {
510 kind: OptionKind::Call,
511 exercise: OptionExercise::American,
512 expiry: DateTime::<Utc>::MAX_UTC,
513 strike: Decimal::ONE,
514 })
515 }
516
517 #[test]
518 fn test_lse_options_supports_only_the_option_kind() {
519 assert!(exchange_supports_instrument_kind(
520 ExchangeId::LseOptions,
521 &option_kind()
522 ));
523 for denied in [
524 MarketDataInstrumentKind::Spot,
525 MarketDataInstrumentKind::Cfd,
526 MarketDataInstrumentKind::Perpetual,
527 ] {
528 assert!(
529 !exchange_supports_instrument_kind(ExchangeId::LseOptions, &denied),
530 "LseOptions should not support {denied:?}"
531 );
532 }
533 }
534
535 #[test]
536 fn test_lse_options_serves_public_trades_only() {
537 assert!(exchange_supports_instrument_kind_sub_kind(
538 &ExchangeId::LseOptions,
539 &option_kind(),
540 SubKind::PublicTrades
541 ));
542
543 for sub_kind in [
545 SubKind::OrderBooksL1,
546 SubKind::OrderBooksL2,
547 SubKind::OrderBooksL3,
548 ] {
549 assert!(
550 !exchange_supports_instrument_kind_sub_kind(
551 &ExchangeId::LseOptions,
552 &option_kind(),
553 sub_kind
554 ),
555 "LseOptions should not serve {sub_kind}"
556 );
557 }
558
559 assert!(
560 !exchange_supports_instrument_kind_sub_kind(
561 &ExchangeId::LseOptions,
562 &MarketDataInstrumentKind::Spot,
563 SubKind::PublicTrades
564 ),
565 "LseOptions serves option contracts only"
566 );
567 }
568
569 #[test]
570 fn test_cfd_denied_for_non_cfd_exchanges() {
571 for exchange in [
572 ExchangeId::BinanceSpot,
573 ExchangeId::Coinbase,
574 ExchangeId::Okx,
575 ExchangeId::GateioOptions,
576 ExchangeId::Mock,
577 ] {
578 assert!(
579 !exchange_supports_instrument_kind(exchange, &MarketDataInstrumentKind::Cfd),
580 "{exchange:?} should not support Cfd"
581 );
582 }
583 }
584
585 #[test]
586 fn test_lse_supports_both_tick_derived_sub_kinds_for_its_own_kind_only() {
587 for (exchange, supported) in LSE {
591 for sub_kind in [SubKind::PublicTrades, SubKind::OrderBooksL1] {
592 assert!(
593 exchange_supports_instrument_kind_sub_kind(&exchange, &supported, sub_kind),
594 "{exchange:?} should support ({supported:?}, {sub_kind})"
595 );
596
597 for denied in [
598 MarketDataInstrumentKind::Spot,
599 MarketDataInstrumentKind::Cfd,
600 MarketDataInstrumentKind::Perpetual,
601 ] {
602 if denied == supported {
603 continue;
604 }
605 assert!(
606 !exchange_supports_instrument_kind_sub_kind(
607 &exchange, &denied, sub_kind
608 ),
609 "{exchange:?} should not support ({denied:?}, {sub_kind})"
610 );
611 }
612 }
613 }
614 }
615
616 #[test]
617 fn test_lse_serves_no_candle_stream_at_any_interval() {
618 for (exchange, kind) in LSE {
622 for interval in CandleInterval::ALL {
623 assert!(
624 !exchange_supports_instrument_kind_sub_kind(
625 &exchange,
626 &kind,
627 SubKind::Candles { interval },
628 ),
629 "{exchange:?} serves no candle stream, including {interval:?}"
630 );
631 }
632 }
633 }
634
635 #[test]
636 fn test_lse_denies_sub_kinds_its_tick_cannot_serve() {
637 for (exchange, kind) in LSE {
641 for sub_kind in [
642 SubKind::OrderBooksL2,
643 SubKind::OrderBooksL3,
644 SubKind::Liquidations,
645 SubKind::Quotes,
646 ] {
647 assert!(
648 !exchange_supports_instrument_kind_sub_kind(&exchange, &kind, sub_kind),
649 "{exchange:?} should not support ({kind:?}, {sub_kind})"
650 );
651 }
652 }
653 }
654 }
655
656 mod supports_massive {
657 use super::*;
658 use rustrade_instrument::instrument::{
659 kind::option::{OptionExercise, OptionKind},
660 market_data::kind::MarketDataOptionContract,
661 };
662
663 fn option_kind() -> MarketDataInstrumentKind {
664 MarketDataInstrumentKind::Option(MarketDataOptionContract {
665 kind: OptionKind::Call,
666 exercise: OptionExercise::American,
667 expiry: chrono::DateTime::<chrono::Utc>::MAX_UTC,
668 strike: rust_decimal::Decimal::ONE,
669 })
670 }
671
672 fn clusters() -> [(ExchangeId, MarketDataInstrumentKind); 4] {
674 [
675 (ExchangeId::MassiveStocks, MarketDataInstrumentKind::Spot),
676 (ExchangeId::MassiveCrypto, MarketDataInstrumentKind::Spot),
677 (ExchangeId::MassiveForex, MarketDataInstrumentKind::Spot),
678 (ExchangeId::MassiveOptions, option_kind()),
679 ]
680 }
681
682 fn other_kinds() -> [MarketDataInstrumentKind; 4] {
683 [
684 MarketDataInstrumentKind::Spot,
685 MarketDataInstrumentKind::Perpetual,
686 MarketDataInstrumentKind::Cfd,
687 option_kind(),
688 ]
689 }
690
691 #[test]
692 fn each_cluster_supports_exactly_its_own_instrument_kind() {
693 for (exchange, supported) in clusters() {
694 assert!(
695 exchange_supports_instrument_kind(exchange, &supported),
696 "{exchange:?} should support {supported:?}"
697 );
698 for denied in other_kinds().into_iter().filter(|kind| *kind != supported) {
699 assert!(
700 !exchange_supports_instrument_kind(exchange, &denied),
701 "{exchange:?} should not support {denied:?}"
702 );
703 assert!(
704 !exchange_supports_instrument_kind_sub_kind(
705 &exchange,
706 &denied,
707 SubKind::OrderBooksL1
708 ),
709 "{exchange:?} should not serve {denied:?}"
710 );
711 }
712 }
713 }
714
715 #[test]
716 fn every_cluster_but_forex_serves_trades_and_every_cluster_serves_top_of_book() {
717 for (exchange, kind) in clusters() {
718 assert_eq!(
719 exchange_supports_instrument_kind_sub_kind(
720 &exchange,
721 &kind,
722 SubKind::PublicTrades
723 ),
724 exchange != ExchangeId::MassiveForex,
725 "{exchange:?} trades"
726 );
727 assert!(
728 exchange_supports_instrument_kind_sub_kind(
729 &exchange,
730 &kind,
731 SubKind::OrderBooksL1
732 ),
733 "{exchange:?} top of book"
734 );
735 }
736 }
737
738 #[test]
739 fn candles_are_served_at_one_second_and_one_minute_only() {
740 for (exchange, kind) in clusters() {
741 for interval in CandleInterval::ALL {
742 assert_eq!(
743 exchange_supports_instrument_kind_sub_kind(
744 &exchange,
745 &kind,
746 SubKind::Candles { interval }
747 ),
748 matches!(interval, CandleInterval::Sec1 | CandleInterval::Min1),
749 "{exchange:?} candles at {interval:?}"
750 );
751 }
752 }
753 }
754
755 #[test]
756 fn quotes_depth_and_liquidations_are_refused() {
757 for (exchange, kind) in clusters() {
758 for sub_kind in [
759 SubKind::Quotes,
760 SubKind::OrderBooksL2,
761 SubKind::OrderBooksL3,
762 SubKind::Liquidations,
763 ] {
764 assert!(
765 !exchange_supports_instrument_kind_sub_kind(&exchange, &kind, sub_kind),
766 "{exchange:?} should not serve {sub_kind}"
767 );
768 }
769 }
770 }
771
772 #[test]
773 fn the_rest_client_id_streams_nothing() {
774 for kind in other_kinds() {
775 for sub_kind in [SubKind::PublicTrades, SubKind::OrderBooksL1] {
776 assert!(!exchange_supports_instrument_kind_sub_kind(
777 &ExchangeId::Massive,
778 &kind,
779 sub_kind
780 ));
781 }
782 }
783 }
784 }
785
786 mod subscription {
787 use super::*;
788 use crate::{
789 exchange::{coinbase::Coinbase, okx::Okx},
790 subscription::trade::PublicTrades,
791 };
792 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
793
794 mod de {
795 use super::*;
796 use crate::{
797 exchange::{
798 binance::{futures::BinanceFuturesUsd, spot::BinanceSpot},
799 gateio::perpetual::GateioPerpetualsUsd,
800 okx::Okx,
801 },
802 subscription::{book::OrderBooksL2, trade::PublicTrades},
803 };
804 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
805
806 #[test]
807 fn test_subscription_okx_spot_public_trades() {
808 let input = r#"
809 {
810 "exchange": "okx",
811 "base": "btc",
812 "quote": "usdt",
813 "instrument_kind": "spot",
814 "kind": "public_trades"
815 }
816 "#;
817
818 serde_json::from_str::<Subscription<Okx, MarketDataInstrument, PublicTrades>>(
819 input,
820 )
821 .unwrap();
822 }
823
824 #[test]
825 fn test_subscription_binance_spot_public_trades() {
826 let input = r#"
827 {
828 "exchange": "binance_spot",
829 "base": "btc",
830 "quote": "usdt",
831 "instrument_kind": "spot",
832 "kind": "public_trades"
833 }
834 "#;
835
836 serde_json::from_str::<Subscription<BinanceSpot, MarketDataInstrument, PublicTrades>>(input)
837 .unwrap();
838 }
839
840 #[test]
841 fn test_subscription_binance_futures_usd_order_books_l2() {
842 let input = r#"
843 {
844 "exchange": "binance_futures_usd",
845 "base": "btc",
846 "quote": "usdt",
847 "instrument_kind": "perpetual",
848 "kind": "order_books_l2"
849 }
850 "#;
851
852 serde_json::from_str::<
853 Subscription<BinanceFuturesUsd, MarketDataInstrument, OrderBooksL2>,
854 >(input)
855 .unwrap();
856 }
857
858 #[test]
859 fn subscription_gateio_futures_usd_public_trades() {
860 let input = r#"
861 {
862 "exchange": "gateio_perpetuals_usd",
863 "base": "btc",
864 "quote": "usdt",
865 "instrument_kind": "perpetual",
866 "kind": "public_trades"
867 }
868 "#;
869
870 serde_json::from_str::<
871 Subscription<GateioPerpetualsUsd, MarketDataInstrument, PublicTrades>,
872 >(input)
873 .unwrap();
874 }
875 }
876
877 #[test]
878 fn test_validate_bitfinex_public_trades() {
879 struct TestCase {
880 input: Subscription<Coinbase, MarketDataInstrument, PublicTrades>,
881 expected:
882 Result<Subscription<Coinbase, MarketDataInstrument, PublicTrades>, SocketError>,
883 }
884
885 let tests = vec![
886 TestCase {
887 input: Subscription::from((
889 Coinbase,
890 "base",
891 "quote",
892 MarketDataInstrumentKind::Spot,
893 PublicTrades,
894 )),
895 expected: Ok(Subscription::from((
896 Coinbase,
897 "base",
898 "quote",
899 MarketDataInstrumentKind::Spot,
900 PublicTrades,
901 ))),
902 },
903 TestCase {
904 input: Subscription::from((
906 Coinbase,
907 "base",
908 "quote",
909 MarketDataInstrumentKind::Perpetual,
910 PublicTrades,
911 )),
912 expected: Err(SocketError::Unsupported {
913 entity: "".to_string(),
914 item: "".to_string(),
915 }),
916 },
917 ];
918
919 for (index, test) in tests.into_iter().enumerate() {
920 let actual = test.input.validate();
921 match (actual, test.expected) {
922 (Ok(actual), Ok(expected)) => {
923 assert_eq!(actual, expected, "TC{} failed", index)
924 }
925 (Err(_), Err(_)) => {
926 }
928 (actual, expected) => {
929 panic!(
931 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
932 );
933 }
934 }
935 }
936 }
937
938 #[test]
939 fn test_validate_okx_public_trades() {
940 struct TestCase {
941 input: Subscription<Okx, MarketDataInstrument, PublicTrades>,
942 expected:
943 Result<Subscription<Okx, MarketDataInstrument, PublicTrades>, SocketError>,
944 }
945
946 let tests = vec![
947 TestCase {
948 input: Subscription::from((
950 Okx,
951 "base",
952 "quote",
953 MarketDataInstrumentKind::Spot,
954 PublicTrades,
955 )),
956 expected: Ok(Subscription::from((
957 Okx,
958 "base",
959 "quote",
960 MarketDataInstrumentKind::Spot,
961 PublicTrades,
962 ))),
963 },
964 TestCase {
965 input: Subscription::from((
967 Okx,
968 "base",
969 "quote",
970 MarketDataInstrumentKind::Perpetual,
971 PublicTrades,
972 )),
973 expected: Ok(Subscription::from((
974 Okx,
975 "base",
976 "quote",
977 MarketDataInstrumentKind::Perpetual,
978 PublicTrades,
979 ))),
980 },
981 ];
982
983 for (index, test) in tests.into_iter().enumerate() {
984 let actual = test.input.validate();
985 match (actual, test.expected) {
986 (Ok(actual), Ok(expected)) => {
987 assert_eq!(actual, expected, "TC{} failed", index)
988 }
989 (Err(_), Err(_)) => {
990 }
992 (actual, expected) => {
993 panic!(
995 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
996 );
997 }
998 }
999 }
1000 }
1001 }
1002
1003 mod sub_kind {
1004 use super::*;
1005
1006 #[test]
1007 fn candles_variant_serde_round_trips() {
1008 let kind = SubKind::Candles {
1009 interval: CandleInterval::Min1,
1010 };
1011
1012 let json = serde_json::to_string(&kind).unwrap();
1013 let back = serde_json::from_str::<SubKind>(&json).unwrap();
1014
1015 assert_eq!(kind, back, "SubKind::Candles must serde round-trip");
1016 }
1017
1018 #[test]
1019 fn candles_variant_serde_shape_is_externally_tagged() {
1020 let json = serde_json::to_string(&SubKind::Candles {
1025 interval: CandleInterval::Min1,
1026 })
1027 .unwrap();
1028 assert_eq!(json, r#"{"Candles":{"interval":"1m"}}"#);
1029 }
1030
1031 #[test]
1032 fn candles_display_tag_is_interval_independent() {
1033 assert_eq!(
1035 SubKind::Candles {
1036 interval: CandleInterval::Sec1
1037 }
1038 .to_string(),
1039 "candles"
1040 );
1041 assert_eq!(
1042 SubKind::Candles {
1043 interval: CandleInterval::Month1
1044 }
1045 .to_string(),
1046 "candles"
1047 );
1048 }
1049
1050 #[test]
1051 fn candles_supported_per_interval_on_both_binance_venues() {
1052 for interval in CandleInterval::ALL {
1056 let expected = crate::exchange::binance::supports_candle_interval(interval);
1057 assert_eq!(
1058 exchange_supports_instrument_kind_sub_kind(
1059 &ExchangeId::BinanceSpot,
1060 &MarketDataInstrumentKind::Spot,
1061 SubKind::Candles { interval },
1062 ),
1063 expected,
1064 "BinanceSpot/Spot Candles {interval:?}"
1065 );
1066 assert_eq!(
1067 exchange_supports_instrument_kind_sub_kind(
1068 &ExchangeId::BinanceFuturesUsd,
1069 &MarketDataInstrumentKind::Perpetual,
1070 SubKind::Candles { interval },
1071 ),
1072 expected,
1073 "BinanceFuturesUsd/Perpetual Candles {interval:?}"
1074 );
1075 }
1076 }
1077
1078 #[test]
1079 fn candles_rejected_for_unsupported_venue_kind_pairing() {
1080 assert!(
1082 !exchange_supports_instrument_kind_sub_kind(
1083 &ExchangeId::BinanceSpot,
1084 &MarketDataInstrumentKind::Perpetual,
1085 SubKind::Candles {
1086 interval: CandleInterval::Min1,
1087 },
1088 ),
1089 "BinanceSpot does not serve Perpetual candles"
1090 );
1091 }
1092 }
1093
1094 mod instrument_map {
1095 use super::*;
1096 use rustrade_instrument::instrument::market_data::MarketDataInstrument;
1097
1098 #[test]
1099 fn test_find_instrument() {
1100 let ids = Map(FnvHashMap::from_iter([(
1102 SubscriptionId::from("present"),
1103 MarketDataInstrument::from(("base", "quote", MarketDataInstrumentKind::Spot)),
1104 )]));
1105
1106 struct TestCase {
1107 input: SubscriptionId,
1108 expected: Result<MarketDataInstrument, SocketError>,
1109 }
1110
1111 let cases = vec![
1112 TestCase {
1113 input: SubscriptionId::from("present"),
1115 expected: Ok(MarketDataInstrument::from((
1116 "base",
1117 "quote",
1118 MarketDataInstrumentKind::Spot,
1119 ))),
1120 },
1121 TestCase {
1122 input: SubscriptionId::from("not present"),
1124 expected: Err(SocketError::Unidentifiable(SubscriptionId::from(
1125 "not present",
1126 ))),
1127 },
1128 ];
1129
1130 for (index, test) in cases.into_iter().enumerate() {
1131 let actual = ids.find(&test.input);
1132 match (actual, test.expected) {
1133 (Ok(actual), Ok(expected)) => {
1134 assert_eq!(*actual, expected, "TC{} failed", index)
1135 }
1136 (Err(_), Err(_)) => {
1137 }
1139 (actual, expected) => {
1140 panic!(
1142 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
1143 );
1144 }
1145 }
1146 }
1147 }
1148 }
1149}