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::sync::Arc;
19use tokio::sync::RwLock;
20use tracing::{debug, info};
21
22#[derive(Debug, Clone)]
24pub struct ExchangeConfig {
25 pub id: String,
27 pub name: String,
29 pub api_key: Option<String>,
31 pub secret: Option<String>,
33 pub password: Option<String>,
35 pub uid: Option<String>,
37 pub account_id: Option<String>,
39 pub enable_rate_limit: bool,
41 pub rate_limit: f64,
43 pub timeout: u64,
45 pub sandbox: bool,
47 pub user_agent: Option<String>,
49 pub proxy: Option<String>,
51 pub verbose: bool,
53 pub options: HashMap<String, Value>,
55 pub url_overrides: HashMap<String, String>,
57}
58
59impl Default for ExchangeConfig {
60 fn default() -> Self {
61 Self {
62 id: String::new(),
63 name: String::new(),
64 api_key: None,
65 secret: None,
66 password: None,
67 uid: None,
68 account_id: None,
69 enable_rate_limit: true,
70 rate_limit: 2000.0,
71 timeout: 30,
72 sandbox: false,
73 user_agent: Some(format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
74 proxy: None,
75 verbose: false,
76 options: HashMap::new(),
77 url_overrides: HashMap::new(),
78 }
79 }
80}
81
82impl ExchangeConfig {
83 pub fn builder() -> ExchangeConfigBuilder {
99 ExchangeConfigBuilder::default()
100 }
101}
102
103#[derive(Debug, Clone, Default)]
121pub struct ExchangeConfigBuilder {
122 config: ExchangeConfig,
123}
124
125impl ExchangeConfigBuilder {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn id(mut self, id: impl Into<String>) -> Self {
133 self.config.id = id.into();
134 self
135 }
136
137 pub fn name(mut self, name: impl Into<String>) -> Self {
139 self.config.name = name.into();
140 self
141 }
142
143 pub fn api_key(mut self, key: impl Into<String>) -> Self {
145 self.config.api_key = Some(key.into());
146 self
147 }
148
149 pub fn secret(mut self, secret: impl Into<String>) -> Self {
151 self.config.secret = Some(secret.into());
152 self
153 }
154
155 pub fn password(mut self, password: impl Into<String>) -> Self {
157 self.config.password = Some(password.into());
158 self
159 }
160
161 pub fn uid(mut self, uid: impl Into<String>) -> Self {
163 self.config.uid = Some(uid.into());
164 self
165 }
166
167 pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
169 self.config.account_id = Some(account_id.into());
170 self
171 }
172
173 pub fn enable_rate_limit(mut self, enabled: bool) -> Self {
175 self.config.enable_rate_limit = enabled;
176 self
177 }
178
179 pub fn rate_limit(mut self, rate_limit: f64) -> Self {
181 self.config.rate_limit = rate_limit;
182 self
183 }
184
185 pub fn timeout(mut self, seconds: u64) -> Self {
187 self.config.timeout = seconds;
188 self
189 }
190
191 pub fn sandbox(mut self, enabled: bool) -> Self {
193 self.config.sandbox = enabled;
194 self
195 }
196
197 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
199 self.config.user_agent = Some(user_agent.into());
200 self
201 }
202
203 pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
205 self.config.proxy = Some(proxy.into());
206 self
207 }
208
209 pub fn verbose(mut self, enabled: bool) -> Self {
211 self.config.verbose = enabled;
212 self
213 }
214
215 pub fn option(mut self, key: impl Into<String>, value: Value) -> Self {
217 self.config.options.insert(key.into(), value);
218 self
219 }
220
221 pub fn options(mut self, options: HashMap<String, Value>) -> Self {
223 self.config.options.extend(options);
224 self
225 }
226
227 pub fn url_override(mut self, key: impl Into<String>, url: impl Into<String>) -> Self {
229 self.config.url_overrides.insert(key.into(), url.into());
230 self
231 }
232
233 pub fn build(self) -> ExchangeConfig {
235 self.config
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct MarketCache {
242 pub markets: HashMap<String, Market>,
244 pub markets_by_id: HashMap<String, Market>,
246 pub currencies: HashMap<String, Currency>,
248 pub currencies_by_id: HashMap<String, Currency>,
250 pub symbols: Vec<String>,
252 pub codes: Vec<String>,
254 pub ids: Vec<String>,
256 pub loaded: bool,
258}
259
260impl Default for MarketCache {
261 fn default() -> Self {
262 Self {
263 markets: HashMap::new(),
264 markets_by_id: HashMap::new(),
265 currencies: HashMap::new(),
266 currencies_by_id: HashMap::new(),
267 symbols: Vec::new(),
268 codes: Vec::new(),
269 ids: Vec::new(),
270 loaded: false,
271 }
272 }
273}
274
275use crate::exchange::ExchangeCapabilities;
278
279#[derive(Debug)]
281pub struct BaseExchange {
282 pub config: ExchangeConfig,
284 pub http_client: HttpClient,
286 pub rate_limiter: Option<RateLimiter>,
288 pub market_cache: Arc<RwLock<MarketCache>>,
290 pub capabilities: ExchangeCapabilities,
292 pub urls: HashMap<String, String>,
294 pub timeframes: HashMap<String, String>,
296 pub precision_mode: PrecisionMode,
298}
299
300impl BaseExchange {
301 pub fn new(config: ExchangeConfig) -> Result<Self> {
315 info!("Initializing exchange: {}", config.id);
316
317 let http_config = HttpConfig {
318 timeout: config.timeout,
319 #[allow(deprecated)]
320 max_retries: 3,
321 verbose: false,
322 user_agent: config
323 .user_agent
324 .clone()
325 .unwrap_or_else(|| format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
326 return_response_headers: false,
327 proxy: config.proxy.clone(),
328 enable_rate_limit: true,
329 retry_config: None,
330 };
331
332 let http_client = HttpClient::new(http_config)?;
333
334 let rate_limiter = if config.enable_rate_limit {
335 let rate_config = RateLimiterConfig::new(
336 config.rate_limit as u32,
337 std::time::Duration::from_millis(1000),
338 );
339 Some(RateLimiter::new(rate_config))
340 } else {
341 None
342 };
343
344 Ok(Self {
345 config,
346 http_client,
347 rate_limiter,
348 market_cache: Arc::new(RwLock::new(MarketCache::default())),
349 capabilities: ExchangeCapabilities::default(),
350 urls: HashMap::new(),
351 timeframes: HashMap::new(),
352 precision_mode: PrecisionMode::DecimalPlaces,
353 })
354 }
355
356 pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
371 let cache = self.market_cache.write().await;
372
373 if cache.loaded && !reload {
374 debug!("Returning cached markets for {}", self.config.id);
375 return Ok(cache.markets.clone());
376 }
377
378 info!("Loading markets for {}", self.config.id);
379
380 drop(cache);
381
382 Err(Error::not_implemented(
383 "load_markets must be implemented by exchange",
384 ))
385 }
386
387 pub async fn set_markets(
402 &self,
403 markets: Vec<Market>,
404 currencies: Option<Vec<Currency>>,
405 ) -> Result<Vec<Market>> {
406 let mut cache = self.market_cache.write().await;
407
408 cache.markets.clear();
409 cache.markets_by_id.clear();
410 cache.symbols.clear();
411 cache.ids.clear();
412
413 for market in &markets {
414 cache.symbols.push(market.symbol.clone());
415 cache.ids.push(market.id.clone());
416 cache
417 .markets_by_id
418 .insert(market.id.clone(), market.clone());
419 cache.markets.insert(market.symbol.clone(), market.clone());
420 }
421
422 if let Some(currencies) = currencies {
423 cache.currencies.clear();
424 cache.currencies_by_id.clear();
425 cache.codes.clear();
426
427 for currency in currencies {
428 cache.codes.push(currency.code.clone());
429 cache
430 .currencies_by_id
431 .insert(currency.id.clone(), currency.clone());
432 cache.currencies.insert(currency.code.clone(), currency);
433 }
434 }
435
436 cache.loaded = true;
437 info!(
438 "Loaded {} markets and {} currencies for {}",
439 cache.markets.len(),
440 cache.currencies.len(),
441 self.config.id
442 );
443
444 Ok(markets)
445 }
446
447 pub async fn market(&self, symbol: &str) -> Result<Market> {
461 let cache = self.market_cache.read().await;
462
463 if !cache.loaded {
464 drop(cache);
465 return Err(Error::exchange(
466 "-1",
467 "Markets not loaded. Call load_markets() first.",
468 ));
469 }
470
471 cache
472 .markets
473 .get(symbol)
474 .cloned()
475 .ok_or_else(|| Error::bad_symbol(format!("Market {} not found", symbol)))
476 }
477
478 pub async fn market_by_id(&self, id: &str) -> Result<Market> {
492 let cache = self.market_cache.read().await;
493
494 cache
495 .markets_by_id
496 .get(id)
497 .cloned()
498 .ok_or_else(|| Error::bad_symbol(format!("Market with id {} not found", id)))
499 }
500
501 pub async fn currency(&self, code: &str) -> Result<Currency> {
515 let cache = self.market_cache.read().await;
516
517 cache
518 .currencies
519 .get(code)
520 .cloned()
521 .ok_or_else(|| Error::bad_symbol(format!("Currency {} not found", code)))
522 }
523
524 pub async fn symbols(&self) -> Result<Vec<String>> {
530 let cache = self.market_cache.read().await;
531 Ok(cache.symbols.clone())
532 }
533
534 pub async fn throttle(&self) -> Result<()> {
542 if let Some(limiter) = &self.rate_limiter {
543 limiter.acquire(1).await;
544 }
545 Ok(())
546 }
547
548 pub fn check_required_credentials(&self) -> Result<()> {
558 if self.config.api_key.is_none() {
559 return Err(Error::authentication("API key is required"));
560 }
561 if self.config.secret.is_none() {
562 return Err(Error::authentication("API secret is required"));
563 }
564 Ok(())
565 }
566
567 pub fn nonce(&self) -> i64 {
573 crate::time::milliseconds()
574 }
575
576 pub fn build_query_string(&self, params: &HashMap<String, Value>) -> String {
586 if params.is_empty() {
587 return String::new();
588 }
589
590 let pairs: Vec<String> = params
591 .iter()
592 .map(|(k, v)| {
593 let value_str = match v {
594 Value::String(s) => s.clone(),
595 Value::Number(n) => n.to_string(),
596 Value::Bool(b) => b.to_string(),
597 _ => v.to_string(),
598 };
599 format!("{}={}", k, urlencoding::encode(&value_str))
600 })
601 .collect();
602
603 pairs.join("&")
604 }
605
606 pub fn parse_json(&self, response: &str) -> Result<Value> {
620 serde_json::from_str(response).map_err(|e| Error::invalid_request(e.to_string()))
621 }
622
623 pub fn handle_http_error(&self, status_code: u16, response: &str) -> Error {
634 match status_code {
635 400 => Error::invalid_request(response.to_string()),
636 401 | 403 => Error::authentication(response.to_string()),
637 404 => Error::invalid_request(format!("Endpoint not found: {}", response)),
638 429 => Error::rate_limit(response.to_string(), None),
639 500..=599 => Error::exchange(status_code.to_string(), response),
640 _ => Error::network(format!("HTTP {}: {}", status_code, response)),
641 }
642 }
643
644 pub fn safe_string(&self, dict: &Value, key: &str) -> Option<String> {
655 dict.get(key)
656 .and_then(|v| v.as_str())
657 .map(|s| s.to_string())
658 }
659
660 pub fn safe_integer(&self, dict: &Value, key: &str) -> Option<i64> {
671 dict.get(key).and_then(|v| v.as_i64())
672 }
673
674 pub fn safe_float(&self, dict: &Value, key: &str) -> Option<f64> {
685 dict.get(key).and_then(|v| v.as_f64())
686 }
687
688 pub fn safe_bool(&self, dict: &Value, key: &str) -> Option<bool> {
699 dict.get(key).and_then(|v| v.as_bool())
700 }
701
702 pub fn parse_ticker(&self, ticker_data: &Value, market: Option<&Market>) -> Result<Ticker> {
721 let symbol = if let Some(m) = market {
722 m.symbol.clone()
723 } else {
724 self.safe_string(ticker_data, "symbol")
725 .ok_or_else(|| ParseError::missing_field("symbol"))?
726 };
727
728 let timestamp = self.safe_integer(ticker_data, "timestamp").unwrap_or(0);
729
730 Ok(Ticker {
731 symbol,
732 timestamp,
733 datetime: self.safe_string(ticker_data, "datetime"),
734 high: self.safe_decimal(ticker_data, "high").map(Price::new),
735 low: self.safe_decimal(ticker_data, "low").map(Price::new),
736 bid: self.safe_decimal(ticker_data, "bid").map(Price::new),
737 bid_volume: self.safe_decimal(ticker_data, "bidVolume").map(Amount::new),
738 ask: self.safe_decimal(ticker_data, "ask").map(Price::new),
739 ask_volume: self.safe_decimal(ticker_data, "askVolume").map(Amount::new),
740 vwap: self.safe_decimal(ticker_data, "vwap").map(Price::new),
741 open: self.safe_decimal(ticker_data, "open").map(Price::new),
742 close: self.safe_decimal(ticker_data, "close").map(Price::new),
743 last: self.safe_decimal(ticker_data, "last").map(Price::new),
744 previous_close: self
745 .safe_decimal(ticker_data, "previousClose")
746 .map(Price::new),
747 change: self.safe_decimal(ticker_data, "change").map(Price::new),
748 percentage: self.safe_decimal(ticker_data, "percentage"),
749 average: self.safe_decimal(ticker_data, "average").map(Price::new),
750 base_volume: self
751 .safe_decimal(ticker_data, "baseVolume")
752 .map(Amount::new),
753 quote_volume: self
754 .safe_decimal(ticker_data, "quoteVolume")
755 .map(Amount::new),
756 info: std::collections::HashMap::new(),
757 })
758 }
759
760 pub fn parse_trade(&self, trade_data: &Value, market: Option<&Market>) -> Result<Trade> {
775 let symbol = if let Some(m) = market {
776 m.symbol.clone()
777 } else {
778 self.safe_string(trade_data, "symbol")
779 .ok_or_else(|| ParseError::missing_field("symbol"))?
780 };
781
782 let side = self
783 .safe_string(trade_data, "side")
784 .and_then(|s| match s.to_lowercase().as_str() {
785 "buy" => Some(OrderSide::Buy),
786 "sell" => Some(OrderSide::Sell),
787 _ => None,
788 })
789 .ok_or_else(|| ParseError::missing_field("side"))?;
790
791 let trade_type =
792 self.safe_string(trade_data, "type")
793 .and_then(|t| match t.to_lowercase().as_str() {
794 "limit" => Some(OrderType::Limit),
795 "market" => Some(OrderType::Market),
796 _ => None,
797 });
798
799 let taker_or_maker = self.safe_string(trade_data, "takerOrMaker").and_then(|s| {
800 match s.to_lowercase().as_str() {
801 "taker" => Some(TakerOrMaker::Taker),
802 "maker" => Some(TakerOrMaker::Maker),
803 _ => None,
804 }
805 });
806
807 Ok(Trade {
808 id: self.safe_string(trade_data, "id"),
809 order: self.safe_string(trade_data, "orderId"),
810 timestamp: self.safe_integer(trade_data, "timestamp").unwrap_or(0),
811 datetime: self.safe_string(trade_data, "datetime"),
812 symbol,
813 trade_type,
814 side,
815 taker_or_maker,
816 price: Price::new(
817 self.safe_decimal(trade_data, "price")
818 .unwrap_or(Decimal::ZERO),
819 ),
820 amount: Amount::new(
821 self.safe_decimal(trade_data, "amount")
822 .unwrap_or(Decimal::ZERO),
823 ),
824 cost: self.safe_decimal(trade_data, "cost").map(Cost::new),
825 fee: None,
826 info: if let Some(obj) = trade_data.as_object() {
827 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
828 } else {
829 HashMap::new()
830 },
831 })
832 }
833
834 pub fn parse_order(&self, order_data: &Value, market: Option<&Market>) -> Result<Order> {
845 let symbol = if let Some(m) = market {
846 m.symbol.clone()
847 } else {
848 self.safe_string(order_data, "symbol")
849 .ok_or_else(|| ParseError::missing_field("symbol"))?
850 };
851
852 let order_type = self
853 .safe_string(order_data, "type")
854 .and_then(|t| match t.to_lowercase().as_str() {
855 "limit" => Some(OrderType::Limit),
856 "market" => Some(OrderType::Market),
857 _ => None,
858 })
859 .unwrap_or(OrderType::Limit);
860
861 let side = self
862 .safe_string(order_data, "side")
863 .and_then(|s| match s.to_lowercase().as_str() {
864 "buy" => Some(OrderSide::Buy),
865 "sell" => Some(OrderSide::Sell),
866 _ => None,
867 })
868 .unwrap_or(OrderSide::Buy);
869
870 let status_str = self
871 .safe_string(order_data, "status")
872 .unwrap_or_else(|| "open".to_string());
873 let status = match status_str.to_lowercase().as_str() {
874 "open" => OrderStatus::Open,
875 "closed" => OrderStatus::Closed,
876 "canceled" | "cancelled" => OrderStatus::Canceled,
877 "expired" => OrderStatus::Expired,
878 "rejected" => OrderStatus::Rejected,
879 _ => OrderStatus::Open,
880 };
881
882 let id = self
883 .safe_string(order_data, "id")
884 .unwrap_or_else(|| format!("order_{}", chrono::Utc::now().timestamp_millis()));
885
886 let amount = self
887 .safe_decimal(order_data, "amount")
888 .unwrap_or(Decimal::ZERO);
889
890 Ok(Order {
891 id,
892 client_order_id: self.safe_string(order_data, "clientOrderId"),
893 timestamp: self.safe_integer(order_data, "timestamp"),
894 datetime: self.safe_string(order_data, "datetime"),
895 last_trade_timestamp: self.safe_integer(order_data, "lastTradeTimestamp"),
896 symbol,
897 order_type,
898 time_in_force: self.safe_string(order_data, "timeInForce"),
899 post_only: self
900 .safe_string(order_data, "postOnly")
901 .and_then(|s| s.parse::<bool>().ok()),
902 reduce_only: self
903 .safe_string(order_data, "reduceOnly")
904 .and_then(|s| s.parse::<bool>().ok()),
905 side,
906 price: self.safe_decimal(order_data, "price"),
907 stop_price: self.safe_decimal(order_data, "stopPrice"),
908 trigger_price: self.safe_decimal(order_data, "triggerPrice"),
909 take_profit_price: self.safe_decimal(order_data, "takeProfitPrice"),
910 stop_loss_price: self.safe_decimal(order_data, "stopLossPrice"),
911 average: self.safe_decimal(order_data, "average"),
912 amount,
913 filled: self.safe_decimal(order_data, "filled"),
914 remaining: self.safe_decimal(order_data, "remaining"),
915 cost: self.safe_decimal(order_data, "cost"),
916 status,
917 fee: None,
918 fees: None,
919 trades: None,
920 trailing_delta: self.safe_decimal(order_data, "trailingDelta"),
921 trailing_percent: self.safe_decimal(order_data, "trailingPercent"),
922 activation_price: self.safe_decimal(order_data, "activationPrice"),
923 callback_rate: self.safe_decimal(order_data, "callbackRate"),
924 working_type: self.safe_string(order_data, "workingType"),
925 info: if let Some(obj) = order_data.as_object() {
926 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
927 } else {
928 HashMap::new()
929 },
930 })
931 }
932
933 pub fn parse_balance(&self, balance_data: &Value) -> Result<Balance> {
943 let mut balance = Balance::new();
944
945 if let Some(obj) = balance_data.as_object() {
946 for (currency, balance_info) in obj {
947 if currency == "timestamp" || currency == "datetime" || currency == "info" {
948 continue;
949 }
950 let free = self
951 .safe_decimal(balance_info, "free")
952 .unwrap_or(Decimal::ZERO);
953 let used = self
954 .safe_decimal(balance_info, "used")
955 .unwrap_or(Decimal::ZERO);
956 let total = self
957 .safe_decimal(balance_info, "total")
958 .unwrap_or(free + used);
959
960 let entry = BalanceEntry { free, used, total };
961 balance.set(currency.clone(), entry);
962 }
963 }
964
965 if let Some(obj) = balance_data.as_object() {
966 balance.info = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
967 }
968
969 Ok(balance)
970 }
971
972 pub fn parse_order_book(
983 &self,
984 orderbook_data: &Value,
985 timestamp: Option<i64>,
986 ) -> Result<OrderBook> {
987 let mut bids_side = OrderBookSide::new();
988 let mut asks_side = OrderBookSide::new();
989
990 if let Some(bids_array) = orderbook_data.get("bids").and_then(|v| v.as_array()) {
991 for bid in bids_array {
992 if let Some(arr) = bid.as_array() {
993 if arr.len() >= 2 {
994 let price = self.safe_decimal_from_value(&arr[0]);
995 let amount = self.safe_decimal_from_value(&arr[1]);
996 if let (Some(p), Some(a)) = (price, amount) {
997 bids_side.push(OrderBookEntry {
998 price: Price::new(p),
999 amount: Amount::new(a),
1000 });
1001 }
1002 }
1003 }
1004 }
1005 }
1006
1007 if let Some(asks_array) = orderbook_data.get("asks").and_then(|v| v.as_array()) {
1008 for ask in asks_array {
1009 if let Some(arr) = ask.as_array() {
1010 if arr.len() >= 2 {
1011 let price = self.safe_decimal_from_value(&arr[0]);
1012 let amount = self.safe_decimal_from_value(&arr[1]);
1013 if let (Some(p), Some(a)) = (price, amount) {
1014 asks_side.push(OrderBookEntry {
1015 price: Price::new(p),
1016 amount: Amount::new(a),
1017 });
1018 }
1019 }
1020 }
1021 }
1022 }
1023
1024 Ok(OrderBook {
1025 symbol: self
1026 .safe_string(orderbook_data, "symbol")
1027 .unwrap_or_default(),
1028 bids: bids_side,
1029 asks: asks_side,
1030 timestamp: timestamp
1031 .or_else(|| self.safe_integer(orderbook_data, "timestamp"))
1032 .unwrap_or(0),
1033 datetime: self.safe_string(orderbook_data, "datetime"),
1034 nonce: self.safe_integer(orderbook_data, "nonce"),
1035 info: if let Some(obj) = orderbook_data.as_object() {
1036 obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1037 } else {
1038 HashMap::new()
1039 },
1040 buffered_deltas: std::collections::VecDeque::new(),
1042 bids_map: std::collections::BTreeMap::new(),
1043 asks_map: std::collections::BTreeMap::new(),
1044 is_synced: false,
1045 needs_resync: false,
1047 last_resync_time: 0,
1048 })
1049 }
1050
1051 fn safe_decimal(&self, data: &Value, key: &str) -> Option<Decimal> {
1053 data.get(key).and_then(|v| self.safe_decimal_from_value(v))
1054 }
1055
1056 fn safe_decimal_from_value(&self, value: &Value) -> Option<Decimal> {
1058 match value {
1059 Value::Number(n) => {
1060 if let Some(f) = n.as_f64() {
1061 Decimal::from_f64_retain(f)
1062 } else {
1063 None
1064 }
1065 }
1066 Value::String(s) => Decimal::from_str(s).ok(),
1067 _ => None,
1068 }
1069 }
1070
1071 pub async fn calculate_fee(
1090 &self,
1091 symbol: &str,
1092 _order_type: OrderType,
1093 _side: OrderSide,
1094 amount: Decimal,
1095 price: Decimal,
1096 taker_or_maker: Option<&str>,
1097 ) -> Result<Fee> {
1098 let market = self.market(symbol).await?;
1099
1100 let rate = if let Some(tom) = taker_or_maker {
1101 if tom == "taker" {
1102 market.taker.unwrap_or(Decimal::ZERO)
1103 } else {
1104 market.maker.unwrap_or(Decimal::ZERO)
1105 }
1106 } else {
1107 market.taker.unwrap_or(Decimal::ZERO)
1108 };
1109
1110 let cost = amount * price;
1111 let fee_cost = cost * rate;
1112
1113 Ok(Fee {
1114 currency: market.quote.clone(),
1115 cost: fee_cost,
1116 rate: Some(rate),
1117 })
1118 }
1119
1120 pub async fn amount_to_precision(&self, symbol: &str, amount: Decimal) -> Result<Decimal> {
1122 let market = self.market(symbol).await?;
1123 match market.precision.amount {
1124 Some(precision_value) => Ok(self.round_to_precision(amount, precision_value)),
1125 None => Ok(amount),
1126 }
1127 }
1128
1129 pub async fn price_to_precision(&self, symbol: &str, price: Decimal) -> Result<Decimal> {
1131 let market = self.market(symbol).await?;
1132 match market.precision.price {
1133 Some(precision_value) => Ok(self.round_to_precision(price, precision_value)),
1134 None => Ok(price),
1135 }
1136 }
1137
1138 pub async fn cost_to_precision(&self, symbol: &str, cost: Decimal) -> Result<Decimal> {
1140 let market = self.market(symbol).await?;
1141 match market.precision.price {
1142 Some(precision_value) => Ok(self.round_to_precision(cost, precision_value)),
1143 None => Ok(cost),
1144 }
1145 }
1146
1147 fn round_to_precision(&self, value: Decimal, precision_value: Decimal) -> Decimal {
1151 if precision_value < Decimal::ONE {
1152 let steps = (value / precision_value).round();
1154 steps * precision_value
1155 } else {
1156 let digits = precision_value.to_u32().unwrap_or(8);
1158 let multiplier = Decimal::from(10_i64.pow(digits));
1159 let scaled = value * multiplier;
1160 let rounded = scaled.round();
1161 rounded / multiplier
1162 }
1163 }
1164
1165 pub async fn calculate_cost(
1177 &self,
1178 symbol: &str,
1179 amount: Decimal,
1180 price: Decimal,
1181 ) -> Result<Decimal> {
1182 let _market = self.market(symbol).await?;
1183 Ok(amount * price)
1184 }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190
1191 #[tokio::test]
1192 async fn test_base_exchange_creation() {
1193 let config = ExchangeConfig {
1194 id: "test".to_string(),
1195 name: "Test Exchange".to_string(),
1196 ..Default::default()
1197 };
1198
1199 let exchange = BaseExchange::new(config).unwrap();
1200 assert_eq!(exchange.config.id, "test");
1201 assert!(exchange.rate_limiter.is_some());
1202 }
1203
1204 #[tokio::test]
1205 async fn test_market_cache() {
1206 let config = ExchangeConfig {
1207 id: "test".to_string(),
1208 ..Default::default()
1209 };
1210
1211 let exchange = BaseExchange::new(config).unwrap();
1212
1213 let markets = vec![Market {
1214 id: "btcusdt".to_string(),
1215 symbol: "BTC/USDT".to_string(),
1216 parsed_symbol: None,
1217 base: "BTC".to_string(),
1218 quote: "USDT".to_string(),
1219 active: true,
1220 market_type: MarketType::Spot,
1221 margin: false,
1222 settle: None,
1223 base_id: None,
1224 quote_id: None,
1225 settle_id: None,
1226 contract: None,
1227 linear: None,
1228 inverse: None,
1229 contract_size: None,
1230 expiry: None,
1231 expiry_datetime: None,
1232 strike: None,
1233 option_type: None,
1234 precision: Default::default(),
1235 limits: Default::default(),
1236 maker: None,
1237 taker: None,
1238 percentage: None,
1239 tier_based: None,
1240 fee_side: None,
1241 info: Default::default(),
1242 }];
1243
1244 let _ = exchange.set_markets(markets, None).await.unwrap();
1245
1246 let market = exchange.market("BTC/USDT").await.unwrap();
1247 assert_eq!(market.symbol, "BTC/USDT");
1248
1249 let symbols = exchange.symbols().await.unwrap();
1250 assert_eq!(symbols.len(), 1);
1251 }
1252
1253 #[test]
1254 fn test_build_query_string() {
1255 let config = ExchangeConfig::default();
1256 let exchange = BaseExchange::new(config).unwrap();
1257
1258 let mut params = HashMap::new();
1259 params.insert("symbol".to_string(), Value::String("BTC/USDT".to_string()));
1260 params.insert("limit".to_string(), Value::Number(100.into()));
1261
1262 let query = exchange.build_query_string(¶ms);
1263 assert!(query.contains("symbol="));
1264 assert!(query.contains("limit="));
1265 }
1266
1267 #[test]
1268 fn test_capabilities() {
1269 let default_caps = ExchangeCapabilities::default();
1271 assert!(!default_caps.has("fetchMarkets"));
1272 assert!(!default_caps.has("fetchOHLCV"));
1273
1274 let public_caps = ExchangeCapabilities::public_only();
1276 assert!(public_caps.has("fetchMarkets"));
1277 assert!(public_caps.has("fetchTicker"));
1278 assert!(public_caps.has("fetchOHLCV"));
1279 assert!(!public_caps.has("createOrder"));
1280
1281 let all_caps = ExchangeCapabilities::all();
1283 assert!(all_caps.has("fetchMarkets"));
1284 assert!(all_caps.has("fetchOHLCV"));
1285 assert!(all_caps.has("createOrder"));
1286 }
1287
1288 #[test]
1289 fn test_exchange_config_builder() {
1290 let config = ExchangeConfigBuilder::new()
1291 .id("binance")
1292 .name("Binance")
1293 .api_key("test-key")
1294 .secret("test-secret")
1295 .sandbox(true)
1296 .timeout(60)
1297 .verbose(true)
1298 .build();
1299
1300 assert_eq!(config.id, "binance");
1301 assert_eq!(config.name, "Binance");
1302 assert_eq!(config.api_key, Some("test-key".to_string()));
1303 assert_eq!(config.secret, Some("test-secret".to_string()));
1304 assert!(config.sandbox);
1305 assert_eq!(config.timeout, 60);
1306 assert!(config.verbose);
1307 }
1308
1309 #[test]
1310 fn test_exchange_config_builder_defaults() {
1311 let config = ExchangeConfigBuilder::new().build();
1312
1313 assert_eq!(config.id, "");
1314 assert_eq!(config.api_key, None);
1315 assert!(config.enable_rate_limit);
1316 assert_eq!(config.timeout, 30);
1317 assert!(!config.sandbox);
1318 }
1319
1320 #[test]
1321 fn test_exchange_config_builder_from_config() {
1322 let config = ExchangeConfig::builder().id("test").api_key("key").build();
1323
1324 assert_eq!(config.id, "test");
1325 assert_eq!(config.api_key, Some("key".to_string()));
1326 }
1327}
1328
1329#[cfg(test)]
1330mod parse_tests {
1331 use super::*;
1332 use serde_json::json;
1333
1334 async fn create_test_exchange() -> BaseExchange {
1335 let config = ExchangeConfig {
1336 id: "".to_string(),
1337 name: "".to_string(),
1338 api_key: None,
1339 secret: None,
1340 password: None,
1341 uid: None,
1342 timeout: 10000,
1343 sandbox: false,
1344 user_agent: None,
1345 enable_rate_limit: true,
1346 verbose: false,
1347 account_id: None,
1348 rate_limit: 0.0,
1349 proxy: None,
1350 options: Default::default(),
1351 url_overrides: Default::default(),
1352 };
1353
1354 let exchange = BaseExchange::new(config).unwrap();
1355
1356 let cache = MarketCache::default();
1358 *exchange.market_cache.write().await = cache;
1359
1360 exchange
1361 }
1362
1363 #[tokio::test]
1364 async fn test_parse_ticker() {
1365 let exchange = create_test_exchange().await;
1366
1367 let ticker_data = json!({
1368 "symbol": "BTC/USDT",
1369 "timestamp": 1609459200000i64,
1370 "datetime": "2021-01-01T00:00:00.000Z",
1371 "high": 30000.0,
1372 "low": 28000.0,
1373 "bid": 29000.0,
1374 "bidVolume": 10.5,
1375 "ask": 29100.0,
1376 "askVolume": 8.3,
1377 "vwap": 29500.0,
1378 "open": 28500.0,
1379 "close": 29000.0,
1380 "last": 29000.0,
1381 "previousClose": 28500.0,
1382 "change": 500.0,
1383 "percentage": 1.75,
1384 "average": 28750.0,
1385 "baseVolume": 1000.0,
1386 "quoteVolume": 29000000.0
1387 });
1388
1389 let result = exchange.parse_ticker(&ticker_data, None);
1390 assert!(result.is_ok());
1391
1392 let ticker = result.unwrap();
1393 assert_eq!(ticker.symbol, "BTC/USDT");
1394 assert_eq!(ticker.timestamp, 1609459200000);
1395 assert_eq!(
1396 ticker.high,
1397 Some(Price::from(Decimal::from_str_radix("30000.0", 10).unwrap()))
1398 );
1399 assert_eq!(
1400 ticker.low,
1401 Some(Price::from(Decimal::from_str_radix("28000.0", 10).unwrap()))
1402 );
1403 assert_eq!(
1404 ticker.bid,
1405 Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1406 );
1407 assert_eq!(
1408 ticker.ask,
1409 Some(Price::from(Decimal::from_str_radix("29100.0", 10).unwrap()))
1410 );
1411 assert_eq!(
1412 ticker.last,
1413 Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1414 );
1415 assert_eq!(
1416 ticker.base_volume,
1417 Some(Amount::from(Decimal::from_str_radix("1000.0", 10).unwrap()))
1418 );
1419 assert_eq!(
1420 ticker.quote_volume,
1421 Some(Amount::from(
1422 Decimal::from_str_radix("29000000.0", 10).unwrap()
1423 ))
1424 );
1425 }
1426
1427 #[tokio::test]
1428 async fn test_parse_trade() {
1429 let exchange = create_test_exchange().await;
1430
1431 let trade_data = json!({
1432 "id": "12345",
1433 "symbol": "BTC/USDT",
1434 "timestamp": 1609459200000i64,
1435 "datetime": "2021-01-01T00:00:00.000Z",
1436 "order": "order123",
1437 "type": "limit",
1438 "side": "buy",
1439 "takerOrMaker": "taker",
1440 "price": 29000.0,
1441 "amount": 0.5,
1442 "cost": 14500.0
1443 });
1444
1445 let result = exchange.parse_trade(&trade_data, None);
1446 assert!(result.is_ok());
1447
1448 let trade = result.unwrap();
1449 assert_eq!(trade.id, Some("12345".to_string()));
1450 assert_eq!(trade.symbol, "BTC/USDT");
1451 assert_eq!(trade.timestamp, 1609459200000);
1452 assert_eq!(trade.side, OrderSide::Buy);
1453 assert_eq!(
1454 trade.price,
1455 Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1456 );
1457 assert_eq!(
1458 trade.amount,
1459 Amount::from(Decimal::from_str_radix("0.5", 10).unwrap())
1460 );
1461 assert_eq!(
1462 trade.cost,
1463 Some(Cost::from(Decimal::from_str_radix("14500.0", 10).unwrap()))
1464 );
1465 assert_eq!(trade.taker_or_maker, Some(TakerOrMaker::Taker));
1466 }
1467
1468 #[tokio::test]
1469 async fn test_parse_order() {
1470 let exchange = create_test_exchange().await;
1471
1472 let order_data = json!({
1473 "id": "order123",
1474 "clientOrderId": "client456",
1475 "symbol": "BTC/USDT",
1476 "timestamp": 1609459200000i64,
1477 "datetime": "2021-01-01T00:00:00.000Z",
1478 "lastTradeTimestamp": 1609459300000i64,
1479 "status": "closed",
1480 "type": "limit",
1481 "timeInForce": "GTC",
1482 "side": "buy",
1483 "price": 29000.0,
1484 "average": 29050.0,
1485 "amount": 0.5,
1486 "filled": 0.5,
1487 "remaining": 0.0,
1488 "cost": 14525.0
1489 });
1490
1491 let result = exchange.parse_order(&order_data, None);
1492 assert!(result.is_ok());
1493
1494 let order = result.unwrap();
1495 assert_eq!(order.id, "order123");
1496 assert_eq!(order.client_order_id, Some("client456".to_string()));
1497 assert_eq!(order.symbol, "BTC/USDT");
1498 assert_eq!(order.status, OrderStatus::Closed);
1499 assert_eq!(order.order_type, OrderType::Limit);
1500 assert_eq!(order.side, OrderSide::Buy);
1501 assert_eq!(order.time_in_force, Some("GTC".to_string()));
1502 assert_eq!(
1503 order.price,
1504 Some(Decimal::from_str_radix("29000.0", 10).unwrap())
1505 );
1506 assert_eq!(order.amount, Decimal::from_str_radix("0.5", 10).unwrap());
1507 assert_eq!(
1508 order.filled,
1509 Some(Decimal::from_str_radix("0.5", 10).unwrap())
1510 );
1511 assert_eq!(
1512 order.remaining,
1513 Some(Decimal::from_str_radix("0.0", 10).unwrap())
1514 );
1515 assert_eq!(
1516 order.cost,
1517 Some(Decimal::from_str_radix("14525.0", 10).unwrap())
1518 );
1519 }
1520
1521 #[tokio::test]
1522 async fn test_parse_balance() {
1523 let exchange = create_test_exchange().await;
1524
1525 let balance_data = json!({
1526 "timestamp": 1609459200000i64,
1527 "datetime": "2021-01-01T00:00:00.000Z",
1528 "BTC": {
1529 "free": 1.5,
1530 "used": 0.5,
1531 "total": 2.0
1532 },
1533 "USDT": {
1534 "free": 10000.0,
1535 "used": 5000.0,
1536 "total": 15000.0
1537 }
1538 });
1539
1540 let result = exchange.parse_balance(&balance_data);
1541 assert!(result.is_ok());
1542
1543 let balance = result.unwrap();
1544 assert_eq!(balance.balances.len(), 2);
1545
1546 let btc_balance = balance.balances.get("BTC").unwrap();
1547 assert_eq!(
1548 btc_balance.free,
1549 Decimal::from_str_radix("1.5", 10).unwrap()
1550 );
1551 assert_eq!(
1552 btc_balance.used,
1553 Decimal::from_str_radix("0.5", 10).unwrap()
1554 );
1555 assert_eq!(
1556 btc_balance.total,
1557 Decimal::from_str_radix("2.0", 10).unwrap()
1558 );
1559
1560 let usdt_balance = balance.balances.get("USDT").unwrap();
1561 assert_eq!(
1562 usdt_balance.free,
1563 Decimal::from_str_radix("10000.0", 10).unwrap()
1564 );
1565 assert_eq!(
1566 usdt_balance.used,
1567 Decimal::from_str_radix("5000.0", 10).unwrap()
1568 );
1569 assert_eq!(
1570 usdt_balance.total,
1571 Decimal::from_str_radix("15000.0", 10).unwrap()
1572 );
1573 }
1574
1575 #[tokio::test]
1576 async fn test_parse_order_book() {
1577 let exchange = create_test_exchange().await;
1578
1579 let orderbook_data = json!({
1580 "symbol": "BTC/USDT",
1581 "bids": [
1582 [29000.0, 1.5],
1583 [28900.0, 2.0],
1584 [28800.0, 3.5]
1585 ],
1586 "asks": [
1587 [29100.0, 1.0],
1588 [29200.0, 2.5],
1589 [29300.0, 1.8]
1590 ]
1591 });
1592
1593 let result = exchange.parse_order_book(&orderbook_data, Some(1609459200000));
1594 assert!(result.is_ok());
1595
1596 let orderbook = result.unwrap();
1597 assert_eq!(orderbook.symbol, "BTC/USDT");
1598 assert_eq!(orderbook.timestamp, 1609459200000);
1599 assert_eq!(orderbook.bids.len(), 3);
1600 assert_eq!(orderbook.asks.len(), 3);
1601
1602 assert_eq!(
1604 orderbook.bids[0].price,
1605 Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1606 );
1607 assert_eq!(
1608 orderbook.bids[1].price,
1609 Price::from(Decimal::from_str_radix("28900.0", 10).unwrap())
1610 );
1611 assert_eq!(
1612 orderbook.bids[2].price,
1613 Price::from(Decimal::from_str_radix("28800.0", 10).unwrap())
1614 );
1615
1616 assert_eq!(
1618 orderbook.asks[0].price,
1619 Price::from(Decimal::from_str_radix("29100.0", 10).unwrap())
1620 );
1621 assert_eq!(
1622 orderbook.asks[1].price,
1623 Price::from(Decimal::from_str_radix("29200.0", 10).unwrap())
1624 );
1625 assert_eq!(
1626 orderbook.asks[2].price,
1627 Price::from(Decimal::from_str_radix("29300.0", 10).unwrap())
1628 );
1629 }
1630
1631 #[test]
1632 fn test_calculate_fee() {
1633 }
1636
1637 #[test]
1638 fn test_amount_to_precision() {
1639 }
1642
1643 #[test]
1644 fn test_price_to_precision() {
1645 }
1648
1649 #[test]
1650 fn test_has_method() {
1651 }
1654
1655 #[test]
1656 fn test_timeframes() {
1657 }
1660
1661 #[test]
1662 fn test_filter_by_type() {
1663 }
1666}