Skip to main content

mkt_core/
handle.rs

1use std::sync::Arc;
2
3use crate::{
4    Account, Capabilities, Capability, CapabilityUnavailableReason, Error, ExchangeInfo,
5    FuturesTrading, MarketData, PrivateStream, PublicStream, Result, SpotTrading,
6};
7
8#[derive(Clone)]
9pub struct ExchangeHandle {
10    info: Arc<dyn ExchangeInfo>,
11    market_data: Option<Arc<dyn MarketData>>,
12    spot_trading: Option<Arc<dyn SpotTrading>>,
13    futures_trading: Option<Arc<dyn FuturesTrading>>,
14    account: Option<Arc<dyn Account>>,
15    public_stream: Option<Arc<dyn PublicStream>>,
16    private_stream: Option<Arc<dyn PrivateStream>>,
17}
18
19impl ExchangeHandle {
20    pub fn builder(info: Arc<dyn ExchangeInfo>) -> Builder {
21        Builder::new(info)
22    }
23
24    pub fn info(&self) -> &dyn ExchangeInfo {
25        self.info.as_ref()
26    }
27
28    pub fn market_data(&self) -> Result<&dyn MarketData> {
29        self.market_data
30            .as_deref()
31            .ok_or_else(|| self.capability_error(Capability::MarketData))
32    }
33
34    pub fn spot_trading(&self) -> Result<&dyn SpotTrading> {
35        self.spot_trading
36            .as_deref()
37            .ok_or_else(|| self.capability_error(Capability::SpotTrading))
38    }
39
40    pub fn futures_trading(&self) -> Result<&dyn FuturesTrading> {
41        self.futures_trading
42            .as_deref()
43            .ok_or_else(|| self.capability_error(Capability::FuturesTrading))
44    }
45
46    pub fn account(&self) -> Result<&dyn Account> {
47        self.account
48            .as_deref()
49            .ok_or_else(|| self.capability_error(Capability::Account))
50    }
51
52    pub fn public_stream(&self) -> Result<&dyn PublicStream> {
53        self.public_stream
54            .as_deref()
55            .ok_or_else(|| self.capability_error(Capability::PublicStream))
56    }
57
58    pub fn private_stream(&self) -> Result<&dyn PrivateStream> {
59        self.private_stream
60            .as_deref()
61            .ok_or_else(|| self.capability_error(Capability::PrivateStream))
62    }
63
64    pub fn capabilities(&self) -> Capabilities {
65        let advertised = self.info.capabilities();
66
67        Capabilities::new(self.info.id())
68            .with_markets(advertised.markets)
69            .with_transport(advertised.transport)
70            .with_capabilities(
71                [
72                    self.market_data.is_some().then_some(Capability::MarketData),
73                    self.spot_trading
74                        .is_some()
75                        .then_some(Capability::SpotTrading),
76                    self.futures_trading
77                        .is_some()
78                        .then_some(Capability::FuturesTrading),
79                    self.account.is_some().then_some(Capability::Account),
80                    self.public_stream
81                        .is_some()
82                        .then_some(Capability::PublicStream),
83                    self.private_stream
84                        .is_some()
85                        .then_some(Capability::PrivateStream),
86                ]
87                .into_iter()
88                .flatten(),
89            )
90    }
91
92    fn capability_error(&self, capability: Capability) -> Error {
93        let reason = if self.info.capabilities().supports_capability(capability) {
94            CapabilityUnavailableReason::NotBound
95        } else {
96            CapabilityUnavailableReason::NotAdvertised
97        };
98
99        Error::capability_unavailable(self.info.id(), capability, reason)
100    }
101}
102
103#[derive(Clone)]
104pub struct Builder {
105    info: Arc<dyn ExchangeInfo>,
106    market_data: Option<Arc<dyn MarketData>>,
107    spot_trading: Option<Arc<dyn SpotTrading>>,
108    futures_trading: Option<Arc<dyn FuturesTrading>>,
109    account: Option<Arc<dyn Account>>,
110    public_stream: Option<Arc<dyn PublicStream>>,
111    private_stream: Option<Arc<dyn PrivateStream>>,
112}
113
114impl Builder {
115    pub fn new(info: Arc<dyn ExchangeInfo>) -> Self {
116        Self {
117            info,
118            market_data: None,
119            spot_trading: None,
120            futures_trading: None,
121            account: None,
122            public_stream: None,
123            private_stream: None,
124        }
125    }
126
127    pub fn market_data(mut self, market_data: Arc<dyn MarketData>) -> Self {
128        self.market_data = Some(market_data);
129        self
130    }
131
132    pub fn spot_trading(mut self, spot_trading: Arc<dyn SpotTrading>) -> Self {
133        self.spot_trading = Some(spot_trading);
134        self
135    }
136
137    pub fn futures_trading(mut self, futures_trading: Arc<dyn FuturesTrading>) -> Self {
138        self.futures_trading = Some(futures_trading);
139        self
140    }
141
142    pub fn account(mut self, account: Arc<dyn Account>) -> Self {
143        self.account = Some(account);
144        self
145    }
146
147    pub fn public_stream(mut self, public_stream: Arc<dyn PublicStream>) -> Self {
148        self.public_stream = Some(public_stream);
149        self
150    }
151
152    pub fn private_stream(mut self, private_stream: Arc<dyn PrivateStream>) -> Self {
153        self.private_stream = Some(private_stream);
154        self
155    }
156
157    pub fn build(self) -> ExchangeHandle {
158        ExchangeHandle {
159            info: self.info,
160            market_data: self.market_data,
161            spot_trading: self.spot_trading,
162            futures_trading: self.futures_trading,
163            account: self.account,
164            public_stream: self.public_stream,
165            private_stream: self.private_stream,
166        }
167    }
168}
169
170impl std::fmt::Debug for ExchangeHandle {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("ExchangeHandle")
173            .field("exchange_id", &self.info.id())
174            .field("has_market_data", &self.market_data.is_some())
175            .field("has_spot_trading", &self.spot_trading.is_some())
176            .field("has_futures_trading", &self.futures_trading.is_some())
177            .field("has_account", &self.account.is_some())
178            .field("has_public_stream", &self.public_stream.is_some())
179            .field("has_private_stream", &self.private_stream.is_some())
180            .finish()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use std::sync::Arc;
187
188    use mkt_types::{ExchangeId, KnownExchange};
189
190    use super::ExchangeHandle;
191    use crate::{Capabilities, Capability, CapabilityUnavailableReason, Error, ExchangeInfo};
192
193    struct StubExchange {
194        capabilities: Capabilities,
195    }
196
197    impl ExchangeInfo for StubExchange {
198        fn id(&self) -> ExchangeId {
199            self.capabilities.exchange_id.clone()
200        }
201
202        fn capabilities(&self) -> Capabilities {
203            self.capabilities.clone()
204        }
205    }
206
207    #[test]
208    fn missing_unadvertised_capability_reports_not_advertised() {
209        let exchange_id = ExchangeId::from(KnownExchange::Binance);
210        let handle = ExchangeHandle::builder(Arc::new(StubExchange {
211            capabilities: Capabilities::new(exchange_id.clone()),
212        }))
213        .build();
214
215        let err = handle
216            .market_data()
217            .err()
218            .expect("market data is not advertised");
219
220        assert!(matches!(
221            err,
222            Error::CapabilityUnavailable {
223                exchange,
224                capability: Capability::MarketData,
225                reason: CapabilityUnavailableReason::NotAdvertised,
226            } if exchange == exchange_id
227        ));
228    }
229
230    #[test]
231    fn advertised_but_unbound_capability_reports_binding_gap() {
232        let exchange_id = ExchangeId::from(KnownExchange::Binance);
233        let handle = ExchangeHandle::builder(Arc::new(StubExchange {
234            capabilities: Capabilities::new(exchange_id.clone())
235                .with_capabilities([Capability::MarketData]),
236        }))
237        .build();
238
239        let err = handle
240            .market_data()
241            .err()
242            .expect("market data was advertised but not bound");
243
244        assert!(matches!(
245            err,
246            Error::CapabilityUnavailable {
247                exchange,
248                capability: Capability::MarketData,
249                reason: CapabilityUnavailableReason::NotBound,
250            } if exchange == exchange_id
251        ));
252    }
253}