1use crate::config::{ProxyConfig, RetryPolicy};
11use crate::credentials::SecretString;
12use crate::error::{Error, ParseError, Result};
13use crate::http_client::{HttpClient, HttpConfig};
14use crate::rate_limiter::{RateLimiter, RateLimiterConfig};
15#[allow(clippy::wildcard_imports)]
19use crate::types::*;
20use rust_decimal::Decimal;
21use rust_decimal::prelude::{FromStr, ToPrimitive};
22use serde_json::Value;
23use std::collections::HashMap;
24use std::future::Future;
25use std::sync::Arc;
26use std::time::Duration;
27use tokio::sync::{Mutex, RwLock};
28use tracing::{debug, info, warn};
29
30#[derive(Debug, Clone)]
32pub struct ExchangeConfig {
33 pub id: String,
35 pub name: String,
37 pub api_key: Option<SecretString>,
39 pub secret: Option<SecretString>,
41 pub password: Option<SecretString>,
43 pub uid: Option<String>,
45 pub account_id: Option<String>,
47 pub enable_rate_limit: bool,
49 pub rate_limit: u32,
51 pub timeout: Duration,
53 pub connect_timeout: Duration,
55 pub retry_policy: Option<RetryPolicy>,
57 pub sandbox: bool,
59 pub user_agent: Option<String>,
61 pub proxy: Option<ProxyConfig>,
63 pub verbose: bool,
65 pub options: HashMap<String, Value>,
67 pub url_overrides: HashMap<String, String>,
69}
70
71impl Default for ExchangeConfig {
72 fn default() -> Self {
73 Self {
74 id: String::new(),
75 name: String::new(),
76 api_key: None,
77 secret: None,
78 password: None,
79 uid: None,
80 account_id: None,
81 enable_rate_limit: true,
82 rate_limit: 10,
83 timeout: Duration::from_secs(30),
84 connect_timeout: Duration::from_secs(10),
85 retry_policy: None,
86 sandbox: false,
87 user_agent: Some(format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
88 proxy: None,
89 verbose: false,
90 options: HashMap::new(),
91 url_overrides: HashMap::new(),
92 }
93 }
94}
95
96impl ExchangeConfig {
97 pub fn builder() -> ExchangeConfigBuilder {
113 ExchangeConfigBuilder::default()
114 }
115}
116
117#[derive(Debug, Clone, Default)]
136pub struct ExchangeConfigBuilder {
137 config: ExchangeConfig,
138}
139
140impl ExchangeConfigBuilder {
141 pub fn new() -> Self {
143 Self::default()
144 }
145
146 pub fn id(mut self, id: impl Into<String>) -> Self {
148 self.config.id = id.into();
149 self
150 }
151
152 pub fn name(mut self, name: impl Into<String>) -> Self {
154 self.config.name = name.into();
155 self
156 }
157
158 pub fn api_key(mut self, key: impl Into<String>) -> Self {
160 self.config.api_key = Some(SecretString::new(key));
161 self
162 }
163
164 pub fn secret(mut self, secret: impl Into<String>) -> Self {
166 self.config.secret = Some(SecretString::new(secret));
167 self
168 }
169
170 pub fn password(mut self, password: impl Into<String>) -> Self {
172 self.config.password = Some(SecretString::new(password));
173 self
174 }
175
176 pub fn uid(mut self, uid: impl Into<String>) -> Self {
178 self.config.uid = Some(uid.into());
179 self
180 }
181
182 pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
184 self.config.account_id = Some(account_id.into());
185 self
186 }
187
188 pub fn enable_rate_limit(mut self, enabled: bool) -> Self {
190 self.config.enable_rate_limit = enabled;
191 self
192 }
193
194 pub fn rate_limit(mut self, rate_limit: u32) -> Self {
196 self.config.rate_limit = rate_limit;
197 self
198 }
199
200 pub fn timeout(mut self, timeout: Duration) -> Self {
202 self.config.timeout = timeout;
203 self
204 }
205
206 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
208 self.config.connect_timeout = timeout;
209 self
210 }
211
212 pub fn connect_timeout_secs(mut self, seconds: u64) -> Self {
214 self.config.connect_timeout = Duration::from_secs(seconds);
215 self
216 }
217
218 pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
220 self.config.retry_policy = Some(policy);
221 self
222 }
223
224 pub fn sandbox(mut self, enabled: bool) -> Self {
226 self.config.sandbox = enabled;
227 self
228 }
229
230 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
232 self.config.user_agent = Some(user_agent.into());
233 self
234 }
235
236 pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
238 self.config.proxy = Some(proxy);
239 self
240 }
241
242 pub fn proxy_url(mut self, url: impl Into<String>) -> Self {
244 self.config.proxy = Some(ProxyConfig::new(url));
245 self
246 }
247
248 pub fn verbose(mut self, enabled: bool) -> Self {
250 self.config.verbose = enabled;
251 self
252 }
253
254 pub fn option(mut self, key: impl Into<String>, value: Value) -> Self {
256 self.config.options.insert(key.into(), value);
257 self
258 }
259
260 pub fn options(mut self, options: HashMap<String, Value>) -> Self {
262 self.config.options.extend(options);
263 self
264 }
265
266 pub fn url_override(mut self, key: impl Into<String>, url: impl Into<String>) -> Self {
268 self.config.url_overrides.insert(key.into(), url.into());
269 self
270 }
271
272 pub fn build(self) -> ExchangeConfig {
274 self.config
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct MarketCache {
281 pub markets: Arc<HashMap<String, Arc<Market>>>,
283 pub markets_by_id: HashMap<String, Arc<Market>>,
285 pub currencies: HashMap<String, Arc<Currency>>,
287 pub currencies_by_id: HashMap<String, Arc<Currency>>,
289 pub symbols: Vec<String>,
291 pub codes: Vec<String>,
293 pub ids: Vec<String>,
295 pub loaded: bool,
297}
298
299impl Default for MarketCache {
300 fn default() -> Self {
301 Self {
302 markets: Arc::new(HashMap::new()),
303 markets_by_id: HashMap::new(),
304 currencies: HashMap::new(),
305 currencies_by_id: HashMap::new(),
306 symbols: Vec::new(),
307 codes: Vec::new(),
308 ids: Vec::new(),
309 loaded: false,
310 }
311 }
312}
313
314use crate::exchange::ExchangeCapabilities;
317
318#[derive(Debug)]
320pub struct BaseExchange {
321 pub config: ExchangeConfig,
323 pub http_client: HttpClient,
325 pub market_cache: Arc<RwLock<MarketCache>>,
327 pub market_loading_lock: Arc<Mutex<()>>,
329 pub capabilities: ExchangeCapabilities,
331 pub urls: HashMap<String, String>,
333 pub timeframes: HashMap<String, String>,
335 pub precision_mode: PrecisionMode,
337}
338
339impl BaseExchange {
340 pub fn new(config: ExchangeConfig) -> Result<Self> {
354 info!("Initializing exchange: {}", config.id);
355
356 if config.timeout.is_zero() {
358 return Err(Error::invalid_request("timeout cannot be zero"));
359 }
360 if config.connect_timeout.is_zero() {
361 return Err(Error::invalid_request("connect_timeout cannot be zero"));
362 }
363
364 if config.timeout > Duration::from_secs(300) {
366 warn!(
367 timeout_secs = config.timeout.as_secs(),
368 "Request timeout exceeds 5 minutes"
369 );
370 }
371
372 let http_config = HttpConfig {
373 timeout: config.timeout,
374 connect_timeout: config.connect_timeout,
375 #[allow(deprecated)]
376 #[allow(clippy::map_unwrap_or)]
379 max_retries: config.retry_policy.map(|p| p.max_retries).unwrap_or(3),
380 verbose: false,
381 user_agent: config
382 .user_agent
383 .clone()
384 .unwrap_or_else(|| format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
385 return_response_headers: false,
386 proxy: config.proxy.clone(),
387 enable_rate_limit: true,
388 retry_config: config
389 .retry_policy
390 .map(|p| crate::retry_strategy::RetryConfig {
391 max_retries: p.max_retries,
392 #[allow(clippy::cast_possible_truncation)]
393 base_delay_ms: p.delay.as_millis() as u64,
394 strategy_type: crate::retry_strategy::RetryStrategyType::Fixed,
395 ..crate::retry_strategy::RetryConfig::default()
396 }),
397 };
398
399 let mut http_client = HttpClient::new(http_config)?;
400
401 if config.enable_rate_limit {
403 let rate_config =
404 RateLimiterConfig::new(config.rate_limit, std::time::Duration::from_millis(1000));
405 let limiter = RateLimiter::new(rate_config);
406 http_client.set_rate_limiter(limiter);
407 }
408
409 Ok(Self {
410 config,
411 http_client,
412 market_cache: Arc::new(RwLock::new(MarketCache::default())),
413 market_loading_lock: Arc::new(Mutex::new(())),
414 capabilities: ExchangeCapabilities::default(),
415 urls: HashMap::new(),
416 timeframes: HashMap::new(),
417 precision_mode: PrecisionMode::DecimalPlaces,
418 })
419 }
420
421 pub async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
436 let cache = self.market_cache.write().await;
437
438 if cache.loaded && !reload {
439 debug!("Returning cached markets for {}", self.config.id);
440 return Ok(cache.markets.clone());
441 }
442
443 info!("Loading markets for {}", self.config.id);
444
445 drop(cache);
446
447 Err(Error::not_implemented(
448 "load_markets must be implemented by exchange",
449 ))
450 }
451
452 pub async fn load_markets_with_loader<F, Fut>(
481 &self,
482 reload: bool,
483 loader: F,
484 ) -> Result<Arc<HashMap<String, Arc<Market>>>>
485 where
486 F: FnOnce() -> Fut,
487 Fut: Future<Output = Result<(Vec<Market>, Option<Vec<Currency>>)>>,
488 {
489 let _loading_guard = self.market_loading_lock.lock().await;
491
492 {
494 let cache = self.market_cache.read().await;
495 if cache.loaded && !reload {
496 debug!(
497 "Returning cached markets for {} ({} markets)",
498 self.config.id,
499 cache.markets.len()
500 );
501 return Ok(cache.markets.clone());
502 }
503 }
504
505 info!(
507 "Loading markets for {} (reload: {})",
508 self.config.id, reload
509 );
510 let (markets, currencies) = loader().await?;
511
512 self.set_markets(markets, currencies).await?;
514
515 let cache = self.market_cache.read().await;
517 Ok(cache.markets.clone())
518 }
519
520 pub async fn set_markets(
535 &self,
536 markets: Vec<Market>,
537 currencies: Option<Vec<Currency>>,
538 ) -> Result<Arc<HashMap<String, Arc<Market>>>> {
539 let mut cache = self.market_cache.write().await;
540
541 let mut markets_map = HashMap::new();
542 cache.markets_by_id.clear();
543 cache.symbols.clear();
544 cache.ids.clear();
545
546 for market in markets {
547 cache.symbols.push(market.symbol.clone());
548 cache.ids.push(market.id.clone());
549 let arc_market = Arc::new(market);
550 cache
551 .markets_by_id
552 .insert(arc_market.id.clone(), Arc::clone(&arc_market));
553 markets_map.insert(arc_market.symbol.clone(), arc_market);
554 }
555 cache.markets = Arc::new(markets_map);
556
557 if let Some(currencies) = currencies {
558 cache.currencies.clear();
559 cache.currencies_by_id.clear();
560 cache.codes.clear();
561
562 for currency in currencies {
563 cache.codes.push(currency.code.clone());
564 let arc_currency = Arc::new(currency);
565 cache
566 .currencies_by_id
567 .insert(arc_currency.id.clone(), Arc::clone(&arc_currency));
568 cache
569 .currencies
570 .insert(arc_currency.code.clone(), arc_currency);
571 }
572 }
573
574 cache.loaded = true;
575 info!(
576 "Loaded {} markets and {} currencies for {}",
577 cache.markets.len(),
578 cache.currencies.len(),
579 self.config.id
580 );
581
582 Ok(cache.markets.clone())
583 }
584
585 pub async fn market(&self, symbol: &str) -> Result<Arc<Market>> {
599 let cache = self.market_cache.read().await;
600
601 if !cache.loaded {
602 drop(cache);
603 return Err(Error::exchange(
604 "-1",
605 "Markets not loaded. Call load_markets() first.",
606 ));
607 }
608
609 cache
610 .markets
611 .get(symbol)
612 .cloned()
613 .ok_or_else(|| Error::bad_symbol(format!("Market {symbol} not found")))
614 }
615
616 pub async fn market_by_id(&self, id: &str) -> Result<Arc<Market>> {
630 let cache = self.market_cache.read().await;
631
632 cache
633 .markets_by_id
634 .get(id)
635 .cloned()
636 .ok_or_else(|| Error::bad_symbol(format!("Market with id {id} not found")))
637 }
638
639 pub async fn currency(&self, code: &str) -> Result<Arc<Currency>> {
653 let cache = self.market_cache.read().await;
654
655 cache
656 .currencies
657 .get(code)
658 .cloned()
659 .ok_or_else(|| Error::bad_symbol(format!("Currency {code} not found")))
660 }
661
662 pub async fn symbols(&self) -> Result<Vec<String>> {
668 let cache = self.market_cache.read().await;
669 Ok(cache.symbols.clone())
670 }
671
672 #[deprecated(
679 since = "0.2.0",
680 note = "Rate limiting is now handled internally by HttpClient. This method is a no-op."
681 )]
682 pub fn throttle(&self) -> Result<()> {
683 Ok(())
685 }
686
687 pub fn check_required_credentials(&self) -> Result<()> {
697 if self.config.api_key.is_none() {
698 return Err(Error::authentication("API key is required"));
699 }
700 if self.config.secret.is_none() {
701 return Err(Error::authentication("API secret is required"));
702 }
703 Ok(())
704 }
705
706 pub fn nonce(&self) -> i64 {
712 crate::time::milliseconds()
713 }
714
715 pub fn build_query_string(&self, params: &HashMap<String, Value>) -> String {
725 if params.is_empty() {
726 return String::new();
727 }
728
729 let pairs: Vec<String> = params
730 .iter()
731 .map(|(k, v)| {
732 let value_str = match v {
733 Value::String(s) => s.clone(),
734 Value::Number(n) => n.to_string(),
735 Value::Bool(b) => b.to_string(),
736 _ => v.to_string(),
737 };
738 format!("{}={}", k, urlencoding::encode(&value_str))
739 })
740 .collect();
741
742 pairs.join("&")
743 }
744
745 pub fn parse_json(&self, response: &str) -> Result<Value> {
759 serde_json::from_str(response).map_err(|e| Error::invalid_request(e.to_string()))
760 }
761
762 pub fn handle_http_error(&self, status_code: u16, response: &str) -> Error {
773 match status_code {
774 400 => Error::invalid_request(response.to_string()),
775 401 | 403 => Error::authentication(response.to_string()),
776 404 => Error::invalid_request(format!("Endpoint not found: {response}")),
777 429 => Error::rate_limit(response.to_string(), None),
778 500..=599 => Error::exchange(status_code.to_string(), response),
779 _ => Error::network(format!("HTTP {status_code}: {response}")),
780 }
781 }
782
783 pub fn safe_string(&self, dict: &Value, key: &str) -> Option<String> {
794 dict.get(key)
795 .and_then(|v| v.as_str())
796 .map(ToString::to_string)
797 }
798
799 pub fn safe_integer(&self, dict: &Value, key: &str) -> Option<i64> {
810 dict.get(key).and_then(Value::as_i64)
811 }
812
813 pub fn safe_float(&self, dict: &Value, key: &str) -> Option<f64> {
824 dict.get(key).and_then(Value::as_f64)
825 }
826
827 pub fn safe_bool(&self, dict: &Value, key: &str) -> Option<bool> {
838 dict.get(key).and_then(Value::as_bool)
839 }
840
841 pub fn parse_ticker(&self, ticker_data: &Value, market: Option<&Market>) -> Result<Ticker> {
860 let symbol = if let Some(m) = market {
861 m.symbol.clone()
862 } else {
863 self.safe_string(ticker_data, "symbol")
864 .ok_or_else(|| ParseError::missing_field("symbol"))?
865 };
866
867 let timestamp = self.safe_integer(ticker_data, "timestamp").unwrap_or(0);
868
869 Ok(Ticker {
870 symbol,
871 timestamp,
872 datetime: self.safe_string(ticker_data, "datetime"),
873 high: self.safe_decimal(ticker_data, "high").map(Price::new),
874 low: self.safe_decimal(ticker_data, "low").map(Price::new),
875 bid: self.safe_decimal(ticker_data, "bid").map(Price::new),
876 bid_volume: self.safe_decimal(ticker_data, "bidVolume").map(Amount::new),
877 ask: self.safe_decimal(ticker_data, "ask").map(Price::new),
878 ask_volume: self.safe_decimal(ticker_data, "askVolume").map(Amount::new),
879 vwap: self.safe_decimal(ticker_data, "vwap").map(Price::new),
880 open: self.safe_decimal(ticker_data, "open").map(Price::new),
881 close: self.safe_decimal(ticker_data, "close").map(Price::new),
882 last: self.safe_decimal(ticker_data, "last").map(Price::new),
883 previous_close: self
884 .safe_decimal(ticker_data, "previousClose")
885 .map(Price::new),
886 change: self.safe_decimal(ticker_data, "change").map(Price::new),
887 percentage: self.safe_decimal(ticker_data, "percentage"),
888 average: self.safe_decimal(ticker_data, "average").map(Price::new),
889 base_volume: self
890 .safe_decimal(ticker_data, "baseVolume")
891 .map(Amount::new),
892 quote_volume: self
893 .safe_decimal(ticker_data, "quoteVolume")
894 .map(Amount::new),
895 info: std::collections::HashMap::new(),
896 })
897 }
898
899 pub fn parse_trade(&self, trade_data: &Value, market: Option<&Market>) -> Result<Trade> {
914 let symbol = if let Some(m) = market {
915 m.symbol.clone()
916 } else {
917 self.safe_string(trade_data, "symbol")
918 .ok_or_else(|| ParseError::missing_field("symbol"))?
919 };
920
921 let side = self
922 .safe_string(trade_data, "side")
923 .and_then(|s| match s.to_lowercase().as_str() {
924 "buy" => Some(OrderSide::Buy),
925 "sell" => Some(OrderSide::Sell),
926 _ => None,
927 })
928 .ok_or_else(|| ParseError::missing_field("side"))?;
929
930 let trade_type =
931 self.safe_string(trade_data, "type")
932 .and_then(|t| match t.to_lowercase().as_str() {
933 "limit" => Some(OrderType::Limit),
934 "market" => Some(OrderType::Market),
935 _ => None,
936 });
937
938 let taker_or_maker = self.safe_string(trade_data, "takerOrMaker").and_then(|s| {
939 match s.to_lowercase().as_str() {
940 "taker" => Some(TakerOrMaker::Taker),
941 "maker" => Some(TakerOrMaker::Maker),
942 _ => None,
943 }
944 });
945
946 Ok(Trade {
947 id: self.safe_string(trade_data, "id"),
948 order: self.safe_string(trade_data, "orderId"),
949 timestamp: self.safe_integer(trade_data, "timestamp").unwrap_or(0),
950 datetime: self.safe_string(trade_data, "datetime"),
951 symbol,
952 trade_type,
953 side,
954 taker_or_maker,
955 price: Price::new(
956 self.safe_decimal(trade_data, "price")
957 .unwrap_or(Decimal::ZERO),
958 ),
959 amount: Amount::new(
960 self.safe_decimal(trade_data, "amount")
961 .unwrap_or(Decimal::ZERO),
962 ),
963 cost: self.safe_decimal(trade_data, "cost").map(Cost::new),
964 fee: None,
965 info: if let Some(obj) = trade_data.as_object() {
966 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
967 } else {
968 HashMap::new()
969 },
970 })
971 }
972
973 pub fn parse_order(&self, order_data: &Value, market: Option<&Market>) -> Result<Order> {
984 let symbol = if let Some(m) = market {
985 m.symbol.clone()
986 } else {
987 self.safe_string(order_data, "symbol")
988 .ok_or_else(|| ParseError::missing_field("symbol"))?
989 };
990
991 let order_type = self
992 .safe_string(order_data, "type")
993 .and_then(|t| match t.to_lowercase().as_str() {
994 "limit" => Some(OrderType::Limit),
995 "market" => Some(OrderType::Market),
996 _ => None,
997 })
998 .unwrap_or(OrderType::Limit);
999
1000 let side = self
1001 .safe_string(order_data, "side")
1002 .and_then(|s| match s.to_lowercase().as_str() {
1003 "buy" => Some(OrderSide::Buy),
1004 "sell" => Some(OrderSide::Sell),
1005 _ => None,
1006 })
1007 .unwrap_or(OrderSide::Buy);
1008
1009 let status_str = self
1010 .safe_string(order_data, "status")
1011 .unwrap_or_else(|| "open".to_string());
1012 #[allow(clippy::match_same_arms)]
1015 let status = match status_str.to_lowercase().as_str() {
1016 "open" => OrderStatus::Open,
1017 "closed" => OrderStatus::Closed,
1018 "canceled" | "cancelled" => OrderStatus::Cancelled,
1019 "expired" => OrderStatus::Expired,
1020 "rejected" => OrderStatus::Rejected,
1021 _ => OrderStatus::Open,
1022 };
1023
1024 let id = self
1025 .safe_string(order_data, "id")
1026 .unwrap_or_else(|| format!("order_{}", chrono::Utc::now().timestamp_millis()));
1027
1028 let amount = self
1029 .safe_decimal(order_data, "amount")
1030 .unwrap_or(Decimal::ZERO);
1031
1032 Ok(Order {
1033 id,
1034 client_order_id: self.safe_string(order_data, "clientOrderId"),
1035 timestamp: self.safe_integer(order_data, "timestamp"),
1036 datetime: self.safe_string(order_data, "datetime"),
1037 last_trade_timestamp: self.safe_integer(order_data, "lastTradeTimestamp"),
1038 symbol,
1039 order_type,
1040 time_in_force: self.safe_string(order_data, "timeInForce"),
1041 post_only: self
1042 .safe_string(order_data, "postOnly")
1043 .and_then(|s| s.parse::<bool>().ok()),
1044 reduce_only: self
1045 .safe_string(order_data, "reduceOnly")
1046 .and_then(|s| s.parse::<bool>().ok()),
1047 side,
1048 price: self.safe_decimal(order_data, "price"),
1049 stop_price: self.safe_decimal(order_data, "stopPrice"),
1050 trigger_price: self.safe_decimal(order_data, "triggerPrice"),
1051 take_profit_price: self.safe_decimal(order_data, "takeProfitPrice"),
1052 stop_loss_price: self.safe_decimal(order_data, "stopLossPrice"),
1053 average: self.safe_decimal(order_data, "average"),
1054 amount,
1055 filled: self.safe_decimal(order_data, "filled"),
1056 remaining: self.safe_decimal(order_data, "remaining"),
1057 cost: self.safe_decimal(order_data, "cost"),
1058 status,
1059 fee: None,
1060 fees: None,
1061 trades: None,
1062 trailing_delta: self.safe_decimal(order_data, "trailingDelta"),
1063 trailing_percent: self.safe_decimal(order_data, "trailingPercent"),
1064 activation_price: self.safe_decimal(order_data, "activationPrice"),
1065 callback_rate: self.safe_decimal(order_data, "callbackRate"),
1066 working_type: self.safe_string(order_data, "workingType"),
1067 info: if let Some(obj) = order_data.as_object() {
1068 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1069 } else {
1070 HashMap::new()
1071 },
1072 })
1073 }
1074
1075 pub fn parse_balance(&self, balance_data: &Value) -> Result<Balance> {
1085 let mut balance = Balance::new();
1086
1087 if let Some(obj) = balance_data.as_object() {
1088 for (currency, balance_info) in obj {
1089 if currency == "timestamp" || currency == "datetime" || currency == "info" {
1090 continue;
1091 }
1092 let free = self
1093 .safe_decimal(balance_info, "free")
1094 .unwrap_or(Decimal::ZERO);
1095 let used = self
1096 .safe_decimal(balance_info, "used")
1097 .unwrap_or(Decimal::ZERO);
1098 let total = self
1099 .safe_decimal(balance_info, "total")
1100 .unwrap_or(free + used);
1101
1102 let entry = BalanceEntry { free, used, total };
1103 balance.set(currency.clone(), entry);
1104 }
1105 }
1106
1107 if let Some(obj) = balance_data.as_object() {
1108 balance.info = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1109 }
1110
1111 Ok(balance)
1112 }
1113
1114 pub fn parse_order_book(
1125 &self,
1126 orderbook_data: &Value,
1127 timestamp: Option<i64>,
1128 ) -> Result<OrderBook> {
1129 let mut bids_side = OrderBookSide::new();
1130 let mut asks_side = OrderBookSide::new();
1131
1132 if let Some(bids_array) = orderbook_data.get("bids").and_then(|v| v.as_array()) {
1133 for bid in bids_array {
1134 if let Some(arr) = bid.as_array() {
1135 #[allow(clippy::collapsible_if)]
1138 if arr.len() >= 2 {
1139 let price = self.safe_decimal_from_value(&arr[0]);
1140 let amount = self.safe_decimal_from_value(&arr[1]);
1141 if let (Some(p), Some(a)) = (price, amount) {
1142 bids_side.push(OrderBookEntry {
1143 price: Price::new(p),
1144 amount: Amount::new(a),
1145 });
1146 }
1147 }
1148 }
1149 }
1150 }
1151
1152 if let Some(asks_array) = orderbook_data.get("asks").and_then(|v| v.as_array()) {
1153 for ask in asks_array {
1154 if let Some(arr) = ask.as_array() {
1155 #[allow(clippy::collapsible_if)]
1158 if arr.len() >= 2 {
1159 let price = self.safe_decimal_from_value(&arr[0]);
1160 let amount = self.safe_decimal_from_value(&arr[1]);
1161 if let (Some(p), Some(a)) = (price, amount) {
1162 asks_side.push(OrderBookEntry {
1163 price: Price::new(p),
1164 amount: Amount::new(a),
1165 });
1166 }
1167 }
1168 }
1169 }
1170 }
1171
1172 Ok(OrderBook {
1173 symbol: self
1174 .safe_string(orderbook_data, "symbol")
1175 .unwrap_or_default(),
1176 bids: bids_side,
1177 asks: asks_side,
1178 timestamp: timestamp
1179 .or_else(|| self.safe_integer(orderbook_data, "timestamp"))
1180 .unwrap_or(0),
1181 datetime: self.safe_string(orderbook_data, "datetime"),
1182 nonce: self.safe_integer(orderbook_data, "nonce"),
1183 info: if let Some(obj) = orderbook_data.as_object() {
1184 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1185 } else {
1186 HashMap::new()
1187 },
1188 buffered_deltas: std::collections::VecDeque::new(),
1190 bids_map: std::collections::BTreeMap::new(),
1191 asks_map: std::collections::BTreeMap::new(),
1192 is_synced: false,
1193 needs_resync: false,
1195 last_resync_time: 0,
1196 })
1197 }
1198
1199 fn safe_decimal(&self, data: &Value, key: &str) -> Option<Decimal> {
1201 data.get(key).and_then(|v| self.safe_decimal_from_value(v))
1202 }
1203
1204 #[allow(clippy::unused_self)]
1208 fn safe_decimal_from_value(&self, value: &Value) -> Option<Decimal> {
1209 match value {
1210 Value::Number(n) => {
1211 if let Some(f) = n.as_f64() {
1212 Decimal::from_f64_retain(f)
1213 } else {
1214 None
1215 }
1216 }
1217 Value::String(s) => Decimal::from_str(s).ok(),
1218 _ => None,
1219 }
1220 }
1221
1222 pub async fn calculate_fee(
1241 &self,
1242 symbol: &str,
1243 _order_type: OrderType,
1244 _side: OrderSide,
1245 amount: Decimal,
1246 price: Decimal,
1247 taker_or_maker: Option<&str>,
1248 ) -> Result<Fee> {
1249 let market = self.market(symbol).await?;
1250
1251 let rate = if let Some(tom) = taker_or_maker {
1252 if tom == "taker" {
1253 market.taker.unwrap_or(Decimal::ZERO)
1254 } else {
1255 market.maker.unwrap_or(Decimal::ZERO)
1256 }
1257 } else {
1258 market.taker.unwrap_or(Decimal::ZERO)
1259 };
1260
1261 let cost = amount * price;
1262 let fee_cost = cost * rate;
1263
1264 Ok(Fee {
1265 currency: market.quote.clone(),
1266 cost: fee_cost,
1267 rate: Some(rate),
1268 })
1269 }
1270
1271 pub async fn amount_to_precision(&self, symbol: &str, amount: Decimal) -> Result<Decimal> {
1273 let market = self.market(symbol).await?;
1274 match market.precision.amount {
1275 Some(precision_value) => Ok(self.round_to_precision(amount, precision_value)),
1276 None => Ok(amount),
1277 }
1278 }
1279
1280 pub async fn price_to_precision(&self, symbol: &str, price: Decimal) -> Result<Decimal> {
1282 let market = self.market(symbol).await?;
1283 match market.precision.price {
1284 Some(precision_value) => Ok(self.round_to_precision(price, precision_value)),
1285 None => Ok(price),
1286 }
1287 }
1288
1289 pub async fn cost_to_precision(&self, symbol: &str, cost: Decimal) -> Result<Decimal> {
1291 let market = self.market(symbol).await?;
1292 match market.precision.price {
1293 Some(precision_value) => Ok(self.round_to_precision(cost, precision_value)),
1294 None => Ok(cost),
1295 }
1296 }
1297
1298 #[allow(clippy::unused_self)]
1304 fn round_to_precision(&self, value: Decimal, precision_value: Decimal) -> Decimal {
1305 if precision_value < Decimal::ONE {
1306 let steps = (value / precision_value).round();
1308 steps * precision_value
1309 } else {
1310 let digits = precision_value.to_u32().unwrap_or(8);
1312 let multiplier = Decimal::from(10_i64.pow(digits));
1313 let scaled = value * multiplier;
1314 let rounded = scaled.round();
1315 rounded / multiplier
1316 }
1317 }
1318
1319 pub async fn calculate_cost(
1331 &self,
1332 symbol: &str,
1333 amount: Decimal,
1334 price: Decimal,
1335 ) -> Result<Decimal> {
1336 let _market = self.market(symbol).await?;
1337 Ok(amount * price)
1338 }
1339}
1340
1341#[cfg(test)]
1342mod tests {
1343 use super::*;
1344
1345 #[tokio::test]
1346 async fn test_base_exchange_creation() {
1347 let config = ExchangeConfig {
1348 id: "test".to_string(),
1349 name: "Test Exchange".to_string(),
1350 ..Default::default()
1351 };
1352
1353 let exchange = BaseExchange::new(config).unwrap();
1354 assert_eq!(exchange.config.id, "test");
1355 assert!(exchange.config.enable_rate_limit);
1357 }
1358
1359 #[tokio::test]
1360 async fn test_market_cache() {
1361 let config = ExchangeConfig {
1362 id: "test".to_string(),
1363 ..Default::default()
1364 };
1365
1366 let exchange = BaseExchange::new(config).unwrap();
1367
1368 let markets = vec![Market {
1369 id: "btcusdt".to_string(),
1370 symbol: "BTC/USDT".to_string(),
1371 parsed_symbol: None,
1372 base: "BTC".to_string(),
1373 quote: "USDT".to_string(),
1374 active: true,
1375 market_type: MarketType::Spot,
1376 margin: false,
1377 settle: None,
1378 base_id: None,
1379 quote_id: None,
1380 settle_id: None,
1381 contract: None,
1382 linear: None,
1383 inverse: None,
1384 contract_size: None,
1385 expiry: None,
1386 expiry_datetime: None,
1387 strike: None,
1388 option_type: None,
1389 precision: Default::default(),
1390 limits: Default::default(),
1391 maker: None,
1392 taker: None,
1393 percentage: None,
1394 tier_based: None,
1395 fee_side: None,
1396 info: Default::default(),
1397 }];
1398
1399 let _ = exchange.set_markets(markets, None).await.unwrap();
1400
1401 let market = exchange.market("BTC/USDT").await.unwrap();
1402 assert_eq!(market.symbol, "BTC/USDT");
1403
1404 let symbols = exchange.symbols().await.unwrap();
1405 assert_eq!(symbols.len(), 1);
1406 }
1407
1408 #[test]
1409 fn test_build_query_string() {
1410 let config = ExchangeConfig::default();
1411 let exchange = BaseExchange::new(config).unwrap();
1412
1413 let mut params = HashMap::new();
1414 params.insert("symbol".to_string(), Value::String("BTC/USDT".to_string()));
1415 params.insert("limit".to_string(), Value::Number(100.into()));
1416
1417 let query = exchange.build_query_string(¶ms);
1418 assert!(query.contains("symbol="));
1419 assert!(query.contains("limit="));
1420 }
1421
1422 #[test]
1423 fn test_capabilities() {
1424 let default_caps = ExchangeCapabilities::default();
1426 assert!(!default_caps.has("fetchMarkets"));
1427 assert!(!default_caps.has("fetchOHLCV"));
1428
1429 let public_caps = ExchangeCapabilities::public_only();
1431 assert!(public_caps.has("fetchMarkets"));
1432 assert!(public_caps.has("fetchTicker"));
1433 assert!(public_caps.has("fetchOHLCV"));
1434 assert!(!public_caps.has("createOrder"));
1435
1436 let all_caps = ExchangeCapabilities::all();
1438 assert!(all_caps.has("fetchMarkets"));
1439 assert!(all_caps.has("fetchOHLCV"));
1440 assert!(all_caps.has("createOrder"));
1441 }
1442
1443 #[test]
1444 fn test_exchange_config_builder() {
1445 let config = ExchangeConfigBuilder::new()
1446 .id("binance")
1447 .name("Binance")
1448 .api_key("test-key")
1449 .secret("test-secret")
1450 .sandbox(true)
1451 .timeout(Duration::from_secs(60))
1452 .verbose(true)
1453 .build();
1454
1455 assert_eq!(config.id, "binance");
1456 assert_eq!(config.name, "Binance");
1457 assert_eq!(
1458 config.api_key.as_ref().map(|s| s.expose_secret()),
1459 Some("test-key")
1460 );
1461 assert_eq!(
1462 config.secret.as_ref().map(|s| s.expose_secret()),
1463 Some("test-secret")
1464 );
1465 assert!(config.sandbox);
1466 assert_eq!(config.timeout, Duration::from_secs(60));
1467 assert!(config.verbose);
1468 }
1469
1470 #[test]
1471 fn test_exchange_config_builder_defaults() {
1472 let config = ExchangeConfigBuilder::new().build();
1473
1474 assert_eq!(config.id, "");
1475 assert!(config.api_key.is_none());
1476 assert!(config.enable_rate_limit);
1477 assert_eq!(config.timeout, Duration::from_secs(30));
1478 assert_eq!(config.connect_timeout, Duration::from_secs(10));
1479 assert!(!config.sandbox);
1480 }
1481
1482 #[test]
1483 fn test_exchange_config_builder_connect_timeout() {
1484 let config = ExchangeConfigBuilder::new()
1486 .connect_timeout(Duration::from_secs(5))
1487 .build();
1488 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1489
1490 let config = ExchangeConfigBuilder::new()
1492 .connect_timeout_secs(15)
1493 .build();
1494 assert_eq!(config.connect_timeout, Duration::from_secs(15));
1495 }
1496
1497 #[test]
1498 fn test_exchange_config_builder_from_config() {
1499 let config = ExchangeConfig::builder().id("test").api_key("key").build();
1500
1501 assert_eq!(config.id, "test");
1502 assert_eq!(
1503 config.api_key.as_ref().map(|s| s.expose_secret()),
1504 Some("key")
1505 );
1506 }
1507}
1508
1509#[cfg(test)]
1510mod parse_tests {
1511 use super::*;
1512 use serde_json::json;
1513
1514 async fn create_test_exchange() -> BaseExchange {
1515 let config = ExchangeConfig {
1516 id: "".to_string(),
1517 name: "".to_string(),
1518 api_key: None,
1519 secret: None,
1520 password: None,
1521 uid: None,
1522 timeout: Duration::from_millis(10000),
1523 connect_timeout: Duration::from_secs(10),
1524 sandbox: false,
1525 user_agent: None,
1526 enable_rate_limit: true,
1527 verbose: false,
1528 account_id: None,
1529 rate_limit: 0,
1530 retry_policy: None,
1531 proxy: None,
1532 options: Default::default(),
1533 url_overrides: Default::default(),
1534 };
1535
1536 let exchange = BaseExchange::new(config).unwrap();
1537
1538 let cache = MarketCache::default();
1540 *exchange.market_cache.write().await = cache;
1541
1542 exchange
1543 }
1544
1545 #[tokio::test]
1546 async fn test_parse_ticker() {
1547 let exchange = create_test_exchange().await;
1548
1549 let ticker_data = json!({
1550 "symbol": "BTC/USDT",
1551 "timestamp": 1609459200000i64,
1552 "datetime": "2021-01-01T00:00:00.000Z",
1553 "high": 30000.0,
1554 "low": 28000.0,
1555 "bid": 29000.0,
1556 "bidVolume": 10.5,
1557 "ask": 29100.0,
1558 "askVolume": 8.3,
1559 "vwap": 29500.0,
1560 "open": 28500.0,
1561 "close": 29000.0,
1562 "last": 29000.0,
1563 "previousClose": 28500.0,
1564 "change": 500.0,
1565 "percentage": 1.75,
1566 "average": 28750.0,
1567 "baseVolume": 1000.0,
1568 "quoteVolume": 29000000.0
1569 });
1570
1571 let result = exchange.parse_ticker(&ticker_data, None);
1572 assert!(result.is_ok());
1573
1574 let ticker = result.unwrap();
1575 assert_eq!(ticker.symbol, "BTC/USDT");
1576 assert_eq!(ticker.timestamp, 1609459200000);
1577 assert_eq!(
1578 ticker.high,
1579 Some(Price::from(Decimal::from_str_radix("30000.0", 10).unwrap()))
1580 );
1581 assert_eq!(
1582 ticker.low,
1583 Some(Price::from(Decimal::from_str_radix("28000.0", 10).unwrap()))
1584 );
1585 assert_eq!(
1586 ticker.bid,
1587 Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1588 );
1589 assert_eq!(
1590 ticker.ask,
1591 Some(Price::from(Decimal::from_str_radix("29100.0", 10).unwrap()))
1592 );
1593 assert_eq!(
1594 ticker.last,
1595 Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1596 );
1597 assert_eq!(
1598 ticker.base_volume,
1599 Some(Amount::from(Decimal::from_str_radix("1000.0", 10).unwrap()))
1600 );
1601 assert_eq!(
1602 ticker.quote_volume,
1603 Some(Amount::from(
1604 Decimal::from_str_radix("29000000.0", 10).unwrap()
1605 ))
1606 );
1607 }
1608
1609 #[tokio::test]
1610 async fn test_parse_trade() {
1611 let exchange = create_test_exchange().await;
1612
1613 let trade_data = json!({
1614 "id": "12345",
1615 "symbol": "BTC/USDT",
1616 "timestamp": 1609459200000i64,
1617 "datetime": "2021-01-01T00:00:00.000Z",
1618 "order": "order123",
1619 "type": "limit",
1620 "side": "buy",
1621 "takerOrMaker": "taker",
1622 "price": 29000.0,
1623 "amount": 0.5,
1624 "cost": 14500.0
1625 });
1626
1627 let result = exchange.parse_trade(&trade_data, None);
1628 assert!(result.is_ok());
1629
1630 let trade = result.unwrap();
1631 assert_eq!(trade.id, Some("12345".to_string()));
1632 assert_eq!(trade.symbol, "BTC/USDT");
1633 assert_eq!(trade.timestamp, 1609459200000);
1634 assert_eq!(trade.side, OrderSide::Buy);
1635 assert_eq!(
1636 trade.price,
1637 Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1638 );
1639 assert_eq!(
1640 trade.amount,
1641 Amount::from(Decimal::from_str_radix("0.5", 10).unwrap())
1642 );
1643 assert_eq!(
1644 trade.cost,
1645 Some(Cost::from(Decimal::from_str_radix("14500.0", 10).unwrap()))
1646 );
1647 assert_eq!(trade.taker_or_maker, Some(TakerOrMaker::Taker));
1648 }
1649
1650 #[tokio::test]
1651 async fn test_parse_order() {
1652 let exchange = create_test_exchange().await;
1653
1654 let order_data = json!({
1655 "id": "order123",
1656 "clientOrderId": "client456",
1657 "symbol": "BTC/USDT",
1658 "timestamp": 1609459200000i64,
1659 "datetime": "2021-01-01T00:00:00.000Z",
1660 "lastTradeTimestamp": 1609459300000i64,
1661 "status": "closed",
1662 "type": "limit",
1663 "timeInForce": "GTC",
1664 "side": "buy",
1665 "price": 29000.0,
1666 "average": 29050.0,
1667 "amount": 0.5,
1668 "filled": 0.5,
1669 "remaining": 0.0,
1670 "cost": 14525.0
1671 });
1672
1673 let result = exchange.parse_order(&order_data, None);
1674 assert!(result.is_ok());
1675
1676 let order = result.unwrap();
1677 assert_eq!(order.id, "order123");
1678 assert_eq!(order.client_order_id, Some("client456".to_string()));
1679 assert_eq!(order.symbol, "BTC/USDT");
1680 assert_eq!(order.status, OrderStatus::Closed);
1681 assert_eq!(order.order_type, OrderType::Limit);
1682 assert_eq!(order.side, OrderSide::Buy);
1683 assert_eq!(order.time_in_force, Some("GTC".to_string()));
1684 assert_eq!(
1685 order.price,
1686 Some(Decimal::from_str_radix("29000.0", 10).unwrap())
1687 );
1688 assert_eq!(order.amount, Decimal::from_str_radix("0.5", 10).unwrap());
1689 assert_eq!(
1690 order.filled,
1691 Some(Decimal::from_str_radix("0.5", 10).unwrap())
1692 );
1693 assert_eq!(
1694 order.remaining,
1695 Some(Decimal::from_str_radix("0.0", 10).unwrap())
1696 );
1697 assert_eq!(
1698 order.cost,
1699 Some(Decimal::from_str_radix("14525.0", 10).unwrap())
1700 );
1701 }
1702
1703 #[tokio::test]
1704 async fn test_parse_balance() {
1705 let exchange = create_test_exchange().await;
1706
1707 let balance_data = json!({
1708 "timestamp": 1609459200000i64,
1709 "datetime": "2021-01-01T00:00:00.000Z",
1710 "BTC": {
1711 "free": 1.5,
1712 "used": 0.5,
1713 "total": 2.0
1714 },
1715 "USDT": {
1716 "free": 10000.0,
1717 "used": 5000.0,
1718 "total": 15000.0
1719 }
1720 });
1721
1722 let result = exchange.parse_balance(&balance_data);
1723 assert!(result.is_ok());
1724
1725 let balance = result.unwrap();
1726 assert_eq!(balance.balances.len(), 2);
1727
1728 let btc_balance = balance.balances.get("BTC").unwrap();
1729 assert_eq!(
1730 btc_balance.free,
1731 Decimal::from_str_radix("1.5", 10).unwrap()
1732 );
1733 assert_eq!(
1734 btc_balance.used,
1735 Decimal::from_str_radix("0.5", 10).unwrap()
1736 );
1737 assert_eq!(
1738 btc_balance.total,
1739 Decimal::from_str_radix("2.0", 10).unwrap()
1740 );
1741
1742 let usdt_balance = balance.balances.get("USDT").unwrap();
1743 assert_eq!(
1744 usdt_balance.free,
1745 Decimal::from_str_radix("10000.0", 10).unwrap()
1746 );
1747 assert_eq!(
1748 usdt_balance.used,
1749 Decimal::from_str_radix("5000.0", 10).unwrap()
1750 );
1751 assert_eq!(
1752 usdt_balance.total,
1753 Decimal::from_str_radix("15000.0", 10).unwrap()
1754 );
1755 }
1756
1757 #[tokio::test]
1758 async fn test_parse_order_book() {
1759 let exchange = create_test_exchange().await;
1760
1761 let orderbook_data = json!({
1762 "symbol": "BTC/USDT",
1763 "bids": [
1764 [29000.0, 1.5],
1765 [28900.0, 2.0],
1766 [28800.0, 3.5]
1767 ],
1768 "asks": [
1769 [29100.0, 1.0],
1770 [29200.0, 2.5],
1771 [29300.0, 1.8]
1772 ]
1773 });
1774
1775 let result = exchange.parse_order_book(&orderbook_data, Some(1609459200000));
1776 assert!(result.is_ok());
1777
1778 let orderbook = result.unwrap();
1779 assert_eq!(orderbook.symbol, "BTC/USDT");
1780 assert_eq!(orderbook.timestamp, 1609459200000);
1781 assert_eq!(orderbook.bids.len(), 3);
1782 assert_eq!(orderbook.asks.len(), 3);
1783
1784 assert_eq!(
1786 orderbook.bids[0].price,
1787 Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1788 );
1789 assert_eq!(
1790 orderbook.bids[1].price,
1791 Price::from(Decimal::from_str_radix("28900.0", 10).unwrap())
1792 );
1793 assert_eq!(
1794 orderbook.bids[2].price,
1795 Price::from(Decimal::from_str_radix("28800.0", 10).unwrap())
1796 );
1797
1798 assert_eq!(
1800 orderbook.asks[0].price,
1801 Price::from(Decimal::from_str_radix("29100.0", 10).unwrap())
1802 );
1803 assert_eq!(
1804 orderbook.asks[1].price,
1805 Price::from(Decimal::from_str_radix("29200.0", 10).unwrap())
1806 );
1807 assert_eq!(
1808 orderbook.asks[2].price,
1809 Price::from(Decimal::from_str_radix("29300.0", 10).unwrap())
1810 );
1811 }
1812
1813 #[test]
1814 fn test_calculate_fee() {
1815 }
1818
1819 #[test]
1820 fn test_amount_to_precision() {
1821 }
1824
1825 #[test]
1826 fn test_price_to_precision() {
1827 }
1830
1831 #[test]
1832 fn test_has_method() {
1833 }
1836
1837 #[test]
1838 fn test_timeframes() {
1839 }
1842
1843 #[test]
1844 fn test_filter_by_type() {
1845 }
1848}