1pub mod native;
4
5use std::sync::Arc;
6
7use parking_lot::RwLock;
8use rust_decimal::Decimal;
9use serde::Deserialize;
10use serde_json::Value;
11
12use bat_markets_core::{
13 AccountSnapshot, AggressorSide, AssetCode, Balance, BatMarketsConfig, CapabilitySet,
14 ClientOrderId, CommandOperation, CommandReceipt, CommandStatus, ErrorKind, Execution,
15 FastKline, FastOrderBookDelta, FastTicker, FastTrade, FundingRate, InstrumentCatalog,
16 InstrumentId, InstrumentSpec, InstrumentStatus, InstrumentSupport, Kline, KlineInterval,
17 Leverage, Liquidity, MarginMode, MarketError, MarketType, Notional, OpenInterest, Order,
18 OrderId, OrderStatus, OrderType, Position, PositionDirection, PositionId, PositionMode, Price,
19 PrivateLaneEvent, Product, PublicLaneEvent, Quantity, Rate, RequestId, Result, Side, Ticker,
20 TimeInForce, TimestampMs, TradeId, Venue, VenueAdapter,
21};
22
23#[derive(Clone, Debug)]
25pub struct MexcLinearFuturesAdapter {
26 config: BatMarketsConfig,
27 capabilities: CapabilitySet,
28 lane_set: bat_markets_core::LaneSet,
29 instruments: Arc<RwLock<InstrumentCatalog>>,
30}
31
32impl Default for MexcLinearFuturesAdapter {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl MexcLinearFuturesAdapter {
39 #[must_use]
40 pub fn new() -> Self {
41 Self::with_config(BatMarketsConfig::new(Venue::Mexc, Product::LinearUsdt))
42 }
43
44 #[must_use]
45 pub fn with_config(config: BatMarketsConfig) -> Self {
46 Self {
47 config,
48 capabilities: mexc_capabilities(),
49 lane_set: bat_markets_core::LaneSet::linear_futures_defaults(),
50 instruments: Arc::new(RwLock::new(InstrumentCatalog::new([
51 btc_spec(),
52 eth_spec(),
53 ]))),
54 }
55 }
56
57 pub fn replace_instruments(&self, instruments: Vec<InstrumentSpec>) {
58 self.instruments.write().replace(instruments);
59 }
60
61 pub fn parse_native_public(&self, payload: &str) -> Result<native::PublicEnvelope> {
62 serde_json::from_str(payload).map_err(|error| {
63 decode_message(format!("failed to parse mexc public payload: {error}"))
64 })
65 }
66
67 pub fn parse_metadata_snapshot(&self, payload: &str) -> Result<Vec<InstrumentSpec>> {
68 let response =
69 serde_json::from_str::<native::RestResponse<Vec<native::ContractInfo>>>(payload)
70 .map_err(|error| {
71 decode_message(format!(
72 "failed to parse mexc contract detail response: {error}"
73 ))
74 })?;
75 ensure_success(response.success, response.code, response.message.as_deref())?;
76
77 response
78 .data
79 .into_iter()
80 .filter(|contract| contract.quote_coin == "USDT" && contract.settle_coin == "USDT")
81 .map(contract_to_spec)
82 .collect()
83 }
84
85 pub fn parse_server_time(&self, payload: &str) -> Result<TimestampMs> {
86 let response =
87 serde_json::from_str::<native::ServerTimeResponse>(payload).map_err(|error| {
88 decode_message(format!(
89 "failed to parse mexc server-time response: {error}"
90 ))
91 })?;
92 Ok(TimestampMs::new(response.data))
93 }
94
95 pub fn parse_ticker_snapshot(&self, payload: &str, spec: &InstrumentSpec) -> Result<Ticker> {
96 let response = serde_json::from_str::<native::RestResponse<Value>>(payload)
97 .map_err(|error| decode_message(format!("failed to parse mexc ticker: {error}")))?;
98 ensure_success(response.success, response.code, response.message.as_deref())?;
99 let data = find_symbol_object(response.data, spec.native_symbol.as_ref())?;
100 let ticker = serde_json::from_value::<native::TickerData>(data).map_err(|error| {
101 decode_message(format!("failed to decode mexc ticker data: {error}"))
102 })?;
103 ticker_to_unified(ticker, spec)
104 }
105
106 pub fn parse_tickers_snapshot(
107 &self,
108 payload: &str,
109 specs: &[InstrumentSpec],
110 ) -> Result<Vec<Ticker>> {
111 let response = serde_json::from_str::<native::RestResponse<Value>>(payload)
112 .map_err(|error| decode_message(format!("failed to parse mexc tickers: {error}")))?;
113 ensure_success(response.success, response.code, response.message.as_deref())?;
114 let items = if let Some(items) = response.data.as_array() {
115 items.clone()
116 } else if response.data.is_object() {
117 vec![response.data.clone()]
118 } else {
119 Vec::new()
120 };
121 items
122 .into_iter()
123 .filter_map(|item| {
124 let ticker = serde_json::from_value::<native::TickerData>(item).ok()?;
125 let spec = specs
126 .iter()
127 .find(|spec| spec.native_symbol.as_ref() == ticker.symbol.as_str())?;
128 Some(ticker_to_unified(ticker, spec))
129 })
130 .collect()
131 }
132
133 pub fn parse_trades_snapshot(
134 &self,
135 payload: &str,
136 spec: &InstrumentSpec,
137 ) -> Result<Vec<bat_markets_core::TradeTick>> {
138 let response = serde_json::from_str::<native::RestResponse<Vec<native::DealData>>>(payload)
139 .map_err(|error| {
140 decode_message(format!("failed to parse mexc deals response: {error}"))
141 })?;
142 ensure_success(response.success, response.code, response.message.as_deref())?;
143 response
144 .data
145 .into_iter()
146 .enumerate()
147 .map(|(index, deal)| trade_to_unified(deal, spec, index))
148 .collect()
149 }
150
151 pub fn parse_order_book_snapshot(
152 &self,
153 payload: &str,
154 spec: &InstrumentSpec,
155 ) -> Result<bat_markets_core::OrderBookSnapshot> {
156 let response = serde_json::from_str::<native::RestResponse<native::DepthData>>(payload)
157 .map_err(|error| {
158 decode_message(format!("failed to parse mexc order book response: {error}"))
159 })?;
160 ensure_success(response.success, response.code, response.message.as_deref())?;
161 Ok(bat_markets_core::OrderBookSnapshot {
162 instrument_id: spec.instrument_id.clone(),
163 bids: response
164 .data
165 .bids
166 .into_iter()
167 .map(level_to_book_level)
168 .collect::<Result<Vec<_>>>()?,
169 asks: response
170 .data
171 .asks
172 .into_iter()
173 .map(level_to_book_level)
174 .collect::<Result<Vec<_>>>()?,
175 event_time: TimestampMs::new(response.data.timestamp.unwrap_or_else(now_ms)),
176 })
177 }
178
179 pub fn parse_ohlcv_snapshot(
180 &self,
181 payload: &str,
182 request: &bat_markets_core::FetchOhlcvRequest,
183 ) -> Result<Vec<Kline>> {
184 let instrument_id = request.single_instrument_id()?.clone();
185 self.resolve_instrument(&instrument_id).ok_or_else(|| {
186 MarketError::new(
187 ErrorKind::Unsupported,
188 format!("unknown mexc instrument {instrument_id}"),
189 )
190 })?;
191 let interval = parse_kline_interval(&request.interval)?;
192 let response = serde_json::from_str::<native::RestResponse<native::KlineRestData>>(payload)
193 .map_err(|error| {
194 decode_message(format!("failed to parse mexc kline response: {error}"))
195 })?;
196 ensure_success(response.success, response.code, response.message.as_deref())?;
197
198 let len = response.data.time.len();
199 if response.data.open.len() != len
200 || response.data.close.len() != len
201 || response.data.high.len() != len
202 || response.data.low.len() != len
203 || response.data.vol.len() != len
204 {
205 return Err(decode_message(
206 "mexc kline arrays have inconsistent lengths".to_owned(),
207 ));
208 }
209
210 (0..len)
211 .map(|index| {
212 let open_time = response.data.time[index] * 1_000;
213 Ok(Kline {
214 instrument_id: instrument_id.clone(),
215 interval: interval.into(),
216 open: Price::new(decimal_from_value(&response.data.open[index])?),
217 high: Price::new(decimal_from_value(&response.data.high[index])?),
218 low: Price::new(decimal_from_value(&response.data.low[index])?),
219 close: Price::new(decimal_from_value(&response.data.close[index])?),
220 volume: Quantity::new(decimal_from_value(&response.data.vol[index])?),
221 open_time: TimestampMs::new(open_time),
222 close_time: TimestampMs::new(
223 interval.close_time_ms(open_time).unwrap_or(open_time),
224 ),
225 closed: true,
226 })
227 })
228 .collect()
229 }
230
231 pub fn parse_funding_rate_snapshot(
232 &self,
233 payload: &str,
234 spec: &InstrumentSpec,
235 ) -> Result<FundingRate> {
236 let response =
237 serde_json::from_str::<native::RestResponse<native::FundingRateData>>(payload)
238 .map_err(|error| {
239 decode_message(format!("failed to parse mexc funding rate: {error}"))
240 })?;
241 ensure_success(response.success, response.code, response.message.as_deref())?;
242 Ok(FundingRate {
243 instrument_id: spec.instrument_id.clone(),
244 value: Rate::new(decimal_from_value(&response.data.funding_rate)?),
245 mark_price: None,
246 event_time: TimestampMs::new(response.data.timestamp.unwrap_or_else(now_ms)),
247 })
248 }
249
250 pub fn parse_open_interest_snapshot(
251 &self,
252 payload: &str,
253 spec: &InstrumentSpec,
254 ) -> Result<OpenInterest> {
255 let response =
256 serde_json::from_str::<native::RestResponse<Value>>(payload).map_err(|error| {
257 decode_message(format!(
258 "failed to parse mexc open interest ticker: {error}"
259 ))
260 })?;
261 ensure_success(response.success, response.code, response.message.as_deref())?;
262 let data = find_symbol_object(response.data, spec.native_symbol.as_ref())?;
263 let ticker = serde_json::from_value::<native::TickerData>(data).map_err(|error| {
264 decode_message(format!(
265 "failed to decode mexc open interest ticker data: {error}"
266 ))
267 })?;
268 let value = ticker
269 .hold_vol
270 .as_ref()
271 .map(decimal_from_value)
272 .transpose()?
273 .map(Quantity::new)
274 .unwrap_or_else(|| Quantity::new(Decimal::ZERO));
275 Ok(OpenInterest {
276 instrument_id: spec.instrument_id.clone(),
277 value,
278 event_time: TimestampMs::new(ticker.timestamp.unwrap_or_else(now_ms)),
279 })
280 }
281
282 pub fn parse_account_snapshot(
283 &self,
284 payload: &str,
285 observed_at: TimestampMs,
286 ) -> Result<AccountSnapshot> {
287 let response = serde_json::from_str::<native::RestResponse<Vec<native::AssetData>>>(
288 payload,
289 )
290 .map_err(|error| decode_message(format!("failed to parse mexc account assets: {error}")))?;
291 ensure_success(response.success, response.code, response.message.as_deref())?;
292 let mut total_wallet = Decimal::ZERO;
293 let mut total_available = Decimal::ZERO;
294 let mut total_unrealized = Decimal::ZERO;
295 let balances = response
296 .data
297 .into_iter()
298 .map(|asset| {
299 let wallet = decimal_from_optional_value(
300 asset.wallet_balance.as_ref().or(asset.equity.as_ref()),
301 )?;
302 let available = decimal_from_optional_value(
303 asset
304 .available_balance
305 .as_ref()
306 .or(asset.available.as_ref()),
307 )?;
308 let unrealized = decimal_from_optional_value(
309 asset.unrealized.as_ref().or(asset.unrealised_value()),
310 )?;
311 total_wallet += wallet;
312 total_available += available;
313 total_unrealized += unrealized;
314 Ok(Balance {
315 asset: AssetCode::from(asset.currency),
316 wallet_balance: bat_markets_core::Amount::new(wallet),
317 available_balance: bat_markets_core::Amount::new(available),
318 updated_at: observed_at,
319 })
320 })
321 .collect::<Result<Vec<_>>>()?;
322
323 Ok(AccountSnapshot {
324 balances,
325 summary: Some(bat_markets_core::AccountSummary {
326 total_wallet_balance: bat_markets_core::Amount::new(total_wallet),
327 total_available_balance: bat_markets_core::Amount::new(total_available),
328 total_unrealized_pnl: bat_markets_core::Amount::new(total_unrealized),
329 updated_at: observed_at,
330 }),
331 })
332 }
333
334 pub fn parse_positions_snapshot(
335 &self,
336 payload: &str,
337 observed_at: TimestampMs,
338 ) -> Result<Vec<Position>> {
339 let response =
340 serde_json::from_str::<native::RestResponse<Vec<native::PositionData>>>(payload)
341 .map_err(|error| {
342 decode_message(format!("failed to parse mexc positions: {error}"))
343 })?;
344 ensure_success(response.success, response.code, response.message.as_deref())?;
345 response
346 .data
347 .into_iter()
348 .filter_map(
349 |position| match decimal_from_optional_value(position.hold_vol.as_ref()) {
350 Ok(size) if size.is_zero() => None,
351 Ok(size) => Some(self.position_from_data(position, observed_at, size)),
352 Err(error) => Some(Err(error)),
353 },
354 )
355 .collect()
356 }
357
358 pub fn parse_open_orders_snapshot(
359 &self,
360 payload: &str,
361 observed_at: TimestampMs,
362 ) -> Result<Vec<Order>> {
363 parse_order_list_payload(payload)?
364 .into_iter()
365 .map(|order| self.order_from_data(order, observed_at))
366 .collect()
367 }
368
369 pub fn parse_order_snapshot(&self, payload: &str, observed_at: TimestampMs) -> Result<Order> {
370 let response = serde_json::from_str::<native::RestResponse<native::OrderData>>(payload)
371 .map_err(|error| decode_message(format!("failed to parse mexc order: {error}")))?;
372 ensure_success(response.success, response.code, response.message.as_deref())?;
373 self.order_from_data(response.data, observed_at)
374 }
375
376 pub fn parse_executions_snapshot(&self, payload: &str) -> Result<Vec<Execution>> {
377 let response =
378 serde_json::from_str::<native::RestResponse<Vec<native::ExecutionData>>>(payload)
379 .map_err(|error| {
380 decode_message(format!("failed to parse mexc executions: {error}"))
381 })?;
382 ensure_success(response.success, response.code, response.message.as_deref())?;
383 response
384 .data
385 .into_iter()
386 .map(|execution| self.execution_from_data(execution))
387 .collect()
388 }
389
390 fn position_from_data(
391 &self,
392 position: native::PositionData,
393 observed_at: TimestampMs,
394 size: Decimal,
395 ) -> Result<Position> {
396 let spec = require_native_symbol(self, &position.symbol)?;
397 Ok(Position {
398 position_id: PositionId::from(value_to_id_string(&position.position_id)),
399 instrument_id: spec.instrument_id,
400 direction: parse_position_direction(position.position_type),
401 size: Quantity::new(size),
402 entry_price: decimal_from_optional_value(position.open_avg_price.as_ref())
403 .ok()
404 .filter(|value| !value.is_zero())
405 .map(Price::new),
406 mark_price: decimal_from_optional_value(position.mark_price.as_ref())
407 .ok()
408 .map(Price::new),
409 unrealized_pnl: decimal_from_optional_value(
410 position
411 .unrealized
412 .as_ref()
413 .or(position.unrealised.as_ref()),
414 )
415 .ok()
416 .map(bat_markets_core::Amount::new),
417 leverage: decimal_from_optional_value(position.leverage.as_ref())
418 .ok()
419 .map(Leverage::new),
420 margin_mode: parse_margin_mode(position.open_type),
421 position_mode: parse_position_mode(position.position_mode),
422 updated_at: TimestampMs::new(
423 position
424 .update_time
425 .or(position.create_time)
426 .unwrap_or(observed_at.value()),
427 ),
428 })
429 }
430
431 fn order_from_data(&self, order: native::OrderData, observed_at: TimestampMs) -> Result<Order> {
432 let spec = require_native_symbol(self, &order.symbol)?;
433 Ok(Order {
434 order_id: OrderId::from(value_to_id_string(&order.order_id)),
435 client_order_id: order
436 .external_oid
437 .as_deref()
438 .filter(|value| !value.is_empty())
439 .map(ClientOrderId::from),
440 instrument_id: spec.instrument_id,
441 side: parse_order_side(order.side),
442 order_type: parse_order_type(order.order_type.or(order.category).unwrap_or(1)),
443 time_in_force: Some(parse_time_in_force(order.order_type.or(order.category))),
444 status: parse_order_status(order.state),
445 price: Some(Price::new(decimal_from_value(&order.price)?)),
446 quantity: Quantity::new(decimal_from_value(&order.vol)?),
447 filled_quantity: Quantity::new(decimal_from_optional_value(order.deal_vol.as_ref())?),
448 average_fill_price: decimal_from_optional_value(order.deal_avg_price.as_ref())
449 .ok()
450 .filter(|value| !value.is_zero())
451 .map(Price::new),
452 reduce_only: order.reduce_only.unwrap_or(matches!(order.side, 2 | 4)),
453 post_only: matches!(order.order_type.or(order.category), Some(2)),
454 created_at: TimestampMs::new(order.create_time.unwrap_or(observed_at.value())),
455 updated_at: TimestampMs::new(order.update_time.unwrap_or(observed_at.value())),
456 venue_status: Some(order.state.to_string().into()),
457 })
458 }
459
460 fn execution_from_data(&self, execution: native::ExecutionData) -> Result<Execution> {
461 let spec = require_native_symbol(self, &execution.symbol)?;
462 Ok(Execution {
463 execution_id: TradeId::from(value_to_id_string(&execution.id)),
464 order_id: OrderId::from(value_to_id_string(&execution.order_id)),
465 client_order_id: None,
466 instrument_id: spec.instrument_id,
467 side: parse_order_side(execution.side),
468 quantity: Quantity::new(decimal_from_value(&execution.vol)?),
469 price: Price::new(decimal_from_value(&execution.price)?),
470 fee: execution
471 .fee
472 .as_ref()
473 .map(decimal_from_value)
474 .transpose()?
475 .map(bat_markets_core::Amount::new),
476 fee_asset: execution.fee_currency.map(AssetCode::from),
477 liquidity: execution.is_taker.map(|is_taker| {
478 if is_taker {
479 Liquidity::Taker
480 } else {
481 Liquidity::Maker
482 }
483 }),
484 executed_at: TimestampMs::new(
485 execution
486 .timestamp
487 .as_ref()
488 .and_then(value_to_i64)
489 .unwrap_or_else(now_ms),
490 ),
491 })
492 }
493}
494
495impl VenueAdapter for MexcLinearFuturesAdapter {
496 fn venue(&self) -> Venue {
497 Venue::Mexc
498 }
499
500 fn product(&self) -> Product {
501 Product::LinearUsdt
502 }
503
504 fn config(&self) -> &BatMarketsConfig {
505 &self.config
506 }
507
508 fn capabilities(&self) -> CapabilitySet {
509 self.capabilities
510 }
511
512 fn lane_set(&self) -> bat_markets_core::LaneSet {
513 self.lane_set
514 }
515
516 fn instrument_specs(&self) -> Vec<InstrumentSpec> {
517 self.instruments.read().all().to_vec()
518 }
519
520 fn resolve_instrument(&self, instrument_id: &InstrumentId) -> Option<InstrumentSpec> {
521 self.instruments.read().get(instrument_id)
522 }
523
524 fn resolve_native_symbol(&self, native_symbol: &str) -> Option<InstrumentSpec> {
525 self.instruments.read().by_native_symbol(native_symbol)
526 }
527
528 fn parse_public(&self, payload: &str) -> Result<Vec<PublicLaneEvent>> {
529 let envelope = self.parse_native_public(payload)?;
530 match envelope.channel.as_str() {
531 "push.ticker" => {
532 let ticker = serde_json::from_value::<native::TickerData>(envelope.data).map_err(
533 |error| decode_message(format!("failed to decode mexc ticker ws: {error}")),
534 )?;
535 let spec = require_native_symbol(self, &ticker.symbol)?;
536 Ok(vec![PublicLaneEvent::Ticker(fast_ticker(ticker, &spec)?)])
537 }
538 "push.tickers" => {
539 let tickers = serde_json::from_value::<Vec<native::TickerData>>(envelope.data)
540 .map_err(|error| {
541 decode_message(format!("failed to decode mexc tickers ws: {error}"))
542 })?;
543 tickers
544 .into_iter()
545 .map(|ticker| {
546 let spec = require_native_symbol(self, &ticker.symbol)?;
547 Ok(PublicLaneEvent::Ticker(fast_ticker(ticker, &spec)?))
548 })
549 .collect()
550 }
551 "push.deal" => {
552 let deal =
553 serde_json::from_value::<native::DealData>(envelope.data).map_err(|error| {
554 decode_message(format!("failed to decode mexc deal ws: {error}"))
555 })?;
556 let symbol = envelope.symbol.as_deref().ok_or_else(|| {
557 decode_message("mexc deal websocket payload missing symbol".to_owned())
558 })?;
559 let spec = require_native_symbol(self, symbol)?;
560 Ok(vec![PublicLaneEvent::Trade(fast_trade(deal, &spec, 0)?)])
561 }
562 "push.depth" | "push.depth.full" => {
563 let depth = serde_json::from_value::<native::DepthData>(envelope.data).map_err(
564 |error| decode_message(format!("failed to decode mexc depth ws: {error}")),
565 )?;
566 let symbol = envelope.symbol.as_deref().ok_or_else(|| {
567 decode_message("mexc depth websocket payload missing symbol".to_owned())
568 })?;
569 let spec = require_native_symbol(self, symbol)?;
570 Ok(vec![PublicLaneEvent::OrderBookDelta(FastOrderBookDelta {
571 instrument_id: spec.instrument_id.clone(),
572 bids: depth
573 .bids
574 .into_iter()
575 .map(|level| fast_book_tuple(level, &spec))
576 .collect::<Result<Vec<_>>>()?,
577 asks: depth
578 .asks
579 .into_iter()
580 .map(|level| fast_book_tuple(level, &spec))
581 .collect::<Result<Vec<_>>>()?,
582 event_time: TimestampMs::new(
583 depth.timestamp.or(envelope.ts).unwrap_or_else(now_ms),
584 ),
585 })])
586 }
587 "push.kline" => {
588 let kline = serde_json::from_value::<native::KlineData>(envelope.data).map_err(
589 |error| decode_message(format!("failed to decode mexc kline ws: {error}")),
590 )?;
591 let symbol = kline
592 .symbol
593 .as_deref()
594 .or(envelope.symbol.as_deref())
595 .ok_or_else(|| {
596 decode_message("mexc kline websocket payload missing symbol".to_owned())
597 })?;
598 let spec = require_native_symbol(self, symbol)?;
599 Ok(vec![PublicLaneEvent::Kline(fast_kline(kline, &spec)?)])
600 }
601 "pong" | "rs.sub.ticker" | "rs.sub.tickers" | "rs.sub.deal" | "rs.sub.depth"
602 | "rs.sub.kline" => Ok(Vec::new()),
603 other => Err(MarketError::new(
604 ErrorKind::Unsupported,
605 format!("unsupported mexc public channel '{other}'"),
606 )
607 .with_venue(Venue::Mexc, Product::LinearUsdt)),
608 }
609 }
610
611 fn parse_private(&self, payload: &str) -> Result<Vec<PrivateLaneEvent>> {
612 let envelope = self.parse_native_public(payload)?;
613 match envelope.channel.as_str() {
614 "push.personal.asset" => {
615 let asset = serde_json::from_value::<native::AssetData>(envelope.data).map_err(
616 |error| decode_message(format!("failed to decode mexc asset ws: {error}")),
617 )?;
618 Ok(vec![PrivateLaneEvent::Balance(Balance {
619 asset: AssetCode::from(asset.currency),
620 wallet_balance: bat_markets_core::Amount::new(decimal_from_optional_value(
621 asset.wallet_balance.as_ref().or(asset.equity.as_ref()),
622 )?),
623 available_balance: bat_markets_core::Amount::new(decimal_from_optional_value(
624 asset
625 .available_balance
626 .as_ref()
627 .or(asset.available.as_ref()),
628 )?),
629 updated_at: TimestampMs::new(envelope.ts.unwrap_or_else(now_ms)),
630 })])
631 }
632 "push.personal.position" => {
633 let position = serde_json::from_value::<native::PositionData>(envelope.data)
634 .map_err(|error| {
635 decode_message(format!("failed to decode mexc position ws: {error}"))
636 })?;
637 let size = decimal_from_optional_value(position.hold_vol.as_ref())?;
638 Ok(vec![PrivateLaneEvent::Position(self.position_from_data(
639 position,
640 TimestampMs::new(envelope.ts.unwrap_or_else(now_ms)),
641 size,
642 )?)])
643 }
644 "push.personal.order" => {
645 let order = serde_json::from_value::<native::OrderData>(envelope.data).map_err(
646 |error| decode_message(format!("failed to decode mexc order ws: {error}")),
647 )?;
648 Ok(vec![PrivateLaneEvent::Order(self.order_from_data(
649 order,
650 TimestampMs::new(envelope.ts.unwrap_or_else(now_ms)),
651 )?)])
652 }
653 "push.personal.order.deal" => {
654 let execution = serde_json::from_value::<native::ExecutionData>(envelope.data)
655 .map_err(|error| {
656 decode_message(format!("failed to decode mexc execution ws: {error}"))
657 })?;
658 Ok(vec![PrivateLaneEvent::Execution(
659 self.execution_from_data(execution)?,
660 )])
661 }
662 "rs.login" | "pong" => Ok(Vec::new()),
663 other => Err(MarketError::new(
664 ErrorKind::Unsupported,
665 format!("unsupported mexc private channel '{other}'"),
666 )
667 .with_venue(Venue::Mexc, Product::LinearUsdt)),
668 }
669 }
670
671 fn classify_command(
672 &self,
673 operation: CommandOperation,
674 payload: Option<&str>,
675 request_id: Option<RequestId>,
676 ) -> Result<CommandReceipt> {
677 let Some(payload) = payload else {
678 return Ok(command_receipt(MexcCommandReceipt {
679 operation,
680 status: CommandStatus::UnknownExecution,
681 instrument_id: None,
682 order_id: None,
683 request_id,
684 message: Some("command outcome requires reconcile".into()),
685 native_code: None,
686 retriable: true,
687 }));
688 };
689 let value = serde_json::from_str::<Value>(payload).map_err(|error| {
690 decode_message(format!("failed to parse mexc command payload: {error}"))
691 })?;
692 let success = value
693 .get("success")
694 .and_then(Value::as_bool)
695 .unwrap_or(false);
696 let code = value.get("code").and_then(value_to_i64).unwrap_or_default();
697 let message = value
698 .get("message")
699 .and_then(Value::as_str)
700 .or_else(|| value.get("msg").and_then(Value::as_str))
701 .map(Box::<str>::from);
702 let item_error = mexc_command_item_error(value.get("data"));
703 let item_error_code = item_error
704 .as_ref()
705 .map(|(error_code, _)| *error_code)
706 .unwrap_or(code);
707 let order_id = mexc_command_order_id(value.get("data"));
708 Ok(command_receipt(MexcCommandReceipt {
709 operation,
710 status: if success && code == 0 && item_error.is_none() {
711 CommandStatus::Accepted
712 } else {
713 CommandStatus::Rejected
714 },
715 instrument_id: None,
716 order_id,
717 request_id,
718 message: item_error
719 .as_ref()
720 .and_then(|(_, error_msg)| error_msg.clone())
721 .or(message)
722 .or_else(|| Some(if success { "accepted" } else { "rejected" }.into())),
723 native_code: Some(item_error_code.to_string().into()),
724 retriable: matches!(item_error_code, 500 | 501 | 510 | 603 | 2037),
725 }))
726 }
727}
728
729trait AssetDataExt {
730 fn unrealised_value(&self) -> Option<&Value>;
731}
732
733impl AssetDataExt for native::AssetData {
734 fn unrealised_value(&self) -> Option<&Value> {
735 self.unrealized.as_ref()
736 }
737}
738
739fn mexc_capabilities() -> CapabilitySet {
740 let mut capabilities = CapabilitySet::linear_futures_defaults();
741 capabilities.market.liquidations = false;
742 capabilities.trade.create = true;
743 capabilities.trade.batch_create = true;
744 capabilities.trade.amend = false;
745 capabilities.trade.cancel = true;
746 capabilities.trade.batch_cancel = true;
747 capabilities.trade.cancel_all = true;
748 capabilities.trade.validate = false;
749 capabilities.position.leverage_set = true;
750 capabilities.position.margin_mode_set = true;
751 capabilities.native.ws_order_entry = false;
752 capabilities.native.special_orders = false;
753 capabilities
754}
755
756fn contract_to_spec(contract: native::ContractInfo) -> Result<InstrumentSpec> {
757 let tick_size = decimal_from_value(&contract.price_unit)?;
758 let step_size = decimal_from_value(&contract.vol_unit)?;
759 let min_qty = decimal_from_value(&contract.min_vol)?;
760 let contract_size = decimal_from_value(&contract.contract_size)?;
761 let price_scale = contract.price_scale;
762 let qty_scale = contract.vol_scale;
763 let quote_scale = contract
764 .amount_scale
765 .unwrap_or_else(|| price_scale.saturating_add(qty_scale));
766 Ok(InstrumentSpec {
767 venue: Venue::Mexc,
768 product: Product::LinearUsdt,
769 market_type: MarketType::LinearPerpetual,
770 instrument_id: InstrumentId::from(canonical_symbol(
771 &contract.base_coin,
772 &contract.quote_coin,
773 &contract.settle_coin,
774 )),
775 canonical_symbol: canonical_symbol(
776 &contract.base_coin,
777 &contract.quote_coin,
778 &contract.settle_coin,
779 )
780 .into(),
781 native_symbol: contract.symbol.into(),
782 base: AssetCode::from(contract.base_coin),
783 quote: AssetCode::from(contract.quote_coin),
784 settle: AssetCode::from(contract.settle_coin),
785 contract_size: Quantity::new(contract_size),
786 tick_size: Price::new(tick_size),
787 step_size: Quantity::new(step_size),
788 min_qty: Quantity::new(min_qty),
789 min_notional: Notional::new(tick_size * min_qty * contract_size),
790 price_scale,
791 qty_scale,
792 quote_scale,
793 max_leverage: contract
794 .max_leverage
795 .as_ref()
796 .map(decimal_from_value)
797 .transpose()?
798 .map(Leverage::new),
799 support: InstrumentSupport {
800 public_streams: true,
801 private_trading: contract.api_allowed.unwrap_or(false),
802 leverage_set: false,
803 margin_mode_set: false,
804 funding_rate: true,
805 open_interest: true,
806 },
807 status: if contract.state.unwrap_or(0) == 0 {
808 InstrumentStatus::Active
809 } else {
810 InstrumentStatus::Halted
811 },
812 })
813}
814
815fn ticker_to_unified(ticker: native::TickerData, spec: &InstrumentSpec) -> Result<Ticker> {
816 Ok(Ticker {
817 instrument_id: spec.instrument_id.clone(),
818 last_price: Price::new(decimal_from_value(&ticker.last_price)?),
819 mark_price: ticker
820 .fair_price
821 .as_ref()
822 .map(decimal_from_value)
823 .transpose()?
824 .map(Price::new),
825 index_price: ticker
826 .index_price
827 .as_ref()
828 .map(decimal_from_value)
829 .transpose()?
830 .map(Price::new),
831 volume_24h: ticker
832 .volume24
833 .as_ref()
834 .map(decimal_from_value)
835 .transpose()?
836 .map(Quantity::new),
837 turnover_24h: ticker
838 .amount24
839 .as_ref()
840 .map(decimal_from_value)
841 .transpose()?
842 .map(Notional::new),
843 event_time: TimestampMs::new(ticker.timestamp.unwrap_or_else(now_ms)),
844 })
845}
846
847fn fast_ticker(ticker: native::TickerData, spec: &InstrumentSpec) -> Result<FastTicker> {
848 let event_time = TimestampMs::new(ticker.timestamp.unwrap_or_else(now_ms));
849 Ok(FastTicker {
850 instrument_id: spec.instrument_id.clone(),
851 last_price: Price::new(decimal_from_value(&ticker.last_price)?)
852 .quantize(spec.price_scale)?,
853 mark_price: ticker
854 .fair_price
855 .as_ref()
856 .map(decimal_from_value)
857 .transpose()?
858 .map(Price::new)
859 .map(|price| price.quantize(spec.price_scale))
860 .transpose()?,
861 index_price: ticker
862 .index_price
863 .as_ref()
864 .map(decimal_from_value)
865 .transpose()?
866 .map(Price::new)
867 .map(|price| price.quantize(spec.price_scale))
868 .transpose()?,
869 volume_24h: ticker
870 .volume24
871 .as_ref()
872 .map(decimal_from_value)
873 .transpose()?
874 .map(Quantity::new)
875 .map(|quantity| quantity.quantize(spec.qty_scale))
876 .transpose()?,
877 turnover_24h: ticker
878 .amount24
879 .as_ref()
880 .map(decimal_from_value)
881 .transpose()?
882 .map(Notional::new)
883 .map(|notional| notional.quantize(spec.quote_scale))
884 .transpose()
885 .unwrap_or(None),
886 event_time,
887 })
888}
889
890fn trade_to_unified(
891 deal: native::DealData,
892 spec: &InstrumentSpec,
893 index: usize,
894) -> Result<bat_markets_core::TradeTick> {
895 Ok(bat_markets_core::TradeTick {
896 instrument_id: spec.instrument_id.clone(),
897 trade_id: TradeId::from(format!("{}-{}", deal.t.unwrap_or_else(now_ms), index)),
898 price: Price::new(decimal_from_value(&deal.p)?),
899 quantity: Quantity::new(decimal_from_value(&deal.v)?),
900 aggressor_side: if deal.side == 1 {
901 AggressorSide::Buyer
902 } else {
903 AggressorSide::Seller
904 },
905 event_time: TimestampMs::new(deal.t.unwrap_or_else(now_ms)),
906 })
907}
908
909fn fast_trade(deal: native::DealData, spec: &InstrumentSpec, index: usize) -> Result<FastTrade> {
910 let trade = trade_to_unified(deal, spec, index)?;
911 Ok(FastTrade {
912 instrument_id: trade.instrument_id,
913 trade_id: trade.trade_id,
914 price: trade.price.quantize(spec.price_scale)?,
915 quantity: trade.quantity.quantize(spec.qty_scale)?,
916 aggressor_side: trade.aggressor_side,
917 event_time: trade.event_time,
918 })
919}
920
921fn fast_kline(kline: native::KlineData, spec: &InstrumentSpec) -> Result<FastKline> {
922 let interval = kline
923 .interval
924 .as_deref()
925 .and_then(mexc_interval_to_core)
926 .ok_or_else(|| {
927 decode_message(format!(
928 "unsupported or missing mexc kline interval '{}'",
929 kline.interval.as_deref().unwrap_or("<missing>")
930 ))
931 })?;
932 let open_time = kline.t.unwrap_or_else(|| now_ms() / 1_000) * 1_000;
933 Ok(FastKline {
934 instrument_id: spec.instrument_id.clone(),
935 interval: interval.into(),
936 open: Price::new(decimal_from_value(&kline.o)?).quantize(spec.price_scale)?,
937 high: Price::new(decimal_from_value(&kline.h)?).quantize(spec.price_scale)?,
938 low: Price::new(decimal_from_value(&kline.l)?).quantize(spec.price_scale)?,
939 close: Price::new(decimal_from_value(&kline.c)?).quantize(spec.price_scale)?,
940 volume: Quantity::new(decimal_from_optional_value(
941 kline.a.as_ref().or(kline.q.as_ref()),
942 )?)
943 .quantize(spec.qty_scale)?,
944 open_time: TimestampMs::new(open_time),
945 close_time: TimestampMs::new(interval.close_time_ms(open_time).unwrap_or(open_time)),
946 closed: false,
947 })
948}
949
950fn level_to_book_level(level: Vec<Value>) -> Result<bat_markets_core::OrderBookLevel> {
951 if level.len() < 2 {
952 return Err(decode_message(
953 "mexc depth level has fewer than two fields".to_owned(),
954 ));
955 }
956 Ok(bat_markets_core::OrderBookLevel {
957 price: Price::new(decimal_from_value(&level[0])?),
958 quantity: Quantity::new(decimal_from_value(&level[1])?),
959 })
960}
961
962fn fast_book_tuple(
963 level: Vec<Value>,
964 spec: &InstrumentSpec,
965) -> Result<(bat_markets_core::FastPrice, bat_markets_core::FastQuantity)> {
966 let level = level_to_book_level(level)?;
967 Ok((
968 level.price.quantize(spec.price_scale)?,
969 level.quantity.quantize(spec.qty_scale)?,
970 ))
971}
972
973fn parse_order_list_payload(payload: &str) -> Result<Vec<native::OrderData>> {
974 #[derive(Deserialize)]
975 struct Page {
976 #[serde(default)]
977 result_list: Vec<native::OrderData>,
978 }
979 let response = serde_json::from_str::<native::RestResponse<Value>>(payload)
980 .map_err(|error| decode_message(format!("failed to parse mexc order list: {error}")))?;
981 ensure_success(response.success, response.code, response.message.as_deref())?;
982 if response.data.is_array() {
983 return serde_json::from_value(response.data)
984 .map_err(|error| decode_message(format!("failed to decode mexc order list: {error}")));
985 }
986 let page = serde_json::from_value::<Page>(response.data)
987 .map_err(|error| decode_message(format!("failed to decode mexc order page: {error}")))?;
988 Ok(page.result_list)
989}
990
991fn find_symbol_object(data: Value, native_symbol: &str) -> Result<Value> {
992 if let Some(items) = data.as_array() {
993 return items
994 .iter()
995 .find(|item| item.get("symbol").and_then(Value::as_str) == Some(native_symbol))
996 .cloned()
997 .ok_or_else(|| {
998 decode_message(format!("mexc ticker response missing {native_symbol}"))
999 });
1000 }
1001 Ok(data)
1002}
1003
1004fn parse_kline_interval(raw: &str) -> Result<KlineInterval> {
1005 KlineInterval::parse(raw)
1006 .or_else(|| mexc_interval_to_core(raw))
1007 .ok_or_else(|| {
1008 MarketError::new(
1009 ErrorKind::Unsupported,
1010 format!("unsupported mexc interval '{raw}'"),
1011 )
1012 })
1013}
1014
1015fn mexc_interval_to_core(raw: &str) -> Option<KlineInterval> {
1016 match raw {
1017 "Min1" => Some(KlineInterval::Minute1),
1018 "Min5" => Some(KlineInterval::Minute5),
1019 "Min15" => Some(KlineInterval::Minute15),
1020 "Min30" => Some(KlineInterval::Minute30),
1021 "Min60" => Some(KlineInterval::Hour1),
1022 "Hour4" => Some(KlineInterval::Hour4),
1023 "Day1" => Some(KlineInterval::Day1),
1024 "Week1" => Some(KlineInterval::Week1),
1025 "Month1" => Some(KlineInterval::Month1),
1026 _ => None,
1027 }
1028}
1029
1030fn parse_order_side(value: i64) -> Side {
1031 match value {
1032 1 | 4 => Side::Buy,
1033 _ => Side::Sell,
1034 }
1035}
1036
1037fn parse_position_direction(value: i64) -> PositionDirection {
1038 match value {
1039 1 => PositionDirection::Long,
1040 2 => PositionDirection::Short,
1041 _ => PositionDirection::Flat,
1042 }
1043}
1044
1045fn parse_order_type(value: i64) -> OrderType {
1046 match value {
1047 5 | 6 => OrderType::Market,
1048 _ => OrderType::Limit,
1049 }
1050}
1051
1052fn parse_time_in_force(value: Option<i64>) -> TimeInForce {
1053 match value {
1054 Some(2) => TimeInForce::PostOnly,
1055 Some(3) => TimeInForce::Ioc,
1056 Some(4) => TimeInForce::Fok,
1057 _ => TimeInForce::Gtc,
1058 }
1059}
1060
1061fn parse_order_status(value: i64) -> OrderStatus {
1062 match value {
1063 1 | 2 => OrderStatus::New,
1064 3 => OrderStatus::Filled,
1065 4 => OrderStatus::Canceled,
1066 5 => OrderStatus::Rejected,
1067 _ => OrderStatus::Expired,
1068 }
1069}
1070
1071fn parse_margin_mode(value: Option<i64>) -> MarginMode {
1072 match value {
1073 Some(1) => MarginMode::Isolated,
1074 _ => MarginMode::Cross,
1075 }
1076}
1077
1078fn parse_position_mode(value: Option<i64>) -> PositionMode {
1079 match value {
1080 Some(1) => PositionMode::Hedge,
1081 _ => PositionMode::OneWay,
1082 }
1083}
1084
1085struct MexcCommandReceipt {
1086 operation: CommandOperation,
1087 status: CommandStatus,
1088 instrument_id: Option<InstrumentId>,
1089 order_id: Option<OrderId>,
1090 request_id: Option<RequestId>,
1091 message: Option<Box<str>>,
1092 native_code: Option<Box<str>>,
1093 retriable: bool,
1094}
1095
1096fn command_receipt(parts: MexcCommandReceipt) -> CommandReceipt {
1097 CommandReceipt {
1098 operation: parts.operation,
1099 status: parts.status,
1100 venue: Venue::Mexc,
1101 product: Product::LinearUsdt,
1102 instrument_id: parts.instrument_id,
1103 order_id: parts.order_id,
1104 client_order_id: None,
1105 request_id: parts.request_id,
1106 message: parts.message,
1107 native_code: parts.native_code,
1108 retriable: parts.retriable,
1109 }
1110}
1111
1112fn require_native_symbol(
1113 adapter: &MexcLinearFuturesAdapter,
1114 symbol: &str,
1115) -> Result<InstrumentSpec> {
1116 adapter.resolve_native_symbol(symbol).ok_or_else(|| {
1117 MarketError::new(
1118 ErrorKind::Unsupported,
1119 format!("unknown mexc symbol {symbol}"),
1120 )
1121 .with_venue(Venue::Mexc, Product::LinearUsdt)
1122 })
1123}
1124
1125fn decimal_from_optional_value(value: Option<&Value>) -> Result<Decimal> {
1126 value
1127 .map(decimal_from_value)
1128 .transpose()
1129 .map(|value| value.unwrap_or(Decimal::ZERO))
1130}
1131
1132fn decimal_from_value(value: &Value) -> Result<Decimal> {
1133 match value {
1134 Value::Number(number) => decimal_from_str(&number.to_string()),
1135 Value::String(value) => decimal_from_str(value),
1136 Value::Null => Ok(Decimal::ZERO),
1137 other => decimal_from_str(&other.to_string()),
1138 }
1139 .map_err(|error| decode_message(format!("invalid mexc decimal '{value}': {error}")))
1140}
1141
1142fn decimal_from_str(value: &str) -> std::result::Result<Decimal, String> {
1143 let Some((mantissa, exponent)) = value.split_once(['e', 'E']) else {
1144 return Decimal::from_str_exact(value).map_err(|error| error.to_string());
1145 };
1146 let mut parsed = Decimal::from_str_exact(mantissa).map_err(|error| error.to_string())?;
1147 let exponent = exponent
1148 .parse::<i32>()
1149 .map_err(|error| format!("invalid exponent: {error}"))?;
1150 let ten = Decimal::from(10);
1151 for _ in 0..exponent.unsigned_abs() {
1152 if exponent >= 0 {
1153 parsed *= ten;
1154 } else {
1155 parsed /= ten;
1156 }
1157 }
1158 Ok(parsed)
1159}
1160
1161fn value_to_id_string(value: &Value) -> String {
1162 match value {
1163 Value::String(value) => value.clone(),
1164 Value::Number(value) => value.to_string(),
1165 Value::Null => String::new(),
1166 value => value.to_string(),
1167 }
1168}
1169
1170fn value_to_i64(value: &Value) -> Option<i64> {
1171 value.as_i64().or_else(|| value.as_str()?.parse().ok())
1172}
1173
1174fn mexc_command_order_id(data: Option<&Value>) -> Option<OrderId> {
1175 let value = match data? {
1176 Value::Array(items) => items.first()?.get("orderId")?,
1177 Value::Object(map) => map.get("orderId").or_else(|| map.get("order_id"))?,
1178 value => value,
1179 };
1180 Some(value_to_id_string(value))
1181 .filter(|id| !id.is_empty() && id != "null")
1182 .map(OrderId::from)
1183}
1184
1185fn mexc_command_item_error(data: Option<&Value>) -> Option<(i64, Option<Box<str>>)> {
1186 let item = match data? {
1187 Value::Array(items) => items.first()?,
1188 Value::Object(_) => data?,
1189 _ => return None,
1190 };
1191 let error_code = item
1192 .get("errorCode")
1193 .or_else(|| item.get("error_code"))
1194 .and_then(value_to_i64)?;
1195 if error_code == 0 {
1196 return None;
1197 }
1198 let error_msg = item
1199 .get("errorMsg")
1200 .or_else(|| item.get("error_msg"))
1201 .and_then(Value::as_str)
1202 .map(Box::<str>::from);
1203 Some((error_code, error_msg))
1204}
1205
1206fn canonical_symbol(base: &str, quote: &str, settle: &str) -> String {
1207 format!("{base}/{quote}:{settle}")
1208}
1209
1210fn ensure_success(success: bool, code: i64, message: Option<&str>) -> Result<()> {
1211 if success && code == 0 {
1212 return Ok(());
1213 }
1214 Err(MarketError::new(
1215 ErrorKind::ExchangeReject,
1216 message.unwrap_or("mexc request rejected"),
1217 )
1218 .with_venue(Venue::Mexc, Product::LinearUsdt))
1219}
1220
1221fn decode_message(message: String) -> MarketError {
1222 MarketError::new(ErrorKind::DecodeError, message).with_venue(Venue::Mexc, Product::LinearUsdt)
1223}
1224
1225fn now_ms() -> i64 {
1226 std::time::SystemTime::now()
1227 .duration_since(std::time::UNIX_EPOCH)
1228 .map(|duration| duration.as_millis().min(i64::MAX as u128) as i64)
1229 .unwrap_or_default()
1230}
1231
1232fn btc_spec() -> InstrumentSpec {
1233 fixture_spec("BTC", 2, 0, "0.1", "1", 125)
1234}
1235
1236fn eth_spec() -> InstrumentSpec {
1237 fixture_spec("ETH", 2, 0, "0.01", "1", 100)
1238}
1239
1240fn fixture_spec(
1241 base: &str,
1242 price_scale: u32,
1243 qty_scale: u32,
1244 tick: &str,
1245 step: &str,
1246 leverage: i64,
1247) -> InstrumentSpec {
1248 let native = format!("{base}_USDT");
1249 InstrumentSpec {
1250 venue: Venue::Mexc,
1251 product: Product::LinearUsdt,
1252 market_type: MarketType::LinearPerpetual,
1253 instrument_id: InstrumentId::from(canonical_symbol(base, "USDT", "USDT")),
1254 canonical_symbol: canonical_symbol(base, "USDT", "USDT").into(),
1255 native_symbol: native.into(),
1256 base: AssetCode::from(base),
1257 quote: AssetCode::from("USDT"),
1258 settle: AssetCode::from("USDT"),
1259 contract_size: Quantity::new(Decimal::ONE),
1260 tick_size: Price::new(Decimal::new(
1261 tick.replace('.', "").parse::<i64>().unwrap_or(1),
1262 decimal_places(tick),
1263 )),
1264 step_size: Quantity::new(Decimal::new(
1265 step.replace('.', "").parse::<i64>().unwrap_or(1),
1266 decimal_places(step),
1267 )),
1268 min_qty: Quantity::new(Decimal::new(
1269 step.replace('.', "").parse::<i64>().unwrap_or(1),
1270 decimal_places(step),
1271 )),
1272 min_notional: Notional::new(Decimal::new(
1273 tick.replace('.', "").parse::<i64>().unwrap_or(1),
1274 decimal_places(tick),
1275 )),
1276 price_scale,
1277 qty_scale,
1278 quote_scale: price_scale + qty_scale,
1279 max_leverage: Some(Leverage::new(Decimal::from(leverage))),
1280 support: InstrumentSupport {
1281 public_streams: true,
1282 private_trading: false,
1283 leverage_set: false,
1284 margin_mode_set: false,
1285 funding_rate: true,
1286 open_interest: true,
1287 },
1288 status: InstrumentStatus::Active,
1289 }
1290}
1291
1292fn decimal_places(value: &str) -> u32 {
1293 value
1294 .split_once('.')
1295 .map(|(_, fraction)| fraction.len() as u32)
1296 .unwrap_or(0)
1297}
1298
1299#[cfg(test)]
1300mod tests {
1301 use super::*;
1302 use bat_markets_core::{PrivateLaneEvent, PublicLaneEvent};
1303
1304 const CONTRACT_DETAIL: &str = include_str!("../../../fixtures/mexc/contract_detail.json");
1305 const PUBLIC_TICKER: &str = include_str!("../../../fixtures/mexc/public_ticker.json");
1306 const PUBLIC_DEPTH: &str = include_str!("../../../fixtures/mexc/public_depth.json");
1307 const PUBLIC_KLINE: &str = include_str!("../../../fixtures/mexc/public_kline.json");
1308 const PRIVATE_ORDER: &str = include_str!("../../../fixtures/mexc/private_order.json");
1309
1310 #[test]
1311 fn parse_mexc_contract_metadata() {
1312 let adapter = MexcLinearFuturesAdapter::new();
1313 let specs = adapter
1314 .parse_metadata_snapshot(CONTRACT_DETAIL)
1315 .unwrap_or_else(|error| panic!("mexc metadata should parse: {error}"));
1316 assert_eq!(specs[0].native_symbol.as_ref(), "BTC_USDT");
1317 assert_eq!(specs[0].instrument_id.as_ref(), "BTC/USDT:USDT");
1318 assert_eq!(specs[0].tick_size.to_string(), "0.1");
1319 assert!(!specs[0].support.private_trading);
1320 }
1321
1322 #[test]
1323 fn parse_mexc_scientific_decimal_values() {
1324 let value = serde_json::json!(1e-8);
1325 let parsed = decimal_from_value(&value)
1326 .unwrap_or_else(|error| panic!("scientific decimal should parse: {error}"));
1327 assert_eq!(parsed, Decimal::new(1, 8));
1328 }
1329
1330 #[test]
1331 fn parse_mexc_public_ticker_ws() {
1332 let adapter = MexcLinearFuturesAdapter::new();
1333 let events = adapter
1334 .parse_public(PUBLIC_TICKER)
1335 .unwrap_or_else(|error| panic!("mexc ticker should parse: {error}"));
1336 let PublicLaneEvent::Ticker(ticker) = &events[0] else {
1337 panic!("expected ticker");
1338 };
1339 assert_eq!(ticker.instrument_id.as_ref(), "BTC/USDT:USDT");
1340 assert_eq!(ticker.last_price.value(), 6543210);
1341 }
1342
1343 #[test]
1344 fn parse_mexc_open_interest_uses_hold_volume() {
1345 let adapter = MexcLinearFuturesAdapter::new();
1346 let spec = adapter
1347 .resolve_native_symbol("BTC_USDT")
1348 .unwrap_or_else(|| panic!("mexc fixture symbol should exist"));
1349 let payload = r#"{
1350 "success": true,
1351 "code": 0,
1352 "data": {
1353 "symbol": "BTC_USDT",
1354 "lastPrice": 65432.1,
1355 "volume24": 1200,
1356 "holdVol": 4321,
1357 "timestamp": 1761879567135
1358 }
1359 }"#;
1360 let open_interest = adapter
1361 .parse_open_interest_snapshot(payload, &spec)
1362 .unwrap_or_else(|error| panic!("mexc open interest should parse: {error}"));
1363 assert_eq!(open_interest.value.value(), Decimal::new(4321, 0));
1364 }
1365
1366 #[test]
1367 fn parse_mexc_public_depth_ws() {
1368 let adapter = MexcLinearFuturesAdapter::new();
1369 let events = adapter
1370 .parse_public(PUBLIC_DEPTH)
1371 .unwrap_or_else(|error| panic!("mexc depth should parse: {error}"));
1372 let PublicLaneEvent::OrderBookDelta(delta) = &events[0] else {
1373 panic!("expected depth");
1374 };
1375 assert_eq!(delta.bids[0].0.value(), 6543200);
1376 assert_eq!(delta.asks[0].1.value(), 2);
1377 }
1378
1379 #[test]
1380 fn parse_mexc_public_kline_ws() {
1381 let adapter = MexcLinearFuturesAdapter::new();
1382 let events = adapter
1383 .parse_public(PUBLIC_KLINE)
1384 .unwrap_or_else(|error| panic!("mexc kline should parse: {error}"));
1385 let PublicLaneEvent::Kline(kline) = &events[0] else {
1386 panic!("expected kline");
1387 };
1388 assert_eq!(kline.interval.as_ref(), "1m");
1389 assert_eq!(kline.close.value(), 6544500);
1390 }
1391
1392 #[test]
1393 fn parse_mexc_private_order_ws() {
1394 let adapter = MexcLinearFuturesAdapter::new();
1395 let events = adapter
1396 .parse_private(PRIVATE_ORDER)
1397 .unwrap_or_else(|error| panic!("mexc private order should parse: {error}"));
1398 let PrivateLaneEvent::Order(order) = &events[0] else {
1399 panic!("expected order");
1400 };
1401 assert_eq!(order.order_id.as_ref(), "123456789");
1402 assert_eq!(order.status, OrderStatus::New);
1403 assert_eq!(order.side, Side::Buy);
1404 }
1405
1406 #[test]
1407 fn mexc_write_capabilities_expose_documented_rest_order_paths() {
1408 let adapter = MexcLinearFuturesAdapter::new();
1409 let capabilities = adapter.capabilities();
1410 assert!(capabilities.trade.create);
1411 assert!(capabilities.trade.batch_create);
1412 assert!(capabilities.trade.cancel);
1413 assert!(capabilities.trade.batch_cancel);
1414 assert!(capabilities.trade.cancel_all);
1415 assert!(capabilities.position.leverage_set);
1416 assert!(capabilities.position.margin_mode_set);
1417 assert!(!capabilities.trade.amend);
1418 assert!(!capabilities.trade.validate);
1419 assert!(!capabilities.native.ws_order_entry);
1420 assert!(capabilities.trade.get);
1421 assert!(capabilities.market.public_streams);
1422 }
1423
1424 #[test]
1425 fn mexc_command_classification_reads_nested_order_id_and_item_errors() {
1426 let adapter = MexcLinearFuturesAdapter::new();
1427 let accepted = adapter
1428 .classify_command(
1429 CommandOperation::CreateOrder,
1430 Some(r#"{"success":true,"code":0,"data":{"orderId":"739113577038255616","ts":1761888808839}}"#),
1431 None,
1432 )
1433 .unwrap_or_else(|error| panic!("mexc command should parse: {error}"));
1434 assert_eq!(accepted.status, CommandStatus::Accepted);
1435 assert_eq!(
1436 accepted.order_id.as_ref().map(OrderId::as_ref),
1437 Some("739113577038255616")
1438 );
1439
1440 let rejected = adapter
1441 .classify_command(
1442 CommandOperation::CancelOrder,
1443 Some(r#"{"success":true,"code":0,"data":[{"orderId":101716841474621953,"errorCode":2040,"errorMsg":"order not exist"}]}"#),
1444 None,
1445 )
1446 .unwrap_or_else(|error| panic!("mexc cancel command should parse: {error}"));
1447 assert_eq!(rejected.status, CommandStatus::Rejected);
1448 assert_eq!(rejected.native_code.as_deref(), Some("2040"));
1449 }
1450}