diff --git a/README.md b/README.md index 950566cf..6010eb95 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The following cryptocurrency exchanges are supported: | KuCoin | x | x | T R B | | | LBank | x | x | R | | | Livecoin | x | x | | | -| MEXC | x | x | | | +| MEXC | x | x | B | | | NDAX | x | x | T R | | | OKCoin | x | x | R B | | | OKEx | x | x | T R B O | | diff --git a/src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs b/src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs index 613d7153..fb5fddf6 100644 --- a/src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/MEXC/ExchangeMEXCAPI.cs @@ -1,9 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ExchangeSharp.Models; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using ExchangeSharp.API.Exchanges.MEXC.Models; namespace ExchangeSharp { @@ -21,6 +23,7 @@ private ExchangeMEXCAPI() MarketSymbolSeparator = string.Empty; MarketSymbolIsUppercase = true; RateLimit = new RateGate(20, TimeSpan.FromSeconds(2)); + WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; } public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) @@ -369,6 +372,110 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy await MakeJsonRequestAsync("/order", BaseUrl, payload, "DELETE"); } + protected override async Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols + ) + { + if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + } + + var initialSequenceIds = new Dictionary(); + + return await ConnectPublicWebSocketAsync( + string.Empty, + (_socket, msg) => + { + var json = msg.ToStringFromUTF8(); + + MarketDepthDiffUpdate update = null; + try + { + update = JsonConvert.DeserializeObject(json, SerializerSettings); + } + catch + { + } + + if (update == null || string.IsNullOrWhiteSpace(update.Channel) || update.Details == null) + { + return Task.CompletedTask; + } + + if (update.Details.Version < initialSequenceIds[update.Symbol]) + { + // A deprecated update should be ignored + return Task.CompletedTask; + } + + var eventTimeDateTime = update.EventTime.UnixTimeStampToDateTimeMilliseconds(); + + var orderBook = new ExchangeOrderBook + { + IsFromSnapshot = false, + ExchangeName = Name, + SequenceId = update.Details.Version, + MarketSymbol = update.Symbol, + LastUpdatedUtc = eventTimeDateTime, + }; + + if (update.Details.Asks != null) + { + foreach (var ask in update.Details.Asks) + { + orderBook.Asks[ask.Price] = new ExchangeOrderPrice + { + Price = ask.Price, + Amount = ask.Volume, + }; + } + } + + if (update.Details.Bids != null) + { + foreach (var bid in update.Details.Bids) + { + orderBook.Bids[bid.Price] = new ExchangeOrderPrice + { + Price = bid.Price, + Amount = bid.Volume, + }; + } + } + + callback(orderBook); + + return Task.CompletedTask; + }, + async (_socket) => + { + foreach (var marketSymbol in marketSymbols) // "Every websocket connection maximum support 30 subscriptions at one time." - API docs + { + var initialBook = await OnGetOrderBookAsync(marketSymbol, maxCount); + initialBook.IsFromSnapshot = true; + + callback(initialBook); + + initialSequenceIds[marketSymbol] = initialBook.SequenceId; + + var subscriptionParams = new List + { + $"spot@public.increase.depth.v3.api@{marketSymbol}" + }; + + await _socket.SendMessageAsync(new WebSocketSubscription + { + Method = "SUBSCRIPTION", + Params = subscriptionParams, + }); + } + } + ); + } + private async Task GetBalance() { var token = await MakeJsonRequestAsync("/account", payload: await GetNoncePayloadAsync()); diff --git a/src/ExchangeSharp/API/Exchanges/MEXC/Models/MarketDepthDiffUpdate.cs b/src/ExchangeSharp/API/Exchanges/MEXC/Models/MarketDepthDiffUpdate.cs new file mode 100644 index 00000000..e9c71b27 --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/MEXC/Models/MarketDepthDiffUpdate.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace ExchangeSharp.API.Exchanges.MEXC.Models +{ + internal class MarketDepthDiffUpdateDetailItem + { + [JsonProperty("p")] + public decimal Price { get; set; } + + [JsonProperty("v")] + public decimal Volume { get; set; } + } + + internal class MarketDepthDiffUpdateDetails + { + public List Asks { get; set; } + + public List Bids { get; set; } + + [JsonProperty("e")] + public string EventType { get; set; } + + [JsonProperty("r")] + public long Version { get; set; } + } + + internal class MarketDepthDiffUpdate + { + [JsonProperty("c")] + public string Channel { get; set; } + + [JsonProperty("d")] + public MarketDepthDiffUpdateDetails Details { get; set; } + + [JsonProperty("s")] + public string Symbol { get; set; } + + /// + /// Milliseconds since Unix epoch + /// + [JsonProperty("t")] + public long EventTime { get; set; } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/MEXC/Models/WebSocketSubscription.cs b/src/ExchangeSharp/API/Exchanges/MEXC/Models/WebSocketSubscription.cs new file mode 100644 index 00000000..01fb0c05 --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/MEXC/Models/WebSocketSubscription.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ExchangeSharp.API.Exchanges.MEXC.Models +{ + internal class WebSocketSubscription + { + public string Method { get; set; } + + public List Params { get; set; } + } +}