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