1use crate::error::{Error, ParseError, Result};
11use crate::http_client::{HttpClient, HttpConfig};
12use crate::rate_limiter::{RateLimiter, RateLimiterConfig};
13use crate::types::*;
14use rust_decimal::Decimal;
15use rust_decimal::prelude::{FromStr, ToPrimitive};
16use serde_json::Value;
17use std::collections::HashMap;
18use std::future::Future;
19use std::sync::Arc;
20use tokio::sync::{Mutex, RwLock};
21use tracing::{debug, info};
22
23#[derive(Debug, Clone)]
25pub struct ExchangeConfig {
26 pub id: String,
28 pub name: String,
30 pub api_key: Option<String>,
32 pub secret: Option<String>,
34 pub password: Option<String>,
36 pub uid: Option<String>,
38 pub account_id: Option<String>,
40 pub enable_rate_limit: bool,
42 pub rate_limit: u32,
44 pub timeout: u64,
46 pub sandbox: bool,
48 pub user_agent: Option<String>,
50 pub proxy: Option<String>,
52 pub verbose: bool,
54 pub options: HashMap<String, Value>,
56 pub url_overrides: HashMap<String, String>,
58}
59
60impl Default for ExchangeConfig {
61 fn default() -> Self {
62 Self {
63 id: String::new(),
64 name: String::new(),
65 api_key: None,
66 secret: None,
67 password: None,
68 uid: None,
69 account_id: None,
70 enable_rate_limit: true,
71 rate_limit: 10,
72 timeout: 30,
73 sandbox: false,
74 user_agent: Some(format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
75 proxy: None,
76 verbose: false,
77 options: HashMap::new(),
78 url_overrides: HashMap::new(),
79 }
80 }
81}
82
83impl ExchangeConfig {
84 pub fn builder() -> ExchangeConfigBuilder {
100 ExchangeConfigBuilder::default()
101 }
102}
103
104#[derive(Debug, Clone, Default)]
122pub struct ExchangeConfigBuilder {
123 config: ExchangeConfig,
124}
125
126impl ExchangeConfigBuilder {
127 pub fn new() -> Self {
129 Self::default()
130 }
131
132 pub fn id(mut self, id: impl Into<String>) -> Self {
134 self.config.id = id.into();
135 self
136 }
137
138 pub fn name(mut self, name: impl Into<String>) -> Self {
140 self.config.name = name.into();
141 self
142 }
143
144 pub fn api_key(mut self, key: impl Into<String>) -> Self {
146 self.config.api_key = Some(key.into());
147 self
148 }
149
150 pub fn secret(mut self, secret: impl Into<String>) -> Self {
152 self.config.secret = Some(secret.into());
153 self
154 }
155
156 pub fn password(mut self, password: impl Into<String>) -> Self {
158 self.config.password = Some(password.into());
159 self
160 }
161
162 pub fn uid(mut self, uid: impl Into<String>) -> Self {
164 self.config.uid = Some(uid.into());
165 self
166 }
167
168 pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
170 self.config.account_id = Some(account_id.into());
171 self
172 }
173
174 pub fn enable_rate_limit(mut self, enabled: bool) -> Self {
176 self.config.enable_rate_limit = enabled;
177 self
178 }
179
180 pub fn rate_limit(mut self, rate_limit: u32) -> Self {
182 self.config.rate_limit = rate_limit;
183 self
184 }
185
186 pub fn timeout(mut self, seconds: u64) -> Self {
188 self.config.timeout = seconds;
189 self
190 }
191
192 pub fn sandbox(mut self, enabled: bool) -> Self {
194 self.config.sandbox = enabled;
195 self
196 }
197
198 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
200 self.config.user_agent = Some(user_agent.into());
201 self
202 }
203
204 pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
206 self.config.proxy = Some(proxy.into());
207 self
208 }
209
210 pub fn verbose(mut self, enabled: bool) -> Self {
212 self.config.verbose = enabled;
213 self
214 }
215
216 pub fn option(mut self, key: impl Into<String>, value: Value) -> Self {
218 self.config.options.insert(key.into(), value);
219 self
220 }
221
222 pub fn options(mut self, options: HashMap<String, Value>) -> Self {
224 self.config.options.extend(options);
225 self
226 }
227
228 pub fn url_override(mut self, key: impl Into<String>, url: impl Into<String>) -> Self {
230 self.config.url_overrides.insert(key.into(), url.into());
231 self
232 }
233
234 pub fn build(self) -> ExchangeConfig {
236 self.config
237 }
238}
239
240#[derive(Debug, Clone)]
242pub struct MarketCache {
243 pub markets: HashMap<String, Arc<Market>>,
245 pub markets_by_id: HashMap<String, Arc<Market>>,
247 pub currencies: HashMap<String, Arc<Currency>>,
249 pub currencies_by_id: HashMap<String, Arc<Currency>>,
251 pub symbols: Vec<String>,
253 pub codes: Vec<String>,
255 pub ids: Vec<String>,
257 pub loaded: bool,
259}
260
261impl Default for MarketCache {
262 fn default() -> Self {
263 Self {
264 markets: HashMap::new(),
265 markets_by_id: HashMap::new(),
266 currencies: HashMap::new(),
267 currencies_by_id: HashMap::new(),
268 symbols: Vec::new(),
269 codes: Vec::new(),
270 ids: Vec::new(),
271 loaded: false,
272 }
273 }
274}
275
276use crate::exchange::ExchangeCapabilities;
279
280#[derive(Debug)]
282pub struct BaseExchange {
283 pub config: ExchangeConfig,
285 pub http_client: HttpClient,
287 pub market_cache: Arc<RwLock<MarketCache>>,
289 pub market_loading_lock: Arc<Mutex<()>>,
291 pub capabilities: ExchangeCapabilities,
293 pub urls: HashMap<String, String>,
295 pub timeframes: HashMap<String, String>,
297 pub precision_mode: PrecisionMode,
299}
300
301impl BaseExchange {
302 pub fn new(config: ExchangeConfig) -> Result<Self> {
316 info!("Initializing exchange: {}", config.id);
317
318 let http_config = HttpConfig {
319 timeout: config.timeout,
320 #[allow(deprecated)]
321 max_retries: 3,
322 verbose: false,
323 user_agent: config
324 .user_agent
325 .clone()
326 .unwrap_or_else(|| format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
327 return_response_headers: false,
328 proxy: config.proxy.clone(),
329 enable_rate_limit: true,
330 retry_config: None,
331 };
332
333 let mut http_client = HttpClient::new(http_config)?;
334
335 if config.enable_rate_limit {
337 let rate_config =
338 RateLimiterConfig::new(config.rate_limit, std::time::Duration::from_millis(1000));
339 let limiter = RateLimiter::new(rate_config);
340 http_client.set_rate_limiter(limiter);
341 }
342
343 Ok(Self {
344 config,
345 http_client,
346 market_cache: Arc::new(RwLock::new(MarketCache::default())),
347 market_loading_lock: Arc::new(Mutex::new(())),
348 capabilities: ExchangeCapabilities::default(),
349 urls: HashMap::new(),
350 timeframes: HashMap::new(),
351 precision_mode: PrecisionMode::DecimalPlaces,
352 })
353 }
354
355 pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
370 let cache = self.market_cache.write().await;
371
372 if cache.loaded && !reload {
373 debug!("Returning cached markets for {}", self.config.id);
374 return Ok(cache
375 .markets
376 .iter()
377 .map(|(k, v)| (k.clone(), (**v).clone()))
378 .collect());
379 }
380
381 info!("Loading markets for {}", self.config.id);
382
383 drop(cache);
384
385 Err(Error::not_implemented(
386 "load_markets must be implemented by exchange",
387 ))
388 }
389
390 pub async fn load_markets_with_loader<F, Fut>(
419 &self,
420 reload: bool,
421 loader: F,
422 ) -> Result<HashMap<String, Arc<Market>>>
423 where
424 F: FnOnce() -> Fut,
425 Fut: Future<Output = Result<(Vec<Market>, Option<Vec<Currency>>)>>,
426 {
427 let _loading_guard = self.market_loading_lock.lock().await;
429
430 {
432 let cache = self.market_cache.read().await;
433 if cache.loaded && !reload {
434 debug!(
435 "Returning cached markets for {} ({} markets)",
436 self.config.id,
437 cache.markets.len()
438 );
439 return Ok(cache.markets.clone());
440 }
441 }
442
443 info!(
445 "Loading markets for {} (reload: {})",
446 self.config.id, reload
447 );
448 let (markets, currencies) = loader().await?;
449
450 self.set_markets(markets, currencies).await?;
452
453 let cache = self.market_cache.read().await;
455 Ok(cache.markets.clone())
456 }
457
458 pub async fn set_markets(
473 &self,
474 markets: Vec<Market>,
475 currencies: Option<Vec<Currency>>,
476 ) -> Result<HashMap<String, Arc<Market>>> {
477 let mut cache = self.market_cache.write().await;
478
479 cache.markets.clear();
480 cache.markets_by_id.clear();
481 cache.symbols.clear();
482 cache.ids.clear();
483
484 for market in markets {
485 cache.symbols.push(market.symbol.clone());
486 cache.ids.push(market.id.clone());
487 let arc_market = Arc::new(market);
488 cache
489 .markets_by_id
490 .insert(arc_market.id.clone(), Arc::clone(&arc_market));
491 cache.markets.insert(arc_market.symbol.clone(), arc_market);
492 }
493
494 if let Some(currencies) = currencies {
495 cache.currencies.clear();
496 cache.currencies_by_id.clear();
497 cache.codes.clear();
498
499 for currency in currencies {
500 cache.codes.push(currency.code.clone());
501 let arc_currency = Arc::new(currency);
502 cache
503 .currencies_by_id
504 .insert(arc_currency.id.clone(), Arc::clone(&arc_currency));
505 cache
506 .currencies
507 .insert(arc_currency.code.clone(), arc_currency);
508 }
509 }
510
511 cache.loaded = true;
512 info!(
513 "Loaded {} markets and {} currencies for {}",
514 cache.markets.len(),
515 cache.currencies.len(),
516 self.config.id
517 );
518
519 Ok(cache.markets.clone())
520 }
521
522 pub async fn market(&self, symbol: &str) -> Result<Arc<Market>> {
536 let cache = self.market_cache.read().await;
537
538 if !cache.loaded {
539 drop(cache);
540 return Err(Error::exchange(
541 "-1",
542 "Markets not loaded. Call load_markets() first.",
543 ));
544 }
545
546 cache
547 .markets
548 .get(symbol)
549 .cloned()
550 .ok_or_else(|| Error::bad_symbol(format!("Market {} not found", symbol)))
551 }
552
553 pub async fn market_by_id(&self, id: &str) -> Result<Arc<Market>> {
567 let cache = self.market_cache.read().await;
568
569 cache
570 .markets_by_id
571 .get(id)
572 .cloned()
573 .ok_or_else(|| Error::bad_symbol(format!("Market with id {} not found", id)))
574 }
575
576 pub async fn currency(&self, code: &str) -> Result<Arc<Currency>> {
590 let cache = self.market_cache.read().await;
591
592 cache
593 .currencies
594 .get(code)
595 .cloned()
596 .ok_or_else(|| Error::bad_symbol(format!("Currency {} not found", code)))
597 }
598
599 pub async fn symbols(&self) -> Result<Vec<String>> {
605 let cache = self.market_cache.read().await;
606 Ok(cache.symbols.clone())
607 }
608
609 #[deprecated(
616 since = "0.2.0",
617 note = "Rate limiting is now handled internally by HttpClient. This method is a no-op."
618 )]
619 pub fn throttle(&self) -> Result<()> {
620 Ok(())
622 }
623
624 pub fn check_required_credentials(&self) -> Result<()> {
634 if self.config.api_key.is_none() {
635 return Err(Error::authentication("API key is required"));
636 }
637 if self.config.secret.is_none() {
638 return Err(Error::authentication("API secret is required"));
639 }
640 Ok(())
641 }
642
643 pub fn nonce(&self) -> i64 {
649 crate::time::milliseconds()
650 }
651
652 pub fn build_query_string(&self, params: &HashMap<String, Value>) -> String {
662 if params.is_empty() {
663 return String::new();
664 }
665
666 let pairs: Vec<String> = params
667 .iter()
668 .map(|(k, v)| {
669 let value_str = match v {
670 Value::String(s) => s.clone(),
671 Value::Number(n) => n.to_string(),
672 Value::Bool(b) => b.to_string(),
673 _ => v.to_string(),
674 };
675 format!("{}={}", k, urlencoding::encode(&value_str))
676 })
677 .collect();
678
679 pairs.join("&")
680 }
681
682 pub fn parse_json(&self, response: &str) -> Result<Value> {
696 serde_json::from_str(response).map_err(|e| Error::invalid_request(e.to_string()))
697 }
698
699 pub fn handle_http_error(&self, status_code: u16, response: &str) -> Error {
710 match status_code {
711 400 => Error::invalid_request(response.to_string()),
712 401 | 403 => Error::authentication(response.to_string()),
713 404 => Error::invalid_request(format!("Endpoint not found: {}", response)),
714 429 => Error::rate_limit(response.to_string(), None),
715 500..=599 => Error::exchange(status_code.to_string(), response),
716 _ => Error::network(format!("HTTP {}: {}", status_code, response)),
717 }
718 }
719
720 pub fn safe_string(&self, dict: &Value, key: &str) -> Option<String> {
731 dict.get(key)
732 .and_then(|v| v.as_str())
733 .map(|s| s.to_string())
734 }
735
736 pub fn safe_integer(&self, dict: &Value, key: &str) -> Option<i64> {
747 dict.get(key).and_then(|v| v.as_i64())
748 }
749
750 pub fn safe_float(&self, dict: &Value, key: &str) -> Option<f64> {
761 dict.get(key).and_then(|v| v.as_f64())
762 }
763
764 pub fn safe_bool(&self, dict: &Value, key: &str) -> Option<bool> {
775 dict.get(key).and_then(|v| v.as_bool())
776 }
777
778 pub fn parse_ticker(&self, ticker_data: &Value, market: Option<&Market>) -> Result<Ticker> {
797 let symbol = if let Some(m) = market {
798 m.symbol.clone()
799 } else {
800 self.safe_string(ticker_data, "symbol")
801 .ok_or_else(|| ParseError::missing_field("symbol"))?
802 };
803
804 let timestamp = self.safe_integer(ticker_data, "timestamp").unwrap_or(0);
805
806 Ok(Ticker {
807 symbol,
808 timestamp,
809 datetime: self.safe_string(ticker_data, "datetime"),
810 high: self.safe_decimal(ticker_data, "high").map(Price::new),
811 low: self.safe_decimal(ticker_data, "low").map(Price::new),
812 bid: self.safe_decimal(ticker_data, "bid").map(Price::new),
813 bid_volume: self.safe_decimal(ticker_data, "bidVolume").map(Amount::new),
814 ask: self.safe_decimal(ticker_data, "ask").map(Price::new),
815 ask_volume: self.safe_decimal(ticker_data, "askVolume").map(Amount::new),
816 vwap: self.safe_decimal(ticker_data, "vwap").map(Price::new),
817 open: self.safe_decimal(ticker_data, "open").map(Price::new),
818 close: self.safe_decimal(ticker_data, "close").map(Price::new),
819 last: self.safe_decimal(ticker_data, "last").map(Price::new),
820 previous_close: self
821 .safe_decimal(ticker_data, "previousClose")
822 .map(Price::new),
823 change: self.safe_decimal(ticker_data, "change").map(Price::new),
824 percentage: self.safe_decimal(ticker_data, "percentage"),
825 average: self.safe_decimal(ticker_data, "average").map(Price::new),
826 base_volume: self
827 .safe_decimal(ticker_data, "baseVolume")
828 .map(Amount::new),
829 quote_volume: self
830 .safe_decimal(ticker_data, "quoteVolume")
831 .map(Amount::new),
832 info: std::collections::HashMap::new(),
833 })
834 }
835
836 pub fn parse_trade(&self, trade_data: &Value, market: Option<&Market>) -> Result<Trade> {
851 let symbol = if let Some(m) = market {
852 m.symbol.clone()
853 } else {
854 self.safe_string(trade_data, "symbol")
855 .ok_or_else(|| ParseError::missing_field("symbol"))?
856 };
857
858 let side = self
859 .safe_string(trade_data, "side")
860 .and_then(|s| match s.to_lowercase().as_str() {
861 "buy" => Some(OrderSide::Buy),
862 "sell" => Some(OrderSide::Sell),
863 _ => None,
864 })
865 .ok_or_else(|| ParseError::missing_field("side"))?;
866
867 let trade_type =
868 self.safe_string(trade_data, "type")
869 .and_then(|t| match t.to_lowercase().as_str() {
870 "limit" => Some(OrderType::Limit),
871 "market" => Some(OrderType::Market),
872 _ => None,
873 });
874
875 let taker_or_maker = self.safe_string(trade_data, "takerOrMaker").and_then(|s| {
876 match s.to_lowercase().as_str() {
877 "taker" => Some(TakerOrMaker::Taker),
878 "maker" => Some(TakerOrMaker::Maker),
879 _ => None,
880 }
881 });
882
883 Ok(Trade {
884 id: self.safe_string(trade_data, "id"),
885 order: self.safe_string(trade_data, "orderId"),
886 timestamp: self.safe_integer(trade_data, "timestamp").unwrap_or(0),
887 datetime: self.safe_string(trade_data, "datetime"),
888 symbol,
889 trade_type,
890 side,
891 taker_or_maker,
892 price: Price::new(
893 self.safe_decimal(trade_data, "price")
894 .unwrap_or(Decimal::ZERO),
895 ),
896 amount: Amount::new(
897 self.safe_decimal(trade_data, "amount")
898 .unwrap_or(Decimal::ZERO),
899 ),
900 cost: self.safe_decimal(trade_data, "cost").map(Cost::new),
901 fee: None,
902 info: if let Some(obj) = trade_data.as_object() {
903 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
904 } else {
905 HashMap::new()
906 },
907 })
908 }
909
910 pub fn parse_order(&self, order_data: &Value, market: Option<&Market>) -> Result<Order> {
921 let symbol = if let Some(m) = market {
922 m.symbol.clone()
923 } else {
924 self.safe_string(order_data, "symbol")
925 .ok_or_else(|| ParseError::missing_field("symbol"))?
926 };
927
928 let order_type = self
929 .safe_string(order_data, "type")
930 .and_then(|t| match t.to_lowercase().as_str() {
931 "limit" => Some(OrderType::Limit),
932 "market" => Some(OrderType::Market),
933 _ => None,
934 })
935 .unwrap_or(OrderType::Limit);
936
937 let side = self
938 .safe_string(order_data, "side")
939 .and_then(|s| match s.to_lowercase().as_str() {
940 "buy" => Some(OrderSide::Buy),
941 "sell" => Some(OrderSide::Sell),
942 _ => None,
943 })
944 .unwrap_or(OrderSide::Buy);
945
946 let status_str = self
947 .safe_string(order_data, "status")
948 .unwrap_or_else(|| "open".to_string());
949 let status = match status_str.to_lowercase().as_str() {
950 "open" => OrderStatus::Open,
951 "closed" => OrderStatus::Closed,
952 "canceled" | "cancelled" => OrderStatus::Cancelled,
953 "expired" => OrderStatus::Expired,
954 "rejected" => OrderStatus::Rejected,
955 _ => OrderStatus::Open,
956 };
957
958 let id = self
959 .safe_string(order_data, "id")
960 .unwrap_or_else(|| format!("order_{}", chrono::Utc::now().timestamp_millis()));
961
962 let amount = self
963 .safe_decimal(order_data, "amount")
964 .unwrap_or(Decimal::ZERO);
965
966 Ok(Order {
967 id,
968 client_order_id: self.safe_string(order_data, "clientOrderId"),
969 timestamp: self.safe_integer(order_data, "timestamp"),
970 datetime: self.safe_string(order_data, "datetime"),
971 last_trade_timestamp: self.safe_integer(order_data, "lastTradeTimestamp"),
972 symbol,
973 order_type,
974 time_in_force: self.safe_string(order_data, "timeInForce"),
975 post_only: self
976 .safe_string(order_data, "postOnly")
977 .and_then(|s| s.parse::<bool>().ok()),
978 reduce_only: self
979 .safe_string(order_data, "reduceOnly")
980 .and_then(|s| s.parse::<bool>().ok()),
981 side,
982 price: self.safe_decimal(order_data, "price"),
983 stop_price: self.safe_decimal(order_data, "stopPrice"),
984 trigger_price: self.safe_decimal(order_data, "triggerPrice"),
985 take_profit_price: self.safe_decimal(order_data, "takeProfitPrice"),
986 stop_loss_price: self.safe_decimal(order_data, "stopLossPrice"),
987 average: self.safe_decimal(order_data, "average"),
988 amount,
989 filled: self.safe_decimal(order_data, "filled"),
990 remaining: self.safe_decimal(order_data, "remaining"),
991 cost: self.safe_decimal(order_data, "cost"),
992 status,
993 fee: None,
994 fees: None,
995 trades: None,
996 trailing_delta: self.safe_decimal(order_data, "trailingDelta"),
997 trailing_percent: self.safe_decimal(order_data, "trailingPercent"),
998 activation_price: self.safe_decimal(order_data, "activationPrice"),
999 callback_rate: self.safe_decimal(order_data, "callbackRate"),
1000 working_type: self.safe_string(order_data, "workingType"),
1001 info: if let Some(obj) = order_data.as_object() {
1002 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1003 } else {
1004 HashMap::new()
1005 },
1006 })
1007 }
1008
1009 pub fn parse_balance(&self, balance_data: &Value) -> Result<Balance> {
1019 let mut balance = Balance::new();
1020
1021 if let Some(obj) = balance_data.as_object() {
1022 for (currency, balance_info) in obj {
1023 if currency == "timestamp" || currency == "datetime" || currency == "info" {
1024 continue;
1025 }
1026 let free = self
1027 .safe_decimal(balance_info, "free")
1028 .unwrap_or(Decimal::ZERO);
1029 let used = self
1030 .safe_decimal(balance_info, "used")
1031 .unwrap_or(Decimal::ZERO);
1032 let total = self
1033 .safe_decimal(balance_info, "total")
1034 .unwrap_or(free + used);
1035
1036 let entry = BalanceEntry { free, used, total };
1037 balance.set(currency.clone(), entry);
1038 }
1039 }
1040
1041 if let Some(obj) = balance_data.as_object() {
1042 balance.info = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1043 }
1044
1045 Ok(balance)
1046 }
1047
1048 pub fn parse_order_book(
1059 &self,
1060 orderbook_data: &Value,
1061 timestamp: Option<i64>,
1062 ) -> Result<OrderBook> {
1063 let mut bids_side = OrderBookSide::new();
1064 let mut asks_side = OrderBookSide::new();
1065
1066 if let Some(bids_array) = orderbook_data.get("bids").and_then(|v| v.as_array()) {
1067 for bid in bids_array {
1068 if let Some(arr) = bid.as_array() {
1069 if arr.len() >= 2 {
1070 let price = self.safe_decimal_from_value(&arr[0]);
1071 let amount = self.safe_decimal_from_value(&arr[1]);
1072 if let (Some(p), Some(a)) = (price, amount) {
1073 bids_side.push(OrderBookEntry {
1074 price: Price::new(p),
1075 amount: Amount::new(a),
1076 });
1077 }
1078 }
1079 }
1080 }
1081 }
1082
1083 if let Some(asks_array) = orderbook_data.get("asks").and_then(|v| v.as_array()) {
1084 for ask in asks_array {
1085 if let Some(arr) = ask.as_array() {
1086 if arr.len() >= 2 {
1087 let price = self.safe_decimal_from_value(&arr[0]);
1088 let amount = self.safe_decimal_from_value(&arr[1]);
1089 if let (Some(p), Some(a)) = (price, amount) {
1090 asks_side.push(OrderBookEntry {
1091 price: Price::new(p),
1092 amount: Amount::new(a),
1093 });
1094 }
1095 }
1096 }
1097 }
1098 }
1099
1100 Ok(OrderBook {
1101 symbol: self
1102 .safe_string(orderbook_data, "symbol")
1103 .unwrap_or_default(),
1104 bids: bids_side,
1105 asks: asks_side,
1106 timestamp: timestamp
1107 .or_else(|| self.safe_integer(orderbook_data, "timestamp"))
1108 .unwrap_or(0),
1109 datetime: self.safe_string(orderbook_data, "datetime"),
1110 nonce: self.safe_integer(orderbook_data, "nonce"),
1111 info: if let Some(obj) = orderbook_data.as_object() {
1112 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1113 } else {
1114 HashMap::new()
1115 },
1116 buffered_deltas: std::collections::VecDeque::new(),
1118 bids_map: std::collections::BTreeMap::new(),
1119 asks_map: std::collections::BTreeMap::new(),
1120 is_synced: false,
1121 needs_resync: false,
1123 last_resync_time: 0,
1124 })
1125 }
1126
1127 fn safe_decimal(&self, data: &Value, key: &str) -> Option<Decimal> {
1129 data.get(key).and_then(|v| self.safe_decimal_from_value(v))
1130 }
1131
1132 fn safe_decimal_from_value(&self, value: &Value) -> Option<Decimal> {
1134 match value {
1135 Value::Number(n) => {
1136 if let Some(f) = n.as_f64() {
1137 Decimal::from_f64_retain(f)
1138 } else {
1139 None
1140 }
1141 }
1142 Value::String(s) => Decimal::from_str(s).ok(),
1143 _ => None,
1144 }
1145 }
1146
1147 pub async fn calculate_fee(
1166 &self,
1167 symbol: &str,
1168 _order_type: OrderType,
1169 _side: OrderSide,
1170 amount: Decimal,
1171 price: Decimal,
1172 taker_or_maker: Option<&str>,
1173 ) -> Result<Fee> {
1174 let market = self.market(symbol).await?;
1175
1176 let rate = if let Some(tom) = taker_or_maker {
1177 if tom == "taker" {
1178 market.taker.unwrap_or(Decimal::ZERO)
1179 } else {
1180 market.maker.unwrap_or(Decimal::ZERO)
1181 }
1182 } else {
1183 market.taker.unwrap_or(Decimal::ZERO)
1184 };
1185
1186 let cost = amount * price;
1187 let fee_cost = cost * rate;
1188
1189 Ok(Fee {
1190 currency: market.quote.clone(),
1191 cost: fee_cost,
1192 rate: Some(rate),
1193 })
1194 }
1195
1196 pub async fn amount_to_precision(&self, symbol: &str, amount: Decimal) -> Result<Decimal> {
1198 let market = self.market(symbol).await?;
1199 match market.precision.amount {
1200 Some(precision_value) => Ok(self.round_to_precision(amount, precision_value)),
1201 None => Ok(amount),
1202 }
1203 }
1204
1205 pub async fn price_to_precision(&self, symbol: &str, price: Decimal) -> Result<Decimal> {
1207 let market = self.market(symbol).await?;
1208 match market.precision.price {
1209 Some(precision_value) => Ok(self.round_to_precision(price, precision_value)),
1210 None => Ok(price),
1211 }
1212 }
1213
1214 pub async fn cost_to_precision(&self, symbol: &str, cost: Decimal) -> Result<Decimal> {
1216 let market = self.market(symbol).await?;
1217 match market.precision.price {
1218 Some(precision_value) => Ok(self.round_to_precision(cost, precision_value)),
1219 None => Ok(cost),
1220 }
1221 }
1222
1223 fn round_to_precision(&self, value: Decimal, precision_value: Decimal) -> Decimal {
1227 if precision_value < Decimal::ONE {
1228 let steps = (value / precision_value).round();
1230 steps * precision_value
1231 } else {
1232 let digits = precision_value.to_u32().unwrap_or(8);
1234 let multiplier = Decimal::from(10_i64.pow(digits));
1235 let scaled = value * multiplier;
1236 let rounded = scaled.round();
1237 rounded / multiplier
1238 }
1239 }
1240
1241 pub async fn calculate_cost(
1253 &self,
1254 symbol: &str,
1255 amount: Decimal,
1256 price: Decimal,
1257 ) -> Result<Decimal> {
1258 let _market = self.market(symbol).await?;
1259 Ok(amount * price)
1260 }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265 use super::*;
1266
1267 #[tokio::test]
1268 async fn test_base_exchange_creation() {
1269 let config = ExchangeConfig {
1270 id: "test".to_string(),
1271 name: "Test Exchange".to_string(),
1272 ..Default::default()
1273 };
1274
1275 let exchange = BaseExchange::new(config).unwrap();
1276 assert_eq!(exchange.config.id, "test");
1277 assert!(exchange.config.enable_rate_limit);
1279 }
1280
1281 #[tokio::test]
1282 async fn test_market_cache() {
1283 let config = ExchangeConfig {
1284 id: "test".to_string(),
1285 ..Default::default()
1286 };
1287
1288 let exchange = BaseExchange::new(config).unwrap();
1289
1290 let markets = vec![Market {
1291 id: "btcusdt".to_string(),
1292 symbol: "BTC/USDT".to_string(),
1293 parsed_symbol: None,
1294 base: "BTC".to_string(),
1295 quote: "USDT".to_string(),
1296 active: true,
1297 market_type: MarketType::Spot,
1298 margin: false,
1299 settle: None,
1300 base_id: None,
1301 quote_id: None,
1302 settle_id: None,
1303 contract: None,
1304 linear: None,
1305 inverse: None,
1306 contract_size: None,
1307 expiry: None,
1308 expiry_datetime: None,
1309 strike: None,
1310 option_type: None,
1311 precision: Default::default(),
1312 limits: Default::default(),
1313 maker: None,
1314 taker: None,
1315 percentage: None,
1316 tier_based: None,
1317 fee_side: None,
1318 info: Default::default(),
1319 }];
1320
1321 let _ = exchange.set_markets(markets, None).await.unwrap();
1322
1323 let market = exchange.market("BTC/USDT").await.unwrap();
1324 assert_eq!(market.symbol, "BTC/USDT");
1325
1326 let symbols = exchange.symbols().await.unwrap();
1327 assert_eq!(symbols.len(), 1);
1328 }
1329
1330 #[test]
1331 fn test_build_query_string() {
1332 let config = ExchangeConfig::default();
1333 let exchange = BaseExchange::new(config).unwrap();
1334
1335 let mut params = HashMap::new();
1336 params.insert("symbol".to_string(), Value::String("BTC/USDT".to_string()));
1337 params.insert("limit".to_string(), Value::Number(100.into()));
1338
1339 let query = exchange.build_query_string(¶ms);
1340 assert!(query.contains("symbol="));
1341 assert!(query.contains("limit="));
1342 }
1343
1344 #[test]
1345 fn test_capabilities() {
1346 let default_caps = ExchangeCapabilities::default();
1348 assert!(!default_caps.has("fetchMarkets"));
1349 assert!(!default_caps.has("fetchOHLCV"));
1350
1351 let public_caps = ExchangeCapabilities::public_only();
1353 assert!(public_caps.has("fetchMarkets"));
1354 assert!(public_caps.has("fetchTicker"));
1355 assert!(public_caps.has("fetchOHLCV"));
1356 assert!(!public_caps.has("createOrder"));
1357
1358 let all_caps = ExchangeCapabilities::all();
1360 assert!(all_caps.has("fetchMarkets"));
1361 assert!(all_caps.has("fetchOHLCV"));
1362 assert!(all_caps.has("createOrder"));
1363 }
1364
1365 #[test]
1366 fn test_exchange_config_builder() {
1367 let config = ExchangeConfigBuilder::new()
1368 .id("binance")
1369 .name("Binance")
1370 .api_key("test-key")
1371 .secret("test-secret")
1372 .sandbox(true)
1373 .timeout(60)
1374 .verbose(true)
1375 .build();
1376
1377 assert_eq!(config.id, "binance");
1378 assert_eq!(config.name, "Binance");
1379 assert_eq!(config.api_key, Some("test-key".to_string()));
1380 assert_eq!(config.secret, Some("test-secret".to_string()));
1381 assert!(config.sandbox);
1382 assert_eq!(config.timeout, 60);
1383 assert!(config.verbose);
1384 }
1385
1386 #[test]
1387 fn test_exchange_config_builder_defaults() {
1388 let config = ExchangeConfigBuilder::new().build();
1389
1390 assert_eq!(config.id, "");
1391 assert_eq!(config.api_key, None);
1392 assert!(config.enable_rate_limit);
1393 assert_eq!(config.timeout, 30);
1394 assert!(!config.sandbox);
1395 }
1396
1397 #[test]
1398 fn test_exchange_config_builder_from_config() {
1399 let config = ExchangeConfig::builder().id("test").api_key("key").build();
1400
1401 assert_eq!(config.id, "test");
1402 assert_eq!(config.api_key, Some("key".to_string()));
1403 }
1404}
1405
1406#[cfg(test)]
1407mod parse_tests {
1408 use super::*;
1409 use serde_json::json;
1410
1411 async fn create_test_exchange() -> BaseExchange {
1412 let config = ExchangeConfig {
1413 id: "".to_string(),
1414 name: "".to_string(),
1415 api_key: None,
1416 secret: None,
1417 password: None,
1418 uid: None,
1419 timeout: 10000,
1420 sandbox: false,
1421 user_agent: None,
1422 enable_rate_limit: true,
1423 verbose: false,
1424 account_id: None,
1425 rate_limit: 0,
1426 proxy: None,
1427 options: Default::default(),
1428 url_overrides: Default::default(),
1429 };
1430
1431 let exchange = BaseExchange::new(config).unwrap();
1432
1433 let cache = MarketCache::default();
1435 *exchange.market_cache.write().await = cache;
1436
1437 exchange
1438 }
1439
1440 #[tokio::test]
1441 async fn test_parse_ticker() {
1442 let exchange = create_test_exchange().await;
1443
1444 let ticker_data = json!({
1445 "symbol": "BTC/USDT",
1446 "timestamp": 1609459200000i64,
1447 "datetime": "2021-01-01T00:00:00.000Z",
1448 "high": 30000.0,
1449 "low": 28000.0,
1450 "bid": 29000.0,
1451 "bidVolume": 10.5,
1452 "ask": 29100.0,
1453 "askVolume": 8.3,
1454 "vwap": 29500.0,
1455 "open": 28500.0,
1456 "close": 29000.0,
1457 "last": 29000.0,
1458 "previousClose": 28500.0,
1459 "change": 500.0,
1460 "percentage": 1.75,
1461 "average": 28750.0,
1462 "baseVolume": 1000.0,
1463 "quoteVolume": 29000000.0
1464 });
1465
1466 let result = exchange.parse_ticker(&ticker_data, None);
1467 assert!(result.is_ok());
1468
1469 let ticker = result.unwrap();
1470 assert_eq!(ticker.symbol, "BTC/USDT");
1471 assert_eq!(ticker.timestamp, 1609459200000);
1472 assert_eq!(
1473 ticker.high,
1474 Some(Price::from(Decimal::from_str_radix("30000.0", 10).unwrap()))
1475 );
1476 assert_eq!(
1477 ticker.low,
1478 Some(Price::from(Decimal::from_str_radix("28000.0", 10).unwrap()))
1479 );
1480 assert_eq!(
1481 ticker.bid,
1482 Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1483 );
1484 assert_eq!(
1485 ticker.ask,
1486 Some(Price::from(Decimal::from_str_radix("29100.0", 10).unwrap()))
1487 );
1488 assert_eq!(
1489 ticker.last,
1490 Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1491 );
1492 assert_eq!(
1493 ticker.base_volume,
1494 Some(Amount::from(Decimal::from_str_radix("1000.0", 10).unwrap()))
1495 );
1496 assert_eq!(
1497 ticker.quote_volume,
1498 Some(Amount::from(
1499 Decimal::from_str_radix("29000000.0", 10).unwrap()
1500 ))
1501 );
1502 }
1503
1504 #[tokio::test]
1505 async fn test_parse_trade() {
1506 let exchange = create_test_exchange().await;
1507
1508 let trade_data = json!({
1509 "id": "12345",
1510 "symbol": "BTC/USDT",
1511 "timestamp": 1609459200000i64,
1512 "datetime": "2021-01-01T00:00:00.000Z",
1513 "order": "order123",
1514 "type": "limit",
1515 "side": "buy",
1516 "takerOrMaker": "taker",
1517 "price": 29000.0,
1518 "amount": 0.5,
1519 "cost": 14500.0
1520 });
1521
1522 let result = exchange.parse_trade(&trade_data, None);
1523 assert!(result.is_ok());
1524
1525 let trade = result.unwrap();
1526 assert_eq!(trade.id, Some("12345".to_string()));
1527 assert_eq!(trade.symbol, "BTC/USDT");
1528 assert_eq!(trade.timestamp, 1609459200000);
1529 assert_eq!(trade.side, OrderSide::Buy);
1530 assert_eq!(
1531 trade.price,
1532 Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1533 );
1534 assert_eq!(
1535 trade.amount,
1536 Amount::from(Decimal::from_str_radix("0.5", 10).unwrap())
1537 );
1538 assert_eq!(
1539 trade.cost,
1540 Some(Cost::from(Decimal::from_str_radix("14500.0", 10).unwrap()))
1541 );
1542 assert_eq!(trade.taker_or_maker, Some(TakerOrMaker::Taker));
1543 }
1544
1545 #[tokio::test]
1546 async fn test_parse_order() {
1547 let exchange = create_test_exchange().await;
1548
1549 let order_data = json!({
1550 "id": "order123",
1551 "clientOrderId": "client456",
1552 "symbol": "BTC/USDT",
1553 "timestamp": 1609459200000i64,
1554 "datetime": "2021-01-01T00:00:00.000Z",
1555 "lastTradeTimestamp": 1609459300000i64,
1556 "status": "closed",
1557 "type": "limit",
1558 "timeInForce": "GTC",
1559 "side": "buy",
1560 "price": 29000.0,
1561 "average": 29050.0,
1562 "amount": 0.5,
1563 "filled": 0.5,
1564 "remaining": 0.0,
1565 "cost": 14525.0
1566 });
1567
1568 let result = exchange.parse_order(&order_data, None);
1569 assert!(result.is_ok());
1570
1571 let order = result.unwrap();
1572 assert_eq!(order.id, "order123");
1573 assert_eq!(order.client_order_id, Some("client456".to_string()));
1574 assert_eq!(order.symbol, "BTC/USDT");
1575 assert_eq!(order.status, OrderStatus::Closed);
1576 assert_eq!(order.order_type, OrderType::Limit);
1577 assert_eq!(order.side, OrderSide::Buy);
1578 assert_eq!(order.time_in_force, Some("GTC".to_string()));
1579 assert_eq!(
1580 order.price,
1581 Some(Decimal::from_str_radix("29000.0", 10).unwrap())
1582 );
1583 assert_eq!(order.amount, Decimal::from_str_radix("0.5", 10).unwrap());
1584 assert_eq!(
1585 order.filled,
1586 Some(Decimal::from_str_radix("0.5", 10).unwrap())
1587 );
1588 assert_eq!(
1589 order.remaining,
1590 Some(Decimal::from_str_radix("0.0", 10).unwrap())
1591 );
1592 assert_eq!(
1593 order.cost,
1594 Some(Decimal::from_str_radix("14525.0", 10).unwrap())
1595 );
1596 }
1597
1598 #[tokio::test]
1599 async fn test_parse_balance() {
1600 let exchange = create_test_exchange().await;
1601
1602 let balance_data = json!({
1603 "timestamp": 1609459200000i64,
1604 "datetime": "2021-01-01T00:00:00.000Z",
1605 "BTC": {
1606 "free": 1.5,
1607 "used": 0.5,
1608 "total": 2.0
1609 },
1610 "USDT": {
1611 "free": 10000.0,
1612 "used": 5000.0,
1613 "total": 15000.0
1614 }
1615 });
1616
1617 let result = exchange.parse_balance(&balance_data);
1618 assert!(result.is_ok());
1619
1620 let balance = result.unwrap();
1621 assert_eq!(balance.balances.len(), 2);
1622
1623 let btc_balance = balance.balances.get("BTC").unwrap();
1624 assert_eq!(
1625 btc_balance.free,
1626 Decimal::from_str_radix("1.5", 10).unwrap()
1627 );
1628 assert_eq!(
1629 btc_balance.used,
1630 Decimal::from_str_radix("0.5", 10).unwrap()
1631 );
1632 assert_eq!(
1633 btc_balance.total,
1634 Decimal::from_str_radix("2.0", 10).unwrap()
1635 );
1636
1637 let usdt_balance = balance.balances.get("USDT").unwrap();
1638 assert_eq!(
1639 usdt_balance.free,
1640 Decimal::from_str_radix("10000.0", 10).unwrap()
1641 );
1642 assert_eq!(
1643 usdt_balance.used,
1644 Decimal::from_str_radix("5000.0", 10).unwrap()
1645 );
1646 assert_eq!(
1647 usdt_balance.total,
1648 Decimal::from_str_radix("15000.0", 10).unwrap()
1649 );
1650 }
1651
1652 #[tokio::test]
1653 async fn test_parse_order_book() {
1654 let exchange = create_test_exchange().await;
1655
1656 let orderbook_data = json!({
1657 "symbol": "BTC/USDT",
1658 "bids": [
1659 [29000.0, 1.5],
1660 [28900.0, 2.0],
1661 [28800.0, 3.5]
1662 ],
1663 "asks": [
1664 [29100.0, 1.0],
1665 [29200.0, 2.5],
1666 [29300.0, 1.8]
1667 ]
1668 });
1669
1670 let result = exchange.parse_order_book(&orderbook_data, Some(1609459200000));
1671 assert!(result.is_ok());
1672
1673 let orderbook = result.unwrap();
1674 assert_eq!(orderbook.symbol, "BTC/USDT");
1675 assert_eq!(orderbook.timestamp, 1609459200000);
1676 assert_eq!(orderbook.bids.len(), 3);
1677 assert_eq!(orderbook.asks.len(), 3);
1678
1679 assert_eq!(
1681 orderbook.bids[0].price,
1682 Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1683 );
1684 assert_eq!(
1685 orderbook.bids[1].price,
1686 Price::from(Decimal::from_str_radix("28900.0", 10).unwrap())
1687 );
1688 assert_eq!(
1689 orderbook.bids[2].price,
1690 Price::from(Decimal::from_str_radix("28800.0", 10).unwrap())
1691 );
1692
1693 assert_eq!(
1695 orderbook.asks[0].price,
1696 Price::from(Decimal::from_str_radix("29100.0", 10).unwrap())
1697 );
1698 assert_eq!(
1699 orderbook.asks[1].price,
1700 Price::from(Decimal::from_str_radix("29200.0", 10).unwrap())
1701 );
1702 assert_eq!(
1703 orderbook.asks[2].price,
1704 Price::from(Decimal::from_str_radix("29300.0", 10).unwrap())
1705 );
1706 }
1707
1708 #[test]
1709 fn test_calculate_fee() {
1710 }
1713
1714 #[test]
1715 fn test_amount_to_precision() {
1716 }
1719
1720 #[test]
1721 fn test_price_to_precision() {
1722 }
1725
1726 #[test]
1727 fn test_has_method() {
1728 }
1731
1732 #[test]
1733 fn test_timeframes() {
1734 }
1737
1738 #[test]
1739 fn test_filter_by_type() {
1740 }
1743}