1use crate::error::{Result, ScopeError};
5use crate::market::configurable_client::ConfigurableExchangeClient;
6use crate::market::descriptor::VenueDescriptor;
7use crate::market::orderbook::{
8 MarketSnapshot, OrderBook, OrderBookClient, Ticker, TickerClient, Trade, TradeHistoryClient,
9};
10
11pub struct ExchangeClient {
17 venue_id: String,
18 venue_name: String,
19 descriptor: VenueDescriptor,
20 order_book: Option<Box<dyn OrderBookClient>>,
21 ticker: Option<Box<dyn TickerClient>>,
22 trade_history: Option<Box<dyn TradeHistoryClient>>,
23}
24
25impl ExchangeClient {
26 pub fn from_descriptor(desc: &VenueDescriptor) -> Self {
31 let client = ConfigurableExchangeClient::new(desc.clone());
32
33 let order_book: Option<Box<dyn OrderBookClient>> = if desc.has_order_book() {
34 Some(Box::new(client.clone()))
35 } else {
36 None
37 };
38 let ticker: Option<Box<dyn TickerClient>> = if desc.has_ticker() {
39 Some(Box::new(client.clone()))
40 } else {
41 None
42 };
43 let trade_history: Option<Box<dyn TradeHistoryClient>> = if desc.has_trades() {
44 Some(Box::new(client))
45 } else {
46 None
47 };
48
49 Self {
50 venue_id: desc.id.clone(),
51 venue_name: desc.name.clone(),
52 descriptor: desc.clone(),
53 order_book,
54 ticker,
55 trade_history,
56 }
57 }
58
59 pub fn venue_id(&self) -> &str {
65 &self.venue_id
66 }
67
68 pub fn venue_name(&self) -> &str {
70 &self.venue_name
71 }
72
73 pub fn format_pair(&self, base: &str) -> String {
75 self.descriptor.format_pair(base, None)
76 }
77
78 pub fn format_pair_with_quote(&self, base: &str, quote: &str) -> String {
80 self.descriptor.format_pair(base, Some(quote))
81 }
82
83 pub fn has_order_book(&self) -> bool {
89 self.order_book.is_some()
90 }
91
92 pub fn has_ticker(&self) -> bool {
94 self.ticker.is_some()
95 }
96
97 pub fn has_trade_history(&self) -> bool {
99 self.trade_history.is_some()
100 }
101
102 pub async fn fetch_order_book(&self, pair: &str) -> Result<OrderBook> {
108 self.order_book
109 .as_ref()
110 .ok_or_else(|| {
111 ScopeError::Chain(format!("{} does not support order book", self.venue_name))
112 })?
113 .fetch_order_book(pair)
114 .await
115 }
116
117 pub async fn fetch_ticker(&self, pair: &str) -> Result<Ticker> {
119 self.ticker
120 .as_ref()
121 .ok_or_else(|| {
122 ScopeError::Chain(format!("{} does not support ticker", self.venue_name))
123 })?
124 .fetch_ticker(pair)
125 .await
126 }
127
128 pub async fn fetch_recent_trades(&self, pair: &str, limit: u32) -> Result<Vec<Trade>> {
130 self.trade_history
131 .as_ref()
132 .ok_or_else(|| {
133 ScopeError::Chain(format!("{} does not support trades", self.venue_name))
134 })?
135 .fetch_recent_trades(pair, limit)
136 .await
137 }
138
139 pub async fn fetch_market_snapshot(&self, pair: &str) -> MarketSnapshot {
148 let order_book = if self.has_order_book() {
149 self.fetch_order_book(pair).await.ok()
150 } else {
151 None
152 };
153
154 let ticker = if self.has_ticker() {
155 self.fetch_ticker(pair).await.ok()
156 } else {
157 None
158 };
159
160 let recent_trades = if self.has_trade_history() {
161 self.fetch_recent_trades(pair, 50).await.ok()
162 } else {
163 None
164 };
165
166 MarketSnapshot {
167 order_book,
168 ticker,
169 recent_trades,
170 }
171 }
172}
173
174impl std::fmt::Debug for ExchangeClient {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("ExchangeClient")
177 .field("venue_id", &self.venue_id)
178 .field("venue_name", &self.venue_name)
179 .field("has_order_book", &self.has_order_book())
180 .field("has_ticker", &self.has_ticker())
181 .field("has_trade_history", &self.has_trade_history())
182 .finish()
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::market::registry::VenueRegistry;
190
191 #[test]
192 fn test_exchange_client_from_binance_descriptor() {
193 let registry = VenueRegistry::default();
194 let desc = registry.get("binance").unwrap();
195 let client = ExchangeClient::from_descriptor(desc);
196
197 assert_eq!(client.venue_id(), "binance");
198 assert_eq!(client.venue_name(), "Binance Spot");
199 assert!(client.has_order_book());
200 assert!(client.has_ticker());
201 assert!(client.has_trade_history());
202 }
203
204 #[test]
205 fn test_exchange_client_format_pair() {
206 let registry = VenueRegistry::default();
207 let desc = registry.get("binance").unwrap();
208 let client = ExchangeClient::from_descriptor(desc);
209 assert_eq!(client.format_pair("BTC"), "BTCUSDT");
210 assert_eq!(client.format_pair_with_quote("ETH", "USD"), "ETHUSD");
211 }
212
213 #[test]
214 fn test_exchange_client_all_venues() {
215 let registry = VenueRegistry::default();
216 for venue_id in registry.list() {
217 let desc = registry.get(venue_id).unwrap();
218 let client = ExchangeClient::from_descriptor(desc);
219 assert_eq!(client.venue_id(), venue_id);
220 assert!(
222 client.has_order_book(),
223 "Venue {} missing order_book capability",
224 venue_id
225 );
226 }
227 }
228
229 #[test]
230 fn test_exchange_client_debug() {
231 let registry = VenueRegistry::default();
232 let desc = registry.get("okx").unwrap();
233 let client = ExchangeClient::from_descriptor(desc);
234 let debug = format!("{:?}", client);
235 assert!(debug.contains("okx"));
236 assert!(debug.contains("has_order_book: true"));
237 }
238}