@@ -16,7 +16,9 @@ The above copyright notice and this permission notice shall be included in all c
1616using System . Linq ;
1717using System . Security . Cryptography ;
1818using System . Text ;
19+ using System . Threading ;
1920using System . Threading . Tasks ;
21+ using System . Xml ;
2022using ExchangeSharp . OKGroup ;
2123using Newtonsoft . Json ;
2224using 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