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