From dbf6c4e1b02442acf6e3d620e01e8f57ea260c40 Mon Sep 17 00:00:00 2001 From: vslee Date: Wed, 23 Oct 2019 12:21:37 -0700 Subject: [PATCH 1/2] NDAX: added Trade stream (websocket) - could not figure out how to use the JSON deserialization as the trades are returned as arrays (@Kukks if you know how...?) - created custom NDAXTrade subclass w/ additional properties - fixed BaseUrlWebSocket - account for null or missing marketSymbols param - store instrumentId in MarketSymbol.AltMarketSymbol - [affects all exchanges] fixed bug in ExchangeAPIExtensions.ParseTradeComponents() where IsBuy flag was not being set --- .../API/Exchanges/NDAX/ExchangeNDAXAPI.cs | 99 +++++++++++++++-- .../API/Exchanges/NDAX/Models/Instrument.cs | 3 +- .../API/Exchanges/NDAX/Models/Level1Data.cs | 3 + .../API/Exchanges/NDAX/Models/TradeData.cs | 105 ++++++++++++++++++ .../API/Exchanges/NDAX/Models/TradeHistory.cs | 3 + .../Exchanges/_Base/ExchangeAPIExtensions.cs | 22 +++- 6 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 ExchangeSharp/API/Exchanges/NDAX/Models/TradeData.cs diff --git a/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs b/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs index 8fa8718b..ac9e5087 100644 --- a/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs +++ b/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using ExchangeSharp.NDAX; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -11,7 +12,7 @@ namespace ExchangeSharp public sealed partial class ExchangeNDAXAPI : ExchangeAPI { public override string BaseUrl { get; set; } = "https://api.ndax.io:8443/AP"; - public override string BaseUrlWebSocket { get; set; } = "wss://apindaxstage.cdnhop.net/WSGateway"; + public override string BaseUrlWebSocket { get; set; } = "wss://api.ndax.io/WSGateway"; private AuthenticateResult authenticationDetails = null; public override string Name => ExchangeName.NDAX; @@ -366,9 +367,11 @@ private async Task GetMarketSymbolFromInstrumentId(long instrumentId) protected override async Task OnGetTickersWebSocketAsync(Action>> tickers, params string[] marketSymbols) { - var instrumentIds = await GetInstrumentIdFromMarketSymbol(marketSymbols); + var instrumentIds = marketSymbols == null || marketSymbols.Length == 0 ? + (await GetMarketSymbolsMetadataAsync()).Select(s => (long?)long.Parse(s.AltMarketSymbol)).ToArray() : + await GetInstrumentIdFromMarketSymbol(marketSymbols); - return await ConnectWebSocketAsync("", async (socket, bytes) => + return await ConnectWebSocketAsync("", async (socket, bytes) => { var messageFrame = JsonConvert.DeserializeObject(bytes.ToStringFromUTF8().TrimEnd('\0')); @@ -377,14 +380,20 @@ protected override async Task OnGetTickersWebSocketAsync(Action(); - var symbol = await GetMarketSymbolFromInstrumentId(rawPayload.InstrumentId); - tickers.Invoke(new[] - { - new KeyValuePair(symbol, rawPayload.ToExchangeTicker(symbol)), - }); - } - }, + var token = JToken.Parse(messageFrame.Payload); + if (token["errormsg"] == null) + { + var rawPayload = messageFrame.PayloadAs(); + var symbol = await GetMarketSymbolFromInstrumentId(rawPayload.InstrumentId); + tickers.Invoke(new[] + { + new KeyValuePair(symbol, rawPayload.ToExchangeTicker(symbol)), + }); + } + else // "{\"result\":false,\"errormsg\":\"Resource Not Found\",\"errorcode\":104,\"detail\":\"Instrument not Found\"}" + Logger.Info(messageFrame.Payload); + } + }, async socket => { foreach (var instrumentId in instrumentIds) @@ -405,7 +414,73 @@ await socket.SendMessageAsync(new MessageFrame }); } - private long GetNextSequenceNumber() + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { + var instrumentIds = marketSymbols == null || marketSymbols.Length == 0 ? + (await GetMarketSymbolsMetadataAsync()).Select(s => (long?)long.Parse(s.AltMarketSymbol)).ToArray() : + await GetInstrumentIdFromMarketSymbol(marketSymbols); + + return await ConnectWebSocketAsync("", async (socket, bytes) => + { + var messageFrame = + JsonConvert.DeserializeObject(bytes.ToStringFromUTF8().TrimEnd('\0')); + + if (messageFrame.FunctionName.Equals("SubscribeTrades", StringComparison.InvariantCultureIgnoreCase) + || messageFrame.FunctionName.Equals("OrderTradeEvent", StringComparison.InvariantCultureIgnoreCase) + || messageFrame.FunctionName.Equals("TradeDataUpdateEvent", StringComparison.InvariantCultureIgnoreCase)) + { + if (messageFrame.Payload != "[]") + { + var token = JToken.Parse(messageFrame.Payload); + if (token.Type == JTokenType.Array) + { // "[[34838,2,0.4656,10879.5,311801351,311801370,1570134695227,1,0,0,0],[34839,2,0.4674,10881.7,311801352,311801370,1570134695227,1,0,0,0]]" + var jArray = token as JArray; + for (int i = 0; i < jArray.Count; i++) + { + var tradesToken = jArray[i]; + var symbol = await GetMarketSymbolFromInstrumentId(tradesToken[1].ConvertInvariant()); + var trade = tradesToken.ParseTradeNDAX(amountKey: 2, priceKey: 3, + typeKey: 8, timestampKey: 6, + TimestampType.UnixMilliseconds, idKey: 0, + typeKeyIsBuyValue: "0"); + if (messageFrame.FunctionName.Equals("SubscribeTrades", StringComparison.InvariantCultureIgnoreCase)) + { + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + if (i == jArray.Count - 1) + { + trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; + } + } + await callback( + new KeyValuePair(symbol, trade)); + } + } + else // "{\"result\":false,\"errormsg\":\"Invalid Request\",\"errorcode\":100,\"detail\":null}" + Logger.Info(messageFrame.Payload); + } + } + }, + async socket => + { + foreach (var instrumentId in instrumentIds) + { + await socket.SendMessageAsync(new MessageFrame + { + FunctionName = "SubscribeTrades", + MessageType = MessageType.Request, + SequenceNumber = GetNextSequenceNumber(), + Payload = JsonConvert.SerializeObject(new + { + OMSId = 1, + InstrumentId = instrumentId, + IncludeLastCount = 100, + }) + }); + } + }); + } + + private long GetNextSequenceNumber() { // Best practice is to carry an even sequence number. Interlocked.Add(ref _sequenceNumber, 2); diff --git a/ExchangeSharp/API/Exchanges/NDAX/Models/Instrument.cs b/ExchangeSharp/API/Exchanges/NDAX/Models/Instrument.cs index 46c9cf31..84e9c0b1 100644 --- a/ExchangeSharp/API/Exchanges/NDAX/Models/Instrument.cs +++ b/ExchangeSharp/API/Exchanges/NDAX/Models/Instrument.cs @@ -66,7 +66,8 @@ public ExchangeMarket ToExchangeMarket() IsActive = SessionStatus.Equals("running", StringComparison.InvariantCultureIgnoreCase), MarginEnabled = false, MarketId = InstrumentId.ToStringInvariant(), - MarketSymbol = Symbol + MarketSymbol = Symbol, + AltMarketSymbol = InstrumentId.ToStringInvariant(), }; } } diff --git a/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs b/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs index fb926249..51dfc6aa 100644 --- a/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs +++ b/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs @@ -5,6 +5,9 @@ namespace ExchangeSharp { public sealed partial class ExchangeNDAXAPI { + /// + /// For use in SubscribeLevel1 OnGetTickersWebSocketAsync() + /// class Level1Data { [JsonProperty("OMSId")] diff --git a/ExchangeSharp/API/Exchanges/NDAX/Models/TradeData.cs b/ExchangeSharp/API/Exchanges/NDAX/Models/TradeData.cs new file mode 100644 index 00000000..b507dc25 --- /dev/null +++ b/ExchangeSharp/API/Exchanges/NDAX/Models/TradeData.cs @@ -0,0 +1,105 @@ +using ExchangeSharp.NDAX; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeSharp.NDAX +{ + public enum Direction : byte + { + NoChange = 0, + UpTick = 1, + DownTick = 2, + } + public enum TakerSide : byte + { + Buy = 0, + Sell = 1, + } + + public class NDAXTrade : ExchangeTrade + { + public long Order1Id { get; set; } + public long Order2Id { get; set; } + public Direction Direction { get; set; } + public bool IsBlockTrade { get; set; } + public long ClientOrderId { get; set; } + public override string ToString() + { + return string.Format("{0},{1},{2},{3},{4},{5}", base.ToString(), + Order1Id, Order2Id, Direction, IsBlockTrade, ClientOrderId); + } + } +} + +namespace ExchangeSharp +{ + public sealed partial class ExchangeNDAXAPI + { + /// + /// unable to use this in SubscribeTrades OnGetTradesWebSocketAsync() becuase of the array structure + /// + [JsonArray] + class TradeData + { + [JsonProperty(Order = 0)] + public long TradeId { get; set; } + + /// + /// ProductPairCode is the same number and used for the same purpose as InstrumentID. + /// The two are completely equivalent in value. InstrumentId 47 = ProductPairCode 47. + /// + [JsonProperty(Order = 1)] + public long ProductPairCode { get; set; } + + [JsonProperty(Order = 2)] + public long Quantity { get; set; } + + [JsonProperty(Order = 3)] + public long Price { get; set; } + + [JsonProperty(Order = 4)] + public long Order1Id { get; set; } + + [JsonProperty(Order = 5)] + public long Order2Id { get; set; } + + [JsonProperty(Order = 6)] + public long TradeTime { get; set; } + + [JsonProperty(Order = 7)] + public Direction Direction { get; set; } + + [JsonProperty(Order = 8)] + public TakerSide TakerSide { get; set; } + + [JsonProperty(Order = 9)] + public bool IsBlockTrade { get; set; } + + [JsonProperty(Order = 10)] + public long ClientOrderId { get; set; } + + public NDAXTrade ToExchangeTrade() + { + var isBuy = TakerSide == TakerSide.Buy; + return new NDAXTrade() + { + Amount = Quantity, + Id = TradeId.ToStringInvariant(), + Price = Price, + IsBuy = isBuy, + Timestamp = TradeTime.UnixTimeStampToDateTimeMilliseconds(), + Flags = isBuy ? ExchangeTradeFlags.IsBuy : default, + Order1Id = Order1Id, + Order2Id = Order2Id, + Direction = Direction, + IsBlockTrade = IsBlockTrade, + ClientOrderId = ClientOrderId, + }; + } + } + } +} diff --git a/ExchangeSharp/API/Exchanges/NDAX/Models/TradeHistory.cs b/ExchangeSharp/API/Exchanges/NDAX/Models/TradeHistory.cs index 5a011b7e..e8faa1c4 100644 --- a/ExchangeSharp/API/Exchanges/NDAX/Models/TradeHistory.cs +++ b/ExchangeSharp/API/Exchanges/NDAX/Models/TradeHistory.cs @@ -5,6 +5,9 @@ namespace ExchangeSharp { public sealed partial class ExchangeNDAXAPI { + /// + /// For use in GetTradesHistory: OnGetHistoricalTradesAsync() + /// class TradeHistory { [JsonProperty("TradeTimeMS")] diff --git a/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs b/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs index c395fdaf..f20fadf6 100644 --- a/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs +++ b/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -20,6 +20,7 @@ The above copyright notice and this permission notice shall be included in all c using ExchangeSharp.Coinbase; using ExchangeSharp.KuCoin; using Newtonsoft.Json.Linq; +using ExchangeSharp.NDAX; namespace ExchangeSharp { @@ -593,15 +594,29 @@ internal static ExchangeTrade ParseTradeKucoin(this JToken token, object amountK return trade; } + internal static ExchangeTrade ParseTradeNDAX(this JToken token, object amountKey, object priceKey, object typeKey, + object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy") + { + var trade = ParseTradeComponents(token, amountKey, priceKey, typeKey, + timestampKey, timestampType, idKey, typeKeyIsBuyValue); + trade.Order1Id = token[4].ConvertInvariant(); + trade.Order2Id = token[5].ConvertInvariant(); + trade.Direction = (Direction)token[7].ConvertInvariant(); + trade.IsBlockTrade = token[9].ConvertInvariant(); + trade.ClientOrderId = token[10].ConvertInvariant(); + return trade; + } + internal static T ParseTradeComponents(this JToken token, object amountKey, object priceKey, object typeKey, object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy") where T : ExchangeTrade, new() { + var isBuy = token[typeKey].ToStringInvariant().EqualsWithOption(typeKeyIsBuyValue); T trade = new T { Amount = token[amountKey].ConvertInvariant(), Price = token[priceKey].ConvertInvariant(), - IsBuy = (token[typeKey].ToStringInvariant().EqualsWithOption(typeKeyIsBuyValue)), + IsBuy = isBuy, }; trade.Timestamp = (timestampKey == null ? CryptoUtility.UtcNow : CryptoUtility.ParseTimestamp(token[timestampKey], timestampType)); if (idKey == null) @@ -619,6 +634,7 @@ internal static T ParseTradeComponents(this JToken token, object amountKey, o Logger.Info("error parsing trade ID: " + token.ToStringInvariant()); } } + trade.Flags = isBuy ? ExchangeTradeFlags.IsBuy : default; return trade; } #endregion @@ -703,4 +719,4 @@ internal static MarketCandle ParseCandle(this INamed named, JToken token, string return candle; } } -} \ No newline at end of file +} From 9d48aa41c88a9691b0399f4cb86d15453e1c8a72 Mon Sep 17 00:00:00 2001 From: vslee Date: Wed, 23 Oct 2019 12:30:50 -0700 Subject: [PATCH 2/2] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 66a06ffd..a4a9d414 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The following cryptocurrency exchanges are supported: | Poloniex | x | x | T R B | | YoBit | x | x | | | ZB.com | wip | | R | -| NDAX | x | x | T | +| NDAX | x | x | T R | The following cryptocurrency services are supported: - Cryptowatch (partial)