From 00156a795a30bdee7894598db07f86382b9fca69 Mon Sep 17 00:00:00 2001
From: BZ-CO <30245815+BZ-CO@users.noreply.github.com>
Date: Wed, 3 Jul 2024 21:26:46 +0300
Subject: [PATCH 1/4] Add CryptoUtility.SecondsToPeriodInMinutesUpToHourString
Convert seconds to a period string, i.e. 1m, 5m, 60m, 4h, 1d, 1W, 1M.
Used on MEXC.
---
src/ExchangeSharp/Utility/CryptoUtility.cs | 36 ++++++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/src/ExchangeSharp/Utility/CryptoUtility.cs b/src/ExchangeSharp/Utility/CryptoUtility.cs
index cdc5a05a..5ab9809e 100644
--- a/src/ExchangeSharp/Utility/CryptoUtility.cs
+++ b/src/ExchangeSharp/Utility/CryptoUtility.cs
@@ -1309,6 +1309,42 @@ public static byte[] AesEncryption(byte[] input, byte[] password, byte[] salt)
}
}
+ ///
+ /// Convert seconds to a period string, i.e. 1m, 5m, 60m, 4h, 1d, 1W, 1M
+ ///
+ /// Seconds. Use 60 for minute, 3600 for hour, 3600*24 for day, 3600*24*30 for month.
+ /// Period string
+ 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";
+ }
+
///
/// Convert seconds to a period string, i.e. 5s, 1m, 2h, 3d, 1w, 1M, etc.
///
From f629c582a895ea42378b45dee2723ba53f33e48d Mon Sep 17 00:00:00 2001
From: BZ-CO <30245815+BZ-CO@users.noreply.github.com>
Date: Wed, 3 Jul 2024 21:27:20 +0300
Subject: [PATCH 2/4] Add MEXC public REST endpoints
---
.../API/Exchanges/MEXC/ExchangeMEXCAPI.cs | 211 ++++++++++++++++++
1 file changed, 211 insertions(+)
create mode 100644 src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs
diff --git a/src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs b/src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs
new file mode 100644
index 00000000..7d71e4bd
--- /dev/null
+++ b/src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs
@@ -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 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> OnGetMarketSymbolsAsync()
+ {
+ return (await OnGetMarketSymbolsMetadataAsync())
+ .Select(x => x.MarketSymbol);
+ }
+
+ protected internal override async Task> OnGetMarketSymbolsMetadataAsync()
+ {
+ var symbols = await MakeJsonRequestAsync("/exchangeInfo", BaseUrl);
+
+ return (symbols["symbols"] ?? throw new ArgumentNullException())
+ .Select(symbol => new ExchangeMarket()
+ {
+ MarketSymbol = symbol["symbol"].ToStringInvariant(),
+ IsActive = symbol["isSpotTradingAllowed"].ConvertInvariant(),
+ MarginEnabled = symbol["isMarginTradingAllowed"].ConvertInvariant(),
+ BaseCurrency = symbol["baseAsset"].ToStringInvariant(),
+ QuoteCurrency = symbol["quoteAsset"].ToStringInvariant(),
+ QuantityStepSize = symbol["baseSizePrecision"].ConvertInvariant(),
+ // Not 100% sure about this
+ PriceStepSize =
+ CryptoUtility.PrecisionToStepSize(symbol["quoteCommissionPrecision"].ConvertInvariant()),
+ MinTradeSizeInQuoteCurrency = symbol["quoteAmountPrecision"].ConvertInvariant(),
+ MaxTradeSizeInQuoteCurrency = symbol["maxQuoteAmount"].ConvertInvariant()
+ });
+ }
+
+ protected override async Task>> OnGetTickersAsync()
+ {
+ var tickers = new List>();
+ var token = await MakeJsonRequestAsync("/ticker/24hr", BaseUrl);
+ foreach (var t in token)
+ {
+ var symbol = (t["symbol"] ?? throw new ArgumentNullException()).ToStringInvariant();
+ tickers.Add(new KeyValuePair(symbol,
+ await this.ParseTickerAsync(
+ t,
+ symbol,
+ "askPrice",
+ "bidPrice",
+ "lastPrice",
+ "volume",
+ timestampType: TimestampType.UnixMilliseconds,
+ timestampKey: "closeTime")));
+ }
+
+ return tickers;
+ }
+
+ protected override async Task OnGetTickerAsync(string marketSymbol) =>
+ await this.ParseTickerAsync(
+ await MakeJsonRequestAsync($"/ticker/24hr?symbol={marketSymbol.ToUpperInvariant()}", BaseUrl),
+ marketSymbol,
+ "askPrice",
+ "bidPrice",
+ "lastPrice",
+ "volume",
+ timestampType: TimestampType.UnixMilliseconds,
+ timestampKey: "closeTime");
+
+ protected override async Task 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($"/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> 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($"/trades?symbol={marketSymbol}&limit={limit.Value}");
+ return token
+ .Select(t => t.ParseTrade(
+ "qty",
+ "price",
+ "isBuyerMaker",
+ "time",
+ TimestampType.UnixMilliseconds,
+ "id",
+ "true"));
+ }
+
+ protected override async Task> 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(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";
+ }
+}
From 1973215bc98c802b06aefc42748276896a5ad4fe Mon Sep 17 00:00:00 2001
From: BZ-CO <30245815+BZ-CO@users.noreply.github.com>
Date: Wed, 3 Jul 2024 21:27:59 +0300
Subject: [PATCH 3/4] Create ExchangeMEXCAPITests.cs
---
.../ExchangeMEXCAPITests.cs | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 tests/ExchangeSharpTests/ExchangeMEXCAPITests.cs
diff --git a/tests/ExchangeSharpTests/ExchangeMEXCAPITests.cs b/tests/ExchangeSharpTests/ExchangeMEXCAPITests.cs
new file mode 100644
index 00000000..d571e63c
--- /dev/null
+++ b/tests/ExchangeSharpTests/ExchangeMEXCAPITests.cs
@@ -0,0 +1,93 @@
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using ExchangeSharp;
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace ExchangeSharpTests;
+
+[TestClass]
+public class MEXCAPITests
+{
+ private const string MarketSymbol = "ETHBTC";
+ private static IExchangeAPI _api;
+
+ [AssemblyInitialize]
+ public static async Task AssemblyInitialize(TestContext testContext)
+ {
+ _api = await ExchangeAPI.GetExchangeAPIAsync();
+ }
+
+ [TestMethod]
+ public async Task GetMarketSymbolsMetadataAsyncShouldReturnSymbols()
+ {
+ var symbols = (await _api.GetMarketSymbolsMetadataAsync()).ToImmutableArray();
+ symbols.Should().NotBeNull();
+ foreach (var symbol in symbols)
+ {
+ symbol.MarketSymbol.Should().NotBeNull();
+ symbol.BaseCurrency.Should().NotBeNull();
+ symbol.QuoteCurrency.Should().NotBeNull();
+ }
+ }
+
+ [TestMethod]
+ public async Task GetMarketSymbolsAsyncShouldReturnSymbols()
+ {
+ var symbols = (await _api.GetMarketSymbolsAsync()).ToImmutableArray();
+ symbols.Should().NotBeNull();
+ foreach (var symbol in symbols)
+ {
+ symbol.Should().NotBeNull();
+ }
+ }
+
+ [TestMethod]
+ public async Task GetTickersAsyncShouldReturnTickers()
+ {
+ var tickers = (await _api.GetTickersAsync()).ToImmutableArray();
+ tickers.Should().NotBeNull();
+ foreach (var t in tickers)
+ {
+ t.Key.Should().NotBeNull();
+ t.Value.MarketSymbol.Should().NotBeNull();
+ t.Value.Exchange.Should().NotBeNull();
+ t.Value.Volume.Should().NotBeNull();
+ }
+ }
+
+ [TestMethod]
+ public async Task GetTickerAsyncShouldReturnTicker()
+ {
+ var ticker = await _api.GetTickerAsync(MarketSymbol);
+ ticker.Should().NotBeNull();
+ ticker.MarketSymbol.Should().NotBeNull();
+ ticker.Exchange.Should().NotBeNull();
+ ticker.Volume.Should().NotBeNull();
+ }
+
+ [TestMethod]
+ public async Task GetOrderBookAsyncShouldReturlOrderBookData()
+ {
+ var orderBook = await _api.GetOrderBookAsync(MarketSymbol);
+ orderBook.MarketSymbol.Should().NotBeNullOrEmpty();
+ orderBook.Asks.Should().NotBeNull();
+ orderBook.Bids.Should().NotBeNull();
+ }
+
+ [TestMethod]
+ public async Task GetRecentTradesAsyncShouldReturnTrades()
+ {
+ var recentTrades = await _api.GetRecentTradesAsync(MarketSymbol);
+ recentTrades.Should().NotBeNull();
+ }
+
+ [TestMethod]
+ public async Task GetCandlesAsyncShouldReturnCandleData()
+ {
+ var klines = (await _api.GetCandlesAsync(MarketSymbol, 3600)).ToArray();
+ klines.Should().NotBeNull();
+ klines.Length.Should().NotBe(0);
+ }
+}
From 751fed2cb99a1e1af70b12fa60c52149be2182a7 Mon Sep 17 00:00:00 2001
From: BZ-CO <30245815+BZ-CO@users.noreply.github.com>
Date: Wed, 3 Jul 2024 21:38:48 +0300
Subject: [PATCH 4/4] Add MEXC to README.md
---
README.md | 87 ++++++++++++++++++++++++++++---------------------------
1 file changed, 44 insertions(+), 43 deletions(-)
diff --git a/README.md b/README.md
index 91ab16ee..b2e2a975 100644
--- a/README.md
+++ b/README.md
@@ -24,49 +24,50 @@ Feel free to visit the discord channel at 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: