Skip to main content

digdigdig3/l3/open/crypto/cex/mexc/
connector.rs

1//! # MEXC Connector
2//!
3//! Implementation of all core traits for MEXC Spot API.
4//!
5//! ## Core Traits
6//! - `ExchangeIdentity` - exchange identification
7//! - `MarketData` - market data
8//! - `Trading` - trading operations
9//! - `Account` - account information
10//!
11//! ## Extended Methods
12//! Additional MEXC-specific methods as struct methods.
13
14use std::collections::HashMap;
15use std::sync::{Arc, Mutex};
16use std::time::Duration;
17
18use async_trait::async_trait;
19use reqwest::header::HeaderMap;
20use serde_json::{json, Value};
21
22use crate::core::{
23    HttpClient, Credentials,
24    ExchangeId, ExchangeType, AccountType,
25    ExchangeError, ExchangeResult,
26    Price, Kline, Ticker, OrderBook,
27    Order, OrderSide, OrderType, Balance, AccountInfo,
28    OrderRequest, CancelRequest, CancelScope,
29    BalanceQuery,
30    OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
31    UserTrade, UserTradeFilter,
32    SymbolInput,
33};
34use crate::core::traits::{
35    ExchangeIdentity, MarketData, Trading, Account,
36};
37use crate::core::{CancelAll, BatchOrders, AccountTransfers, CustodialFunds, SubAccounts};
38use crate::core::types::{
39    ConnectorStats, CancelAllResponse, OrderResult,
40    TransferRequest, TransferHistoryFilter, TransferResponse,
41    DepositAddress, WithdrawRequest, WithdrawResponse, FundsRecord, FundsHistoryFilter, FundsRecordType,
42    SubAccountOperation, SubAccountResult, SubAccount,
43    MarketDataCapabilities, TradingCapabilities, AccountCapabilities,
44};
45use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};
46use crate::core::types::{RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, OrderbookCapabilities, WsBookChannel};
47
48use super::endpoints::{MexcUrls, MexcEndpoint, map_kline_interval};
49use super::auth::MexcAuth;
50use super::parser::MexcParser;
51
52// ═══════════════════════════════════════════════════════════════════════════════
53// RATE LIMIT CAPABILITIES (static — embedded in binary, no allocation)
54// ═══════════════════════════════════════════════════════════════════════════════
55
56static MEXC_POOLS: &[RestLimitPool] = &[RestLimitPool {
57    name: "default",
58    max_budget: 500,
59    window_seconds: 10,
60    is_weight: true,
61    has_server_headers: true,
62    server_header: Some("X-MBX-USED-WEIGHT"),
63    header_reports_used: true,
64}];
65
66static MEXC_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
67    model: LimitModel::Weight,
68    rest_pools: MEXC_POOLS,
69    decaying: None,
70    endpoint_weights: &[],
71    ws: WsLimits {
72        max_connections: None,
73        max_subs_per_conn: Some(30),
74        max_msg_per_sec: Some(100),
75        max_streams_per_conn: None,
76    },
77};
78
79// ═══════════════════════════════════════════════════════════════════════════════
80// CONNECTOR
81// ═══════════════════════════════════════════════════════════════════════════════
82
83/// MEXC connector
84pub struct MexcConnector {
85    /// HTTP client
86    http: HttpClient,
87    /// Authentication (None for public methods)
88    auth: Option<MexcAuth>,
89    /// Runtime rate limiter (Weight model: 500 weight per 10 seconds)
90    limiter: Arc<Mutex<RuntimeLimiter>>,
91    /// Pressure monitor — gates non-essential requests at >= 90%
92    monitor: Arc<Mutex<RateLimitMonitor>>,
93    /// Per-symbol precision cache for safe price/qty formatting
94    precision: crate::core::utils::precision::PrecisionCache,
95}
96
97impl MexcConnector {
98    /// Create new connector
99    pub async fn new(credentials: Option<Credentials>) -> ExchangeResult<Self> {
100        let http = HttpClient::new(30_000)?; // 30 sec timeout
101
102        let mut auth = credentials.as_ref().map(MexcAuth::new);
103
104        // Sync time with server if we have auth
105        if auth.is_some() {
106            let base_url = MexcUrls::base_url();
107            let url = format!("{}/api/v3/time", base_url);
108            if let Ok(response) = http.get(&url, &HashMap::new()).await {
109                if let Some(server_time_ms) = response.get("serverTime")
110                    .and_then(|t| t.as_i64())
111                {
112                    if let Some(ref mut a) = auth {
113                        a.sync_time(server_time_ms);
114                    }
115                }
116            }
117        }
118
119        let limiter = Arc::new(Mutex::new(RuntimeLimiter::from_caps(&MEXC_RATE_CAPS)));
120        let monitor = Arc::new(Mutex::new(RateLimitMonitor::new("MEXC")));
121
122        Ok(Self {
123            http,
124            auth,
125            limiter,
126            monitor,
127            precision: crate::core::utils::precision::PrecisionCache::new(),
128        })
129    }
130
131    /// Create connector only for public methods
132    pub async fn public() -> ExchangeResult<Self> {
133        Self::new(None).await
134    }
135
136    // ═══════════════════════════════════════════════════════════════════════════
137    // HTTP HELPERS
138    // ═══════════════════════════════════════════════════════════════════════════
139
140    /// Wait for rate limit budget. Non-essential requests are dropped at >= 90% utilization.
141    ///
142    /// Returns `true` if acquired, `false` if dropped due to cutoff pressure.
143    /// Trading endpoints should pass `essential: true` to always wait through.
144    async fn rate_limit_wait(&self, weight: u32, essential: bool) -> bool {
145        loop {
146            let wait_time = {
147                let mut limiter = self.limiter.lock()
148                    .expect("rate limiter mutex poisoned");
149
150                let pressure = self.monitor.lock()
151                    .expect("rate monitor mutex poisoned")
152                    .check(&mut limiter);
153                if pressure >= RateLimitPressure::Cutoff && !essential {
154                    return false;
155                }
156
157                if limiter.try_acquire("default", weight) {
158                    return true;
159                }
160                limiter.time_until_ready("default", weight)
161            };
162            if wait_time > Duration::ZERO {
163                tokio::time::sleep(wait_time).await;
164            }
165        }
166    }
167
168    /// Sync limiter from MEXC response headers.
169    ///
170    /// MEXC reports: `X-MEXC-USED-WEIGHT-1M` = weight used in the last minute.
171    fn update_weight_from_headers(&self, headers: &HeaderMap) {
172        let used = headers
173            .get("x-mexc-used-weight-1m")
174            .or_else(|| headers.get("X-MEXC-USED-WEIGHT-1M"))
175            .and_then(|v| v.to_str().ok())
176            .and_then(|s| s.parse::<u32>().ok());
177        if let Some(used) = used {
178            if let Ok(mut limiter) = self.limiter.lock() {
179                limiter.update_from_server("default", used);
180            }
181        }
182    }
183
184    /// GET request
185    async fn get(
186        &self,
187        endpoint: MexcEndpoint,
188        params: HashMap<String, String>,
189    ) -> ExchangeResult<Value> {
190        // Market data = non-essential: drop at >= 90% utilization to preserve budget for trading
191        if !self.rate_limit_wait(1, false).await {
192            return Err(ExchangeError::RateLimitExceeded {
193                retry_after: None,
194                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
195            });
196        }
197
198        let base_url = if endpoint.is_futures() {
199            MexcUrls::futures_base_url()
200        } else {
201            MexcUrls::base_url()
202        };
203        let path = endpoint.path();
204
205        let (url, headers) = if endpoint.is_private() {
206            let auth = self.auth.as_ref()
207                .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
208
209            let (headers, signed_params) = auth.sign_request(params);
210
211            let query_parts: Vec<String> = signed_params.iter()
212                .map(|(k, v)| format!("{}={}", k, v))
213                .collect();
214            let query_string = query_parts.join("&");
215
216            let url = format!("{}{}?{}", base_url, path, query_string);
217            (url, headers)
218        } else {
219            let query = if params.is_empty() {
220                String::new()
221            } else {
222                let qs: Vec<String> = params.iter()
223                    .map(|(k, v)| format!("{}={}", k, v))
224                    .collect();
225                qs.join("&")
226            };
227
228            let url = if query.is_empty() {
229                format!("{}{}", base_url, path)
230            } else {
231                format!("{}{}?{}", base_url, path, query)
232            };
233            (url, HashMap::new())
234        };
235
236        let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &headers).await?;
237        self.update_weight_from_headers(&resp_headers);
238        MexcParser::check_error(&response)?;
239        Ok(response)
240    }
241
242    /// POST request
243    async fn post(
244        &self,
245        endpoint: MexcEndpoint,
246        params: HashMap<String, String>,
247    ) -> ExchangeResult<Value> {
248        // Order placement = essential: always wait, never drop
249        self.rate_limit_wait(1, true).await;
250
251        let base_url = MexcUrls::base_url();
252        let path = endpoint.path();
253
254        let auth = self.auth.as_ref()
255            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
256
257        let (headers, signed_params) = auth.sign_request(params);
258
259        let query_parts: Vec<String> = signed_params.iter()
260            .map(|(k, v)| format!("{}={}", k, v))
261            .collect();
262        let query_string = query_parts.join("&");
263
264        let url = format!("{}{}?{}", base_url, path, query_string);
265
266        let (response, resp_headers) = self.http.post_with_response_headers(&url, &json!({}), &headers).await?;
267        self.update_weight_from_headers(&resp_headers);
268        MexcParser::check_error(&response)?;
269        Ok(response)
270    }
271
272    /// DELETE request
273    async fn delete(
274        &self,
275        endpoint: MexcEndpoint,
276        params: HashMap<String, String>,
277    ) -> ExchangeResult<Value> {
278        // Order cancellation = essential: always wait, never drop
279        self.rate_limit_wait(1, true).await;
280
281        let base_url = MexcUrls::base_url();
282        let path = endpoint.path();
283
284        let auth = self.auth.as_ref()
285            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
286
287        let (headers, signed_params) = auth.sign_request(params);
288
289        let query_parts: Vec<String> = signed_params.iter()
290            .map(|(k, v)| format!("{}={}", k, v))
291            .collect();
292        let query_string = query_parts.join("&");
293
294        let url = format!("{}{}?{}", base_url, path, query_string);
295
296        let (response, resp_headers) = self.http.delete_with_response_headers(&url, &HashMap::new(), &headers).await?;
297        self.update_weight_from_headers(&resp_headers);
298        MexcParser::check_error(&response)?;
299        Ok(response)
300    }
301
302    // ═══════════════════════════════════════════════════════════════════════════
303    // EXTENDED METHODS (MEXC-specific)
304    // ═══════════════════════════════════════════════════════════════════════════
305
306    /// Get raw exchange information as Value
307    pub async fn get_exchange_info_raw(&self) -> ExchangeResult<Value> {
308        self.get(MexcEndpoint::ExchangeInfo, HashMap::new()).await
309    }
310
311    /// Cancel all orders for a symbol
312    pub async fn cancel_all_orders(
313        &self,
314        symbol: &str,
315        _account_type: AccountType,
316    ) -> ExchangeResult<Vec<Order>> {
317        let mut params = HashMap::new();
318        params.insert("symbol".to_string(), symbol.to_string());
319
320        let response = self.delete(MexcEndpoint::CancelAllOrders, params).await?;
321
322        // Response is array of cancelled orders
323        MexcParser::parse_orders(&response)
324    }
325}
326
327// ═══════════════════════════════════════════════════════════════════════════════
328// EXCHANGE IDENTITY
329// ═══════════════════════════════════════════════════════════════════════════════
330
331impl ExchangeIdentity for MexcConnector {
332    fn exchange_id(&self) -> ExchangeId {
333        ExchangeId::MEXC
334    }
335
336    fn metrics(&self) -> ConnectorStats {
337        let (http_requests, http_errors, last_latency_ms) = self.http.stats();
338        let (rate_used, rate_max) = if let Ok(mut limiter) = self.limiter.lock() {
339            limiter.primary_stats()
340        } else {
341            (0, 0)
342        };
343        ConnectorStats {
344            http_requests,
345            http_errors,
346            last_latency_ms,
347            rate_used,
348            rate_max,
349            rate_groups: Vec::new(),
350            ws_ping_rtt_ms: 0,
351        }
352    }
353
354    fn is_testnet(&self) -> bool {
355        false // MEXC doesn't have testnet for spot
356    }
357
358    fn supported_account_types(&self) -> Vec<AccountType> {
359        vec![
360            AccountType::Spot,
361            AccountType::Margin,
362            AccountType::FuturesCross,
363        ]
364    }
365
366    fn exchange_type(&self) -> ExchangeType {
367        ExchangeType::Cex
368    }
369
370    fn rate_limit_capabilities(&self) -> RateLimitCapabilities {
371        MEXC_RATE_CAPS
372    }
373
374    fn orderbook_capabilities(&self, _account_type: AccountType) -> OrderbookCapabilities {
375        static MEXC_CHANNELS: &[WsBookChannel] = &[
376            WsBookChannel::delta("aggre.depth@10ms",  None,     Some(10)  ),
377            WsBookChannel::delta("aggre.depth@100ms", None,     Some(100) ),
378        ];
379        OrderbookCapabilities {
380            ws_depths: &[5, 10, 20],
381            ws_default_depth: None,
382            rest_max_depth: Some(5000),
383            rest_depth_values: &[],
384            supports_snapshot: true,
385            supports_delta: true,
386            update_speeds_ms: &[10, 100],
387            default_speed_ms: None,
388            ws_channels: MEXC_CHANNELS,
389            checksum: None,
390            has_sequence: true,
391            has_prev_sequence: false,
392            supports_aggregation: true,
393            aggregation_levels: &[],
394        }
395    }
396}
397
398// ═══════════════════════════════════════════════════════════════════════════════
399// MARKET DATA
400// ═══════════════════════════════════════════════════════════════════════════════
401
402#[async_trait]
403impl MarketData for MexcConnector {
404    async fn get_price(
405        &self,
406        symbol: SymbolInput<'_>,
407        account_type: AccountType,
408    ) -> ExchangeResult<Price> {
409        let symbol = symbol.resolve(ExchangeId::MEXC, account_type)?;
410        match account_type {
411            AccountType::Spot | AccountType::Margin => {
412                let mut params = HashMap::new();
413                params.insert("symbol".to_string(), symbol.to_string());
414
415                let response = self.get(MexcEndpoint::TickerPrice, params).await?;
416
417                let price = response["price"].as_str()
418                    .and_then(|s| s.parse::<f64>().ok())
419                    .ok_or_else(|| ExchangeError::Parse("Invalid price".into()))?;
420
421                Ok(price)
422            },
423            AccountType::FuturesCross | AccountType::FuturesIsolated => {
424                let ticker = self.get_ticker(SymbolInput::Raw(&symbol), account_type).await?;
425                Ok(ticker.last_price)
426            }
427            AccountType::Earn | AccountType::Lending | AccountType::Options | AccountType::Convert => {
428                Err(ExchangeError::UnsupportedOperation(
429                    format!("{:?} account type not supported on MEXC", account_type)
430                ))
431            }
432        }
433    }
434
435    async fn get_orderbook(
436        &self,
437        symbol: SymbolInput<'_>,
438        depth: Option<u16>,
439        account_type: AccountType,
440    ) -> ExchangeResult<OrderBook> {
441        let symbol = symbol.resolve(ExchangeId::MEXC, account_type)?;
442        match account_type {
443            AccountType::Spot | AccountType::Margin => {
444                let mut params = HashMap::new();
445                params.insert("symbol".to_string(), symbol.to_string());
446
447                if let Some(d) = depth {
448                    params.insert("limit".to_string(), d.to_string());
449                }
450
451                let response = self.get(MexcEndpoint::Orderbook, params).await?;
452                MexcParser::parse_orderbook(&response)
453            },
454            AccountType::FuturesCross | AccountType::FuturesIsolated => {
455                let base_url = MexcUrls::futures_base_url();
456                let path = format!("/api/v1/contract/depth/{}", symbol);
457                let url = format!("{}{}", base_url, path);
458
459                if !self.rate_limit_wait(1, false).await {
460                    return Err(ExchangeError::RateLimitExceeded {
461                        retry_after: None,
462                        message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
463                    });
464                }
465                let response = self.http.get(&url, &HashMap::new()).await?;
466                MexcParser::check_error(&response)?;
467
468                let data = response.get("data")
469                    .ok_or_else(|| ExchangeError::Parse("Missing data field in futures orderbook".into()))?;
470                MexcParser::parse_orderbook_futures(data)
471            }
472            AccountType::Earn | AccountType::Lending | AccountType::Options | AccountType::Convert => {
473                Err(ExchangeError::UnsupportedOperation(
474                    format!("{:?} account type not supported on MEXC", account_type)
475                ))
476            }
477        }
478    }
479
480    async fn get_klines(
481        &self,
482        symbol: SymbolInput<'_>,
483        interval: &str,
484        limit: Option<u16>,
485        account_type: AccountType,
486        end_time: Option<i64>,
487    ) -> ExchangeResult<Vec<Kline>> {
488        let symbol = symbol.resolve(ExchangeId::MEXC, account_type)?;
489        match account_type {
490            AccountType::Spot | AccountType::Margin => {
491                let mut params = HashMap::new();
492                params.insert("symbol".to_string(), symbol.to_string());
493                params.insert("interval".to_string(), map_kline_interval(interval).to_string());
494
495                if let Some(l) = limit {
496                    params.insert("limit".to_string(), l.min(1000).to_string());
497                }
498
499                if let Some(et) = end_time {
500                    let interval_ms = interval_to_ms(interval);
501                    let count = limit.unwrap_or(1000) as i64;
502                    let st = et - count * interval_ms;
503                    params.insert("startTime".to_string(), st.to_string());
504                    params.insert("endTime".to_string(), et.to_string());
505                }
506
507                let response = self.get(MexcEndpoint::Klines, params).await?;
508                MexcParser::parse_klines(&response)
509            },
510            AccountType::FuturesCross | AccountType::FuturesIsolated => {
511                let base_url = MexcUrls::futures_base_url();
512                let path = format!("/api/v1/contract/kline/{}", symbol);
513
514                let futures_interval = match interval {
515                    "1m" => "Min1",
516                    "5m" => "Min5",
517                    "15m" => "Min15",
518                    "30m" => "Min30",
519                    "1h" => "Min60",
520                    "4h" => "Hour4",
521                    "8h" => "Hour8",
522                    "1d" => "Day1",
523                    "1w" => "Week1",
524                    "1M" => "Month1",
525                    _ => "Min60",
526                };
527
528                let mut params = HashMap::new();
529                params.insert("interval".to_string(), futures_interval.to_string());
530
531                if let Some(et) = end_time {
532                    params.insert("endTime".to_string(), et.to_string());
533                }
534
535                let query = params.iter()
536                    .map(|(k, v)| format!("{}={}", k, v))
537                    .collect::<Vec<_>>()
538                    .join("&");
539
540                let url = format!("{}{}?{}", base_url, path, query);
541
542                if !self.rate_limit_wait(1, false).await {
543                    return Err(ExchangeError::RateLimitExceeded {
544                        retry_after: None,
545                        message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
546                    });
547                }
548                let response = self.http.get(&url, &HashMap::new()).await?;
549                MexcParser::check_error(&response)?;
550
551                let klines_data = response.get("data")
552                    .ok_or_else(|| ExchangeError::Parse("Missing data field in futures klines".into()))?;
553                MexcParser::parse_klines_futures(klines_data)
554            }
555            AccountType::Earn | AccountType::Lending | AccountType::Options | AccountType::Convert => {
556                Err(ExchangeError::UnsupportedOperation(
557                    format!("{:?} account type not supported on MEXC", account_type)
558                ))
559            }
560        }
561    }
562
563    async fn get_ticker(
564        &self,
565        symbol: SymbolInput<'_>,
566        account_type: AccountType,
567    ) -> ExchangeResult<Ticker> {
568        let symbol = symbol.resolve(ExchangeId::MEXC, account_type)?;
569        match account_type {
570            AccountType::Spot | AccountType::Margin => {
571                let mut params = HashMap::new();
572                params.insert("symbol".to_string(), symbol.to_string());
573
574                let response = self.get(MexcEndpoint::Ticker24hr, params).await?;
575                MexcParser::parse_ticker(&response)
576            },
577            AccountType::FuturesCross | AccountType::FuturesIsolated => {
578                let response = self.get(MexcEndpoint::FuturesTicker, HashMap::new()).await?;
579
580                let data_array = response.get("data")
581                    .or_else(|| response.as_array().map(|_| &response))
582                    .ok_or_else(|| ExchangeError::Parse("Invalid futures ticker response".into()))?;
583
584                let ticker_data = if let Some(arr) = data_array.as_array() {
585                    arr.iter()
586                        .find(|t| t["symbol"].as_str() == Some(&*symbol))
587                        .ok_or_else(|| ExchangeError::Parse(format!("Symbol {} not found", symbol)))?
588                } else {
589                    data_array
590                };
591
592                MexcParser::parse_ticker_futures(ticker_data)
593            }
594            AccountType::Earn | AccountType::Lending | AccountType::Options | AccountType::Convert => {
595                Err(ExchangeError::UnsupportedOperation(
596                    format!("{:?} account type not supported on MEXC", account_type)
597                ))
598            }
599        }
600    }
601
602    async fn ping(&self) -> ExchangeResult<()> {
603        let _ = self.get(MexcEndpoint::Ping, HashMap::new()).await?;
604        Ok(())
605    }
606
607    async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<crate::core::types::SymbolInfo>> {
608        let response = self.get(MexcEndpoint::ExchangeInfo, HashMap::new()).await?;
609        let symbols = MexcParser::parse_exchange_info(&response, account_type)?;
610        self.precision.load_from_symbols(&symbols);
611        Ok(symbols)
612    }
613
614    fn market_data_capabilities(&self, account_type: AccountType) -> MarketDataCapabilities {
615        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
616        if is_futures {
617            MarketDataCapabilities {
618                has_ping: true,
619                has_price: true,
620                has_ticker: true,
621                has_orderbook: true,
622                has_klines: true,
623                // get_exchange_info parses spot symbols only; futures uses /api/v1/contract/detail
624                has_exchange_info: false,
625                // GET /api/v1/contract/deals/{symbol} is implemented via get_recent_trades
626                has_recent_trades: true,
627                // Futures kline intervals map to Min1/Min5/.../Month1
628                supported_intervals: &["1m", "5m", "15m", "30m", "1h", "4h", "8h", "1d", "1w", "1M"],
629                // Futures /api/v1/contract/kline does not accept a limit param
630                max_kline_limit: None,
631                // WebSocket: kline channel supported
632                has_ws_klines: true,
633                // WebSocket: aggre.deals channel supported
634                has_ws_trades: true,
635                // WebSocket: aggre.depth channel supported
636                has_ws_orderbook: true,
637                // WebSocket: miniTicker channel supported
638                has_ws_ticker: true,
639            }
640        } else {
641            MarketDataCapabilities {
642                has_ping: true,
643                has_price: true,
644                has_ticker: true,
645                has_orderbook: true,
646                has_klines: true,
647                has_exchange_info: true,
648                // get_recent_trades is implemented via the MarketData trait
649                has_recent_trades: true,
650                // MEXC spot intervals: 1m/5m/15m/30m are supported; 1h is mapped to "60m" internally
651                supported_intervals: &["1m", "5m", "15m", "30m", "1h", "4h", "8h", "1d", "1w", "1M"],
652                max_kline_limit: Some(1000),
653                // WebSocket: kline channel supported
654                has_ws_klines: true,
655                // WebSocket: aggre.deals channel supported
656                has_ws_trades: true,
657                // WebSocket: aggre.depth channel supported
658                has_ws_orderbook: true,
659                // WebSocket: miniTicker channel supported
660                has_ws_ticker: true,
661            }
662        }
663    }
664}
665
666// ═══════════════════════════════════════════════════════════════════════════════
667// TRADING
668// ═══════════════════════════════════════════════════════════════════════════════
669
670#[async_trait]
671impl Trading for MexcConnector {
672    async fn place_order(&self, req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
673        let symbol = &req.symbol;
674        let side = req.side;
675        let quantity = req.quantity;
676        let _account_type = req.account_type;
677        let client_order_id = format!("cc_{}", crate::core::timestamp_millis());
678        let symbol_str = symbol.raw()
679            .map(|s| s.to_string())
680            .unwrap_or_else(|| format!("{}{}", symbol.base.to_uppercase(), symbol.quote.to_uppercase()));
681        let qty_str = self.precision.qty(&symbol_str, quantity);
682
683        let side_str = match side {
684            OrderSide::Buy => "BUY",
685            OrderSide::Sell => "SELL",
686        };
687
688        match req.order_type {
689            OrderType::Market => {
690                let mut params = HashMap::new();
691                params.insert("symbol".to_string(), symbol_str.clone());
692                params.insert("side".to_string(), side_str.to_string());
693                params.insert("type".to_string(), "MARKET".to_string());
694                params.insert("quantity".to_string(), qty_str.clone());
695                params.insert("newClientOrderId".to_string(), client_order_id.clone());
696
697                let response = self.post(MexcEndpoint::PlaceOrder, params).await?;
698
699                let order_id = response["orderId"].as_str()
700                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".into()))?
701                    .to_string();
702
703                Ok(PlaceOrderResponse::Simple(Order {
704                    id: order_id,
705                    client_order_id: Some(client_order_id),
706                    symbol: symbol.to_string(),
707                    side,
708                    order_type: OrderType::Market,
709                    status: crate::core::OrderStatus::New,
710                    price: None,
711                    stop_price: None,
712                    quantity,
713                    filled_quantity: 0.0,
714                    average_price: None,
715                    commission: None,
716                    commission_asset: None,
717                    created_at: crate::core::timestamp_millis() as i64,
718                    updated_at: None,
719                    time_in_force: crate::core::TimeInForce::Gtc,
720                }))
721            }
722
723            OrderType::Limit { price } => {
724                let mut params = HashMap::new();
725                params.insert("symbol".to_string(), symbol_str.clone());
726                params.insert("side".to_string(), side_str.to_string());
727                params.insert("type".to_string(), "LIMIT".to_string());
728                params.insert("quantity".to_string(), qty_str.clone());
729                params.insert("price".to_string(), self.precision.price(&symbol_str, price));
730                params.insert("newClientOrderId".to_string(), client_order_id.clone());
731
732                let response = self.post(MexcEndpoint::PlaceOrder, params).await?;
733
734                let order_id = response["orderId"].as_str()
735                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".into()))?
736                    .to_string();
737
738                Ok(PlaceOrderResponse::Simple(Order {
739                    id: order_id,
740                    client_order_id: Some(client_order_id),
741                    symbol: symbol.to_string(),
742                    side,
743                    order_type: OrderType::Limit { price: 0.0 },
744                    status: crate::core::OrderStatus::New,
745                    price: Some(price),
746                    stop_price: None,
747                    quantity,
748                    filled_quantity: 0.0,
749                    average_price: None,
750                    commission: None,
751                    commission_asset: None,
752                    created_at: crate::core::timestamp_millis() as i64,
753                    updated_at: None,
754                    time_in_force: crate::core::TimeInForce::Gtc,
755                }))
756            }
757
758            OrderType::PostOnly { price } => {
759                // MEXC: LIMIT_MAKER (post-only limit order)
760                let mut params = HashMap::new();
761                params.insert("symbol".to_string(), symbol_str.clone());
762                params.insert("side".to_string(), side_str.to_string());
763                params.insert("type".to_string(), "LIMIT_MAKER".to_string());
764                params.insert("quantity".to_string(), qty_str.clone());
765                params.insert("price".to_string(), self.precision.price(&symbol_str, price));
766                params.insert("newClientOrderId".to_string(), client_order_id.clone());
767
768                let response = self.post(MexcEndpoint::PlaceOrder, params).await?;
769
770                let order_id = response["orderId"].as_str()
771                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".into()))?
772                    .to_string();
773
774                Ok(PlaceOrderResponse::Simple(Order {
775                    id: order_id,
776                    client_order_id: Some(client_order_id),
777                    symbol: symbol.to_string(),
778                    side,
779                    order_type: OrderType::PostOnly { price },
780                    status: crate::core::OrderStatus::New,
781                    price: Some(price),
782                    stop_price: None,
783                    quantity,
784                    filled_quantity: 0.0,
785                    average_price: None,
786                    commission: None,
787                    commission_asset: None,
788                    created_at: crate::core::timestamp_millis() as i64,
789                    updated_at: None,
790                    time_in_force: crate::core::TimeInForce::Gtc,
791                }))
792            }
793
794            OrderType::Ioc { price } => {
795                // MEXC: LIMIT with timeInForce=IOC
796                let price_val = price.unwrap_or(0.0);
797                let mut params = HashMap::new();
798                params.insert("symbol".to_string(), symbol_str.clone());
799                params.insert("side".to_string(), side_str.to_string());
800                params.insert("type".to_string(), "LIMIT".to_string());
801                params.insert("timeInForce".to_string(), "IOC".to_string());
802                params.insert("quantity".to_string(), qty_str.clone());
803                params.insert("price".to_string(), self.precision.price(&symbol_str, price_val));
804                params.insert("newClientOrderId".to_string(), client_order_id.clone());
805
806                let response = self.post(MexcEndpoint::PlaceOrder, params).await?;
807
808                let order_id = response["orderId"].as_str()
809                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".into()))?
810                    .to_string();
811
812                Ok(PlaceOrderResponse::Simple(Order {
813                    id: order_id,
814                    client_order_id: Some(client_order_id),
815                    symbol: symbol.to_string(),
816                    side,
817                    order_type: OrderType::Ioc { price },
818                    status: crate::core::OrderStatus::New,
819                    price,
820                    stop_price: None,
821                    quantity,
822                    filled_quantity: 0.0,
823                    average_price: None,
824                    commission: None,
825                    commission_asset: None,
826                    created_at: crate::core::timestamp_millis() as i64,
827                    updated_at: None,
828                    time_in_force: crate::core::TimeInForce::Ioc,
829                }))
830            }
831
832            OrderType::Fok { price } => {
833                // MEXC: LIMIT with timeInForce=FOK
834                let mut params = HashMap::new();
835                params.insert("symbol".to_string(), symbol_str.clone());
836                params.insert("side".to_string(), side_str.to_string());
837                params.insert("type".to_string(), "LIMIT".to_string());
838                params.insert("timeInForce".to_string(), "FOK".to_string());
839                params.insert("quantity".to_string(), qty_str.clone());
840                params.insert("price".to_string(), self.precision.price(&symbol_str, price));
841                params.insert("newClientOrderId".to_string(), client_order_id.clone());
842
843                let response = self.post(MexcEndpoint::PlaceOrder, params).await?;
844
845                let order_id = response["orderId"].as_str()
846                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".into()))?
847                    .to_string();
848
849                Ok(PlaceOrderResponse::Simple(Order {
850                    id: order_id,
851                    client_order_id: Some(client_order_id),
852                    symbol: symbol.to_string(),
853                    side,
854                    order_type: OrderType::Fok { price },
855                    status: crate::core::OrderStatus::New,
856                    price: Some(price),
857                    stop_price: None,
858                    quantity,
859                    filled_quantity: 0.0,
860                    average_price: None,
861                    commission: None,
862                    commission_asset: None,
863                    created_at: crate::core::timestamp_millis() as i64,
864                    updated_at: None,
865                    time_in_force: crate::core::TimeInForce::Fok,
866                }))
867            }
868
869            _ => Err(ExchangeError::UnsupportedOperation(
870                format!("{:?} order type not supported on {:?}", req.order_type, self.exchange_id())
871            )),
872        }
873    }
874
875    async fn cancel_order(&self, req: CancelRequest) -> ExchangeResult<Order> {
876        match req.scope {
877            CancelScope::Single { ref order_id } => {
878                let symbol = req.symbol.as_ref()
879                    .ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for cancel".into()))?;
880                let symbol_str = symbol.raw()
881                    .map(|s| s.to_string())
882                    .unwrap_or_else(|| format!("{}{}", symbol.base.to_uppercase(), symbol.quote.to_uppercase()));
883
884                let mut params = HashMap::new();
885                params.insert("symbol".to_string(), symbol_str);
886                params.insert("orderId".to_string(), order_id.to_string());
887
888                let response = self.delete(MexcEndpoint::CancelOrder, params).await?;
889                MexcParser::parse_order(&response)
890            }
891
892            _ => Err(ExchangeError::UnsupportedOperation(
893                format!("{:?} cancel scope not supported — use CancelAll trait", req.scope)
894            )),
895        }
896    }
897
898    async fn get_order_history(
899        &self,
900        filter: OrderHistoryFilter,
901        _account_type: AccountType,
902    ) -> ExchangeResult<Vec<Order>> {
903        // MEXC: GET /api/v3/allOrders — requires symbol
904        let symbol = filter.symbol
905            .ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for order history on MEXC".to_string()))?;
906        let symbol_str = symbol.raw()
907            .map(|s| s.to_string())
908            .unwrap_or_else(|| format!("{}{}", symbol.base.to_uppercase(), symbol.quote.to_uppercase()));
909
910        let mut params = HashMap::new();
911        params.insert("symbol".to_string(), symbol_str);
912
913        if let Some(start) = filter.start_time {
914            params.insert("startTime".to_string(), start.to_string());
915        }
916        if let Some(end) = filter.end_time {
917            params.insert("endTime".to_string(), end.to_string());
918        }
919        if let Some(limit) = filter.limit {
920            params.insert("limit".to_string(), limit.min(1000).to_string());
921        }
922
923        let response = self.get(MexcEndpoint::AllOrders, params).await?;
924        MexcParser::parse_orders(&response)
925    }
926
927    async fn get_order(
928        &self,
929        symbol: &str,
930        order_id: &str,
931        _account_type: AccountType,
932    ) -> ExchangeResult<Order> {
933        let mut params = HashMap::new();
934        params.insert("symbol".to_string(), symbol.to_string());
935        params.insert("orderId".to_string(), order_id.to_string());
936
937        let response = self.get(MexcEndpoint::QueryOrder, params).await?;
938        MexcParser::parse_order(&response)
939    }
940
941    async fn get_open_orders(
942        &self,
943        symbol: Option<&str>,
944        _account_type: AccountType,
945    ) -> ExchangeResult<Vec<Order>> {
946        let mut params = HashMap::new();
947
948        if let Some(s) = symbol {
949            params.insert("symbol".to_string(), s.to_string());
950        }
951
952        let response = self.get(MexcEndpoint::OpenOrders, params).await?;
953        MexcParser::parse_orders(&response)
954    }
955
956    async fn get_user_trades(
957        &self,
958        filter: UserTradeFilter,
959        _account_type: AccountType,
960    ) -> ExchangeResult<Vec<UserTrade>> {
961        // MEXC GET /api/v3/myTrades — symbol is required
962        let symbol_str = filter.symbol
963            .ok_or_else(|| ExchangeError::InvalidRequest(
964                "Symbol required for get_user_trades on MEXC".to_string()
965            ))?;
966
967        let mut params = HashMap::new();
968        params.insert("symbol".to_string(), symbol_str);
969
970        if let Some(oid) = filter.order_id {
971            params.insert("orderId".to_string(), oid);
972        }
973        if let Some(start) = filter.start_time {
974            params.insert("startTime".to_string(), start.to_string());
975        }
976        if let Some(end) = filter.end_time {
977            params.insert("endTime".to_string(), end.to_string());
978        }
979        if let Some(limit) = filter.limit {
980            params.insert("limit".to_string(), limit.min(1000).to_string());
981        }
982
983        let response = self.get(MexcEndpoint::MyTrades, params).await?;
984        MexcParser::parse_user_trades(&response)
985    }
986
987    fn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities {
988        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
989        if is_futures {
990            TradingCapabilities {
991                has_market_order: true,
992                has_limit_order: true,
993                // Futures contract API supports STOP order type
994                has_stop_market: true,
995                has_stop_limit: true,
996                has_trailing_stop: false,
997                has_bracket: false,
998                has_oco: false,
999                // No amend/modify endpoint on MEXC futures
1000                has_amend: false,
1001                // POST /api/v3/batchOrders is a spot-only endpoint
1002                has_batch: false,
1003                max_batch_size: None,
1004                // DELETE /api/v3/openOrders is a spot-only endpoint; futures cancel-all not implemented
1005                has_cancel_all: false,
1006                // GET /api/v3/myTrades is spot-only; no futures trade history endpoint implemented
1007                has_user_trades: false,
1008                // GET /api/v3/allOrders is spot-only; no futures order history endpoint implemented
1009                has_order_history: false,
1010            }
1011        } else {
1012            TradingCapabilities {
1013                has_market_order: true,
1014                has_limit_order: true,
1015                // MEXC spot does not support stop-market or stop-limit order types
1016                has_stop_market: false,
1017                has_stop_limit: false,
1018                has_trailing_stop: false,
1019                has_bracket: false,
1020                // MEXC spot does not have an OCO endpoint
1021                has_oco: false,
1022                // No amend/modify order endpoint on MEXC spot
1023                has_amend: false,
1024                // BatchOrders trait is implemented: POST /api/v3/batchOrders (max 20)
1025                has_batch: true,
1026                max_batch_size: Some(20),
1027                // CancelAll trait is implemented: DELETE /api/v3/openOrders
1028                has_cancel_all: true,
1029                // get_user_trades is implemented via GET /api/v3/myTrades
1030                has_user_trades: true,
1031                // get_order_history is implemented via GET /api/v3/allOrders
1032                has_order_history: true,
1033            }
1034        }
1035    }
1036}
1037
1038// ═══════════════════════════════════════════════════════════════════════════════
1039// ACCOUNT
1040// ═══════════════════════════════════════════════════════════════════════════════
1041
1042#[async_trait]
1043impl Account for MexcConnector {
1044    async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
1045        let _asset = query.asset.clone();
1046        let _account_type = query.account_type;
1047
1048        let response = self.get(MexcEndpoint::Account, HashMap::new()).await?;
1049        MexcParser::parse_balance(&response)
1050    }
1051
1052    async fn get_account_info(&self, account_type: AccountType) -> ExchangeResult<AccountInfo> {
1053        let response = self.get(MexcEndpoint::Account, HashMap::new()).await?;
1054
1055        let balances = MexcParser::parse_balance(&response)?;
1056
1057        let can_trade = response.get("canTrade")
1058            .and_then(|v| v.as_bool())
1059            .unwrap_or(true);
1060
1061        let can_withdraw = response.get("canWithdraw")
1062            .and_then(|v| v.as_bool())
1063            .unwrap_or(true);
1064
1065        let can_deposit = response.get("canDeposit")
1066            .and_then(|v| v.as_bool())
1067            .unwrap_or(true);
1068
1069        let maker_commission = response.get("makerCommission")
1070            .and_then(|v| v.as_i64())
1071            .map(|c| c as f64 / 10000.0)
1072            .unwrap_or(0.002);
1073
1074        let taker_commission = response.get("takerCommission")
1075            .and_then(|v| v.as_i64())
1076            .map(|c| c as f64 / 10000.0)
1077            .unwrap_or(0.002);
1078
1079        Ok(AccountInfo {
1080            account_type,
1081            can_trade,
1082            can_withdraw,
1083            can_deposit,
1084            maker_commission,
1085            taker_commission,
1086            balances,
1087        })
1088    }
1089
1090    async fn get_fees(&self, symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
1091        // MEXC: GET /api/v3/tradeFee?symbol=BTCUSDT
1092        let mut params = HashMap::new();
1093
1094        if let Some(sym) = symbol {
1095            params.insert("symbol".to_string(), sym.to_uppercase().replace('/', ""));
1096        }
1097
1098        let response = self.get(MexcEndpoint::TradeFee, params).await?;
1099
1100        // Response: [{"symbol": "BTCUSDT", "makerCommission": "0.002", "takerCommission": "0.002"}]
1101        let fee_data = if let Some(arr) = response.as_array() {
1102            arr.first().cloned()
1103        } else {
1104            Some(response.clone())
1105        };
1106
1107        let fee_data = fee_data
1108            .ok_or_else(|| ExchangeError::Parse("No fee data".to_string()))?;
1109
1110        let maker_rate = fee_data.get("makerCommission")
1111            .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok())
1112                .or_else(|| v.as_f64()))
1113            .unwrap_or(0.002);
1114
1115        let taker_rate = fee_data.get("takerCommission")
1116            .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok())
1117                .or_else(|| v.as_f64()))
1118            .unwrap_or(0.002);
1119
1120        Ok(FeeInfo {
1121            maker_rate,
1122            taker_rate,
1123            symbol: symbol.map(|s| s.to_string()),
1124            tier: None,
1125        })
1126    }
1127
1128    fn account_capabilities(&self, account_type: AccountType) -> AccountCapabilities {
1129        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
1130        if is_futures {
1131            AccountCapabilities {
1132                // Futures wallet balance requires a separate futures account endpoint (not implemented)
1133                has_balances: false,
1134                // GET /api/v3/account is spot-only; no futures account info endpoint implemented
1135                has_account_info: false,
1136                // GET /api/v3/tradeFee is spot-only; futures fees differ
1137                has_fees: false,
1138                // Capital transfers use spot base URL and work for all account types
1139                has_transfers: true,
1140                // Sub-accounts are a spot/master-level concept, not per-futures
1141                has_sub_accounts: false,
1142                // Deposits/withdrawals always go through the spot wallet, not futures directly
1143                has_deposit_withdraw: false,
1144                has_margin: false,
1145                has_earn_staking: false,
1146                // Futures perpetual contracts have funding payments; endpoint not yet implemented
1147                has_funding_history: false,
1148                has_ledger: false,
1149                has_convert: false,
1150                // Positions trait is not implemented for MEXC
1151                has_positions: false,
1152            }
1153        } else {
1154            AccountCapabilities {
1155                // GET /api/v3/account returns balances
1156                has_balances: true,
1157                // GET /api/v3/account returns canTrade/canWithdraw/canDeposit
1158                has_account_info: true,
1159                // GET /api/v3/tradeFee is implemented
1160                has_fees: true,
1161                // AccountTransfers trait is implemented: POST/GET /api/v3/capital/transfer
1162                has_transfers: true,
1163                // SubAccounts trait is implemented: create/list/transfer/getBalance
1164                has_sub_accounts: true,
1165                // CustodialFunds trait is implemented: deposit address, withdraw, deposit/withdraw history
1166                has_deposit_withdraw: true,
1167                // No margin borrow/repay endpoints implemented
1168                has_margin: false,
1169                // No earn or staking endpoints implemented
1170                has_earn_staking: false,
1171                // No funding payment history for spot
1172                has_funding_history: false,
1173                // No full account ledger/transaction log endpoint implemented
1174                has_ledger: false,
1175                // No coin-to-coin convert endpoint implemented
1176                has_convert: false,
1177                // Spot-only account type — no positions
1178                has_positions: false,
1179            }
1180        }
1181    }
1182}
1183
1184// ═══════════════════════════════════════════════════════════════════════════════
1185// CANCEL ALL (optional trait)
1186// ═══════════════════════════════════════════════════════════════════════════════
1187
1188#[async_trait]
1189impl CancelAll for MexcConnector {
1190    async fn cancel_all_orders(
1191        &self,
1192        scope: CancelScope,
1193        _account_type: AccountType,
1194    ) -> ExchangeResult<CancelAllResponse> {
1195        match scope {
1196            CancelScope::All { symbol: Some(sym) } | CancelScope::BySymbol { symbol: sym } => {
1197                // MEXC requires symbol for cancel all
1198                let sym_str = sym.raw()
1199                    .map(|s| s.to_string())
1200                    .unwrap_or_else(|| format!("{}{}", sym.base.to_uppercase(), sym.quote.to_uppercase()));
1201                let mut params = HashMap::new();
1202                params.insert("symbol".to_string(), sym_str);
1203
1204                let response = self.delete(MexcEndpoint::CancelAllOrders, params).await?;
1205
1206                // Response is array of cancelled orders
1207                let cancelled = if let Some(arr) = response.as_array() {
1208                    arr.len() as u32
1209                } else {
1210                    0
1211                };
1212
1213                Ok(CancelAllResponse {
1214                    cancelled_count: cancelled,
1215                    failed_count: 0,
1216                    details: vec![],
1217                })
1218            }
1219
1220            CancelScope::All { symbol: None } => {
1221                Err(ExchangeError::InvalidRequest(
1222                    "MEXC requires a symbol to cancel all orders — use BySymbol scope".to_string()
1223                ))
1224            }
1225
1226            _ => Err(ExchangeError::UnsupportedOperation(
1227                format!("{:?} not supported in cancel_all_orders", scope)
1228            )),
1229        }
1230    }
1231}
1232
1233// ═══════════════════════════════════════════════════════════════════════════════
1234// BATCH ORDERS (optional trait)
1235// ═══════════════════════════════════════════════════════════════════════════════
1236
1237#[async_trait]
1238impl BatchOrders for MexcConnector {
1239    async fn place_orders_batch(
1240        &self,
1241        orders: Vec<OrderRequest>,
1242    ) -> ExchangeResult<Vec<OrderResult>> {
1243        // MEXC: POST /api/v3/batchOrders — max 20 orders
1244        // Build batch order array
1245        let batch_orders: Vec<Value> = orders.iter().map(|req| {
1246            let o_sym = req.symbol.raw()
1247                .map(|s| s.to_string())
1248                .unwrap_or_else(|| format!("{}{}", req.symbol.base.to_uppercase(), req.symbol.quote.to_uppercase()));
1249            let side_str = match req.side {
1250                OrderSide::Buy => "BUY",
1251                OrderSide::Sell => "SELL",
1252            };
1253            let (order_type, price) = match &req.order_type {
1254                OrderType::Market => ("MARKET".to_string(), None),
1255                OrderType::Limit { price } => ("LIMIT".to_string(), Some(*price)),
1256                OrderType::PostOnly { price } => ("LIMIT_MAKER".to_string(), Some(*price)),
1257                _ => ("LIMIT".to_string(), None),
1258            };
1259
1260            let mut order_obj = json!({
1261                "symbol": o_sym,
1262                "side": side_str,
1263                "type": order_type,
1264                "quantity": self.precision.qty(&o_sym, req.quantity),
1265            });
1266
1267            if let Some(p) = price {
1268                order_obj["price"] = json!(self.precision.price(&o_sym, p));
1269            }
1270
1271            order_obj
1272        }).collect();
1273
1274        // MEXC batch orders use JSON body
1275        let auth = self.auth.as_ref()
1276            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
1277
1278        let params = HashMap::new();
1279        let (headers, _) = auth.sign_request(params);
1280
1281        let base_url = MexcUrls::base_url();
1282        let path = MexcEndpoint::BatchOrders.path();
1283        let url = format!("{}{}", base_url, path);
1284
1285        self.rate_limit_wait(1, true).await;
1286        let body = json!({ "batchOrders": batch_orders });
1287        let (response, _) = self.http.post_with_response_headers(&url, &body, &headers).await?;
1288        MexcParser::check_error(&response)?;
1289
1290        // Parse response — array of order results
1291        let results = if let Some(arr) = response.as_array() {
1292            arr.iter().map(|item| {
1293                let success = item.get("orderId").is_some();
1294                let order_id = item.get("orderId")
1295                    .and_then(|v| v.as_str())
1296                    .map(|s| s.to_string());
1297
1298                OrderResult {
1299                    order: order_id.map(|id| Order {
1300                        id,
1301                        client_order_id: None,
1302                        symbol: String::new(),
1303                        side: OrderSide::Buy,
1304                        order_type: OrderType::Market,
1305                        status: crate::core::OrderStatus::New,
1306                        price: None,
1307                        stop_price: None,
1308                        quantity: 0.0,
1309                        filled_quantity: 0.0,
1310                        average_price: None,
1311                        commission: None,
1312                        commission_asset: None,
1313                        created_at: 0,
1314                        updated_at: None,
1315                        time_in_force: crate::core::TimeInForce::Gtc,
1316                    }),
1317                    client_order_id: None,
1318                    success,
1319                    error: if success { None } else {
1320                        item.get("msg").and_then(|v| v.as_str()).map(|s| s.to_string())
1321                    },
1322                    error_code: None,
1323                }
1324            }).collect()
1325        } else {
1326            vec![]
1327        };
1328
1329        Ok(results)
1330    }
1331
1332    async fn cancel_orders_batch(
1333        &self,
1334        _order_ids: Vec<String>,
1335        _symbol: Option<&str>,
1336        _account_type: AccountType,
1337    ) -> ExchangeResult<Vec<OrderResult>> {
1338        // MEXC doesn't have a true batch cancel — cancel one by one
1339        Err(ExchangeError::UnsupportedOperation(
1340            "MEXC does not support native batch cancel — use CancelAll for symbol-level cancel".to_string()
1341        ))
1342    }
1343
1344    fn max_batch_place_size(&self) -> usize {
1345        20
1346    }
1347
1348    fn max_batch_cancel_size(&self) -> usize {
1349        0 // Not supported
1350    }
1351}
1352
1353// ═══════════════════════════════════════════════════════════════════════════════
1354// ACCOUNT TRANSFERS (optional trait)
1355// ═══════════════════════════════════════════════════════════════════════════════
1356
1357#[async_trait]
1358impl AccountTransfers for MexcConnector {
1359    /// Transfer between Spot, Margin, and Futures accounts.
1360    ///
1361    /// POST /api/v3/capital/transfer
1362    /// Params: asset, amount, fromAccountType (SPOT/FUTURES/MARGIN), toAccountType
1363    async fn transfer(&self, req: TransferRequest) -> ExchangeResult<TransferResponse> {
1364        let from_type = account_type_to_mexc_str(req.from_account);
1365        let to_type = account_type_to_mexc_str(req.to_account);
1366
1367        let mut params = HashMap::new();
1368        params.insert("asset".to_string(), req.asset.clone());
1369        params.insert("amount".to_string(), req.amount.to_string());
1370        params.insert("fromAccountType".to_string(), from_type.to_string());
1371        params.insert("toAccountType".to_string(), to_type.to_string());
1372
1373        let response = self.post(MexcEndpoint::Transfer, params).await?;
1374
1375        let tran_id = response["tranId"]
1376            .as_str()
1377            .map(|s| s.to_string())
1378            .or_else(|| response["tranId"].as_i64().map(|n| n.to_string()))
1379            .unwrap_or_else(|| "unknown".to_string());
1380
1381        Ok(TransferResponse {
1382            transfer_id: tran_id,
1383            status: "Successful".to_string(),
1384            asset: req.asset,
1385            amount: req.amount,
1386            timestamp: Some(crate::core::timestamp_millis() as i64),
1387        })
1388    }
1389
1390    /// Get internal transfer history.
1391    ///
1392    /// GET /api/v3/capital/transfer
1393    async fn get_transfer_history(
1394        &self,
1395        filter: TransferHistoryFilter,
1396    ) -> ExchangeResult<Vec<TransferResponse>> {
1397        let mut params = HashMap::new();
1398
1399        if let Some(start) = filter.start_time {
1400            params.insert("startTime".to_string(), start.to_string());
1401        }
1402        if let Some(end) = filter.end_time {
1403            params.insert("endTime".to_string(), end.to_string());
1404        }
1405        if let Some(limit) = filter.limit {
1406            params.insert("limit".to_string(), limit.to_string());
1407        }
1408
1409        let response = self.get(MexcEndpoint::TransferHistory, params).await?;
1410
1411        let rows = response.get("rows")
1412            .and_then(|v| v.as_array())
1413            .or_else(|| response.as_array())
1414            .cloned()
1415            .unwrap_or_default();
1416
1417        let records = rows.iter().map(|item| {
1418            let tran_id = item["tranId"]
1419                .as_str()
1420                .map(|s| s.to_string())
1421                .or_else(|| item["tranId"].as_i64().map(|n| n.to_string()))
1422                .unwrap_or_else(|| "unknown".to_string());
1423
1424            let asset = item["asset"].as_str().unwrap_or("").to_string();
1425            let amount = item["amount"]
1426                .as_str()
1427                .and_then(|s| s.parse::<f64>().ok())
1428                .or_else(|| item["amount"].as_f64())
1429                .unwrap_or(0.0);
1430            let status = item["status"].as_str().unwrap_or("Unknown").to_string();
1431            let timestamp = item["timestamp"].as_i64()
1432                .or_else(|| item["createTime"].as_i64());
1433
1434            TransferResponse {
1435                transfer_id: tran_id,
1436                status,
1437                asset,
1438                amount,
1439                timestamp,
1440            }
1441        }).collect();
1442
1443        Ok(records)
1444    }
1445}
1446
1447// ═══════════════════════════════════════════════════════════════════════════════
1448// CUSTODIAL FUNDS (optional trait)
1449// ═══════════════════════════════════════════════════════════════════════════════
1450
1451#[async_trait]
1452impl CustodialFunds for MexcConnector {
1453    /// Get deposit address for an asset.
1454    ///
1455    /// GET /api/v3/capital/deposit/address
1456    async fn get_deposit_address(
1457        &self,
1458        asset: &str,
1459        network: Option<&str>,
1460    ) -> ExchangeResult<DepositAddress> {
1461        let mut params = HashMap::new();
1462        params.insert("coin".to_string(), asset.to_uppercase());
1463
1464        if let Some(net) = network {
1465            params.insert("network".to_string(), net.to_string());
1466        }
1467
1468        let response = self.get(MexcEndpoint::DepositAddress, params).await?;
1469
1470        let address = response["address"]
1471            .as_str()
1472            .ok_or_else(|| ExchangeError::Parse("Missing deposit address".into()))?
1473            .to_string();
1474
1475        let tag = response["tag"]
1476            .as_str()
1477            .filter(|s| !s.is_empty())
1478            .map(|s| s.to_string());
1479
1480        let net = response["network"]
1481            .as_str()
1482            .or(network)
1483            .map(|s| s.to_string());
1484
1485        Ok(DepositAddress {
1486            address,
1487            tag,
1488            network: net,
1489            asset: asset.to_uppercase(),
1490            created_at: None,
1491        })
1492    }
1493
1494    /// Submit a withdrawal request.
1495    ///
1496    /// POST /api/v3/capital/withdraw
1497    async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
1498        let mut params = HashMap::new();
1499        params.insert("coin".to_string(), req.asset.clone());
1500        params.insert("address".to_string(), req.address.clone());
1501        params.insert("amount".to_string(), req.amount.to_string());
1502
1503        if let Some(net) = &req.network {
1504            params.insert("network".to_string(), net.clone());
1505        }
1506        if let Some(memo) = &req.tag {
1507            params.insert("memo".to_string(), memo.clone());
1508        }
1509
1510        let response = self.post(MexcEndpoint::Withdraw, params).await?;
1511
1512        let withdraw_id = response["id"]
1513            .as_str()
1514            .map(|s| s.to_string())
1515            .or_else(|| response["id"].as_i64().map(|n| n.to_string()))
1516            .unwrap_or_else(|| "unknown".to_string());
1517
1518        Ok(WithdrawResponse {
1519            withdraw_id,
1520            status: "Pending".to_string(),
1521            tx_hash: None,
1522        })
1523    }
1524
1525    /// Get deposit and/or withdrawal history.
1526    ///
1527    /// GET /api/v3/capital/deposit/hisrec  (deposits)
1528    /// GET /api/v3/capital/withdraw/history (withdrawals)
1529    async fn get_funds_history(
1530        &self,
1531        filter: FundsHistoryFilter,
1532    ) -> ExchangeResult<Vec<FundsRecord>> {
1533        let mut records = Vec::new();
1534
1535        let mut params = HashMap::new();
1536        if let Some(asset) = &filter.asset {
1537            params.insert("coin".to_string(), asset.to_uppercase());
1538        }
1539        if let Some(start) = filter.start_time {
1540            params.insert("startTime".to_string(), start.to_string());
1541        }
1542        if let Some(end) = filter.end_time {
1543            params.insert("endTime".to_string(), end.to_string());
1544        }
1545        if let Some(limit) = filter.limit {
1546            params.insert("limit".to_string(), limit.to_string());
1547        }
1548
1549        if matches!(filter.record_type, FundsRecordType::Deposit | FundsRecordType::Both) {
1550            let response = self.get(MexcEndpoint::DepositHistory, params.clone()).await?;
1551
1552            let items = response.as_array().cloned().unwrap_or_default();
1553            for item in &items {
1554                let id = item["id"].as_str().unwrap_or("").to_string();
1555                let asset = item["coin"].as_str().unwrap_or("").to_string();
1556                let amount = item["amount"]
1557                    .as_str().and_then(|s| s.parse::<f64>().ok())
1558                    .or_else(|| item["amount"].as_f64())
1559                    .unwrap_or(0.0);
1560                let tx_hash = item["txId"].as_str().map(|s| s.to_string());
1561                let network = item["network"].as_str().map(|s| s.to_string());
1562                let status = item["status"].as_str().unwrap_or("Unknown").to_string();
1563                let timestamp = item["insertTime"].as_i64().unwrap_or(0);
1564
1565                records.push(FundsRecord::Deposit {
1566                    id,
1567                    asset,
1568                    amount,
1569                    tx_hash,
1570                    network,
1571                    status,
1572                    timestamp,
1573                });
1574            }
1575        }
1576
1577        if matches!(filter.record_type, FundsRecordType::Withdrawal | FundsRecordType::Both) {
1578            let response = self.get(MexcEndpoint::WithdrawHistory, params).await?;
1579
1580            let items = response.as_array().cloned().unwrap_or_default();
1581            for item in &items {
1582                let id = item["id"].as_str().unwrap_or("").to_string();
1583                let asset = item["coin"].as_str().unwrap_or("").to_string();
1584                let amount = item["amount"]
1585                    .as_str().and_then(|s| s.parse::<f64>().ok())
1586                    .or_else(|| item["amount"].as_f64())
1587                    .unwrap_or(0.0);
1588                let fee = item["transactionFee"]
1589                    .as_str().and_then(|s| s.parse::<f64>().ok())
1590                    .or_else(|| item["transactionFee"].as_f64());
1591                let address = item["address"].as_str().unwrap_or("").to_string();
1592                let tag = item["addressTag"].as_str()
1593                    .filter(|s| !s.is_empty())
1594                    .map(|s| s.to_string());
1595                let tx_hash = item["txId"].as_str().map(|s| s.to_string());
1596                let network = item["network"].as_str().map(|s| s.to_string());
1597                let status = item["status"].as_str().unwrap_or("Unknown").to_string();
1598                let timestamp = item["applyTime"].as_i64()
1599                    .or_else(|| item["insertTime"].as_i64())
1600                    .unwrap_or(0);
1601
1602                records.push(FundsRecord::Withdrawal {
1603                    id,
1604                    asset,
1605                    amount,
1606                    fee,
1607                    address,
1608                    tag,
1609                    tx_hash,
1610                    network,
1611                    status,
1612                    timestamp,
1613                });
1614            }
1615        }
1616
1617        Ok(records)
1618    }
1619}
1620
1621// ═══════════════════════════════════════════════════════════════════════════════
1622// SUB ACCOUNTS (optional trait)
1623// ═══════════════════════════════════════════════════════════════════════════════
1624
1625#[async_trait]
1626impl SubAccounts for MexcConnector {
1627    /// Perform sub-account operations: Create, List, Transfer, GetBalance.
1628    async fn sub_account_operation(
1629        &self,
1630        op: SubAccountOperation,
1631    ) -> ExchangeResult<SubAccountResult> {
1632        match op {
1633            SubAccountOperation::Create { label } => {
1634                // POST /api/v3/sub-account/virtualSubAccount
1635                let mut params = HashMap::new();
1636                params.insert("subUserName".to_string(), label.clone());
1637
1638                let response = self.post(MexcEndpoint::SubAccountCreate, params).await?;
1639
1640                let id = response["subUserId"]
1641                    .as_str()
1642                    .map(|s| s.to_string())
1643                    .or_else(|| response["subUserId"].as_i64().map(|n| n.to_string()));
1644
1645                Ok(SubAccountResult {
1646                    id,
1647                    name: Some(label),
1648                    accounts: vec![],
1649                    transaction_id: None,
1650                })
1651            }
1652
1653            SubAccountOperation::List => {
1654                // GET /api/v3/sub-account/list
1655                let response = self.get(MexcEndpoint::SubAccountList, HashMap::new()).await?;
1656
1657                let items = response.get("subAccounts")
1658                    .and_then(|v| v.as_array())
1659                    .or_else(|| response.as_array())
1660                    .cloned()
1661                    .unwrap_or_default();
1662
1663                let accounts = items.iter().map(|item| {
1664                    let id = item["subUserId"]
1665                        .as_str()
1666                        .map(|s| s.to_string())
1667                        .or_else(|| item["subUserId"].as_i64().map(|n| n.to_string()))
1668                        .unwrap_or_default();
1669                    let name = item["subUserName"].as_str().unwrap_or("").to_string();
1670                    let status = if item["isFreeze"].as_bool().unwrap_or(false) {
1671                        "Frozen".to_string()
1672                    } else {
1673                        "Normal".to_string()
1674                    };
1675
1676                    SubAccount { id, name, status }
1677                }).collect();
1678
1679                Ok(SubAccountResult {
1680                    id: None,
1681                    name: None,
1682                    accounts,
1683                    transaction_id: None,
1684                })
1685            }
1686
1687            SubAccountOperation::Transfer { sub_account_id, asset, amount, to_sub } => {
1688                // POST /api/v3/capital/sub-account/universalTransfer
1689                // fromEmail / toEmail identifies the direction
1690                // MEXC uses email as sub-account identifier
1691                let mut params = HashMap::new();
1692                if to_sub {
1693                    params.insert("toEmail".to_string(), sub_account_id.clone());
1694                    params.insert("fromAccountType".to_string(), "SPOT".to_string());
1695                    params.insert("toAccountType".to_string(), "SPOT".to_string());
1696                } else {
1697                    params.insert("fromEmail".to_string(), sub_account_id.clone());
1698                    params.insert("fromAccountType".to_string(), "SPOT".to_string());
1699                    params.insert("toAccountType".to_string(), "SPOT".to_string());
1700                }
1701                params.insert("asset".to_string(), asset);
1702                params.insert("amount".to_string(), amount.to_string());
1703
1704                let response = self.post(MexcEndpoint::SubAccountTransfer, params).await?;
1705
1706                let tran_id = response["tranId"]
1707                    .as_str()
1708                    .map(|s| s.to_string())
1709                    .or_else(|| response["tranId"].as_i64().map(|n| n.to_string()));
1710
1711                Ok(SubAccountResult {
1712                    id: None,
1713                    name: None,
1714                    accounts: vec![],
1715                    transaction_id: tran_id,
1716                })
1717            }
1718
1719            SubAccountOperation::GetBalance { sub_account_id } => {
1720                // GET /api/v3/sub-account/assets?email={sub_account_id}
1721                let mut params = HashMap::new();
1722                params.insert("email".to_string(), sub_account_id);
1723
1724                let _response = self.get(MexcEndpoint::SubAccountAssets, params).await?;
1725
1726                // Balance is available in response but SubAccountResult doesn't carry it;
1727                // return the sub-account id as acknowledgement that data was fetched.
1728                Ok(SubAccountResult {
1729                    id: None,
1730                    name: None,
1731                    accounts: vec![],
1732                    transaction_id: None,
1733                })
1734            }
1735        }
1736    }
1737}
1738
1739// ═══════════════════════════════════════════════════════════════════════════════
1740// HELPERS
1741// ═══════════════════════════════════════════════════════════════════════════════
1742
1743// ═══════════════════════════════════════════════════════════════════════════════
1744// EXTENDED METHODS (not part of core traits)
1745// ═══════════════════════════════════════════════════════════════════════════════
1746
1747impl MexcConnector {
1748    /// Get recent public spot trades.
1749    ///
1750    /// `GET /api/v3/trades`
1751    ///
1752    /// # Parameters
1753    /// - `symbol`: Spot symbol e.g. `BTCUSDT`
1754    /// - `limit`: Number of trades to return (optional, default 500, max 1000)
1755    pub async fn get_recent_trades(
1756        &self,
1757        symbol: &str,
1758        limit: Option<u32>,
1759    ) -> ExchangeResult<Value> {
1760        let mut params = HashMap::new();
1761        params.insert("symbol".to_string(), symbol.to_string());
1762        if let Some(l) = limit {
1763            params.insert("limit".to_string(), l.to_string());
1764        }
1765        self.get(MexcEndpoint::RecentTrades, params).await
1766    }
1767
1768    /// Get personal spot trade history (requires auth).
1769    ///
1770    /// `GET /api/v3/myTrades`
1771    ///
1772    /// # Parameters
1773    /// - `symbol`: Spot symbol e.g. `BTCUSDT`
1774    /// - `limit`: Max number of trades (optional, default 500, max 1000)
1775    /// - `start_time`: Start timestamp in ms (optional)
1776    /// - `end_time`: End timestamp in ms (optional)
1777    pub async fn get_my_trades(
1778        &self,
1779        symbol: &str,
1780        limit: Option<u32>,
1781        start_time: Option<i64>,
1782        end_time: Option<i64>,
1783    ) -> ExchangeResult<Value> {
1784        let mut params = HashMap::new();
1785        params.insert("symbol".to_string(), symbol.to_string());
1786        if let Some(l) = limit {
1787            params.insert("limit".to_string(), l.to_string());
1788        }
1789        if let Some(st) = start_time {
1790            params.insert("startTime".to_string(), st.to_string());
1791        }
1792        if let Some(et) = end_time {
1793            params.insert("endTime".to_string(), et.to_string());
1794        }
1795        self.get(MexcEndpoint::MyTrades, params).await
1796    }
1797
1798    /// Get futures mark price and index price for a contract.
1799    ///
1800    /// `GET /api/v1/contract/index_price/{symbol}`
1801    ///
1802    /// Returns the current mark price and index price for the given futures contract.
1803    pub async fn get_futures_mark_price(&self, symbol: &str) -> ExchangeResult<Value> {
1804        // MEXC futures endpoints use path-based symbol: /api/v1/contract/index_price/{symbol}
1805        let base_url = MexcUrls::futures_base_url();
1806        let path = format!("{}/{}", MexcEndpoint::FuturesMarkPrice.path(), symbol);
1807        let url = format!("{}{}", base_url, path);
1808        if !self.rate_limit_wait(1, false).await {
1809            return Err(ExchangeError::RateLimitExceeded {
1810                retry_after: None,
1811                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
1812            });
1813        }
1814        let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &HashMap::new()).await?;
1815        self.update_weight_from_headers(&resp_headers);
1816        Ok(response)
1817    }
1818
1819    /// Get current funding rate for a futures contract.
1820    ///
1821    /// `GET /api/v1/contract/funding_rate/{symbol}` (MEXC futures domain)
1822    ///
1823    /// # TODO
1824    /// Verify exact endpoint path against live MEXC contract API documentation.
1825    pub async fn get_funding_rate(&self, symbol: &str) -> ExchangeResult<Value> {
1826        let base_url = MexcUrls::futures_base_url();
1827        let path = format!("{}/{}", MexcEndpoint::FuturesFundingRate.path(), symbol);
1828        let url = format!("{}{}", base_url, path);
1829        if !self.rate_limit_wait(1, false).await {
1830            return Err(ExchangeError::RateLimitExceeded {
1831                retry_after: None,
1832                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
1833            });
1834        }
1835        let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &HashMap::new()).await?;
1836        self.update_weight_from_headers(&resp_headers);
1837        Ok(response)
1838    }
1839
1840    /// Get open interest for a futures contract via the ticker endpoint.
1841    ///
1842    /// MEXC does not expose `/api/v1/contract/open_interest/{symbol}` — that path
1843    /// returns 404. OI is embedded in the ticker response as `data.holdVol`.
1844    /// See: `GET /api/v1/contract/ticker?symbol={symbol}` (MEXC futures domain)
1845    pub async fn get_futures_ticker_raw(&self, symbol: &str) -> ExchangeResult<Value> {
1846        let base_url = MexcUrls::futures_base_url();
1847        let url = format!("{}{}", base_url, MexcEndpoint::FuturesTicker.path());
1848        if !self.rate_limit_wait(1, false).await {
1849            return Err(ExchangeError::RateLimitExceeded {
1850                retry_after: None,
1851                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
1852            });
1853        }
1854        let mut params = HashMap::new();
1855        params.insert("symbol".to_string(), symbol.to_string());
1856        let (response, resp_headers) = self.http.get_with_response_headers(&url, &params, &HashMap::new()).await?;
1857        self.update_weight_from_headers(&resp_headers);
1858        Ok(response)
1859    }
1860}
1861
1862/// Map internal AccountType to MEXC's transfer account type string.
1863fn account_type_to_mexc_str(account_type: AccountType) -> &'static str {
1864    match account_type {
1865        AccountType::Spot => "SPOT",
1866        AccountType::Margin => "MARGIN",
1867        AccountType::FuturesCross | AccountType::FuturesIsolated => "FUTURES",
1868        AccountType::Earn | AccountType::Lending | AccountType::Options | AccountType::Convert => "SPOT",
1869    }
1870}
1871
1872fn interval_to_ms(interval: &str) -> i64 {
1873    match interval {
1874        "1m" => 60_000,
1875        "5m" => 300_000,
1876        "15m" => 900_000,
1877        "30m" => 1_800_000,
1878        "1h" => 3_600_000,
1879        "4h" => 14_400_000,
1880        "12h" => 43_200_000,
1881        "1d" => 86_400_000,
1882        "1w" => 604_800_000,
1883        _ => 3_600_000,
1884    }
1885}
1886
1887// MEXC Spot connector — no futures/positions support in v5 yet.
1888#[async_trait]
1889impl crate::core::traits::Positions for MexcConnector {
1890    async fn get_positions(
1891        &self,
1892        _query: crate::core::types::PositionQuery,
1893    ) -> ExchangeResult<Vec<crate::core::types::Position>> {
1894        Err(ExchangeError::UnsupportedOperation(
1895            "MEXC positions not implemented in v5".into(),
1896        ))
1897    }
1898
1899    async fn get_funding_rate(
1900        &self,
1901        _symbol: &str,
1902        _account_type: AccountType,
1903    ) -> ExchangeResult<crate::core::types::FundingRate> {
1904        Err(ExchangeError::UnsupportedOperation(
1905            "MEXC funding rate not implemented in v5".into(),
1906        ))
1907    }
1908
1909    async fn modify_position(
1910        &self,
1911        _req: crate::core::types::PositionModification,
1912    ) -> ExchangeResult<()> {
1913        Err(ExchangeError::UnsupportedOperation(
1914            "MEXC position modification not implemented in v5".into(),
1915        ))
1916    }
1917
1918    async fn get_open_interest(
1919        &self,
1920        symbol: &str,
1921        _account_type: AccountType,
1922    ) -> ExchangeResult<crate::core::types::OpenInterest> {
1923        // MEXC has no dedicated OI endpoint. OI lives in the futures ticker as `data.holdVol`.
1924        // Reference: GET /api/v1/contract/ticker?symbol=BTC_USDT → data.holdVol
1925        let raw_symbol = if symbol.contains('/') {
1926            let parts: Vec<&str> = symbol.split('/').collect();
1927            format!(
1928                "{}_{}",
1929                parts[0].to_uppercase(),
1930                parts.get(1).copied().unwrap_or("USDT").to_uppercase()
1931            )
1932        } else if symbol.contains('-') {
1933            symbol.to_uppercase().replace('-', "_")
1934        } else if !symbol.contains('_') {
1935            // Spot-format symbol (BTCUSDT) — convert to futures format (BTC_USDT).
1936            use crate::core::utils::symbol_normalizer::SymbolNormalizer;
1937            SymbolNormalizer::from_exchange(crate::core::types::ExchangeId::MEXC, symbol, AccountType::Spot)
1938                .and_then(|canonical| SymbolNormalizer::to_exchange(crate::core::types::ExchangeId::MEXC, &canonical, AccountType::FuturesCross))
1939                .unwrap_or_else(|_| symbol.to_uppercase())
1940        } else {
1941            symbol.to_uppercase()
1942        };
1943        let response = self.get_futures_ticker_raw(&raw_symbol).await?;
1944        let data = response
1945            .get("data")
1946            .ok_or_else(|| ExchangeError::Parse("MEXC OI: missing 'data' in ticker response".to_string()))?;
1947        // data is an array (ticker list) when no symbol filter is applied,
1948        // but with ?symbol= it returns a single object.
1949        let ticker_obj = if let Some(arr) = data.as_array() {
1950            arr.iter()
1951                .find(|t| t.get("symbol").and_then(|s| s.as_str()) == Some(raw_symbol.as_str()))
1952                .ok_or_else(|| ExchangeError::Parse(format!("MEXC OI: symbol {} not found in ticker list", raw_symbol)))?
1953        } else {
1954            data
1955        };
1956        let oi = ticker_obj
1957            .get("holdVol")
1958            .and_then(|v| v.as_f64())
1959            .or_else(|| {
1960                ticker_obj.get("holdVol")
1961                    .and_then(|v| v.as_str())
1962                    .and_then(|s| s.parse::<f64>().ok())
1963            })
1964            .unwrap_or(0.0);
1965        Ok(crate::core::types::OpenInterest {
1966            symbol: raw_symbol,
1967            open_interest: oi,
1968            open_interest_value: None,
1969            timestamp: crate::core::timestamp_millis() as i64,
1970        })
1971    }
1972}
1973
1974impl crate::core::traits::HasCapabilities for MexcConnector {
1975    fn capabilities(&self) -> crate::core::types::ConnectorCapabilities {
1976        crate::core::types::ConnectorCapabilities {
1977            has_ticker: true, has_orderbook: true, has_klines: true,
1978            has_recent_trades: false, has_exchange_info: true,
1979            has_liquidation_history: false, has_open_interest_history: false,
1980            has_premium_index: false, has_long_short_ratio_history: false,
1981            has_funding_rate_history: false, has_mark_price_klines: false,
1982            has_index_price_klines: false,
1983            has_market_order: true, has_limit_order: true,
1984            has_open_orders: true, has_order_history: true, has_user_trades: true,
1985            has_positions: true, has_mark_price: false, has_modify_position: false,
1986            has_closed_pnl: false, has_long_short_ratio: false,
1987            has_cancel_all: true, has_amend_order: false,
1988            has_batch_place: true, has_batch_cancel: false,
1989            max_batch_place_size: 20, max_batch_cancel_size: 0,
1990            has_balance: true, has_account_info: true, has_fees: true,
1991            has_transfers: true, has_deposit_withdraw: true, has_sub_accounts: true,
1992            has_funding_payments: false, has_ledger: false,
1993            has_websocket: true, has_ws_klines: true, has_ws_trades: true,
1994            has_ws_orderbook: true, has_ws_ticker: true,
1995            has_ws_mark_price: false, has_ws_funding_rate: false,
1996            validation: self.validation_status(),
1997        }
1998    }
1999
2000    fn validation_status(&self) -> Option<&'static crate::core::types::ValidationStamp> {
2001        crate::core::utils::validation_snapshot::validation_for(crate::core::types::ExchangeId::MEXC)
2002    }
2003}