Skip to content

Commit f761f27

Browse files
authored
NDAX: added Trade stream (websocket) (#471)
* 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<T>() where IsBuy flag was not being set * update README.md
1 parent 0d1c82a commit f761f27

File tree

7 files changed

+220
-17
lines changed

7 files changed

+220
-17
lines changed

ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Threading;
55
using System.Threading.Tasks;
6+
using ExchangeSharp.NDAX;
67
using Newtonsoft.Json;
78
using Newtonsoft.Json.Linq;
89

@@ -11,7 +12,7 @@ namespace ExchangeSharp
1112
public sealed partial class ExchangeNDAXAPI : ExchangeAPI
1213
{
1314
public override string BaseUrl { get; set; } = "https://api.ndax.io:8443/AP";
14-
public override string BaseUrlWebSocket { get; set; } = "wss://apindaxstage.cdnhop.net/WSGateway";
15+
public override string BaseUrlWebSocket { get; set; } = "wss://api.ndax.io/WSGateway";
1516

1617
private AuthenticateResult authenticationDetails = null;
1718
public override string Name => ExchangeName.NDAX;
@@ -366,9 +367,11 @@ private async Task<string> GetMarketSymbolFromInstrumentId(long instrumentId)
366367

367368
protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IReadOnlyCollection<KeyValuePair<string, ExchangeTicker>>> tickers, params string[] marketSymbols)
368369
{
369-
var instrumentIds = await GetInstrumentIdFromMarketSymbol(marketSymbols);
370+
var instrumentIds = marketSymbols == null || marketSymbols.Length == 0 ?
371+
(await GetMarketSymbolsMetadataAsync()).Select(s => (long?)long.Parse(s.AltMarketSymbol)).ToArray() :
372+
await GetInstrumentIdFromMarketSymbol(marketSymbols);
370373

371-
return await ConnectWebSocketAsync("", async (socket, bytes) =>
374+
return await ConnectWebSocketAsync("", async (socket, bytes) =>
372375
{
373376
var messageFrame =
374377
JsonConvert.DeserializeObject<MessageFrame>(bytes.ToStringFromUTF8().TrimEnd('\0'));
@@ -377,14 +380,20 @@ protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IRea
377380
|| messageFrame.FunctionName.Equals("Level1UpdateEvent",
378381
StringComparison.InvariantCultureIgnoreCase))
379382
{
380-
var rawPayload = messageFrame.PayloadAs<Level1Data>();
381-
var symbol = await GetMarketSymbolFromInstrumentId(rawPayload.InstrumentId);
382-
tickers.Invoke(new[]
383-
{
384-
new KeyValuePair<string, ExchangeTicker>(symbol, rawPayload.ToExchangeTicker(symbol)),
385-
});
386-
}
387-
},
383+
var token = JToken.Parse(messageFrame.Payload);
384+
if (token["errormsg"] == null)
385+
{
386+
var rawPayload = messageFrame.PayloadAs<Level1Data>();
387+
var symbol = await GetMarketSymbolFromInstrumentId(rawPayload.InstrumentId);
388+
tickers.Invoke(new[]
389+
{
390+
new KeyValuePair<string, ExchangeTicker>(symbol, rawPayload.ToExchangeTicker(symbol)),
391+
});
392+
}
393+
else // "{\"result\":false,\"errormsg\":\"Resource Not Found\",\"errorcode\":104,\"detail\":\"Instrument not Found\"}"
394+
Logger.Info(messageFrame.Payload);
395+
}
396+
},
388397
async socket =>
389398
{
390399
foreach (var instrumentId in instrumentIds)
@@ -405,7 +414,73 @@ await socket.SendMessageAsync(new MessageFrame
405414
});
406415
}
407416

408-
private long GetNextSequenceNumber()
417+
protected override async Task<IWebSocket> OnGetTradesWebSocketAsync(Func<KeyValuePair<string, ExchangeTrade>, Task> callback, params string[] marketSymbols)
418+
{
419+
var instrumentIds = marketSymbols == null || marketSymbols.Length == 0 ?
420+
(await GetMarketSymbolsMetadataAsync()).Select(s => (long?)long.Parse(s.AltMarketSymbol)).ToArray() :
421+
await GetInstrumentIdFromMarketSymbol(marketSymbols);
422+
423+
return await ConnectWebSocketAsync("", async (socket, bytes) =>
424+
{
425+
var messageFrame =
426+
JsonConvert.DeserializeObject<MessageFrame>(bytes.ToStringFromUTF8().TrimEnd('\0'));
427+
428+
if (messageFrame.FunctionName.Equals("SubscribeTrades", StringComparison.InvariantCultureIgnoreCase)
429+
|| messageFrame.FunctionName.Equals("OrderTradeEvent", StringComparison.InvariantCultureIgnoreCase)
430+
|| messageFrame.FunctionName.Equals("TradeDataUpdateEvent", StringComparison.InvariantCultureIgnoreCase))
431+
{
432+
if (messageFrame.Payload != "[]")
433+
{
434+
var token = JToken.Parse(messageFrame.Payload);
435+
if (token.Type == JTokenType.Array)
436+
{ // "[[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]]"
437+
var jArray = token as JArray;
438+
for (int i = 0; i < jArray.Count; i++)
439+
{
440+
var tradesToken = jArray[i];
441+
var symbol = await GetMarketSymbolFromInstrumentId(tradesToken[1].ConvertInvariant<long>());
442+
var trade = tradesToken.ParseTradeNDAX(amountKey: 2, priceKey: 3,
443+
typeKey: 8, timestampKey: 6,
444+
TimestampType.UnixMilliseconds, idKey: 0,
445+
typeKeyIsBuyValue: "0");
446+
if (messageFrame.FunctionName.Equals("SubscribeTrades", StringComparison.InvariantCultureIgnoreCase))
447+
{
448+
trade.Flags |= ExchangeTradeFlags.IsFromSnapshot;
449+
if (i == jArray.Count - 1)
450+
{
451+
trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot;
452+
}
453+
}
454+
await callback(
455+
new KeyValuePair<string, ExchangeTrade>(symbol, trade));
456+
}
457+
}
458+
else // "{\"result\":false,\"errormsg\":\"Invalid Request\",\"errorcode\":100,\"detail\":null}"
459+
Logger.Info(messageFrame.Payload);
460+
}
461+
}
462+
},
463+
async socket =>
464+
{
465+
foreach (var instrumentId in instrumentIds)
466+
{
467+
await socket.SendMessageAsync(new MessageFrame
468+
{
469+
FunctionName = "SubscribeTrades",
470+
MessageType = MessageType.Request,
471+
SequenceNumber = GetNextSequenceNumber(),
472+
Payload = JsonConvert.SerializeObject(new
473+
{
474+
OMSId = 1,
475+
InstrumentId = instrumentId,
476+
IncludeLastCount = 100,
477+
})
478+
});
479+
}
480+
});
481+
}
482+
483+
private long GetNextSequenceNumber()
409484
{
410485
// Best practice is to carry an even sequence number.
411486
Interlocked.Add(ref _sequenceNumber, 2);

ExchangeSharp/API/Exchanges/NDAX/Models/Instrument.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public ExchangeMarket ToExchangeMarket()
6666
IsActive = SessionStatus.Equals("running", StringComparison.InvariantCultureIgnoreCase),
6767
MarginEnabled = false,
6868
MarketId = InstrumentId.ToStringInvariant(),
69-
MarketSymbol = Symbol
69+
MarketSymbol = Symbol,
70+
AltMarketSymbol = InstrumentId.ToStringInvariant(),
7071
};
7172
}
7273
}

ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace ExchangeSharp
55
{
66
public sealed partial class ExchangeNDAXAPI
77
{
8+
/// <summary>
9+
/// For use in SubscribeLevel1 OnGetTickersWebSocketAsync()
10+
/// </summary>
811
class Level1Data
912
{
1013
[JsonProperty("OMSId")]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using ExchangeSharp.NDAX;
2+
using Newtonsoft.Json;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace ExchangeSharp.NDAX
10+
{
11+
public enum Direction : byte
12+
{
13+
NoChange = 0,
14+
UpTick = 1,
15+
DownTick = 2,
16+
}
17+
public enum TakerSide : byte
18+
{
19+
Buy = 0,
20+
Sell = 1,
21+
}
22+
23+
public class NDAXTrade : ExchangeTrade
24+
{
25+
public long Order1Id { get; set; }
26+
public long Order2Id { get; set; }
27+
public Direction Direction { get; set; }
28+
public bool IsBlockTrade { get; set; }
29+
public long ClientOrderId { get; set; }
30+
public override string ToString()
31+
{
32+
return string.Format("{0},{1},{2},{3},{4},{5}", base.ToString(),
33+
Order1Id, Order2Id, Direction, IsBlockTrade, ClientOrderId);
34+
}
35+
}
36+
}
37+
38+
namespace ExchangeSharp
39+
{
40+
public sealed partial class ExchangeNDAXAPI
41+
{
42+
/// <summary>
43+
/// unable to use this in SubscribeTrades OnGetTradesWebSocketAsync() becuase of the array structure
44+
/// </summary>
45+
[JsonArray]
46+
class TradeData
47+
{
48+
[JsonProperty(Order = 0)]
49+
public long TradeId { get; set; }
50+
51+
/// <summary>
52+
/// ProductPairCode is the same number and used for the same purpose as InstrumentID.
53+
/// The two are completely equivalent in value. InstrumentId 47 = ProductPairCode 47.
54+
/// </summary>
55+
[JsonProperty(Order = 1)]
56+
public long ProductPairCode { get; set; }
57+
58+
[JsonProperty(Order = 2)]
59+
public long Quantity { get; set; }
60+
61+
[JsonProperty(Order = 3)]
62+
public long Price { get; set; }
63+
64+
[JsonProperty(Order = 4)]
65+
public long Order1Id { get; set; }
66+
67+
[JsonProperty(Order = 5)]
68+
public long Order2Id { get; set; }
69+
70+
[JsonProperty(Order = 6)]
71+
public long TradeTime { get; set; }
72+
73+
[JsonProperty(Order = 7)]
74+
public Direction Direction { get; set; }
75+
76+
[JsonProperty(Order = 8)]
77+
public TakerSide TakerSide { get; set; }
78+
79+
[JsonProperty(Order = 9)]
80+
public bool IsBlockTrade { get; set; }
81+
82+
[JsonProperty(Order = 10)]
83+
public long ClientOrderId { get; set; }
84+
85+
public NDAXTrade ToExchangeTrade()
86+
{
87+
var isBuy = TakerSide == TakerSide.Buy;
88+
return new NDAXTrade()
89+
{
90+
Amount = Quantity,
91+
Id = TradeId.ToStringInvariant(),
92+
Price = Price,
93+
IsBuy = isBuy,
94+
Timestamp = TradeTime.UnixTimeStampToDateTimeMilliseconds(),
95+
Flags = isBuy ? ExchangeTradeFlags.IsBuy : default,
96+
Order1Id = Order1Id,
97+
Order2Id = Order2Id,
98+
Direction = Direction,
99+
IsBlockTrade = IsBlockTrade,
100+
ClientOrderId = ClientOrderId,
101+
};
102+
}
103+
}
104+
}
105+
}

ExchangeSharp/API/Exchanges/NDAX/Models/TradeHistory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace ExchangeSharp
55
{
66
public sealed partial class ExchangeNDAXAPI
77
{
8+
/// <summary>
9+
/// For use in GetTradesHistory: OnGetHistoricalTradesAsync()
10+
/// </summary>
811
class TradeHistory
912
{
1013
[JsonProperty("TradeTimeMS")]

ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
MIT LICENSE
33
44
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
2020
using ExchangeSharp.Coinbase;
2121
using ExchangeSharp.KuCoin;
2222
using Newtonsoft.Json.Linq;
23+
using ExchangeSharp.NDAX;
2324

2425
namespace ExchangeSharp
2526
{
@@ -593,15 +594,29 @@ internal static ExchangeTrade ParseTradeKucoin(this JToken token, object amountK
593594
return trade;
594595
}
595596

597+
internal static ExchangeTrade ParseTradeNDAX(this JToken token, object amountKey, object priceKey, object typeKey,
598+
object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy")
599+
{
600+
var trade = ParseTradeComponents<NDAXTrade>(token, amountKey, priceKey, typeKey,
601+
timestampKey, timestampType, idKey, typeKeyIsBuyValue);
602+
trade.Order1Id = token[4].ConvertInvariant<long>();
603+
trade.Order2Id = token[5].ConvertInvariant<long>();
604+
trade.Direction = (Direction)token[7].ConvertInvariant<byte>();
605+
trade.IsBlockTrade = token[9].ConvertInvariant<bool>();
606+
trade.ClientOrderId = token[10].ConvertInvariant<long>();
607+
return trade;
608+
}
609+
596610
internal static T ParseTradeComponents<T>(this JToken token, object amountKey, object priceKey, object typeKey,
597611
object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy")
598612
where T : ExchangeTrade, new()
599613
{
614+
var isBuy = token[typeKey].ToStringInvariant().EqualsWithOption(typeKeyIsBuyValue);
600615
T trade = new T
601616
{
602617
Amount = token[amountKey].ConvertInvariant<decimal>(),
603618
Price = token[priceKey].ConvertInvariant<decimal>(),
604-
IsBuy = (token[typeKey].ToStringInvariant().EqualsWithOption(typeKeyIsBuyValue)),
619+
IsBuy = isBuy,
605620
};
606621
trade.Timestamp = (timestampKey == null ? CryptoUtility.UtcNow : CryptoUtility.ParseTimestamp(token[timestampKey], timestampType));
607622
if (idKey == null)
@@ -619,6 +634,7 @@ internal static T ParseTradeComponents<T>(this JToken token, object amountKey, o
619634
Logger.Info("error parsing trade ID: " + token.ToStringInvariant());
620635
}
621636
}
637+
trade.Flags = isBuy ? ExchangeTradeFlags.IsBuy : default;
622638
return trade;
623639
}
624640
#endregion
@@ -703,4 +719,4 @@ internal static MarketCandle ParseCandle(this INamed named, JToken token, string
703719
return candle;
704720
}
705721
}
706-
}
722+
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ The following cryptocurrency exchanges are supported:
4040
| Poloniex | x | x | T R B |
4141
| YoBit | x | x | |
4242
| ZB.com | wip | | R |
43-
| NDAX | x | x | T |
43+
| NDAX | x | x | T R |
4444

4545
The following cryptocurrency services are supported:
4646
- Cryptowatch (partial)

0 commit comments

Comments
 (0)