Skip to content

Commit 832c9e9

Browse files
authored
Add MEXC private REST endpoints (#839)
* Add MEXC private REST endpoints * Update README.md Add tick for MEXC private REST endpoints
1 parent 7d4cdbc commit 832c9e9

File tree

2 files changed

+214
-1
lines changed

2 files changed

+214
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ The following cryptocurrency exchanges are supported:
6060
| KuCoin | x | x | T R | |
6161
| LBank | x | x | R | |
6262
| Livecoin | x | x | | |
63-
| MEXC | x | | | |
63+
| MEXC | x | x | | |
6464
| NDAX | x | x | T R | |
6565
| OKCoin | x | x | R B | |
6666
| OKEx | x | x | T R B O | |

src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,219 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(
202202
7
203203
));
204204
}
205+
206+
protected override async Task<Dictionary<string, decimal>> OnGetAmountsAsync()
207+
{
208+
var token = await GetBalance();
209+
return (token["balances"] ?? throw new InvalidOperationException())
210+
.Select(
211+
x =>
212+
new
213+
{
214+
Currency = x["asset"].Value<string>(),
215+
TotalBalance = x["free"].Value<decimal>()
216+
+ x["locked"].Value<decimal>()
217+
}
218+
)
219+
.ToDictionary(k => k.Currency, v => v.TotalBalance);
220+
}
221+
222+
protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailableToTradeAsync()
223+
{
224+
var token = await GetBalance();
225+
return (token["balances"] ?? throw new InvalidOperationException())
226+
.Select(
227+
x =>
228+
new
229+
{
230+
Currency = x["asset"].Value<string>(),
231+
AvailableBalance = x["free"].Value<decimal>()
232+
+ x["locked"].Value<decimal>()
233+
}
234+
)
235+
.ToDictionary(k => k.Currency, v => v.AvailableBalance);
236+
}
237+
238+
protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDetailsAsync(
239+
string marketSymbol = null)
240+
{
241+
if (string.IsNullOrEmpty(marketSymbol))
242+
{
243+
throw new ArgumentNullException(nameof(marketSymbol), $"Market symbol cannot be null");
244+
}
245+
246+
var token = await MakeJsonRequestAsync<JToken>($"/openOrders?symbol={marketSymbol.ToUpperInvariant()}",
247+
payload: await GetNoncePayloadAsync());
248+
249+
return token.Select(ParseOrder);
250+
}
251+
252+
protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrderRequest order)
253+
{
254+
if (string.IsNullOrEmpty(order.MarketSymbol))
255+
{
256+
throw new ArgumentNullException(nameof(order.MarketSymbol));
257+
}
258+
if (order.Price == null && order.OrderType != OrderType.Market)
259+
{
260+
throw new ArgumentNullException(nameof(order.Price));
261+
}
262+
263+
var payload = await GetNoncePayloadAsync();
264+
payload["symbol"] = order.MarketSymbol;
265+
payload["quantity"] = order.Amount;
266+
payload["side"] = order.IsBuy ? "BUY" : "SELL";
267+
268+
if (order.OrderType != OrderType.Market)
269+
{
270+
decimal orderPrice = await ClampOrderPrice(order.MarketSymbol, order.Price.Value);
271+
payload["price"] = orderPrice;
272+
if (order.IsPostOnly != true)
273+
payload["type"] = "LIMIT";
274+
}
275+
276+
switch (order.OrderType)
277+
{
278+
case OrderType.Limit when !order.IsPostOnly.GetValueOrDefault():
279+
payload["type"] = "LIMIT";
280+
break;
281+
case OrderType.Limit when order.IsPostOnly.GetValueOrDefault():
282+
payload["type"] = "LIMIT_MAKER";
283+
break;
284+
case OrderType.Market:
285+
payload["type"] = "MARKET";
286+
break;
287+
case OrderType.Stop:
288+
default:
289+
throw new ArgumentOutOfRangeException(nameof(order.OrderType));
290+
}
291+
292+
if (!string.IsNullOrEmpty(order.ClientOrderId))
293+
{
294+
payload["clientOrderId"] = order.ClientOrderId;
295+
}
296+
297+
order.ExtraParameters.CopyTo(payload);
298+
299+
var token = await MakeJsonRequestAsync<JToken>("/order", BaseUrl, payload, "POST");
300+
301+
// MEXC does not return order status with the place order response, so we need to send one more request to get the order details.
302+
return await GetOrderDetailsAsync(
303+
(token["orderId"] ?? throw new InvalidOperationException()).ToStringInvariant(),
304+
order.MarketSymbol
305+
);
306+
}
307+
308+
protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
309+
{
310+
if (string.IsNullOrEmpty(marketSymbol))
311+
{
312+
throw new ArgumentNullException(nameof(marketSymbol), $"Market symbol cannot be null");
313+
}
314+
315+
if (string.IsNullOrEmpty(orderId))
316+
{
317+
throw new ArgumentNullException(
318+
nameof(orderId),
319+
"Order details request requires order ID or client-supplied order ID"
320+
);
321+
}
322+
323+
var param = isClientOrderId ? $"origClientOrderId={orderId}" : $"orderId={orderId}";
324+
var token = await MakeJsonRequestAsync<JToken>($"/order?symbol={marketSymbol.ToUpperInvariant()}&{param}",
325+
payload: await GetNoncePayloadAsync());
326+
327+
return ParseOrder(token);
328+
}
329+
330+
protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
331+
{
332+
if (string.IsNullOrEmpty(orderId))
333+
{
334+
throw new ArgumentNullException(
335+
nameof(orderId),
336+
"Cancel order request requires order ID"
337+
);
338+
}
339+
340+
if (string.IsNullOrEmpty(marketSymbol))
341+
{
342+
throw new ArgumentNullException(
343+
nameof(marketSymbol),
344+
"Cancel order request requires symbol"
345+
);
346+
}
347+
348+
var payload = await GetNoncePayloadAsync();
349+
payload["symbol"] = marketSymbol!;
350+
switch (isClientOrderId)
351+
{
352+
case true:
353+
payload["origClientOrderId"] = orderId;
354+
break;
355+
default:
356+
payload["orderId"] = orderId;
357+
break;
358+
}
359+
360+
await MakeJsonRequestAsync<JToken>("/order", BaseUrl, payload, "DELETE");
361+
}
362+
363+
private async Task<JToken> GetBalance()
364+
{
365+
var token = await MakeJsonRequestAsync<JToken>("/account", payload: await GetNoncePayloadAsync());
366+
return token;
367+
}
368+
369+
private static ExchangeOrderResult ParseOrder(JToken token)
370+
{
371+
// [
372+
// {
373+
// "symbol": "LTCBTC",
374+
// "orderId": "C02__443776347957968896088",
375+
// "orderListId": -1,
376+
// "clientOrderId": "",
377+
// "price": "0.001395",
378+
// "origQty": "0.004",
379+
// "executedQty": "0",
380+
// "cummulativeQuoteQty": "0",
381+
// "status": "NEW",
382+
// "timeInForce": null,
383+
// "type": "LIMIT",
384+
// "side": "SELL",
385+
// "stopPrice": null,
386+
// "icebergQty": null,
387+
// "time": 1721586762185,
388+
// "updateTime": null,
389+
// "isWorking": true,
390+
// "origQuoteOrderQty": "0.00000558"
391+
// }
392+
// ]
393+
394+
return new ExchangeOrderResult
395+
{
396+
OrderId = token["orderId"].ToStringInvariant(),
397+
ClientOrderId = token["orderListId"].ToStringInvariant(),
398+
MarketSymbol = token["symbol"].ToStringInvariant(),
399+
Amount = token["origQty"].ConvertInvariant<decimal>(),
400+
AmountFilled = token["executedQty"].ConvertInvariant<decimal>(),
401+
Price = token["price"].ConvertInvariant<decimal>(),
402+
IsBuy = token["side"].ToStringInvariant() == "BUY",
403+
OrderDate = token["time"].ConvertInvariant<long>().UnixTimeStampToDateTimeMilliseconds(),
404+
Result = ParseOrderStatus(token["status"].ToStringInvariant())
405+
};
406+
}
407+
408+
private static ExchangeAPIOrderResult ParseOrderStatus(string status) =>
409+
status.ToUpperInvariant() switch
410+
{
411+
"NEW" => ExchangeAPIOrderResult.Open,
412+
"PARTIALLY_FILLED" => ExchangeAPIOrderResult.FilledPartially,
413+
"FILLED" => ExchangeAPIOrderResult.Filled,
414+
"PARTIALLY_CANCELED" => ExchangeAPIOrderResult.FilledPartiallyAndCancelled,
415+
"CANCELED" => ExchangeAPIOrderResult.Canceled,
416+
_ => ExchangeAPIOrderResult.Unknown
417+
};
205418
}
206419

207420
public partial class ExchangeName

0 commit comments

Comments
 (0)