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::error::{Error, ParseError, Result};
11use crate::http_client::{HttpClient, HttpConfig};
12use crate::rate_limiter::{RateLimiter, RateLimiterConfig};
13use crate::types::*;
14use rust_decimal::Decimal;
15use rust_decimal::prelude::{FromStr, ToPrimitive};
16use serde_json::Value;
17use std::collections::HashMap;
18use std::future::Future;
19use std::sync::Arc;
20use tokio::sync::{Mutex, RwLock};
21use tracing::{debug, info};
22
23/// Exchange configuration
24#[derive(Debug, Clone)]
25pub struct ExchangeConfig {
26    /// Exchange identifier
27    pub id: String,
28    /// Exchange display name
29    pub name: String,
30    /// API key for authentication
31    pub api_key: Option<String>,
32    /// API secret for authentication
33    pub secret: Option<String>,
34    /// Password (required by some exchanges)
35    pub password: Option<String>,
36    /// User ID (required by some exchanges)
37    pub uid: Option<String>,
38    /// Account ID
39    pub account_id: Option<String>,
40    /// Enable rate limiting
41    pub enable_rate_limit: bool,
42    /// Rate limit in requests per second
43    pub rate_limit: u32,
44    /// Request timeout in seconds
45    pub timeout: u64,
46    /// Enable sandbox/testnet mode
47    pub sandbox: bool,
48    /// Custom user agent string
49    pub user_agent: Option<String>,
50    /// HTTP proxy server URL
51    pub proxy: Option<String>,
52    /// Enable verbose logging
53    pub verbose: bool,
54    /// Custom exchange-specific options
55    pub options: HashMap<String, Value>,
56    /// URL overrides for mocking/testing
57    pub url_overrides: HashMap<String, String>,
58}
59
60impl Default for ExchangeConfig {
61    fn default() -> Self {
62        Self {
63            id: String::new(),
64            name: String::new(),
65            api_key: None,
66            secret: None,
67            password: None,
68            uid: None,
69            account_id: None,
70            enable_rate_limit: true,
71            rate_limit: 10,
72            timeout: 30,
73            sandbox: false,
74            user_agent: Some(format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
75            proxy: None,
76            verbose: false,
77            options: HashMap::new(),
78            url_overrides: HashMap::new(),
79        }
80    }
81}
82
83impl ExchangeConfig {
84    /// Create a new configuration builder
85    ///
86    /// # Example
87    ///
88    /// ```rust
89    /// use ccxt_core::base_exchange::ExchangeConfig;
90    ///
91    /// let config = ExchangeConfig::builder()
92    ///     .id("binance")
93    ///     .name("Binance")
94    ///     .api_key("your-api-key")
95    ///     .secret("your-secret")
96    ///     .sandbox(true)
97    ///     .build();
98    /// ```
99    pub fn builder() -> ExchangeConfigBuilder {
100        ExchangeConfigBuilder::default()
101    }
102}
103
104/// Builder for `ExchangeConfig`
105///
106/// Provides a fluent API for constructing exchange configurations.
107///
108/// # Example
109///
110/// ```rust
111/// use ccxt_core::base_exchange::ExchangeConfigBuilder;
112///
113/// let config = ExchangeConfigBuilder::new()
114///     .id("binance")
115///     .name("Binance")
116///     .api_key("your-api-key")
117///     .secret("your-secret")
118///     .timeout(60)
119///     .build();
120/// ```
121#[derive(Debug, Clone, Default)]
122pub struct ExchangeConfigBuilder {
123    config: ExchangeConfig,
124}
125
126impl ExchangeConfigBuilder {
127    /// Create a new builder with default configuration
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Set the exchange identifier
133    pub fn id(mut self, id: impl Into<String>) -> Self {
134        self.config.id = id.into();
135        self
136    }
137
138    /// Set the exchange display name
139    pub fn name(mut self, name: impl Into<String>) -> Self {
140        self.config.name = name.into();
141        self
142    }
143
144    /// Set the API key for authentication
145    pub fn api_key(mut self, key: impl Into<String>) -> Self {
146        self.config.api_key = Some(key.into());
147        self
148    }
149
150    /// Set the API secret for authentication
151    pub fn secret(mut self, secret: impl Into<String>) -> Self {
152        self.config.secret = Some(secret.into());
153        self
154    }
155
156    /// Set the password (required by some exchanges)
157    pub fn password(mut self, password: impl Into<String>) -> Self {
158        self.config.password = Some(password.into());
159        self
160    }
161
162    /// Set the user ID (required by some exchanges)
163    pub fn uid(mut self, uid: impl Into<String>) -> Self {
164        self.config.uid = Some(uid.into());
165        self
166    }
167
168    /// Set the account ID
169    pub fn account_id(mut self, account_id: impl Into<String>) -> Self {
170        self.config.account_id = Some(account_id.into());
171        self
172    }
173
174    /// Enable or disable rate limiting
175    pub fn enable_rate_limit(mut self, enabled: bool) -> Self {
176        self.config.enable_rate_limit = enabled;
177        self
178    }
179
180    /// Set the rate limit in requests per second
181    pub fn rate_limit(mut self, rate_limit: u32) -> Self {
182        self.config.rate_limit = rate_limit;
183        self
184    }
185
186    /// Set the request timeout in seconds
187    pub fn timeout(mut self, seconds: u64) -> Self {
188        self.config.timeout = seconds;
189        self
190    }
191
192    /// Enable or disable sandbox/testnet mode
193    pub fn sandbox(mut self, enabled: bool) -> Self {
194        self.config.sandbox = enabled;
195        self
196    }
197
198    /// Set a custom user agent string
199    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
200        self.config.user_agent = Some(user_agent.into());
201        self
202    }
203
204    /// Set the HTTP proxy server URL
205    pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
206        self.config.proxy = Some(proxy.into());
207        self
208    }
209
210    /// Enable or disable verbose logging
211    pub fn verbose(mut self, enabled: bool) -> Self {
212        self.config.verbose = enabled;
213        self
214    }
215
216    /// Set a custom option
217    pub fn option(mut self, key: impl Into<String>, value: Value) -> Self {
218        self.config.options.insert(key.into(), value);
219        self
220    }
221
222    /// Set multiple custom options
223    pub fn options(mut self, options: HashMap<String, Value>) -> Self {
224        self.config.options.extend(options);
225        self
226    }
227
228    /// Set a URL override for a specific key (e.g., "public", "private")
229    pub fn url_override(mut self, key: impl Into<String>, url: impl Into<String>) -> Self {
230        self.config.url_overrides.insert(key.into(), url.into());
231        self
232    }
233
234    /// Build the configuration
235    pub fn build(self) -> ExchangeConfig {
236        self.config
237    }
238}
239
240/// Market data cache structure
241#[derive(Debug, Clone)]
242pub struct MarketCache {
243    /// Markets indexed by symbol (e.g., "BTC/USDT")
244    pub markets: HashMap<String, Arc<Market>>,
245    /// Markets indexed by exchange-specific ID
246    pub markets_by_id: HashMap<String, Arc<Market>>,
247    /// Currencies indexed by code (e.g., "BTC")
248    pub currencies: HashMap<String, Arc<Currency>>,
249    /// Currencies indexed by exchange-specific ID
250    pub currencies_by_id: HashMap<String, Arc<Currency>>,
251    /// List of all trading pair symbols
252    pub symbols: Vec<String>,
253    /// List of all currency codes
254    pub codes: Vec<String>,
255    /// List of all market IDs
256    pub ids: Vec<String>,
257    /// Whether markets have been loaded
258    pub loaded: bool,
259}
260
261impl Default for MarketCache {
262    fn default() -> Self {
263        Self {
264            markets: HashMap::new(),
265            markets_by_id: HashMap::new(),
266            currencies: HashMap::new(),
267            currencies_by_id: HashMap::new(),
268            symbols: Vec::new(),
269            codes: Vec::new(),
270            ids: Vec::new(),
271            loaded: false,
272        }
273    }
274}
275
276// Note: ExchangeCapabilities is now defined in crate::exchange module.
277// Import it from there for use in BaseExchange.
278use crate::exchange::ExchangeCapabilities;
279
280/// Base exchange implementation
281#[derive(Debug)]
282pub struct BaseExchange {
283    /// Exchange configuration
284    pub config: ExchangeConfig,
285    /// HTTP client for API requests (handles rate limiting internally)
286    pub http_client: HttpClient,
287    /// Thread-safe market data cache
288    pub market_cache: Arc<RwLock<MarketCache>>,
289    /// Mutex to serialize market loading operations and prevent concurrent API calls
290    pub market_loading_lock: Arc<Mutex<()>>,
291    /// Exchange capability flags
292    pub capabilities: ExchangeCapabilities,
293    /// API endpoint URLs
294    pub urls: HashMap<String, String>,
295    /// Timeframe mappings (e.g., "1m" -> "1")
296    pub timeframes: HashMap<String, String>,
297    /// Precision mode for price/amount formatting
298    pub precision_mode: PrecisionMode,
299}
300
301impl BaseExchange {
302    /// Creates a new exchange instance
303    ///
304    /// # Arguments
305    ///
306    /// * `config` - Exchange configuration
307    ///
308    /// # Returns
309    ///
310    /// Returns a `Result` containing the initialized exchange instance.
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if HTTP client or rate limiter initialization fails.
315    pub fn new(config: ExchangeConfig) -> Result<Self> {
316        info!("Initializing exchange: {}", config.id);
317
318        let http_config = HttpConfig {
319            timeout: config.timeout,
320            #[allow(deprecated)]
321            max_retries: 3,
322            verbose: false,
323            user_agent: config
324                .user_agent
325                .clone()
326                .unwrap_or_else(|| format!("ccxt-rust/{}", env!("CARGO_PKG_VERSION"))),
327            return_response_headers: false,
328            proxy: config.proxy.clone(),
329            enable_rate_limit: true,
330            retry_config: None,
331        };
332
333        let mut http_client = HttpClient::new(http_config)?;
334
335        // Rate limiting is fully managed by HttpClient
336        if config.enable_rate_limit {
337            let rate_config =
338                RateLimiterConfig::new(config.rate_limit, std::time::Duration::from_millis(1000));
339            let limiter = RateLimiter::new(rate_config);
340            http_client.set_rate_limiter(limiter);
341        }
342
343        Ok(Self {
344            config,
345            http_client,
346            market_cache: Arc::new(RwLock::new(MarketCache::default())),
347            market_loading_lock: Arc::new(Mutex::new(())),
348            capabilities: ExchangeCapabilities::default(),
349            urls: HashMap::new(),
350            timeframes: HashMap::new(),
351            precision_mode: PrecisionMode::DecimalPlaces,
352        })
353    }
354
355    /// Loads market data from the exchange
356    ///
357    /// # Arguments
358    ///
359    /// * `reload` - Whether to force reload even if markets are already cached
360    ///
361    /// # Returns
362    ///
363    /// Returns a `HashMap` of markets indexed by symbol.
364    ///
365    /// # Errors
366    ///
367    /// Returns an error if market data cannot be fetched. This base implementation
368    /// always returns `NotImplemented` error as exchanges must override this method.
369    pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
370        let cache = self.market_cache.write().await;
371
372        if cache.loaded && !reload {
373            debug!("Returning cached markets for {}", self.config.id);
374            return Ok(cache
375                .markets
376                .iter()
377                .map(|(k, v)| (k.clone(), (**v).clone()))
378                .collect());
379        }
380
381        info!("Loading markets for {}", self.config.id);
382
383        drop(cache);
384
385        Err(Error::not_implemented(
386            "load_markets must be implemented by exchange",
387        ))
388    }
389
390    /// Loads market data with a custom loader function, ensuring thread-safe concurrent access.
391    ///
392    /// This method serializes market loading operations to prevent multiple concurrent API calls
393    /// when multiple tasks call `load_markets` simultaneously. Only the first caller will
394    /// execute the loader; subsequent callers will wait for the lock and then return cached data.
395    ///
396    /// # Arguments
397    ///
398    /// * `reload` - Whether to force reload even if markets are already cached
399    /// * `loader` - An async closure that performs the actual market data fetching.
400    ///   Should return `Result<()>` and is responsible for calling `set_markets`.
401    ///
402    /// # Returns
403    ///
404    /// Returns a `HashMap` of markets indexed by symbol.
405    ///
406    /// # Errors
407    ///
408    /// Returns an error if the loader function fails.
409    ///
410    /// # Example
411    ///
412    /// ```ignore
413    /// let markets = self.base().load_markets_with_loader(reload, || async {
414    ///     let markets = self.fetch_markets_from_api().await?;
415    ///     Ok((markets, None))
416    /// }).await?;
417    /// ```
418    pub async fn load_markets_with_loader<F, Fut>(
419        &self,
420        reload: bool,
421        loader: F,
422    ) -> Result<HashMap<String, Arc<Market>>>
423    where
424        F: FnOnce() -> Fut,
425        Fut: Future<Output = Result<(Vec<Market>, Option<Vec<Currency>>)>>,
426    {
427        // Acquire the loading lock to serialize concurrent load_markets calls
428        let _loading_guard = self.market_loading_lock.lock().await;
429
430        // Check cache status while holding the lock
431        {
432            let cache = self.market_cache.read().await;
433            if cache.loaded && !reload {
434                debug!(
435                    "Returning cached markets for {} ({} markets)",
436                    self.config.id,
437                    cache.markets.len()
438                );
439                return Ok(cache.markets.clone());
440            }
441        }
442
443        // Execute the loader to fetch market data
444        info!(
445            "Loading markets for {} (reload: {})",
446            self.config.id, reload
447        );
448        let (markets, currencies) = loader().await?;
449
450        // Base layer is responsible for setting markets
451        self.set_markets(markets, currencies).await?;
452
453        // Return the loaded markets
454        let cache = self.market_cache.read().await;
455        Ok(cache.markets.clone())
456    }
457
458    /// Sets market and currency data in the cache
459    ///
460    /// # Arguments
461    ///
462    /// * `markets` - Vector of market definitions to cache
463    /// * `currencies` - Optional vector of currency definitions to cache
464    ///
465    /// # Returns
466    ///
467    /// Returns `Ok(markets)` on successful cache update, preserving ownership for the caller.
468    ///
469    /// # Errors
470    ///
471    /// This method should not fail under normal circumstances.
472    pub async fn set_markets(
473        &self,
474        markets: Vec<Market>,
475        currencies: Option<Vec<Currency>>,
476    ) -> Result<HashMap<String, Arc<Market>>> {
477        let mut cache = self.market_cache.write().await;
478
479        cache.markets.clear();
480        cache.markets_by_id.clear();
481        cache.symbols.clear();
482        cache.ids.clear();
483
484        for market in markets {
485            cache.symbols.push(market.symbol.clone());
486            cache.ids.push(market.id.clone());
487            let arc_market = Arc::new(market);
488            cache
489                .markets_by_id
490                .insert(arc_market.id.clone(), Arc::clone(&arc_market));
491            cache.markets.insert(arc_market.symbol.clone(), arc_market);
492        }
493
494        if let Some(currencies) = currencies {
495            cache.currencies.clear();
496            cache.currencies_by_id.clear();
497            cache.codes.clear();
498
499            for currency in currencies {
500                cache.codes.push(currency.code.clone());
501                let arc_currency = Arc::new(currency);
502                cache
503                    .currencies_by_id
504                    .insert(arc_currency.id.clone(), Arc::clone(&arc_currency));
505                cache
506                    .currencies
507                    .insert(arc_currency.code.clone(), arc_currency);
508            }
509        }
510
511        cache.loaded = true;
512        info!(
513            "Loaded {} markets and {} currencies for {}",
514            cache.markets.len(),
515            cache.currencies.len(),
516            self.config.id
517        );
518
519        Ok(cache.markets.clone())
520    }
521
522    /// Gets market information by trading symbol
523    ///
524    /// # Arguments
525    ///
526    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT")
527    ///
528    /// # Returns
529    ///
530    /// Returns the market definition for the specified symbol.
531    ///
532    /// # Errors
533    ///
534    /// Returns an error if markets are not loaded or the symbol is not found.
535    pub async fn market(&self, symbol: &str) -> Result<Arc<Market>> {
536        let cache = self.market_cache.read().await;
537
538        if !cache.loaded {
539            drop(cache);
540            return Err(Error::exchange(
541                "-1",
542                "Markets not loaded. Call load_markets() first.",
543            ));
544        }
545
546        cache
547            .markets
548            .get(symbol)
549            .cloned()
550            .ok_or_else(|| Error::bad_symbol(format!("Market {} not found", symbol)))
551    }
552
553    /// Gets market information by exchange-specific market ID
554    ///
555    /// # Arguments
556    ///
557    /// * `id` - Exchange-specific market identifier
558    ///
559    /// # Returns
560    ///
561    /// Returns the market definition for the specified ID.
562    ///
563    /// # Errors
564    ///
565    /// Returns an error if the market ID is not found.
566    pub async fn market_by_id(&self, id: &str) -> Result<Arc<Market>> {
567        let cache = self.market_cache.read().await;
568
569        cache
570            .markets_by_id
571            .get(id)
572            .cloned()
573            .ok_or_else(|| Error::bad_symbol(format!("Market with id {} not found", id)))
574    }
575
576    /// Gets currency information by currency code
577    ///
578    /// # Arguments
579    ///
580    /// * `code` - Currency code (e.g., "BTC", "USDT")
581    ///
582    /// # Returns
583    ///
584    /// Returns the currency definition for the specified code.
585    ///
586    /// # Errors
587    ///
588    /// Returns an error if the currency is not found.
589    pub async fn currency(&self, code: &str) -> Result<Arc<Currency>> {
590        let cache = self.market_cache.read().await;
591
592        cache
593            .currencies
594            .get(code)
595            .cloned()
596            .ok_or_else(|| Error::bad_symbol(format!("Currency {} not found", code)))
597    }
598
599    /// Gets all available trading symbols
600    ///
601    /// # Returns
602    ///
603    /// Returns a vector of all trading pair symbols.
604    pub async fn symbols(&self) -> Result<Vec<String>> {
605        let cache = self.market_cache.read().await;
606        Ok(cache.symbols.clone())
607    }
608
609    /// Applies rate limiting if enabled
610    ///
611    /// # Deprecated
612    ///
613    /// Rate limiting is now fully managed by HttpClient internally.
614    /// This method is a no-op and will be removed in a future version.
615    #[deprecated(
616        since = "0.2.0",
617        note = "Rate limiting is now handled internally by HttpClient. This method is a no-op."
618    )]
619    pub fn throttle(&self) -> Result<()> {
620        // Rate limiting is now fully managed by HttpClient
621        Ok(())
622    }
623
624    /// Checks that required API credentials are configured
625    ///
626    /// # Returns
627    ///
628    /// Returns `Ok(())` if both API key and secret are present.
629    ///
630    /// # Errors
631    ///
632    /// Returns an authentication error if credentials are missing.
633    pub fn check_required_credentials(&self) -> Result<()> {
634        if self.config.api_key.is_none() {
635            return Err(Error::authentication("API key is required"));
636        }
637        if self.config.secret.is_none() {
638            return Err(Error::authentication("API secret is required"));
639        }
640        Ok(())
641    }
642
643    /// Gets a nonce value (current timestamp in milliseconds)
644    ///
645    /// # Returns
646    ///
647    /// Returns current Unix timestamp in milliseconds.
648    pub fn nonce(&self) -> i64 {
649        crate::time::milliseconds()
650    }
651
652    /// Builds a URL query string from parameters
653    ///
654    /// # Arguments
655    ///
656    /// * `params` - Parameter key-value pairs
657    ///
658    /// # Returns
659    ///
660    /// Returns a URL-encoded query string (e.g., "key1=value1&key2=value2").
661    pub fn build_query_string(&self, params: &HashMap<String, Value>) -> String {
662        if params.is_empty() {
663            return String::new();
664        }
665
666        let pairs: Vec<String> = params
667            .iter()
668            .map(|(k, v)| {
669                let value_str = match v {
670                    Value::String(s) => s.clone(),
671                    Value::Number(n) => n.to_string(),
672                    Value::Bool(b) => b.to_string(),
673                    _ => v.to_string(),
674                };
675                format!("{}={}", k, urlencoding::encode(&value_str))
676            })
677            .collect();
678
679        pairs.join("&")
680    }
681
682    /// Parses a JSON response string
683    ///
684    /// # Arguments
685    ///
686    /// * `response` - JSON string to parse
687    ///
688    /// # Returns
689    ///
690    /// Returns the parsed `Value` on success.
691    ///
692    /// # Errors
693    ///
694    /// Returns an error if JSON parsing fails.
695    pub fn parse_json(&self, response: &str) -> Result<Value> {
696        serde_json::from_str(response).map_err(|e| Error::invalid_request(e.to_string()))
697    }
698
699    /// Handles HTTP error responses by mapping status codes to appropriate errors
700    ///
701    /// # Arguments
702    ///
703    /// * `status_code` - HTTP status code
704    /// * `response` - Response body text
705    ///
706    /// # Returns
707    ///
708    /// Returns an appropriate `Error` variant based on the status code.
709    pub fn handle_http_error(&self, status_code: u16, response: &str) -> Error {
710        match status_code {
711            400 => Error::invalid_request(response.to_string()),
712            401 | 403 => Error::authentication(response.to_string()),
713            404 => Error::invalid_request(format!("Endpoint not found: {}", response)),
714            429 => Error::rate_limit(response.to_string(), None),
715            500..=599 => Error::exchange(status_code.to_string(), response),
716            _ => Error::network(format!("HTTP {}: {}", status_code, response)),
717        }
718    }
719
720    /// Safely extracts a string value from a JSON object
721    ///
722    /// # Arguments
723    ///
724    /// * `dict` - JSON value to extract from
725    /// * `key` - Key to look up
726    ///
727    /// # Returns
728    ///
729    /// Returns `Some(String)` if the key exists and value is a string, `None` otherwise.
730    pub fn safe_string(&self, dict: &Value, key: &str) -> Option<String> {
731        dict.get(key)
732            .and_then(|v| v.as_str())
733            .map(|s| s.to_string())
734    }
735
736    /// Safely extracts an integer value from a JSON object
737    ///
738    /// # Arguments
739    ///
740    /// * `dict` - JSON value to extract from
741    /// * `key` - Key to look up
742    ///
743    /// # Returns
744    ///
745    /// Returns `Some(i64)` if the key exists and value is an integer, `None` otherwise.
746    pub fn safe_integer(&self, dict: &Value, key: &str) -> Option<i64> {
747        dict.get(key).and_then(|v| v.as_i64())
748    }
749
750    /// Safely extracts a float value from a JSON object
751    ///
752    /// # Arguments
753    ///
754    /// * `dict` - JSON value to extract from
755    /// * `key` - Key to look up
756    ///
757    /// # Returns
758    ///
759    /// Returns `Some(f64)` if the key exists and value is a float, `None` otherwise.
760    pub fn safe_float(&self, dict: &Value, key: &str) -> Option<f64> {
761        dict.get(key).and_then(|v| v.as_f64())
762    }
763
764    /// Safely extracts a boolean value from a JSON object
765    ///
766    /// # Arguments
767    ///
768    /// * `dict` - JSON value to extract from
769    /// * `key` - Key to look up
770    ///
771    /// # Returns
772    ///
773    /// Returns `Some(bool)` if the key exists and value is a boolean, `None` otherwise.
774    pub fn safe_bool(&self, dict: &Value, key: &str) -> Option<bool> {
775        dict.get(key).and_then(|v| v.as_bool())
776    }
777
778    // ============================================================================
779    // Parse Methods
780    // ============================================================================
781
782    /// Parses raw ticker data from exchange API response
783    ///
784    /// # Arguments
785    ///
786    /// * `ticker_data` - Raw ticker JSON data from exchange
787    /// * `market` - Optional market information to populate symbol field
788    ///
789    /// # Returns
790    ///
791    /// Returns a parsed `Ticker` struct.
792    ///
793    /// # Errors
794    ///
795    /// Returns an error if required fields are missing.
796    pub fn parse_ticker(&self, ticker_data: &Value, market: Option<&Market>) -> Result<Ticker> {
797        let symbol = if let Some(m) = market {
798            m.symbol.clone()
799        } else {
800            self.safe_string(ticker_data, "symbol")
801                .ok_or_else(|| ParseError::missing_field("symbol"))?
802        };
803
804        let timestamp = self.safe_integer(ticker_data, "timestamp").unwrap_or(0);
805
806        Ok(Ticker {
807            symbol,
808            timestamp,
809            datetime: self.safe_string(ticker_data, "datetime"),
810            high: self.safe_decimal(ticker_data, "high").map(Price::new),
811            low: self.safe_decimal(ticker_data, "low").map(Price::new),
812            bid: self.safe_decimal(ticker_data, "bid").map(Price::new),
813            bid_volume: self.safe_decimal(ticker_data, "bidVolume").map(Amount::new),
814            ask: self.safe_decimal(ticker_data, "ask").map(Price::new),
815            ask_volume: self.safe_decimal(ticker_data, "askVolume").map(Amount::new),
816            vwap: self.safe_decimal(ticker_data, "vwap").map(Price::new),
817            open: self.safe_decimal(ticker_data, "open").map(Price::new),
818            close: self.safe_decimal(ticker_data, "close").map(Price::new),
819            last: self.safe_decimal(ticker_data, "last").map(Price::new),
820            previous_close: self
821                .safe_decimal(ticker_data, "previousClose")
822                .map(Price::new),
823            change: self.safe_decimal(ticker_data, "change").map(Price::new),
824            percentage: self.safe_decimal(ticker_data, "percentage"),
825            average: self.safe_decimal(ticker_data, "average").map(Price::new),
826            base_volume: self
827                .safe_decimal(ticker_data, "baseVolume")
828                .map(Amount::new),
829            quote_volume: self
830                .safe_decimal(ticker_data, "quoteVolume")
831                .map(Amount::new),
832            info: std::collections::HashMap::new(),
833        })
834    }
835
836    /// Parses raw trade data from exchange API response
837    ///
838    /// # Arguments
839    ///
840    /// * `trade_data` - Raw trade JSON data from exchange
841    /// * `market` - Optional market information to populate symbol field
842    ///
843    /// # Returns
844    ///
845    /// Returns a parsed `Trade` struct.
846    ///
847    /// # Errors
848    ///
849    /// Returns an error if required fields (symbol, side) are missing.
850    pub fn parse_trade(&self, trade_data: &Value, market: Option<&Market>) -> Result<Trade> {
851        let symbol = if let Some(m) = market {
852            m.symbol.clone()
853        } else {
854            self.safe_string(trade_data, "symbol")
855                .ok_or_else(|| ParseError::missing_field("symbol"))?
856        };
857
858        let side = self
859            .safe_string(trade_data, "side")
860            .and_then(|s| match s.to_lowercase().as_str() {
861                "buy" => Some(OrderSide::Buy),
862                "sell" => Some(OrderSide::Sell),
863                _ => None,
864            })
865            .ok_or_else(|| ParseError::missing_field("side"))?;
866
867        let trade_type =
868            self.safe_string(trade_data, "type")
869                .and_then(|t| match t.to_lowercase().as_str() {
870                    "limit" => Some(OrderType::Limit),
871                    "market" => Some(OrderType::Market),
872                    _ => None,
873                });
874
875        let taker_or_maker = self.safe_string(trade_data, "takerOrMaker").and_then(|s| {
876            match s.to_lowercase().as_str() {
877                "taker" => Some(TakerOrMaker::Taker),
878                "maker" => Some(TakerOrMaker::Maker),
879                _ => None,
880            }
881        });
882
883        Ok(Trade {
884            id: self.safe_string(trade_data, "id"),
885            order: self.safe_string(trade_data, "orderId"),
886            timestamp: self.safe_integer(trade_data, "timestamp").unwrap_or(0),
887            datetime: self.safe_string(trade_data, "datetime"),
888            symbol,
889            trade_type,
890            side,
891            taker_or_maker,
892            price: Price::new(
893                self.safe_decimal(trade_data, "price")
894                    .unwrap_or(Decimal::ZERO),
895            ),
896            amount: Amount::new(
897                self.safe_decimal(trade_data, "amount")
898                    .unwrap_or(Decimal::ZERO),
899            ),
900            cost: self.safe_decimal(trade_data, "cost").map(Cost::new),
901            fee: None,
902            info: if let Some(obj) = trade_data.as_object() {
903                obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
904            } else {
905                HashMap::new()
906            },
907        })
908    }
909
910    /// Parses raw order data from exchange API response
911    ///
912    /// # Arguments
913    ///
914    /// * `order_data` - Raw order JSON data from exchange
915    /// * `market` - Optional market information to populate symbol field
916    ///
917    /// # Returns
918    ///
919    /// Returns a parsed `Order` struct with all available fields populated.
920    pub fn parse_order(&self, order_data: &Value, market: Option<&Market>) -> Result<Order> {
921        let symbol = if let Some(m) = market {
922            m.symbol.clone()
923        } else {
924            self.safe_string(order_data, "symbol")
925                .ok_or_else(|| ParseError::missing_field("symbol"))?
926        };
927
928        let order_type = self
929            .safe_string(order_data, "type")
930            .and_then(|t| match t.to_lowercase().as_str() {
931                "limit" => Some(OrderType::Limit),
932                "market" => Some(OrderType::Market),
933                _ => None,
934            })
935            .unwrap_or(OrderType::Limit);
936
937        let side = self
938            .safe_string(order_data, "side")
939            .and_then(|s| match s.to_lowercase().as_str() {
940                "buy" => Some(OrderSide::Buy),
941                "sell" => Some(OrderSide::Sell),
942                _ => None,
943            })
944            .unwrap_or(OrderSide::Buy);
945
946        let status_str = self
947            .safe_string(order_data, "status")
948            .unwrap_or_else(|| "open".to_string());
949        let status = match status_str.to_lowercase().as_str() {
950            "open" => OrderStatus::Open,
951            "closed" => OrderStatus::Closed,
952            "canceled" | "cancelled" => OrderStatus::Cancelled,
953            "expired" => OrderStatus::Expired,
954            "rejected" => OrderStatus::Rejected,
955            _ => OrderStatus::Open,
956        };
957
958        let id = self
959            .safe_string(order_data, "id")
960            .unwrap_or_else(|| format!("order_{}", chrono::Utc::now().timestamp_millis()));
961
962        let amount = self
963            .safe_decimal(order_data, "amount")
964            .unwrap_or(Decimal::ZERO);
965
966        Ok(Order {
967            id,
968            client_order_id: self.safe_string(order_data, "clientOrderId"),
969            timestamp: self.safe_integer(order_data, "timestamp"),
970            datetime: self.safe_string(order_data, "datetime"),
971            last_trade_timestamp: self.safe_integer(order_data, "lastTradeTimestamp"),
972            symbol,
973            order_type,
974            time_in_force: self.safe_string(order_data, "timeInForce"),
975            post_only: self
976                .safe_string(order_data, "postOnly")
977                .and_then(|s| s.parse::<bool>().ok()),
978            reduce_only: self
979                .safe_string(order_data, "reduceOnly")
980                .and_then(|s| s.parse::<bool>().ok()),
981            side,
982            price: self.safe_decimal(order_data, "price"),
983            stop_price: self.safe_decimal(order_data, "stopPrice"),
984            trigger_price: self.safe_decimal(order_data, "triggerPrice"),
985            take_profit_price: self.safe_decimal(order_data, "takeProfitPrice"),
986            stop_loss_price: self.safe_decimal(order_data, "stopLossPrice"),
987            average: self.safe_decimal(order_data, "average"),
988            amount,
989            filled: self.safe_decimal(order_data, "filled"),
990            remaining: self.safe_decimal(order_data, "remaining"),
991            cost: self.safe_decimal(order_data, "cost"),
992            status,
993            fee: None,
994            fees: None,
995            trades: None,
996            trailing_delta: self.safe_decimal(order_data, "trailingDelta"),
997            trailing_percent: self.safe_decimal(order_data, "trailingPercent"),
998            activation_price: self.safe_decimal(order_data, "activationPrice"),
999            callback_rate: self.safe_decimal(order_data, "callbackRate"),
1000            working_type: self.safe_string(order_data, "workingType"),
1001            info: if let Some(obj) = order_data.as_object() {
1002                obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1003            } else {
1004                HashMap::new()
1005            },
1006        })
1007    }
1008
1009    /// Parses raw balance data from exchange API response
1010    ///
1011    /// # Arguments
1012    ///
1013    /// * `balance_data` - Raw balance JSON data from exchange
1014    ///
1015    /// # Returns
1016    ///
1017    /// Returns a `Balance` map containing all currency balances with free, used, and total amounts.
1018    pub fn parse_balance(&self, balance_data: &Value) -> Result<Balance> {
1019        let mut balance = Balance::new();
1020
1021        if let Some(obj) = balance_data.as_object() {
1022            for (currency, balance_info) in obj {
1023                if currency == "timestamp" || currency == "datetime" || currency == "info" {
1024                    continue;
1025                }
1026                let free = self
1027                    .safe_decimal(balance_info, "free")
1028                    .unwrap_or(Decimal::ZERO);
1029                let used = self
1030                    .safe_decimal(balance_info, "used")
1031                    .unwrap_or(Decimal::ZERO);
1032                let total = self
1033                    .safe_decimal(balance_info, "total")
1034                    .unwrap_or(free + used);
1035
1036                let entry = BalanceEntry { free, used, total };
1037                balance.set(currency.clone(), entry);
1038            }
1039        }
1040
1041        if let Some(obj) = balance_data.as_object() {
1042            balance.info = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1043        }
1044
1045        Ok(balance)
1046    }
1047
1048    /// Parses raw order book data from exchange API response
1049    ///
1050    /// # Arguments
1051    ///
1052    /// * `orderbook_data` - Raw order book JSON data from exchange
1053    /// * `timestamp` - Optional timestamp for the order book snapshot
1054    ///
1055    /// # Returns
1056    ///
1057    /// Returns a parsed `OrderBook` struct containing bid and ask sides.
1058    pub fn parse_order_book(
1059        &self,
1060        orderbook_data: &Value,
1061        timestamp: Option<i64>,
1062    ) -> Result<OrderBook> {
1063        let mut bids_side = OrderBookSide::new();
1064        let mut asks_side = OrderBookSide::new();
1065
1066        if let Some(bids_array) = orderbook_data.get("bids").and_then(|v| v.as_array()) {
1067            for bid in bids_array {
1068                if let Some(arr) = bid.as_array() {
1069                    if arr.len() >= 2 {
1070                        let price = self.safe_decimal_from_value(&arr[0]);
1071                        let amount = self.safe_decimal_from_value(&arr[1]);
1072                        if let (Some(p), Some(a)) = (price, amount) {
1073                            bids_side.push(OrderBookEntry {
1074                                price: Price::new(p),
1075                                amount: Amount::new(a),
1076                            });
1077                        }
1078                    }
1079                }
1080            }
1081        }
1082
1083        if let Some(asks_array) = orderbook_data.get("asks").and_then(|v| v.as_array()) {
1084            for ask in asks_array {
1085                if let Some(arr) = ask.as_array() {
1086                    if arr.len() >= 2 {
1087                        let price = self.safe_decimal_from_value(&arr[0]);
1088                        let amount = self.safe_decimal_from_value(&arr[1]);
1089                        if let (Some(p), Some(a)) = (price, amount) {
1090                            asks_side.push(OrderBookEntry {
1091                                price: Price::new(p),
1092                                amount: Amount::new(a),
1093                            });
1094                        }
1095                    }
1096                }
1097            }
1098        }
1099
1100        Ok(OrderBook {
1101            symbol: self
1102                .safe_string(orderbook_data, "symbol")
1103                .unwrap_or_default(),
1104            bids: bids_side,
1105            asks: asks_side,
1106            timestamp: timestamp
1107                .or_else(|| self.safe_integer(orderbook_data, "timestamp"))
1108                .unwrap_or(0),
1109            datetime: self.safe_string(orderbook_data, "datetime"),
1110            nonce: self.safe_integer(orderbook_data, "nonce"),
1111            info: if let Some(obj) = orderbook_data.as_object() {
1112                obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1113            } else {
1114                HashMap::new()
1115            },
1116            // WebSocket order book incremental update fields
1117            buffered_deltas: std::collections::VecDeque::new(),
1118            bids_map: std::collections::BTreeMap::new(),
1119            asks_map: std::collections::BTreeMap::new(),
1120            is_synced: false,
1121            // Auto-resync mechanism fields
1122            needs_resync: false,
1123            last_resync_time: 0,
1124        })
1125    }
1126
1127    /// Safely extracts a `Decimal` value from a JSON object by key
1128    fn safe_decimal(&self, data: &Value, key: &str) -> Option<Decimal> {
1129        data.get(key).and_then(|v| self.safe_decimal_from_value(v))
1130    }
1131
1132    /// Safely extracts a `Decimal` value from a JSON `Value`
1133    fn safe_decimal_from_value(&self, value: &Value) -> Option<Decimal> {
1134        match value {
1135            Value::Number(n) => {
1136                if let Some(f) = n.as_f64() {
1137                    Decimal::from_f64_retain(f)
1138                } else {
1139                    None
1140                }
1141            }
1142            Value::String(s) => Decimal::from_str(s).ok(),
1143            _ => None,
1144        }
1145    }
1146
1147    // ============================================================================
1148    // Fee and Precision Methods
1149    // ============================================================================
1150
1151    /// Calculates trading fee for a given order
1152    ///
1153    /// # Arguments
1154    ///
1155    /// * `symbol` - Trading pair symbol
1156    /// * `order_type` - Order type (limit, market, etc.)
1157    /// * `side` - Order side (buy or sell)
1158    /// * `amount` - Trade amount in base currency
1159    /// * `price` - Trade price in quote currency
1160    /// * `taker_or_maker` - Optional taker or maker designation
1161    ///
1162    /// # Returns
1163    ///
1164    /// Returns a `Fee` struct containing the currency, cost, and rate.
1165    pub async fn calculate_fee(
1166        &self,
1167        symbol: &str,
1168        _order_type: OrderType,
1169        _side: OrderSide,
1170        amount: Decimal,
1171        price: Decimal,
1172        taker_or_maker: Option<&str>,
1173    ) -> Result<Fee> {
1174        let market = self.market(symbol).await?;
1175
1176        let rate = if let Some(tom) = taker_or_maker {
1177            if tom == "taker" {
1178                market.taker.unwrap_or(Decimal::ZERO)
1179            } else {
1180                market.maker.unwrap_or(Decimal::ZERO)
1181            }
1182        } else {
1183            market.taker.unwrap_or(Decimal::ZERO)
1184        };
1185
1186        let cost = amount * price;
1187        let fee_cost = cost * rate;
1188
1189        Ok(Fee {
1190            currency: market.quote.clone(),
1191            cost: fee_cost,
1192            rate: Some(rate),
1193        })
1194    }
1195
1196    /// Converts an amount to the precision required by the market
1197    pub async fn amount_to_precision(&self, symbol: &str, amount: Decimal) -> Result<Decimal> {
1198        let market = self.market(symbol).await?;
1199        match market.precision.amount {
1200            Some(precision_value) => Ok(self.round_to_precision(amount, precision_value)),
1201            None => Ok(amount),
1202        }
1203    }
1204
1205    /// Converts a price to the precision required by the market
1206    pub async fn price_to_precision(&self, symbol: &str, price: Decimal) -> Result<Decimal> {
1207        let market = self.market(symbol).await?;
1208        match market.precision.price {
1209            Some(precision_value) => Ok(self.round_to_precision(price, precision_value)),
1210            None => Ok(price),
1211        }
1212    }
1213
1214    /// Converts a cost to the precision required by the market
1215    pub async fn cost_to_precision(&self, symbol: &str, cost: Decimal) -> Result<Decimal> {
1216        let market = self.market(symbol).await?;
1217        match market.precision.price {
1218            Some(precision_value) => Ok(self.round_to_precision(cost, precision_value)),
1219            None => Ok(cost),
1220        }
1221    }
1222
1223    /// Rounds a value to the specified precision
1224    ///
1225    /// The `precision_value` can represent either decimal places (e.g., 8) or a step size (e.g., 0.01).
1226    fn round_to_precision(&self, value: Decimal, precision_value: Decimal) -> Decimal {
1227        if precision_value < Decimal::ONE {
1228            // Round by step size: round(value / step) * step
1229            let steps = (value / precision_value).round();
1230            steps * precision_value
1231        } else {
1232            // Round by decimal places
1233            let digits = precision_value.to_u32().unwrap_or(8);
1234            let multiplier = Decimal::from(10_i64.pow(digits));
1235            let scaled = value * multiplier;
1236            let rounded = scaled.round();
1237            rounded / multiplier
1238        }
1239    }
1240
1241    /// Calculates the cost of a trade
1242    ///
1243    /// # Arguments
1244    ///
1245    /// * `symbol` - Trading pair symbol
1246    /// * `amount` - Trade amount in base currency
1247    /// * `price` - Trade price in quote currency
1248    ///
1249    /// # Returns
1250    ///
1251    /// Returns the total cost (amount × price) in quote currency.
1252    pub async fn calculate_cost(
1253        &self,
1254        symbol: &str,
1255        amount: Decimal,
1256        price: Decimal,
1257    ) -> Result<Decimal> {
1258        let _market = self.market(symbol).await?;
1259        Ok(amount * price)
1260    }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265    use super::*;
1266
1267    #[tokio::test]
1268    async fn test_base_exchange_creation() {
1269        let config = ExchangeConfig {
1270            id: "test".to_string(),
1271            name: "Test Exchange".to_string(),
1272            ..Default::default()
1273        };
1274
1275        let exchange = BaseExchange::new(config).unwrap();
1276        assert_eq!(exchange.config.id, "test");
1277        // Rate limiting is now managed by HttpClient internally
1278        assert!(exchange.config.enable_rate_limit);
1279    }
1280
1281    #[tokio::test]
1282    async fn test_market_cache() {
1283        let config = ExchangeConfig {
1284            id: "test".to_string(),
1285            ..Default::default()
1286        };
1287
1288        let exchange = BaseExchange::new(config).unwrap();
1289
1290        let markets = vec![Market {
1291            id: "btcusdt".to_string(),
1292            symbol: "BTC/USDT".to_string(),
1293            parsed_symbol: None,
1294            base: "BTC".to_string(),
1295            quote: "USDT".to_string(),
1296            active: true,
1297            market_type: MarketType::Spot,
1298            margin: false,
1299            settle: None,
1300            base_id: None,
1301            quote_id: None,
1302            settle_id: None,
1303            contract: None,
1304            linear: None,
1305            inverse: None,
1306            contract_size: None,
1307            expiry: None,
1308            expiry_datetime: None,
1309            strike: None,
1310            option_type: None,
1311            precision: Default::default(),
1312            limits: Default::default(),
1313            maker: None,
1314            taker: None,
1315            percentage: None,
1316            tier_based: None,
1317            fee_side: None,
1318            info: Default::default(),
1319        }];
1320
1321        let _ = exchange.set_markets(markets, None).await.unwrap();
1322
1323        let market = exchange.market("BTC/USDT").await.unwrap();
1324        assert_eq!(market.symbol, "BTC/USDT");
1325
1326        let symbols = exchange.symbols().await.unwrap();
1327        assert_eq!(symbols.len(), 1);
1328    }
1329
1330    #[test]
1331    fn test_build_query_string() {
1332        let config = ExchangeConfig::default();
1333        let exchange = BaseExchange::new(config).unwrap();
1334
1335        let mut params = HashMap::new();
1336        params.insert("symbol".to_string(), Value::String("BTC/USDT".to_string()));
1337        params.insert("limit".to_string(), Value::Number(100.into()));
1338
1339        let query = exchange.build_query_string(&params);
1340        assert!(query.contains("symbol="));
1341        assert!(query.contains("limit="));
1342    }
1343
1344    #[test]
1345    fn test_capabilities() {
1346        // Test default capabilities (all false)
1347        let default_caps = ExchangeCapabilities::default();
1348        assert!(!default_caps.has("fetchMarkets"));
1349        assert!(!default_caps.has("fetchOHLCV"));
1350
1351        // Test public_only capabilities
1352        let public_caps = ExchangeCapabilities::public_only();
1353        assert!(public_caps.has("fetchMarkets"));
1354        assert!(public_caps.has("fetchTicker"));
1355        assert!(public_caps.has("fetchOHLCV"));
1356        assert!(!public_caps.has("createOrder"));
1357
1358        // Test all capabilities
1359        let all_caps = ExchangeCapabilities::all();
1360        assert!(all_caps.has("fetchMarkets"));
1361        assert!(all_caps.has("fetchOHLCV"));
1362        assert!(all_caps.has("createOrder"));
1363    }
1364
1365    #[test]
1366    fn test_exchange_config_builder() {
1367        let config = ExchangeConfigBuilder::new()
1368            .id("binance")
1369            .name("Binance")
1370            .api_key("test-key")
1371            .secret("test-secret")
1372            .sandbox(true)
1373            .timeout(60)
1374            .verbose(true)
1375            .build();
1376
1377        assert_eq!(config.id, "binance");
1378        assert_eq!(config.name, "Binance");
1379        assert_eq!(config.api_key, Some("test-key".to_string()));
1380        assert_eq!(config.secret, Some("test-secret".to_string()));
1381        assert!(config.sandbox);
1382        assert_eq!(config.timeout, 60);
1383        assert!(config.verbose);
1384    }
1385
1386    #[test]
1387    fn test_exchange_config_builder_defaults() {
1388        let config = ExchangeConfigBuilder::new().build();
1389
1390        assert_eq!(config.id, "");
1391        assert_eq!(config.api_key, None);
1392        assert!(config.enable_rate_limit);
1393        assert_eq!(config.timeout, 30);
1394        assert!(!config.sandbox);
1395    }
1396
1397    #[test]
1398    fn test_exchange_config_builder_from_config() {
1399        let config = ExchangeConfig::builder().id("test").api_key("key").build();
1400
1401        assert_eq!(config.id, "test");
1402        assert_eq!(config.api_key, Some("key".to_string()));
1403    }
1404}
1405
1406#[cfg(test)]
1407mod parse_tests {
1408    use super::*;
1409    use serde_json::json;
1410
1411    async fn create_test_exchange() -> BaseExchange {
1412        let config = ExchangeConfig {
1413            id: "".to_string(),
1414            name: "".to_string(),
1415            api_key: None,
1416            secret: None,
1417            password: None,
1418            uid: None,
1419            timeout: 10000,
1420            sandbox: false,
1421            user_agent: None,
1422            enable_rate_limit: true,
1423            verbose: false,
1424            account_id: None,
1425            rate_limit: 0,
1426            proxy: None,
1427            options: Default::default(),
1428            url_overrides: Default::default(),
1429        };
1430
1431        let exchange = BaseExchange::new(config).unwrap();
1432
1433        // Initialize an empty cache for testing
1434        let cache = MarketCache::default();
1435        *exchange.market_cache.write().await = cache;
1436
1437        exchange
1438    }
1439
1440    #[tokio::test]
1441    async fn test_parse_ticker() {
1442        let exchange = create_test_exchange().await;
1443
1444        let ticker_data = json!({
1445            "symbol": "BTC/USDT",
1446            "timestamp": 1609459200000i64,
1447            "datetime": "2021-01-01T00:00:00.000Z",
1448            "high": 30000.0,
1449            "low": 28000.0,
1450            "bid": 29000.0,
1451            "bidVolume": 10.5,
1452            "ask": 29100.0,
1453            "askVolume": 8.3,
1454            "vwap": 29500.0,
1455            "open": 28500.0,
1456            "close": 29000.0,
1457            "last": 29000.0,
1458            "previousClose": 28500.0,
1459            "change": 500.0,
1460            "percentage": 1.75,
1461            "average": 28750.0,
1462            "baseVolume": 1000.0,
1463            "quoteVolume": 29000000.0
1464        });
1465
1466        let result = exchange.parse_ticker(&ticker_data, None);
1467        assert!(result.is_ok());
1468
1469        let ticker = result.unwrap();
1470        assert_eq!(ticker.symbol, "BTC/USDT");
1471        assert_eq!(ticker.timestamp, 1609459200000);
1472        assert_eq!(
1473            ticker.high,
1474            Some(Price::from(Decimal::from_str_radix("30000.0", 10).unwrap()))
1475        );
1476        assert_eq!(
1477            ticker.low,
1478            Some(Price::from(Decimal::from_str_radix("28000.0", 10).unwrap()))
1479        );
1480        assert_eq!(
1481            ticker.bid,
1482            Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1483        );
1484        assert_eq!(
1485            ticker.ask,
1486            Some(Price::from(Decimal::from_str_radix("29100.0", 10).unwrap()))
1487        );
1488        assert_eq!(
1489            ticker.last,
1490            Some(Price::from(Decimal::from_str_radix("29000.0", 10).unwrap()))
1491        );
1492        assert_eq!(
1493            ticker.base_volume,
1494            Some(Amount::from(Decimal::from_str_radix("1000.0", 10).unwrap()))
1495        );
1496        assert_eq!(
1497            ticker.quote_volume,
1498            Some(Amount::from(
1499                Decimal::from_str_radix("29000000.0", 10).unwrap()
1500            ))
1501        );
1502    }
1503
1504    #[tokio::test]
1505    async fn test_parse_trade() {
1506        let exchange = create_test_exchange().await;
1507
1508        let trade_data = json!({
1509            "id": "12345",
1510            "symbol": "BTC/USDT",
1511            "timestamp": 1609459200000i64,
1512            "datetime": "2021-01-01T00:00:00.000Z",
1513            "order": "order123",
1514            "type": "limit",
1515            "side": "buy",
1516            "takerOrMaker": "taker",
1517            "price": 29000.0,
1518            "amount": 0.5,
1519            "cost": 14500.0
1520        });
1521
1522        let result = exchange.parse_trade(&trade_data, None);
1523        assert!(result.is_ok());
1524
1525        let trade = result.unwrap();
1526        assert_eq!(trade.id, Some("12345".to_string()));
1527        assert_eq!(trade.symbol, "BTC/USDT");
1528        assert_eq!(trade.timestamp, 1609459200000);
1529        assert_eq!(trade.side, OrderSide::Buy);
1530        assert_eq!(
1531            trade.price,
1532            Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1533        );
1534        assert_eq!(
1535            trade.amount,
1536            Amount::from(Decimal::from_str_radix("0.5", 10).unwrap())
1537        );
1538        assert_eq!(
1539            trade.cost,
1540            Some(Cost::from(Decimal::from_str_radix("14500.0", 10).unwrap()))
1541        );
1542        assert_eq!(trade.taker_or_maker, Some(TakerOrMaker::Taker));
1543    }
1544
1545    #[tokio::test]
1546    async fn test_parse_order() {
1547        let exchange = create_test_exchange().await;
1548
1549        let order_data = json!({
1550            "id": "order123",
1551            "clientOrderId": "client456",
1552            "symbol": "BTC/USDT",
1553            "timestamp": 1609459200000i64,
1554            "datetime": "2021-01-01T00:00:00.000Z",
1555            "lastTradeTimestamp": 1609459300000i64,
1556            "status": "closed",
1557            "type": "limit",
1558            "timeInForce": "GTC",
1559            "side": "buy",
1560            "price": 29000.0,
1561            "average": 29050.0,
1562            "amount": 0.5,
1563            "filled": 0.5,
1564            "remaining": 0.0,
1565            "cost": 14525.0
1566        });
1567
1568        let result = exchange.parse_order(&order_data, None);
1569        assert!(result.is_ok());
1570
1571        let order = result.unwrap();
1572        assert_eq!(order.id, "order123");
1573        assert_eq!(order.client_order_id, Some("client456".to_string()));
1574        assert_eq!(order.symbol, "BTC/USDT");
1575        assert_eq!(order.status, OrderStatus::Closed);
1576        assert_eq!(order.order_type, OrderType::Limit);
1577        assert_eq!(order.side, OrderSide::Buy);
1578        assert_eq!(order.time_in_force, Some("GTC".to_string()));
1579        assert_eq!(
1580            order.price,
1581            Some(Decimal::from_str_radix("29000.0", 10).unwrap())
1582        );
1583        assert_eq!(order.amount, Decimal::from_str_radix("0.5", 10).unwrap());
1584        assert_eq!(
1585            order.filled,
1586            Some(Decimal::from_str_radix("0.5", 10).unwrap())
1587        );
1588        assert_eq!(
1589            order.remaining,
1590            Some(Decimal::from_str_radix("0.0", 10).unwrap())
1591        );
1592        assert_eq!(
1593            order.cost,
1594            Some(Decimal::from_str_radix("14525.0", 10).unwrap())
1595        );
1596    }
1597
1598    #[tokio::test]
1599    async fn test_parse_balance() {
1600        let exchange = create_test_exchange().await;
1601
1602        let balance_data = json!({
1603            "timestamp": 1609459200000i64,
1604            "datetime": "2021-01-01T00:00:00.000Z",
1605            "BTC": {
1606                "free": 1.5,
1607                "used": 0.5,
1608                "total": 2.0
1609            },
1610            "USDT": {
1611                "free": 10000.0,
1612                "used": 5000.0,
1613                "total": 15000.0
1614            }
1615        });
1616
1617        let result = exchange.parse_balance(&balance_data);
1618        assert!(result.is_ok());
1619
1620        let balance = result.unwrap();
1621        assert_eq!(balance.balances.len(), 2);
1622
1623        let btc_balance = balance.balances.get("BTC").unwrap();
1624        assert_eq!(
1625            btc_balance.free,
1626            Decimal::from_str_radix("1.5", 10).unwrap()
1627        );
1628        assert_eq!(
1629            btc_balance.used,
1630            Decimal::from_str_radix("0.5", 10).unwrap()
1631        );
1632        assert_eq!(
1633            btc_balance.total,
1634            Decimal::from_str_radix("2.0", 10).unwrap()
1635        );
1636
1637        let usdt_balance = balance.balances.get("USDT").unwrap();
1638        assert_eq!(
1639            usdt_balance.free,
1640            Decimal::from_str_radix("10000.0", 10).unwrap()
1641        );
1642        assert_eq!(
1643            usdt_balance.used,
1644            Decimal::from_str_radix("5000.0", 10).unwrap()
1645        );
1646        assert_eq!(
1647            usdt_balance.total,
1648            Decimal::from_str_radix("15000.0", 10).unwrap()
1649        );
1650    }
1651
1652    #[tokio::test]
1653    async fn test_parse_order_book() {
1654        let exchange = create_test_exchange().await;
1655
1656        let orderbook_data = json!({
1657            "symbol": "BTC/USDT",
1658            "bids": [
1659                [29000.0, 1.5],
1660                [28900.0, 2.0],
1661                [28800.0, 3.5]
1662            ],
1663            "asks": [
1664                [29100.0, 1.0],
1665                [29200.0, 2.5],
1666                [29300.0, 1.8]
1667            ]
1668        });
1669
1670        let result = exchange.parse_order_book(&orderbook_data, Some(1609459200000));
1671        assert!(result.is_ok());
1672
1673        let orderbook = result.unwrap();
1674        assert_eq!(orderbook.symbol, "BTC/USDT");
1675        assert_eq!(orderbook.timestamp, 1609459200000);
1676        assert_eq!(orderbook.bids.len(), 3);
1677        assert_eq!(orderbook.asks.len(), 3);
1678
1679        // 验证bids降序排列
1680        assert_eq!(
1681            orderbook.bids[0].price,
1682            Price::from(Decimal::from_str_radix("29000.0", 10).unwrap())
1683        );
1684        assert_eq!(
1685            orderbook.bids[1].price,
1686            Price::from(Decimal::from_str_radix("28900.0", 10).unwrap())
1687        );
1688        assert_eq!(
1689            orderbook.bids[2].price,
1690            Price::from(Decimal::from_str_radix("28800.0", 10).unwrap())
1691        );
1692
1693        // 验证asks升序排列
1694        assert_eq!(
1695            orderbook.asks[0].price,
1696            Price::from(Decimal::from_str_radix("29100.0", 10).unwrap())
1697        );
1698        assert_eq!(
1699            orderbook.asks[1].price,
1700            Price::from(Decimal::from_str_radix("29200.0", 10).unwrap())
1701        );
1702        assert_eq!(
1703            orderbook.asks[2].price,
1704            Price::from(Decimal::from_str_radix("29300.0", 10).unwrap())
1705        );
1706    }
1707
1708    #[test]
1709    fn test_calculate_fee() {
1710        // Skip this test for now as it requires async context
1711        // TODO: Convert to #[tokio::test] when async test infrastructure is ready
1712    }
1713
1714    #[test]
1715    fn test_amount_to_precision() {
1716        // Skip this test for now as it requires async context
1717        // TODO: Convert to #[tokio::test] when async test infrastructure is ready
1718    }
1719
1720    #[test]
1721    fn test_price_to_precision() {
1722        // Skip this test for now as it requires async context
1723        // TODO: Convert to #[tokio::test] when async test infrastructure is ready
1724    }
1725
1726    #[test]
1727    fn test_has_method() {
1728        // Skip this test for now as it requires changes to BaseExchange API
1729        // TODO: Implement has method in BaseExchange
1730    }
1731
1732    #[test]
1733    fn test_timeframes() {
1734        // Skip this test for now as it requires changes to BaseExchange API
1735        // TODO: Implement timeframes method in BaseExchange
1736    }
1737
1738    #[test]
1739    fn test_filter_by_type() {
1740        // Skip this test for now as it requires changes to BaseExchange API
1741        // TODO: Implement filter_by_type method in BaseExchange
1742    }
1743}