@@ -16,7 +16,9 @@ The above copyright notice and this permission notice shall be included in all c
16
16
using System . Linq ;
17
17
using System . Security . Cryptography ;
18
18
using System . Text ;
19
+ using System . Threading ;
19
20
using System . Threading . Tasks ;
21
+ using System . Xml ;
20
22
using ExchangeSharp . OKGroup ;
21
23
using Newtonsoft . Json ;
22
24
using Newtonsoft . Json . Linq ;
@@ -28,7 +30,7 @@ public sealed partial class ExchangeOKExAPI : OKGroupCommon
28
30
public override string BaseUrl { get ; set ; } = "https://www.okex.com/api/v1" ;
29
31
public override string BaseUrlV2 { get ; set ; } = "https://www.okex.com/v2/spot" ;
30
32
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 " ;
32
34
public string BaseUrlV5 { get ; set ; } = "https://www.okex.com/api/v5" ;
33
35
protected override bool IsFuturesAndSwapEnabled { get ; } = true ;
34
36
@@ -317,6 +319,7 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
317
319
{
318
320
payload [ "clOrdId" ] = order . ClientOrderId ;
319
321
}
322
+
320
323
payload [ "side" ] = order . IsBuy ? "buy" : "sell" ;
321
324
payload [ "posSide" ] = "net" ;
322
325
payload [ "ordType" ] = order . OrderType switch
@@ -330,7 +333,8 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
330
333
payload [ "sz" ] = order . Amount . ToStringInvariant ( ) ;
331
334
if ( order . OrderType != OrderType . Market )
332
335
{
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" ) ;
334
338
payload [ "px" ] = order . Price . ToStringInvariant ( ) ;
335
339
}
336
340
@@ -379,30 +383,269 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti
379
383
}
380
384
}
381
385
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
+
382
619
private async Task < JToken > GetBalance ( )
383
620
{
384
621
return await MakeJsonRequestAsync < JToken > ( "/account/balance" , BaseUrlV5 , await GetNoncePayloadAsync ( ) ) ;
385
622
}
386
623
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
390
630
{
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 ) ;
406
649
407
650
private async Task < ExchangeTicker > ParseTickerV5Async ( JToken t , string symbol )
408
651
{
0 commit comments