ccxt_core/
base_exchange.rs

1//! Base exchange implementation
2//!
3//! Provides core functionality for all exchange implementations:
4//! - Market data caching
5//! - API configuration management
6//! - Common request/response handling
7//! - Authentication and signing
8//! - Rate limiting
9
10use 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// Lint: wildcard_imports
16// Reason: Types module contains many related types used throughout this file;
17// explicit imports would be excessively verbose and harder to maintain
18#[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/// Exchange configuration
31#[derive(Debug, Clone)]
32pub struct ExchangeConfig {
33    /// Exchange identifier
34    pub id: String,
35    /// Exchange display name
36    pub name: String,
37    /// API key for authentication (automatically zeroed on drop)
38    pub api_key: Option<SecretString>,
39    /// API secret for authentication (automatically zeroed on drop)
40    pub secret: Option<SecretString>,
41    /// Password (required by some exchanges, automatically zeroed on drop)
42    pub password: Option<SecretString>,
43    /// User ID (required by some exchanges)
44    pub uid: Option<String>,
45    /// Account ID
46    pub account_id: Option<String>,
47    /// Enable rate limiting
48    pub enable_rate_limit: bool,
49    /// Rate limit in requests per second
50    pub rate_limit: u32,
51    /// Request timeout (default: 30 seconds)
52    pub timeout: Duration,
53    /// TCP connection timeout (default: 10 seconds)
54    pub connect_timeout: Duration,
55    /// Retry policy
56    pub retry_policy: Option<RetryPolicy>,
57    /// Enable sandbox/testnet mode
58    pub sandbox: bool,
59    /// Custom user agent string
60    pub user_agent: Option<String>,
61    /// HTTP proxy configuration
62    pub proxy: Option<ProxyConfig>,
63    /// Enable verbose logging
64    pub verbose: bool,
65    /// Custom exchange-specific options
66    pub options: HashMap<String, Value>,
67    /// URL overrides for mocking/testing
68    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    /// Create a new configuration builder
98    ///
99    /// # Example
100    ///
101    /// ```rust
102    /// use ccxt_core::base_exchange::ExchangeConfig;
103    ///
104    /// let config = ExchangeConfig::builder()
105    ///     .id("binance")
106    ///     .name("Binance")
107    ///     .api_key("your-api-key")
108    ///     .secret("your-secret")
109    ///     .sandbox(true)
110    ///     .build();
111    /// ```
112    pub fn builder() -> ExchangeConfigBuilder {
113        ExchangeConfigBuilder::default()
114    }
115}
116
117/// Builder for `ExchangeConfig`
118///
119/// Provides a fluent API for constructing exchange configurations.
120///
121/// # Example
122///
123/// ```rust
124/// use ccxt_core::base_exchange::ExchangeConfigBuilder;
125/// use std::time::Duration;
126///
127/// let config = ExchangeConfigBuilder::new()
128///     .id("binance")
129///     .name("Binance")
130///     .api_key("your-api-key")
131///     .secret("your-secret")
132///     .timeout(Duration::from_secs(60))
133///     .build();
134/// ```
135#[derive(Debug, Clone, Default)]
136pub struct ExchangeConfigBuilder {
137    config: ExchangeConfig,
138}
139
140impl ExchangeConfigBuilder {
141    /// Create a new builder with default configuration
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    /// Set the exchange identifier
147    pub fn id(mut self, id: impl Into<String>) -> Self {
148        self.config.id = id.into();
149        self
150    }
151
152    /// Set the exchange display name
153    pub fn name(mut self, name: impl Into<String>) -> Self {
154        self.config.name = name.into();
155        self
156    }
157
158    /// Set the API key for authentication
159    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    /// Set the API secret for authentication
165    pub fn secret(mut self, secret: impl Into<String>) -> Self {
166        self.config.secret = Some(SecretString::new(secret));
167        self
168    }
169
170    /// Set the password (required by some exchanges)
171    pub fn password(mut self, password: impl Into<String>) -> Self {
172        self.config.password = Some(SecretString::new(password));
173        self
174    }
175
176    /// Set the user ID (required by some exchanges)
177    pub fn uid(mut self, uid: impl Into<String>) -> Self {
178        self.config.uid = Some(uid.into());
179        self
180    }
181
182    /// Set the account ID
183    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    /// Enable or disable rate limiting
189    pub fn enable_rate_limit(mut self, enabled: bool) -> Self {
190        self.config.enable_rate_limit = enabled;
191        self
192    }
193
194    /// Set the rate limit in requests per second
195    pub fn rate_limit(mut self, rate_limit: u32) -> Self {
196        self.config.rate_limit = rate_limit;
197        self
198    }
199
200    /// Set the request timeout
201    pub fn timeout(mut self, timeout: Duration) -> Self {
202        self.config.timeout = timeout;
203        self
204    }
205
206    /// Set the TCP connection timeout
207    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
208        self.config.connect_timeout = timeout;
209        self
210    }
211
212    /// Set the TCP connection timeout in seconds (convenience method)
213    pub fn connect_timeout_secs(mut self, seconds: u64) -> Self {
214        self.config.connect_timeout = Duration::from_secs(seconds);
215        self
216    }
217
218    /// Set the retry policy
219    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
220        self.config.retry_policy = Some(policy);
221        self
222    }
223
224    /// Enable or disable sandbox/testnet mode
225    pub fn sandbox(mut self, enabled: bool) -> Self {
226        self.config.sandbox = enabled;
227        self
228    }
229
230    /// Set a custom user agent string
231    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    /// Set the HTTP proxy configuration
237    pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
238        self.config.proxy = Some(proxy);
239        self
240    }
241
242    /// Set the HTTP proxy URL (convenience method)
243    pub fn proxy_url(mut self, url: impl Into<String>) -> Self {
244        self.config.proxy = Some(ProxyConfig::new(url));
245        self
246    }
247
248    /// Enable or disable verbose logging
249    pub fn verbose(mut self, enabled: bool) -> Self {
250        self.config.verbose = enabled;
251        self
252    }
253
254    /// Set a custom option
255    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    /// Set multiple custom options
261    pub fn options(mut self, options: HashMap<String, Value>) -> Self {
262        self.config.options.extend(options);
263        self
264    }
265
266    /// Set a URL override for a specific key (e.g., "public", "private")
267    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    /// Build the configuration
273    pub fn build(self) -> ExchangeConfig {
274        self.config
275    }
276}
277
278/// Market data cache structure
279#[derive(Debug, Clone)]
280pub struct MarketCache {
281    /// Markets indexed by symbol (e.g., "BTC/USDT")
282    pub markets: Arc<HashMap<String, Arc<Market>>>,
283    /// Markets indexed by exchange-specific ID
284    pub markets_by_id: HashMap<String, Arc<Market>>,
285    /// Currencies indexed by code (e.g., "BTC")
286    pub currencies: HashMap<String, Arc<Currency>>,
287    /// Currencies indexed by exchange-specific ID
288    pub currencies_by_id: HashMap<String, Arc<Currency>>,
289    /// List of all trading pair symbols
290    pub symbols: Vec<String>,
291    /// List of all currency codes
292    pub codes: Vec<String>,
293    /// List of all market IDs
294    pub ids: Vec<String>,
295    /// Whether markets have been loaded
296    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
314// Note: ExchangeCapabilities is now defined in crate::exchange module.
315// Import it from there for use in BaseExchange.
316use crate::exchange::ExchangeCapabilities;
317
318/// Base exchange implementation
319#[derive(Debug)]
320pub struct BaseExchange {
321    /// Exchange configuration
322    pub config: ExchangeConfig,
323    /// HTTP client for API requests (handles rate limiting internally)
324    pub http_client: HttpClient,
325    /// Thread-safe market data cache
326    pub market_cache: Arc<RwLock<MarketCache>>,
327    /// Mutex to serialize market loading operations and prevent concurrent API calls
328    pub market_loading_lock: Arc<Mutex<()>>,
329    /// Exchange capability flags
330    pub capabilities: ExchangeCapabilities,
331    /// API endpoint URLs
332    pub urls: HashMap<String, String>,
333    /// Timeframe mappings (e.g., "1m" -> "1")
334    pub timeframes: HashMap<String, String>,
335    /// Precision mode for price/amount formatting
336    pub precision_mode: PrecisionMode,
337}
338
339impl BaseExchange {
340    /// Creates a new exchange instance
341    ///
342    /// # Arguments
343    ///
344    /// * `config` - Exchange configuration
345    ///
346    /// # Returns
347    ///
348    /// Returns a `Result` containing the initialized exchange instance.
349    ///
350    /// # Errors
351    ///
352    /// Returns an error if HTTP client or rate limiter initialization fails.
353    pub fn new(config: ExchangeConfig) -> Result<Self> {
354        info!("Initializing exchange: {}", config.id);
355
356        // Validate timeout configuration
357        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        // Warn if timeout exceeds 5 minutes
365        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            // Lint: map_unwrap_or
377            // Reason: map().unwrap_or() is more readable here for extracting nested optional field
378            #[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        // Rate limiting is fully managed by HttpClient
402        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    /// Loads market data from the exchange
422    ///
423    /// # Arguments
424    ///
425    /// * `reload` - Whether to force reload even if markets are already cached
426    ///
427    /// # Returns
428    ///
429    /// Returns a `HashMap` of markets indexed by symbol.
430    ///
431    /// # Errors
432    ///
433    /// Returns an error if market data cannot be fetched. This base implementation
434    /// always returns `NotImplemented` error as exchanges must override this method.
435    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    /// Loads market data with a custom loader function, ensuring thread-safe concurrent access.
453    ///
454    /// This method serializes market loading operations to prevent multiple concurrent API calls
455    /// when multiple tasks call `load_markets` simultaneously. Only the first caller will
456    /// execute the loader; subsequent callers will wait for the lock and then return cached data.
457    ///
458    /// # Arguments
459    ///
460    /// * `reload` - Whether to force reload even if markets are already cached
461    /// * `loader` - An async closure that performs the actual market data fetching.
462    ///   Should return `Result<()>` and is responsible for calling `set_markets`.
463    ///
464    /// # Returns
465    ///
466    /// Returns a `HashMap` of markets indexed by symbol.
467    ///
468    /// # Errors
469    ///
470    /// Returns an error if the loader function fails.
471    ///
472    /// # Example
473    ///
474    /// ```ignore
475    /// let markets = self.base().load_markets_with_loader(reload, || async {
476    ///     let markets = self.fetch_markets_from_api().await?;
477    ///     Ok((markets, None))
478    /// }).await?;
479    /// ```
480    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        // Acquire the loading lock to serialize concurrent load_markets calls
490        let _loading_guard = self.market_loading_lock.lock().await;
491
492        // Check cache status while holding the lock
493        {
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        // Execute the loader to fetch market data
506        info!(
507            "Loading markets for {} (reload: {})",
508            self.config.id, reload
509        );
510        let (markets, currencies) = loader().await?;
511
512        // Base layer is responsible for setting markets
513        self.set_markets(markets, currencies).await?;
514
515        // Return the loaded markets
516        let cache = self.market_cache.read().await;
517        Ok(cache.markets.clone())
518    }
519
520    /// Sets market and currency data in the cache
521    ///
522    /// # Arguments
523    ///
524    /// * `markets` - Vector of market definitions to cache
525    /// * `currencies` - Optional vector of currency definitions to cache
526    ///
527    /// # Returns
528    ///
529    /// Returns `Ok(markets)` on successful cache update, preserving ownership for the caller.
530    ///
531    /// # Errors
532    ///
533    /// This method should not fail under normal circumstances.
534    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    /// Gets market information by trading symbol
586    ///
587    /// # Arguments
588    ///
589    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
590    ///
591    /// # Returns
592    ///
593    /// Returns the market definition for the specified symbol.
594    ///
595    /// # Errors
596    ///
597    /// Returns an error if markets are not loaded or the symbol is not found.
598    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    /// Gets market information by exchange-specific market ID
617    ///
618    /// # Arguments
619    ///
620    /// * `id` - Exchange-specific market identifier
621    ///
622    /// # Returns
623    ///
624    /// Returns the market definition for the specified ID.
625    ///
626    /// # Errors
627    ///
628    /// Returns an error if the market ID is not found.
629    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    /// Gets currency information by currency code
640    ///
641    /// # Arguments
642    ///
643    /// * `code` - Currency code (e.g., "BTC", "USDT")
644    ///
645    /// # Returns
646    ///
647    /// Returns the currency definition for the specified code.
648    ///
649    /// # Errors
650    ///
651    /// Returns an error if the currency is not found.
652    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    /// Gets all available trading symbols
663    ///
664    /// # Returns
665    ///
666    /// Returns a vector of all trading pair symbols.
667    pub async fn symbols(&self) -> Result<Vec<String>> {
668        let cache = self.market_cache.read().await;
669        Ok(cache.symbols.clone())
670    }
671
672    /// Applies rate limiting if enabled
673    ///
674    /// # Deprecated
675    ///
676    /// Rate limiting is now fully managed by HttpClient internally.
677    /// This method is a no-op and will be removed in a future version.
678    #[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        // Rate limiting is now fully managed by HttpClient
684        Ok(())
685    }
686
687    /// Checks that required API credentials are configured
688    ///
689    /// # Returns
690    ///
691    /// Returns `Ok(())` if both API key and secret are present.
692    ///
693    /// # Errors
694    ///
695    /// Returns an authentication error if credentials are missing.
696    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    /// Gets a nonce value (current timestamp in milliseconds)
707    ///
708    /// # Returns
709    ///
710    /// Returns current Unix timestamp in milliseconds.
711    pub fn nonce(&self) -> i64 {
712        crate::time::milliseconds()
713    }
714
715    /// Builds a URL query string from parameters
716    ///
717    /// # Arguments
718    ///
719    /// * `params` - Parameter key-value pairs
720    ///
721    /// # Returns
722    ///
723    /// Returns a URL-encoded query string (e.g., "key1=value1&key2=value2").
724    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    /// Parses a JSON response string
746    ///
747    /// # Arguments
748    ///
749    /// * `response` - JSON string to parse
750    ///
751    /// # Returns
752    ///
753    /// Returns the parsed `Value` on success.
754    ///
755    /// # Errors
756    ///
757    /// Returns an error if JSON parsing fails.
758    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    /// Handles HTTP error responses by mapping status codes to appropriate errors
763    ///
764    /// # Arguments
765    ///
766    /// * `status_code` - HTTP status code
767    /// * `response` - Response body text
768    ///
769    /// # Returns
770    ///
771    /// Returns an appropriate `Error` variant based on the status code.
772    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    /// Safely extracts a string value from a JSON object
784    ///
785    /// # Arguments
786    ///
787    /// * `dict` - JSON value to extract from
788    /// * `key` - Key to look up
789    ///
790    /// # Returns
791    ///
792    /// Returns `Some(String)` if the key exists and value is a string, `None` otherwise.
793    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    /// Safely extracts an integer value from a JSON object
800    ///
801    /// # Arguments
802    ///
803    /// * `dict` - JSON value to extract from
804    /// * `key` - Key to look up
805    ///
806    /// # Returns
807    ///
808    /// Returns `Some(i64)` if the key exists and value is an integer, `None` otherwise.
809    pub fn safe_integer(&self, dict: &Value, key: &str) -> Option<i64> {
810        dict.get(key).and_then(Value::as_i64)
811    }
812
813    /// Safely extracts a float value from a JSON object
814    ///
815    /// # Arguments
816    ///
817    /// * `dict` - JSON value to extract from
818    /// * `key` - Key to look up
819    ///
820    /// # Returns
821    ///
822    /// Returns `Some(f64)` if the key exists and value is a float, `None` otherwise.
823    pub fn safe_float(&self, dict: &Value, key: &str) -> Option<f64> {
824        dict.get(key).and_then(Value::as_f64)
825    }
826
827    /// Safely extracts a boolean value from a JSON object
828    ///
829    /// # Arguments
830    ///
831    /// * `dict` - JSON value to extract from
832    /// * `key` - Key to look up
833    ///
834    /// # Returns
835    ///
836    /// Returns `Some(bool)` if the key exists and value is a boolean, `None` otherwise.
837    pub fn safe_bool(&self, dict: &Value, key: &str) -> Option<bool> {
838        dict.get(key).and_then(Value::as_bool)
839    }
840
841    // ============================================================================
842    // Parse Methods
843    // ============================================================================
844
845    /// Parses raw ticker data from exchange API response
846    ///
847    /// # Arguments
848    ///
849    /// * `ticker_data` - Raw ticker JSON data from exchange
850    /// * `market` - Optional market information to populate symbol field
851    ///
852    /// # Returns
853    ///
854    /// Returns a parsed `Ticker` struct.
855    ///
856    /// # Errors
857    ///
858    /// Returns an error if required fields are missing.
859    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    /// Parses raw trade data from exchange API response
900    ///
901    /// # Arguments
902    ///
903    /// * `trade_data` - Raw trade JSON data from exchange
904    /// * `market` - Optional market information to populate symbol field
905    ///
906    /// # Returns
907    ///
908    /// Returns a parsed `Trade` struct.
909    ///
910    /// # Errors
911    ///
912    /// Returns an error if required fields (symbol, side) are missing.
913    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    /// Parses raw order data from exchange API response
974    ///
975    /// # Arguments
976    ///
977    /// * `order_data` - Raw order JSON data from exchange
978    /// * `market` - Optional market information to populate symbol field
979    ///
980    /// # Returns
981    ///
982    /// Returns a parsed `Order` struct with all available fields populated.
983    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        // Lint: match_same_arms
1013        // Reason: Explicit "open" match documents the expected status value; wildcard is fallback default
1014        #[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    /// Parses raw balance data from exchange API response
1076    ///
1077    /// # Arguments
1078    ///
1079    /// * `balance_data` - Raw balance JSON data from exchange
1080    ///
1081    /// # Returns
1082    ///
1083    /// Returns a `Balance` map containing all currency balances with free, used, and total amounts.
1084    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    /// Parses raw order book data from exchange API response
1115    ///
1116    /// # Arguments
1117    ///
1118    /// * `orderbook_data` - Raw order book JSON data from exchange
1119    /// * `timestamp` - Optional timestamp for the order book snapshot
1120    ///
1121    /// # Returns
1122    ///
1123    /// Returns a parsed `OrderBook` struct containing bid and ask sides.
1124    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                    // Lint: collapsible_if
1136                    // Reason: Keeping separate for clarity - first check array, then check length
1137                    #[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                    // Lint: collapsible_if
1156                    // Reason: Keeping separate for clarity - first check array, then check length
1157                    #[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            // WebSocket order book incremental update fields
1189            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            // Auto-resync mechanism fields
1194            needs_resync: false,
1195            last_resync_time: 0,
1196        })
1197    }
1198
1199    /// Safely extracts a `Decimal` value from a JSON object by key
1200    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    /// Safely extracts a `Decimal` value from a JSON `Value`
1205    // Lint: unused_self
1206    // Reason: Method is part of BaseExchange API for consistency; may use self in future
1207    #[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    // ============================================================================
1223    // Fee and Precision Methods
1224    // ============================================================================
1225
1226    /// Calculates trading fee for a given order
1227    ///
1228    /// # Arguments
1229    ///
1230    /// * `symbol` - Trading pair symbol
1231    /// * `order_type` - Order type (limit, market, etc.)
1232    /// * `side` - Order side (buy or sell)
1233    /// * `amount` - Trade amount in base currency
1234    /// * `price` - Trade price in quote currency
1235    /// * `taker_or_maker` - Optional taker or maker designation
1236    ///
1237    /// # Returns
1238    ///
1239    /// Returns a `Fee` struct containing the currency, cost, and rate.
1240    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    /// Converts an amount to the precision required by the market
1272    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    /// Converts a price to the precision required by the market
1281    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    /// Converts a cost to the precision required by the market
1290    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    /// Rounds a value to the specified precision
1299    ///
1300    /// The `precision_value` can represent either decimal places (e.g., 8) or a step size (e.g., 0.01).
1301    // Lint: unused_self
1302    // Reason: Method is part of BaseExchange API for consistency; may use self.precision_mode in future
1303    #[allow(clippy::unused_self)]
1304    fn round_to_precision(&self, value: Decimal, precision_value: Decimal) -> Decimal {
1305        if precision_value < Decimal::ONE {
1306            // Round by step size: round(value / step) * step
1307            let steps = (value / precision_value).round();
1308            steps * precision_value
1309        } else {
1310            // Round by decimal places
1311            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    /// Calculates the cost of a trade
1320    ///
1321    /// # Arguments
1322    ///
1323    /// * `symbol` - Trading pair symbol
1324    /// * `amount` - Trade amount in base currency
1325    /// * `price` - Trade price in quote currency
1326    ///
1327    /// # Returns
1328    ///
1329    /// Returns the total cost (amount × price) in quote currency.
1330    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        // Rate limiting is now managed by HttpClient internally
1356        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(&params);
1418        assert!(query.contains("symbol="));
1419        assert!(query.contains("limit="));
1420    }
1421
1422    #[test]
1423    fn test_capabilities() {
1424        // Test default capabilities (all false)
1425        let default_caps = ExchangeCapabilities::default();
1426        assert!(!default_caps.has("fetchMarkets"));
1427        assert!(!default_caps.has("fetchOHLCV"));
1428
1429        // Test public_only capabilities
1430        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        // Test all capabilities
1437        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        // Test connect_timeout method with Duration
1485        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        // Test connect_timeout_secs convenience method
1491        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        // Initialize an empty cache for testing
1539        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        // 验证bids降序排列
1785        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        // 验证asks升序排列
1799        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        // Skip this test for now as it requires async context
1816        // TODO: Convert to #[tokio::test] when async test infrastructure is ready
1817    }
1818
1819    #[test]
1820    fn test_amount_to_precision() {
1821        // Skip this test for now as it requires async context
1822        // TODO: Convert to #[tokio::test] when async test infrastructure is ready
1823    }
1824
1825    #[test]
1826    fn test_price_to_precision() {
1827        // Skip this test for now as it requires async context
1828        // TODO: Convert to #[tokio::test] when async test infrastructure is ready
1829    }
1830
1831    #[test]
1832    fn test_has_method() {
1833        // Skip this test for now as it requires changes to BaseExchange API
1834        // TODO: Implement has method in BaseExchange
1835    }
1836
1837    #[test]
1838    fn test_timeframes() {
1839        // Skip this test for now as it requires changes to BaseExchange API
1840        // TODO: Implement timeframes method in BaseExchange
1841    }
1842
1843    #[test]
1844    fn test_filter_by_type() {
1845        // Skip this test for now as it requires changes to BaseExchange API
1846        // TODO: Implement filter_by_type method in BaseExchange
1847    }
1848}