Skip to content

Commit

Permalink
Add MEXC public REST endpoints (#838)
Browse files Browse the repository at this point in the history
* Add CryptoUtility.SecondsToPeriodInMinutesUpToHourString

Convert seconds to a period string, i.e. 1m, 5m, 60m, 4h, 1d, 1W, 1M.
Used on MEXC.

* Add MEXC public REST endpoints

* Create ExchangeMEXCAPITests.cs

* Add MEXC to README.md
  • Loading branch information
BZ-CO authored Jul 3, 2024
1 parent 40fac2f commit 7d4cdbc
Show file tree
Hide file tree
Showing 4 changed files with 384 additions and 43 deletions.
87 changes: 44 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,50 @@ Feel free to visit the discord channel at <https://discord.gg/58ktxXuTVK> and ch
The following cryptocurrency exchanges are supported:
(Web socket key: T = tickers, R = trades, B = orderbook / delta orderbook, O = private orders, U = user data)

| Exchange Name | Public REST | Private REST | Web Socket | Notes |
| ----------------------- | ----------- | ------------ | ------------- | ------------------------------------------- |
| ApolloX | x | x | T R B O U | |
| Aquanow | wip | x | | |
| Binance | x | x | T R B O U | |
| ~~Binance Jersey~~ | ~~x~~ | ~~x~~ | ~~T R B O U~~ | Ceased operations |
| Binance.US | x | x | T R B O U | |
| Binance DEX | | | R | |
| Bitbank | x | x | | |
| Bitfinex | x | x | T R O | |
| Bitflyer | | | R | |
| Bithumb | x | | R | |
| BitMEX | x | x | R O | |
| Bitstamp | x | x | R | |
| Bittrex | x | x | T R | |
| BL3P | x | x | R B | Trades stream does not send trade's ids. |
| Bleutrade | x | x | | |
| BtcTurk | | | R | |
| BTSE | x | x | | |
| Bybit | x | x | R | Has public method for Websocket Positions |
| Coinbase (Advanced) | x | x | T R O U | |
| Coincheck | | | R | |
| Coinmate | x | x | | |
| Crypto.com | | | R | |
| Digifinex | x | x | R B | |
| Dydx | | | R | |
| FTX | x | x | T R | |
| FTX.us | x | x | T R | |
| gate.io | x | x | R | |
| Gemini | x | x | T R B | |
| HitBTC | x | x | R | |
| Huobi | x | x | R B | |
| Kraken | x | x | R | Dark order symbols not supported |
| KuCoin | x | x | T R | |
| LBank | x | x | R | |
| Livecoin | x | x | | |
| NDAX | x | x | T R | |
| OKCoin | x | x | R B | |
| OKEx | x | x | T R B O | |
| Poloniex | x | x | T R B | |
| UPbit | | | R | |
| YoBit | x | x | | |
| ZB.com | wip | | R | |
| Exchange Name | Public REST | Private REST | Web Socket | Notes |
|---------------------| ----------- |--------------| ------------- | ------------------------------------------- |
| ApolloX | x | x | T R B O U | |
| Aquanow | wip | x | | |
| Binance | x | x | T R B O U | |
| ~~Binance Jersey~~ | ~~x~~ | ~~x~~ | ~~T R B O U~~ | Ceased operations |
| Binance.US | x | x | T R B O U | |
| Binance DEX | | | R | |
| Bitbank | x | x | | |
| Bitfinex | x | x | T R O | |
| Bitflyer | | | R | |
| Bithumb | x | | R | |
| BitMEX | x | x | R O | |
| Bitstamp | x | x | R | |
| Bittrex | x | x | T R | |
| BL3P | x | x | R B | Trades stream does not send trade's ids. |
| Bleutrade | x | x | | |
| BtcTurk | | | R | |
| BTSE | x | x | | |
| Bybit | x | x | R | Has public method for Websocket Positions |
| Coinbase (Advanced) | x | x | T R O U | |
| Coincheck | | | R | |
| Coinmate | x | x | | |
| Crypto.com | | | R | |
| Digifinex | x | x | R B | |
| Dydx | | | R | |
| FTX | x | x | T R | |
| FTX.us | x | x | T R | |
| gate.io | x | x | R | |
| Gemini | x | x | T R B | |
| HitBTC | x | x | R | |
| Huobi | x | x | R B | |
| Kraken | x | x | R | Dark order symbols not supported |
| KuCoin | x | x | T R | |
| LBank | x | x | R | |
| Livecoin | x | x | | |
| MEXC | x | | | |
| NDAX | x | x | T R | |
| OKCoin | x | x | R B | |
| OKEx | x | x | T R B O | |
| Poloniex | x | x | T R B | |
| UPbit | | | R | |
| YoBit | x | x | | |
| ZB.com | wip | | R | |

The following cryptocurrency services are supported:

Expand Down
211 changes: 211 additions & 0 deletions src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;

namespace ExchangeSharp
{
public sealed class ExchangeMEXCAPI : ExchangeAPI
{
public override string BaseUrl { get; set; } = "https://api.mexc.com/api/v3";
public override string BaseUrlWebSocket { get; set; } = "wss://wbs.mexc.com/ws";

public override string PeriodSecondsToString(int seconds) =>
CryptoUtility.SecondsToPeriodInMinutesUpToHourString(seconds);

private ExchangeMEXCAPI()
{
NonceStyle = NonceStyle.UnixMilliseconds;
MarketSymbolSeparator = string.Empty;
MarketSymbolIsUppercase = true;
RateLimit = new RateGate(20, TimeSpan.FromSeconds(2));
}

public override Task<string> ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol)
{
var quoteLength = 3;
if (marketSymbol.EndsWith("USDT") ||
marketSymbol.EndsWith("USDC") ||
marketSymbol.EndsWith("TUSD"))
{
quoteLength = 4;
}

var baseSymbol = marketSymbol.Substring(marketSymbol.Length - quoteLength);

return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(
marketSymbol.Replace(baseSymbol, "")
+ GlobalMarketSymbolSeparator
+ baseSymbol);
}

protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
{
return (await OnGetMarketSymbolsMetadataAsync())
.Select(x => x.MarketSymbol);
}

protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
{
var symbols = await MakeJsonRequestAsync<JToken>("/exchangeInfo", BaseUrl);

return (symbols["symbols"] ?? throw new ArgumentNullException())
.Select(symbol => new ExchangeMarket()
{
MarketSymbol = symbol["symbol"].ToStringInvariant(),
IsActive = symbol["isSpotTradingAllowed"].ConvertInvariant<bool>(),
MarginEnabled = symbol["isMarginTradingAllowed"].ConvertInvariant<bool>(),
BaseCurrency = symbol["baseAsset"].ToStringInvariant(),
QuoteCurrency = symbol["quoteAsset"].ToStringInvariant(),
QuantityStepSize = symbol["baseSizePrecision"].ConvertInvariant<decimal>(),
// Not 100% sure about this
PriceStepSize =
CryptoUtility.PrecisionToStepSize(symbol["quoteCommissionPrecision"].ConvertInvariant<decimal>()),
MinTradeSizeInQuoteCurrency = symbol["quoteAmountPrecision"].ConvertInvariant<decimal>(),
MaxTradeSizeInQuoteCurrency = symbol["maxQuoteAmount"].ConvertInvariant<decimal>()
});
}

protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>> OnGetTickersAsync()
{
var tickers = new List<KeyValuePair<string, ExchangeTicker>>();
var token = await MakeJsonRequestAsync<JToken>("/ticker/24hr", BaseUrl);
foreach (var t in token)
{
var symbol = (t["symbol"] ?? throw new ArgumentNullException()).ToStringInvariant();
tickers.Add(new KeyValuePair<string, ExchangeTicker>(symbol,
await this.ParseTickerAsync(
t,
symbol,
"askPrice",
"bidPrice",
"lastPrice",
"volume",
timestampType: TimestampType.UnixMilliseconds,
timestampKey: "closeTime")));
}

return tickers;
}

protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymbol) =>
await this.ParseTickerAsync(
await MakeJsonRequestAsync<JToken>($"/ticker/24hr?symbol={marketSymbol.ToUpperInvariant()}", BaseUrl),
marketSymbol,
"askPrice",
"bidPrice",
"lastPrice",
"volume",
timestampType: TimestampType.UnixMilliseconds,
timestampKey: "closeTime");

protected override async Task<ExchangeOrderBook> OnGetOrderBookAsync(string marketSymbol, int maxCount = 100)
{
const int maxDepth = 5000;
const string sequenceKey = "lastUpdateId";
marketSymbol = marketSymbol.ToUpperInvariant();
if (string.IsNullOrEmpty(marketSymbol))
{
throw new ArgumentOutOfRangeException(nameof(marketSymbol), "Market symbol cannot be empty.");
}

if (maxCount > maxDepth)
{
throw new ArgumentOutOfRangeException(nameof(maxCount), $"Max order book depth is {maxDepth}");
}

var token = await MakeJsonRequestAsync<JToken>($"/depth?symbol={marketSymbol}");
var orderBook = token.ParseOrderBookFromJTokenArrays(sequence: sequenceKey);
orderBook.MarketSymbol = marketSymbol;
orderBook.ExchangeName = Name;
orderBook.LastUpdatedUtc = DateTime.UtcNow;

return orderBook;
}

protected override async Task<IEnumerable<ExchangeTrade>> OnGetRecentTradesAsync(string marketSymbol,
int? limit = null)
{
const int maxLimit = 1000;
const int defaultLimit = 500;
marketSymbol = marketSymbol.ToUpperInvariant();
if (limit == null || limit <= 0)
{
limit = defaultLimit;
}

if (limit > maxLimit)
{
throw new ArgumentOutOfRangeException(nameof(limit), $"Max recent trades limit is {maxLimit}");
}

var token = await MakeJsonRequestAsync<JToken>($"/trades?symbol={marketSymbol}&limit={limit.Value}");
return token
.Select(t => t.ParseTrade(
"qty",
"price",
"isBuyerMaker",
"time",
TimestampType.UnixMilliseconds,
"id",
"true"));
}

protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(
string marketSymbol,
int periodSeconds,
DateTime? startDate = null,
DateTime? endDate = null,
int? limit = null)
{
var period = PeriodSecondsToString(periodSeconds);
const int maxLimit = 1000;
const int defaultLimit = 500;
if (limit == null || limit <= 0)
{
limit = defaultLimit;
}

if (limit > maxLimit)
{
throw new ArgumentOutOfRangeException(nameof(limit), $"Max recent candlesticks limit is {maxLimit}");
}


var url = $"/klines?symbol={marketSymbol}&interval={period}&limit={limit.Value}";
if (startDate != null)
{
url =
$"{url}&startTime={new DateTimeOffset(startDate.Value).ToUnixTimeMilliseconds()}";
}

if (endDate != null)
{
url = $"{url}&endTime={new DateTimeOffset(endDate.Value).ToUnixTimeMilliseconds()}";
}

var candleResponse = await MakeJsonRequestAsync<JToken>(url);
return candleResponse.Select(
cr =>
this.ParseCandle(
cr,
marketSymbol,
periodSeconds,
1,
2,
3,
4,
0,
TimestampType.UnixMilliseconds,
5,
7
));
}
}

public partial class ExchangeName
{
public const string MEXC = "MEXC";
}
}
36 changes: 36 additions & 0 deletions src/ExchangeSharp/Utility/CryptoUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,42 @@ public static byte[] AesEncryption(byte[] input, byte[] password, byte[] salt)
}
}

/// <summary>
/// Convert seconds to a period string, i.e. 1m, 5m, 60m, 4h, 1d, 1W, 1M
/// </summary>
/// <param name="seconds">Seconds. Use 60 for minute, 3600 for hour, 3600*24 for day, 3600*24*30 for month.</param>
/// <returns>Period string</returns>
public static string SecondsToPeriodInMinutesUpToHourString(int seconds)
{
const int minuteThreshold = 60;
const int hourThreshold = 60 * 60;
const int dayThreshold = 60 * 60 * 24;
const int weekThreshold = dayThreshold * 7;
const int monthThreshold = dayThreshold * 30;

if (seconds >= monthThreshold)
{
return seconds / monthThreshold + "M";
}

if (seconds >= weekThreshold)
{
return seconds / weekThreshold + "W";
}

if (seconds >= dayThreshold)
{
return seconds / dayThreshold + "d";
}

if (seconds >= hourThreshold)
{
return seconds / 60 + "m";
}

return seconds / minuteThreshold + "m";
}

/// <summary>
/// Convert seconds to a period string, i.e. 5s, 1m, 2h, 3d, 1w, 1M, etc.
/// </summary>
Expand Down
Loading

0 comments on commit 7d4cdbc

Please sign in to comment.