Skip to content

Commit ad6b98e

Browse files
authored
Moving to OKEx V5 WebSocket API (#685)
* implemented OnGetTickersWebSocketAsync Okex * Update OnGetTradesWebSocketAsync Migration to V5 WS API * Update OnGetDeltaOrderBookWebSocketAsync Migration to V5 WS API * implemented OnGetOrderDetailsWebSocketAsync Okex * ParseOrder added the missing order result states
1 parent fce1fbb commit ad6b98e

File tree

3 files changed

+380
-135
lines changed

3 files changed

+380
-135
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The following cryptocurrency exchanges are supported:
5252
| Livecoin | x | x | |
5353
| NDAX | x | x | T R |
5454
| OKCoin | x | x | R B |
55-
| OKEx | x | x | R B |
55+
| OKEx | x | x | R B O |
5656
| Poloniex | x | x | T R B |
5757
| YoBit | x | x | |
5858
| ZB.com | wip | | R |

src/ExchangeSharp/API/Exchanges/OKGroup/ExchangeOKExAPI.cs

Lines changed: 263 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ The above copyright notice and this permission notice shall be included in all c
1616
using System.Linq;
1717
using System.Security.Cryptography;
1818
using System.Text;
19+
using System.Threading;
1920
using System.Threading.Tasks;
21+
using System.Xml;
2022
using ExchangeSharp.OKGroup;
2123
using Newtonsoft.Json;
2224
using Newtonsoft.Json.Linq;
@@ -28,7 +30,7 @@ public sealed partial class ExchangeOKExAPI : OKGroupCommon
2830
public override string BaseUrl { get; set; } = "https://www.okex.com/api/v1";
2931
public override string BaseUrlV2 { get; set; } = "https://www.okex.com/v2/spot";
3032
public override string BaseUrlV3 { get; set; } = "https://www.okex.com/api";
31-
public override string BaseUrlWebSocket { get; set; } = "wss://real.okex.com:8443/ws/v3";
33+
public override string BaseUrlWebSocket { get; set; } = "wss://ws.okex.com:8443/ws/v5";
3234
public string BaseUrlV5 { get; set; } = "https://www.okex.com/api/v5";
3335
protected override bool IsFuturesAndSwapEnabled { get; } = true;
3436

@@ -317,6 +319,7 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
317319
{
318320
payload["clOrdId"] = order.ClientOrderId;
319321
}
322+
320323
payload["side"] = order.IsBuy ? "buy" : "sell";
321324
payload["posSide"] = "net";
322325
payload["ordType"] = order.OrderType switch
@@ -330,7 +333,8 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
330333
payload["sz"] = order.Amount.ToStringInvariant();
331334
if (order.OrderType != OrderType.Market)
332335
{
333-
if (!order.Price.HasValue) throw new ArgumentNullException(nameof(order.Price), "Okex place order request requires price");
336+
if (!order.Price.HasValue)
337+
throw new ArgumentNullException(nameof(order.Price), "Okex place order request requires price");
334338
payload["px"] = order.Price.ToStringInvariant();
335339
}
336340

@@ -379,30 +383,269 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti
379383
}
380384
}
381385

386+
protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(
387+
Action<IReadOnlyCollection<KeyValuePair<string, ExchangeTicker>>> callback,
388+
params string[] symbols)
389+
{
390+
return await ConnectWebSocketOkexAsync(
391+
async (socket) => { await AddMarketSymbolsToChannel(socket, "tickers", symbols); },
392+
async (socket, symbol, sArray, token) =>
393+
{
394+
var tickers = new List<KeyValuePair<string, ExchangeTicker>>
395+
{
396+
new KeyValuePair<string, ExchangeTicker>(symbol, await ParseTickerV5Async(token, symbol))
397+
};
398+
callback(tickers);
399+
});
400+
}
401+
402+
protected override async Task<IWebSocket> OnGetTradesWebSocketAsync(
403+
Func<KeyValuePair<string, ExchangeTrade>, Task> callback, params string[] marketSymbols)
404+
{
405+
return await ConnectWebSocketOkexAsync(
406+
async (_socket) => { await AddMarketSymbolsToChannel(_socket, "trades", marketSymbols); },
407+
async (_socket, symbol, sArray, token) =>
408+
{
409+
var trade = token.ParseTrade("sz", "px", "side", "ts", TimestampType.UnixMilliseconds, "tradeId");
410+
await callback(new KeyValuePair<string, ExchangeTrade>(symbol, trade));
411+
});
412+
}
413+
414+
protected override async Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(Action<ExchangeOrderBook> callback,
415+
int maxCount = 20, params string[] marketSymbols)
416+
{
417+
return await ConnectWebSocketOkexAsync(
418+
async (_socket) =>
419+
{
420+
marketSymbols = await AddMarketSymbolsToChannel(_socket, "books-l2-tbt", marketSymbols);
421+
}, (_socket, symbol, sArray, token) =>
422+
{
423+
ExchangeOrderBook book = token.ParseOrderBookFromJTokenArrays(maxCount: maxCount);
424+
book.MarketSymbol = symbol;
425+
callback(book);
426+
return Task.CompletedTask;
427+
});
428+
}
429+
430+
protected override async Task<IWebSocket> OnGetOrderDetailsWebSocketAsync(Action<ExchangeOrderResult> callback)
431+
{
432+
return await ConnectPrivateWebSocketOkexAsync(async (_socket) =>
433+
{
434+
await WebsocketLogin(_socket);
435+
await SubscribeForOrderChannel(_socket, "orders");
436+
}, (_socket, symbol, sArray, token) =>
437+
{
438+
callback(ParseOrder(token));
439+
return Task.CompletedTask;
440+
});
441+
}
442+
443+
protected override Task<IWebSocket> ConnectWebSocketOkexAsync(Func<IWebSocket, Task> connected,
444+
Func<IWebSocket, string, string[], JToken, Task> callback, int symbolArrayIndex = 3)
445+
{
446+
Timer pingTimer = null;
447+
return ConnectPublicWebSocketAsync(url: "/public", messageCallback: async (_socket, msg) =>
448+
{
449+
var msgString = msg.ToStringFromUTF8();
450+
if (msgString == "pong")
451+
{
452+
// received reply to our ping
453+
return;
454+
}
455+
456+
JToken token = JToken.Parse(msgString);
457+
var eventProperty = token["event"]?.ToStringInvariant();
458+
if (eventProperty != null)
459+
{
460+
switch (eventProperty)
461+
{
462+
case "error":
463+
Logger.Info("Websocket unable to connect: " + token["msg"]?.ToStringInvariant());
464+
return;
465+
case "subscribe" when token["arg"]["channel"] != null:
466+
{
467+
// subscription successful
468+
pingTimer ??= new Timer(callback: async s => await _socket.SendMessageAsync("ping"),
469+
null, 0, 15000);
470+
return;
471+
}
472+
default:
473+
return;
474+
}
475+
}
476+
477+
var marketSymbol = string.Empty;
478+
if (token["arg"] != null)
479+
{
480+
marketSymbol = token["arg"]["instId"].ToStringInvariant();
481+
}
482+
483+
if (token["data"] != null)
484+
{
485+
var data = token["data"];
486+
foreach (var t in data)
487+
{
488+
await callback(_socket, marketSymbol, null, t);
489+
}
490+
}
491+
}, async (_socket) => await connected(_socket)
492+
, s =>
493+
{
494+
pingTimer?.Dispose();
495+
pingTimer = null;
496+
return Task.CompletedTask;
497+
});
498+
}
499+
500+
protected override Task<IWebSocket> ConnectPrivateWebSocketOkexAsync(Func<IWebSocket, Task> connected,
501+
Func<IWebSocket, string, string[], JToken, Task> callback, int symbolArrayIndex = 3)
502+
{
503+
Timer pingTimer = null;
504+
return ConnectPublicWebSocketAsync(url: "/private", messageCallback: async (_socket, msg) =>
505+
{
506+
var msgString = msg.ToStringFromUTF8();
507+
Logger.Debug(msgString);
508+
if (msgString == "pong")
509+
{
510+
// received reply to our ping
511+
return;
512+
}
513+
514+
JToken token = JToken.Parse(msgString);
515+
var eventProperty = token["event"]?.ToStringInvariant();
516+
if (eventProperty != null)
517+
{
518+
switch (eventProperty)
519+
{
520+
case "error":
521+
Logger.Info("Websocket unable to connect: " + token["msg"]?.ToStringInvariant());
522+
return;
523+
case "subscribe" when token["arg"]["channel"] != null:
524+
{
525+
// subscription successful
526+
pingTimer ??= new Timer(callback: async s => await _socket.SendMessageAsync("ping"),
527+
null, 0, 15000);
528+
return;
529+
}
530+
default:
531+
return;
532+
}
533+
}
534+
535+
var marketSymbol = string.Empty;
536+
if (token["arg"] != null)
537+
{
538+
marketSymbol = token["arg"]["instId"].ToStringInvariant();
539+
}
540+
541+
if (token["data"] != null)
542+
{
543+
var data = token["data"];
544+
foreach (var t in data)
545+
{
546+
await callback(_socket, marketSymbol, null, t);
547+
}
548+
}
549+
}, async (_socket) => await connected(_socket)
550+
, s =>
551+
{
552+
pingTimer?.Dispose();
553+
pingTimer = null;
554+
return Task.CompletedTask;
555+
});
556+
}
557+
558+
protected override async Task<string[]> AddMarketSymbolsToChannel(IWebSocket socket, string channelFormat,
559+
string[] marketSymbols)
560+
{
561+
if (marketSymbols.Length == 0)
562+
{
563+
marketSymbols = (await GetMarketSymbolsAsync()).ToArray();
564+
}
565+
566+
await SendMessageAsync(marketSymbols);
567+
568+
async Task SendMessageAsync(IEnumerable<string> symbolsToSend)
569+
{
570+
var args = symbolsToSend
571+
.Select(s => new { channel = channelFormat, instId = s })
572+
.ToArray();
573+
await socket.SendMessageAsync(new { op = "subscribe", args });
574+
}
575+
576+
return marketSymbols;
577+
}
578+
579+
private async Task WebsocketLogin(IWebSocket socket)
580+
{
581+
var timestamp = (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
582+
var auth = new
583+
{
584+
apiKey = PublicApiKey?.ToUnsecureString(),
585+
passphrase = Passphrase?.ToUnsecureString(),
586+
timestamp,
587+
sign = CryptoUtility.SHA256SignBase64($"{timestamp}GET/users/self/verify",
588+
PrivateApiKey?.ToUnsecureBytesUTF8())
589+
};
590+
var args = new List<dynamic> { auth };
591+
var request = new { op = "login", args };
592+
await socket.SendMessageAsync(request);
593+
}
594+
595+
private async Task SubscribeForOrderChannel(IWebSocket socket, string channelFormat)
596+
{
597+
var marketSymbols = (await GetMarketSymbolsAsync()).ToArray();
598+
await SendMessageAsync(marketSymbols);
599+
600+
async Task SendMessageAsync(IEnumerable<string> symbolsToSend)
601+
{
602+
var args = symbolsToSend
603+
.Select(s => new
604+
{
605+
channel = channelFormat, instId = s, uly = GetUly(s),
606+
instType = GetInstrumentType(s).ToUpperInvariant()
607+
})
608+
.ToArray();
609+
await socket.SendMessageAsync(new { op = "subscribe", args });
610+
}
611+
}
612+
613+
private static string GetUly(string marketSymbol)
614+
{
615+
var symbolSplit = marketSymbol.Split('-');
616+
return symbolSplit.Length == 3 ? $"{symbolSplit[0]}-{symbolSplit[1]}" : marketSymbol;
617+
}
618+
382619
private async Task<JToken> GetBalance()
383620
{
384621
return await MakeJsonRequestAsync<JToken>("/account/balance", BaseUrlV5, await GetNoncePayloadAsync());
385622
}
386623

387-
private IEnumerable<ExchangeOrderResult> ParseOrders(JToken token)
388-
=> token.Select(x =>
389-
new ExchangeOrderResult()
624+
private static ExchangeOrderResult ParseOrder(JToken token) =>
625+
new ExchangeOrderResult()
626+
{
627+
OrderId = token["ordId"].Value<string>(),
628+
OrderDate = DateTimeOffset.FromUnixTimeMilliseconds(token["cTime"].Value<long>()).DateTime,
629+
Result = token["state"].Value<string>() switch
390630
{
391-
OrderId = x["ordId"].Value<string>(),
392-
OrderDate = DateTimeOffset.FromUnixTimeMilliseconds(x["cTime"].Value<long>()).DateTime,
393-
Result = x["state"].Value<string>() == "live"
394-
? ExchangeAPIOrderResult.Open
395-
: ExchangeAPIOrderResult.FilledPartially,
396-
IsBuy = x["side"].Value<string>() == "buy",
397-
IsAmountFilledReversed = false,
398-
Amount = x["sz"].Value<decimal>(),
399-
AmountFilled = x["accFillSz"].Value<decimal>(),
400-
AveragePrice = x["avgPx"].Value<string>() == string.Empty ? default : x["avgPx"].Value<decimal>(),
401-
Price = x["px"].Value<decimal>(),
402-
ClientOrderId = x["clOrdId"].Value<string>(),
403-
FeesCurrency = x["feeCcy"].Value<string>(),
404-
MarketSymbol = x["instId"].Value<string>()
405-
});
631+
"canceled" => ExchangeAPIOrderResult.Canceled,
632+
"live" => ExchangeAPIOrderResult.Open,
633+
"partially_filled" => ExchangeAPIOrderResult.FilledPartially,
634+
"filled" => ExchangeAPIOrderResult.Filled,
635+
_ => ExchangeAPIOrderResult.Unknown
636+
},
637+
IsBuy = token["side"].Value<string>() == "buy",
638+
IsAmountFilledReversed = false,
639+
Amount = token["sz"].Value<decimal>(),
640+
AmountFilled = token["accFillSz"].Value<decimal>(),
641+
AveragePrice = token["avgPx"].Value<string>() == string.Empty ? default : token["avgPx"].Value<decimal>(),
642+
Price = token["px"].Value<decimal>(),
643+
ClientOrderId = token["clOrdId"].Value<string>(),
644+
FeesCurrency = token["feeCcy"].Value<string>(),
645+
MarketSymbol = token["instId"].Value<string>()
646+
};
647+
648+
private static IEnumerable<ExchangeOrderResult> ParseOrders(JToken token) => token.Select(ParseOrder);
406649

407650
private async Task<ExchangeTicker> ParseTickerV5Async(JToken t, string symbol)
408651
{

0 commit comments

Comments
 (0)