1use crate::error::{Result, ScopeError};
5use crate::market::configurable_client::ConfigurableExchangeClient;
6use crate::market::descriptor::VenueDescriptor;
7use crate::market::orderbook::{
8 Candle, MarketSnapshot, OhlcClient, OrderBook, OrderBookClient, Ticker, TickerClient, Trade,
9 TradeHistoryClient,
10};
11
12pub struct ExchangeClient {
18 venue_id: String,
19 venue_name: String,
20 descriptor: VenueDescriptor,
21 order_book: Option<Box<dyn OrderBookClient>>,
22 ticker: Option<Box<dyn TickerClient>>,
23 trade_history: Option<Box<dyn TradeHistoryClient>>,
24 ohlc: Option<Box<dyn OhlcClient>>,
25}
26
27impl ExchangeClient {
28 pub fn from_descriptor(desc: &VenueDescriptor) -> Self {
33 let client = ConfigurableExchangeClient::new(desc.clone());
34
35 let order_book: Option<Box<dyn OrderBookClient>> = if desc.has_order_book() {
36 Some(Box::new(client.clone()))
37 } else {
38 None
39 };
40 let ticker: Option<Box<dyn TickerClient>> = if desc.has_ticker() {
41 Some(Box::new(client.clone()))
42 } else {
43 None
44 };
45 let trade_history: Option<Box<dyn TradeHistoryClient>> = if desc.has_trades() {
46 Some(Box::new(client.clone()))
47 } else {
48 None
49 };
50 let ohlc: Option<Box<dyn OhlcClient>> = if desc.has_ohlc() {
51 Some(Box::new(client))
52 } else {
53 None
54 };
55
56 Self {
57 venue_id: desc.id.clone(),
58 venue_name: desc.name.clone(),
59 descriptor: desc.clone(),
60 order_book,
61 ticker,
62 trade_history,
63 ohlc,
64 }
65 }
66
67 pub fn venue_id(&self) -> &str {
73 &self.venue_id
74 }
75
76 pub fn venue_name(&self) -> &str {
78 &self.venue_name
79 }
80
81 pub fn format_pair(&self, base: &str) -> String {
83 self.descriptor.format_pair(base, None)
84 }
85
86 pub fn format_pair_with_quote(&self, base: &str, quote: &str) -> String {
88 self.descriptor.format_pair(base, Some(quote))
89 }
90
91 pub fn has_order_book(&self) -> bool {
97 self.order_book.is_some()
98 }
99
100 pub fn has_ticker(&self) -> bool {
102 self.ticker.is_some()
103 }
104
105 pub fn has_trade_history(&self) -> bool {
107 self.trade_history.is_some()
108 }
109
110 pub fn has_ohlc(&self) -> bool {
112 self.ohlc.is_some()
113 }
114
115 pub async fn fetch_order_book(&self, pair: &str) -> Result<OrderBook> {
121 self.order_book
122 .as_ref()
123 .ok_or_else(|| {
124 ScopeError::Chain(format!("{} does not support order book", self.venue_name))
125 })?
126 .fetch_order_book(pair)
127 .await
128 }
129
130 pub async fn fetch_ticker(&self, pair: &str) -> Result<Ticker> {
132 self.ticker
133 .as_ref()
134 .ok_or_else(|| {
135 ScopeError::Chain(format!("{} does not support ticker", self.venue_name))
136 })?
137 .fetch_ticker(pair)
138 .await
139 }
140
141 pub async fn fetch_recent_trades(&self, pair: &str, limit: u32) -> Result<Vec<Trade>> {
143 self.trade_history
144 .as_ref()
145 .ok_or_else(|| {
146 ScopeError::Chain(format!("{} does not support trades", self.venue_name))
147 })?
148 .fetch_recent_trades(pair, limit)
149 .await
150 }
151
152 pub async fn fetch_ohlc(&self, pair: &str, interval: &str, limit: u32) -> Result<Vec<Candle>> {
154 self.ohlc
155 .as_ref()
156 .ok_or_else(|| ScopeError::Chain(format!("{} does not support OHLC", self.venue_name)))?
157 .fetch_ohlc(pair, interval, limit)
158 .await
159 }
160
161 pub async fn fetch_market_snapshot(&self, pair: &str) -> MarketSnapshot {
170 let order_book = if self.has_order_book() {
171 self.fetch_order_book(pair).await.ok()
172 } else {
173 None
174 };
175
176 let ticker = if self.has_ticker() {
177 self.fetch_ticker(pair).await.ok()
178 } else {
179 None
180 };
181
182 let recent_trades = if self.has_trade_history() {
183 self.fetch_recent_trades(pair, 50).await.ok()
184 } else {
185 None
186 };
187
188 MarketSnapshot {
189 order_book,
190 ticker,
191 recent_trades,
192 }
193 }
194}
195
196impl std::fmt::Debug for ExchangeClient {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 f.debug_struct("ExchangeClient")
199 .field("venue_id", &self.venue_id)
200 .field("venue_name", &self.venue_name)
201 .field("has_order_book", &self.has_order_book())
202 .field("has_ticker", &self.has_ticker())
203 .field("has_trade_history", &self.has_trade_history())
204 .field("has_ohlc", &self.has_ohlc())
205 .finish()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::market::descriptor::{CapabilitySet, SymbolCase, SymbolConfig, VenueDescriptor};
213 use crate::market::registry::VenueRegistry;
214 use std::collections::HashMap;
215
216 fn make_empty_descriptor() -> VenueDescriptor {
217 VenueDescriptor {
218 id: "empty".to_string(),
219 name: "Empty".to_string(),
220 base_url: "https://example.com".to_string(),
221 timeout_secs: Some(5),
222 rate_limit_per_sec: None,
223 symbol: SymbolConfig {
224 template: "{base}{quote}".to_string(),
225 default_quote: "USDT".to_string(),
226 case: SymbolCase::Upper,
227 },
228 headers: HashMap::new(),
229 capabilities: CapabilitySet::default(),
230 }
231 }
232
233 #[test]
234 fn test_exchange_client_from_binance_descriptor() {
235 let registry = VenueRegistry::default();
236 let desc = registry.get("binance").unwrap();
237 let client = ExchangeClient::from_descriptor(desc);
238
239 assert_eq!(client.venue_id(), "binance");
240 assert_eq!(client.venue_name(), "Binance Spot");
241 assert!(client.has_order_book());
242 assert!(client.has_ticker());
243 assert!(client.has_trade_history());
244 }
245
246 #[test]
247 fn test_exchange_client_format_pair() {
248 let registry = VenueRegistry::default();
249 let desc = registry.get("binance").unwrap();
250 let client = ExchangeClient::from_descriptor(desc);
251 assert_eq!(client.format_pair("BTC"), "BTCUSDT");
252 assert_eq!(client.format_pair_with_quote("ETH", "USD"), "ETHUSD");
253 }
254
255 #[test]
256 fn test_exchange_client_all_venues() {
257 let registry = VenueRegistry::default();
258 for venue_id in registry.list() {
259 let desc = registry.get(venue_id).unwrap();
260 let client = ExchangeClient::from_descriptor(desc);
261 assert_eq!(client.venue_id(), venue_id);
262 assert!(
264 client.has_order_book(),
265 "Venue {} missing order_book capability",
266 venue_id
267 );
268 }
269 }
270
271 #[test]
272 fn test_exchange_client_debug() {
273 let registry = VenueRegistry::default();
274 let desc = registry.get("okx").unwrap();
275 let client = ExchangeClient::from_descriptor(desc);
276 let debug = format!("{:?}", client);
277 assert!(debug.contains("okx"));
278 assert!(debug.contains("has_order_book: true"));
279 }
280
281 #[test]
282 fn test_exchange_client_has_ohlc_for_binance() {
283 let registry = VenueRegistry::default();
284 let desc = registry.get("binance").unwrap();
285 let client = ExchangeClient::from_descriptor(desc);
286 assert!(client.has_ohlc());
287 }
288
289 #[test]
290 fn test_empty_descriptor_has_no_capabilities() {
291 let desc = make_empty_descriptor();
292 let client = ExchangeClient::from_descriptor(&desc);
293
294 assert!(!client.has_order_book());
295 assert!(!client.has_ticker());
296 assert!(!client.has_trade_history());
297 }
298
299 #[tokio::test]
300 async fn test_fetch_order_book_without_capability_returns_error() {
301 let desc = make_empty_descriptor();
302 let client = ExchangeClient::from_descriptor(&desc);
303
304 let err = client.fetch_order_book("BTCUSDT").await.unwrap_err();
305 let msg = err.to_string();
306 assert!(msg.contains("Empty"));
307 assert!(msg.contains("does not support order book"));
308 }
309
310 #[tokio::test]
311 async fn test_fetch_ticker_without_capability_returns_error() {
312 let desc = make_empty_descriptor();
313 let client = ExchangeClient::from_descriptor(&desc);
314
315 let err = client.fetch_ticker("BTCUSDT").await.unwrap_err();
316 let msg = err.to_string();
317 assert!(msg.contains("Empty"));
318 assert!(msg.contains("does not support ticker"));
319 }
320
321 #[tokio::test]
322 async fn test_fetch_ohlc_without_capability_returns_error() {
323 let desc = make_empty_descriptor();
324 let client = ExchangeClient::from_descriptor(&desc);
325
326 let err = client.fetch_ohlc("BTCUSDT", "1h", 100).await.unwrap_err();
327 let msg = err.to_string();
328 assert!(msg.contains("Empty"));
329 assert!(msg.contains("does not support OHLC"));
330 }
331
332 #[tokio::test]
333 async fn test_fetch_recent_trades_without_capability_returns_error() {
334 let desc = make_empty_descriptor();
335 let client = ExchangeClient::from_descriptor(&desc);
336
337 let err = client.fetch_recent_trades("BTCUSDT", 50).await.unwrap_err();
338 let msg = err.to_string();
339 assert!(msg.contains("Empty"));
340 assert!(msg.contains("does not support trades"));
341 }
342
343 #[tokio::test]
344 async fn test_fetch_market_snapshot_empty_descriptor_returns_all_none() {
345 let desc = make_empty_descriptor();
346 let client = ExchangeClient::from_descriptor(&desc);
347
348 let snapshot = client.fetch_market_snapshot("BTCUSDT").await;
349
350 assert!(snapshot.order_book.is_none());
351 assert!(snapshot.ticker.is_none());
352 assert!(snapshot.recent_trades.is_none());
353 }
354}