1use 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
52static 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
79pub struct MexcConnector {
85 http: HttpClient,
87 auth: Option<MexcAuth>,
89 limiter: Arc<Mutex<RuntimeLimiter>>,
91 monitor: Arc<Mutex<RateLimitMonitor>>,
93 precision: crate::core::utils::precision::PrecisionCache,
95}
96
97impl MexcConnector {
98 pub async fn new(credentials: Option<Credentials>) -> ExchangeResult<Self> {
100 let http = HttpClient::new(30_000)?; let mut auth = credentials.as_ref().map(MexcAuth::new);
103
104 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 pub async fn public() -> ExchangeResult<Self> {
133 Self::new(None).await
134 }
135
136 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 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 async fn get(
186 &self,
187 endpoint: MexcEndpoint,
188 params: HashMap<String, String>,
189 ) -> ExchangeResult<Value> {
190 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 async fn post(
244 &self,
245 endpoint: MexcEndpoint,
246 params: HashMap<String, String>,
247 ) -> ExchangeResult<Value> {
248 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 async fn delete(
274 &self,
275 endpoint: MexcEndpoint,
276 params: HashMap<String, String>,
277 ) -> ExchangeResult<Value> {
278 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 pub async fn get_exchange_info_raw(&self) -> ExchangeResult<Value> {
308 self.get(MexcEndpoint::ExchangeInfo, HashMap::new()).await
309 }
310
311 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 MexcParser::parse_orders(&response)
324 }
325}
326
327impl 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 }
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#[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 has_exchange_info: false,
625 has_recent_trades: true,
627 supported_intervals: &["1m", "5m", "15m", "30m", "1h", "4h", "8h", "1d", "1w", "1M"],
629 max_kline_limit: None,
631 has_ws_klines: true,
633 has_ws_trades: true,
635 has_ws_orderbook: true,
637 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 has_recent_trades: true,
650 supported_intervals: &["1m", "5m", "15m", "30m", "1h", "4h", "8h", "1d", "1w", "1M"],
652 max_kline_limit: Some(1000),
653 has_ws_klines: true,
655 has_ws_trades: true,
657 has_ws_orderbook: true,
659 has_ws_ticker: true,
661 }
662 }
663 }
664}
665
666#[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 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 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 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 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 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 has_stop_market: true,
995 has_stop_limit: true,
996 has_trailing_stop: false,
997 has_bracket: false,
998 has_oco: false,
999 has_amend: false,
1001 has_batch: false,
1003 max_batch_size: None,
1004 has_cancel_all: false,
1006 has_user_trades: false,
1008 has_order_history: false,
1010 }
1011 } else {
1012 TradingCapabilities {
1013 has_market_order: true,
1014 has_limit_order: true,
1015 has_stop_market: false,
1017 has_stop_limit: false,
1018 has_trailing_stop: false,
1019 has_bracket: false,
1020 has_oco: false,
1022 has_amend: false,
1024 has_batch: true,
1026 max_batch_size: Some(20),
1027 has_cancel_all: true,
1029 has_user_trades: true,
1031 has_order_history: true,
1033 }
1034 }
1035 }
1036}
1037
1038#[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 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 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 has_balances: false,
1134 has_account_info: false,
1136 has_fees: false,
1138 has_transfers: true,
1140 has_sub_accounts: false,
1142 has_deposit_withdraw: false,
1144 has_margin: false,
1145 has_earn_staking: false,
1146 has_funding_history: false,
1148 has_ledger: false,
1149 has_convert: false,
1150 has_positions: false,
1152 }
1153 } else {
1154 AccountCapabilities {
1155 has_balances: true,
1157 has_account_info: true,
1159 has_fees: true,
1161 has_transfers: true,
1163 has_sub_accounts: true,
1165 has_deposit_withdraw: true,
1167 has_margin: false,
1169 has_earn_staking: false,
1171 has_funding_history: false,
1173 has_ledger: false,
1175 has_convert: false,
1177 has_positions: false,
1179 }
1180 }
1181 }
1182}
1183
1184#[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 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 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#[async_trait]
1238impl BatchOrders for MexcConnector {
1239 async fn place_orders_batch(
1240 &self,
1241 orders: Vec<OrderRequest>,
1242 ) -> ExchangeResult<Vec<OrderResult>> {
1243 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 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 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 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 }
1351}
1352
1353#[async_trait]
1358impl AccountTransfers for MexcConnector {
1359 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 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#[async_trait]
1452impl CustodialFunds for MexcConnector {
1453 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 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 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#[async_trait]
1626impl SubAccounts for MexcConnector {
1627 async fn sub_account_operation(
1629 &self,
1630 op: SubAccountOperation,
1631 ) -> ExchangeResult<SubAccountResult> {
1632 match op {
1633 SubAccountOperation::Create { label } => {
1634 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 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 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 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 Ok(SubAccountResult {
1729 id: None,
1730 name: None,
1731 accounts: vec![],
1732 transaction_id: None,
1733 })
1734 }
1735 }
1736 }
1737}
1738
1739impl MexcConnector {
1748 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 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 pub async fn get_futures_mark_price(&self, symbol: &str) -> ExchangeResult<Value> {
1804 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 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 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, ¶ms, &HashMap::new()).await?;
1857 self.update_weight_from_headers(&resp_headers);
1858 Ok(response)
1859 }
1860}
1861
1862fn 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#[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 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 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 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}