@@ -10,11 +10,15 @@ The above copyright notice and this permission notice shall be included in all c
10
10
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11
11
*/
12
12
13
+ #nullable enable
13
14
using System ;
14
15
using System . Collections . Generic ;
15
16
using System . Linq ;
17
+ using System . Security . Cryptography ;
18
+ using System . Text ;
16
19
using System . Threading . Tasks ;
17
20
using ExchangeSharp . OKGroup ;
21
+ using Newtonsoft . Json ;
18
22
using Newtonsoft . Json . Linq ;
19
23
20
24
namespace ExchangeSharp
@@ -25,7 +29,7 @@ public sealed partial class ExchangeOKExAPI : OKGroupCommon
25
29
public override string BaseUrlV2 { get ; set ; } = "https://www.okex.com/v2/spot" ;
26
30
public override string BaseUrlV3 { get ; set ; } = "https://www.okex.com/api" ;
27
31
public override string BaseUrlWebSocket { get ; set ; } = "wss://real.okex.com:8443/ws/v3" ;
28
- public string BaseUrlV5 { get ; set ; } = "https://okex.com/api/v5" ;
32
+ public string BaseUrlV5 { get ; set ; } = "https://www. okex.com/api/v5" ;
29
33
protected override bool IsFuturesAndSwapEnabled { get ; } = true ;
30
34
31
35
public override string PeriodSecondsToString ( int seconds )
@@ -68,33 +72,39 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS
68
72
}
69
73
*/
70
74
var markets = new List < ExchangeMarket > ( ) ;
71
- parseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
75
+ ParseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
72
76
"/public/instruments?instType=SPOT" , BaseUrlV5 ) ) ;
73
77
if ( ! IsFuturesAndSwapEnabled )
74
78
return markets ;
75
- parseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
79
+ ParseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
76
80
"/public/instruments?instType=FUTURES" , BaseUrlV5 ) ) ;
77
- parseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
81
+ ParseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
78
82
"/public/instruments?instType=SWAP" , BaseUrlV5 ) ) ;
79
83
return markets ;
80
84
81
- void parseMarketSymbolTokens ( JToken allMarketSymbolTokens )
85
+ void ParseMarketSymbolTokens ( JToken allMarketSymbolTokens )
82
86
{
83
87
markets . AddRange ( from marketSymbolToken in allMarketSymbolTokens
84
- let isSpot = marketSymbolToken [ "instType" ] . Value < string > ( ) == "SPOT"
85
- let baseCurrency = isSpot ? marketSymbolToken [ "baseCcy" ] . Value < string > ( ) : marketSymbolToken [ "settleCcy" ] . Value < string > ( )
86
- let quoteCurrency = isSpot ? marketSymbolToken [ "quoteCcy" ] . Value < string > ( ) : marketSymbolToken [ "ctValCcy" ] . Value < string > ( )
87
- select new ExchangeMarket
88
- {
89
- MarketSymbol = marketSymbolToken [ "instId" ] . Value < string > ( ) ,
90
- IsActive = marketSymbolToken [ "state" ] . Value < string > ( ) == "live" ,
91
- QuoteCurrency = quoteCurrency ,
92
- BaseCurrency = baseCurrency ,
93
- PriceStepSize = marketSymbolToken [ "tickSz" ] . ConvertInvariant < decimal > ( ) ,
94
- MinPrice = marketSymbolToken [ "tickSz" ] . ConvertInvariant < decimal > ( ) , // assuming that this is also the min price since it isn't provided explicitly by the exchange
95
- MinTradeSize = marketSymbolToken [ "minSz" ] . ConvertInvariant < decimal > ( ) ,
96
- QuantityStepSize = marketSymbolToken [ "lotSz" ] . ConvertInvariant < decimal > ( )
97
- } ) ;
88
+ let isSpot = marketSymbolToken [ "instType" ] . Value < string > ( ) == "SPOT"
89
+ let baseCurrency = isSpot
90
+ ? marketSymbolToken [ "baseCcy" ] . Value < string > ( )
91
+ : marketSymbolToken [ "settleCcy" ] . Value < string > ( )
92
+ let quoteCurrency = isSpot
93
+ ? marketSymbolToken [ "quoteCcy" ] . Value < string > ( )
94
+ : marketSymbolToken [ "ctValCcy" ] . Value < string > ( )
95
+ select new ExchangeMarket
96
+ {
97
+ MarketSymbol = marketSymbolToken [ "instId" ] . Value < string > ( ) ,
98
+ IsActive = marketSymbolToken [ "state" ] . Value < string > ( ) == "live" ,
99
+ QuoteCurrency = quoteCurrency ,
100
+ BaseCurrency = baseCurrency ,
101
+ PriceStepSize = marketSymbolToken [ "tickSz" ] . ConvertInvariant < decimal > ( ) ,
102
+ MinPrice = marketSymbolToken [ "tickSz" ]
103
+ . ConvertInvariant <
104
+ decimal > ( ) , // assuming that this is also the min price since it isn't provided explicitly by the exchange
105
+ MinTradeSize = marketSymbolToken [ "minSz" ] . ConvertInvariant < decimal > ( ) ,
106
+ QuantityStepSize = marketSymbolToken [ "lotSz" ] . ConvertInvariant < decimal > ( )
107
+ } ) ;
98
108
}
99
109
}
100
110
@@ -108,14 +118,14 @@ protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymb
108
118
protected override async Task < IEnumerable < KeyValuePair < string , ExchangeTicker > > > OnGetTickersAsync ( )
109
119
{
110
120
var tickers = new List < KeyValuePair < string , ExchangeTicker > > ( ) ;
111
- await parseData ( await MakeJsonRequestAsync < JToken > ( "/market/tickers?instType=SPOT" , BaseUrlV5 ) ) ;
121
+ await ParseData ( await MakeJsonRequestAsync < JToken > ( "/market/tickers?instType=SPOT" , BaseUrlV5 ) ) ;
112
122
if ( ! IsFuturesAndSwapEnabled )
113
123
return tickers ;
114
- await parseData ( await MakeJsonRequestAsync < JToken > ( "/market/tickers?instType=FUTURES" , BaseUrlV5 ) ) ;
115
- await parseData ( await MakeJsonRequestAsync < JToken > ( "/market/tickers?instType=SWAP" , BaseUrlV5 ) ) ;
124
+ await ParseData ( await MakeJsonRequestAsync < JToken > ( "/market/tickers?instType=FUTURES" , BaseUrlV5 ) ) ;
125
+ await ParseData ( await MakeJsonRequestAsync < JToken > ( "/market/tickers?instType=SWAP" , BaseUrlV5 ) ) ;
116
126
return tickers ;
117
127
118
- async Task parseData ( JToken tickerResponse )
128
+ async Task ParseData ( JToken tickerResponse )
119
129
{
120
130
/*{
121
131
"code":"0",
@@ -167,11 +177,13 @@ protected override async Task<IEnumerable<ExchangeTrade>> OnGetRecentTradesAsync
167
177
168
178
protected override async Task < ExchangeOrderBook > OnGetOrderBookAsync ( string marketSymbol , int maxCount = 100 )
169
179
{
170
- var token = await MakeJsonRequestAsync < JToken > ( $ "/market/books?instId={ marketSymbol } &sz={ maxCount } ", BaseUrlV5 ) ;
180
+ var token = await MakeJsonRequestAsync < JToken > ( $ "/market/books?instId={ marketSymbol } &sz={ maxCount } ",
181
+ BaseUrlV5 ) ;
171
182
return token [ 0 ] . ParseOrderBookFromJTokenArrays ( maxCount : maxCount ) ;
172
183
}
173
184
174
- protected override async Task < IEnumerable < MarketCandle > > OnGetCandlesAsync ( string marketSymbol , int periodSeconds , DateTime ? startDate = null , DateTime ? endDate = null , int ? limit = null )
185
+ protected override async Task < IEnumerable < MarketCandle > > OnGetCandlesAsync ( string marketSymbol ,
186
+ int periodSeconds , DateTime ? startDate = null , DateTime ? endDate = null , int ? limit = null )
175
187
{
176
188
/*
177
189
{
@@ -203,10 +215,195 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
203
215
url += $ "&bar={ periodString } ";
204
216
var obj = await MakeJsonRequestAsync < JToken > ( url , BaseUrlV5 ) ;
205
217
foreach ( JArray token in obj )
206
- candles . Add ( this . ParseCandle ( token , marketSymbol , periodSeconds , 1 , 2 , 3 , 4 , 0 , TimestampType . UnixMilliseconds , 5 , 6 ) ) ;
218
+ candles . Add ( this . ParseCandle ( token , marketSymbol , periodSeconds , 1 , 2 , 3 , 4 , 0 ,
219
+ TimestampType . UnixMilliseconds , 5 , 6 ) ) ;
207
220
return candles ;
208
221
}
209
222
223
+ protected override async Task < Dictionary < string , decimal > > OnGetAmountsAsync ( )
224
+ {
225
+ var token = await GetBalance ( ) ;
226
+ return token [ 0 ] [ "details" ]
227
+ . Select ( x => new { Currency = x [ "ccy" ] . Value < string > ( ) , TotalBalance = x [ "cashBal" ] . Value < decimal > ( ) } )
228
+ . ToDictionary ( k => k . Currency , v => v . TotalBalance ) ;
229
+ }
230
+
231
+ protected override async Task < Dictionary < string , decimal > > OnGetAmountsAvailableToTradeAsync ( )
232
+ {
233
+ var token = await GetBalance ( ) ;
234
+ return token [ 0 ] [ "details" ]
235
+ . Select ( x => new
236
+ { Currency = x [ "ccy" ] . Value < string > ( ) , AvailableBalance = x [ "availBal" ] . Value < decimal > ( ) } )
237
+ . ToDictionary ( k => k . Currency , v => v . AvailableBalance ) ;
238
+ }
239
+
240
+ protected override async Task < Dictionary < string , decimal > > OnGetMarginAmountsAvailableToTradeAsync (
241
+ bool includeZeroBalances )
242
+ {
243
+ var token = await GetBalance ( ) ;
244
+ var availableEquity = token [ 0 ] [ "details" ]
245
+ . Select ( x => new
246
+ {
247
+ Currency = x [ "ccy" ] . Value < string > ( ) ,
248
+ AvailableEquity = x [ "availEq" ] . Value < string > ( ) == string . Empty ? 0 : x [ "availEq" ] . Value < decimal > ( )
249
+ } )
250
+ . ToDictionary ( k => k . Currency , v => v . AvailableEquity ) ;
251
+
252
+ return includeZeroBalances
253
+ ? availableEquity
254
+ : availableEquity
255
+ . Where ( x => x . Value > 0 )
256
+ . ToDictionary ( k => k . Key , v => v . Value ) ;
257
+ }
258
+
259
+ protected override async Task < IEnumerable < ExchangeOrderResult > > OnGetOpenOrderDetailsAsync ( string marketSymbol )
260
+ {
261
+ var token = await MakeJsonRequestAsync < JToken > ( "/trade/orders-pending" , BaseUrlV5 ,
262
+ await GetNoncePayloadAsync ( ) ) ;
263
+ return ParseOrders ( token ) ;
264
+ }
265
+
266
+ protected override async Task < ExchangeOrderResult > OnGetOrderDetailsAsync ( string orderId ,
267
+ string marketSymbol , bool isClientOrderId = false )
268
+ {
269
+ if ( string . IsNullOrEmpty ( marketSymbol ) )
270
+ {
271
+ throw new ArgumentNullException ( nameof ( marketSymbol ) ,
272
+ "Okex single order details request requires symbol" ) ;
273
+ }
274
+
275
+ if ( string . IsNullOrEmpty ( orderId ) )
276
+ {
277
+ throw new ArgumentNullException ( nameof ( orderId ) ,
278
+ "Okex single order details request requires order ID or client-supplied order ID" ) ;
279
+ }
280
+
281
+ var param = isClientOrderId ? $ "clOrdId={ orderId } " : $ "ordId={ orderId } ";
282
+ var token = await MakeJsonRequestAsync < JToken > ( $ "/trade/order?{ param } &instId={ marketSymbol } ", BaseUrlV5 ,
283
+ await GetNoncePayloadAsync ( ) ) ;
284
+
285
+ return ParseOrders ( token ) . First ( ) ;
286
+ }
287
+
288
+ protected override async Task OnCancelOrderAsync ( string orderId , string marketSymbol )
289
+ {
290
+ if ( string . IsNullOrEmpty ( orderId ) )
291
+ {
292
+ throw new ArgumentNullException ( nameof ( orderId ) , "Okex cancel order request requires order ID" ) ;
293
+ }
294
+
295
+ if ( string . IsNullOrEmpty ( marketSymbol ) )
296
+ {
297
+ throw new ArgumentNullException ( nameof ( marketSymbol ) , "Okex cancel order request requires symbol" ) ;
298
+ }
299
+
300
+ var payload = await GetNoncePayloadAsync ( ) ;
301
+ payload [ "ordId" ] = orderId ;
302
+ payload [ "instId" ] = marketSymbol ;
303
+ await MakeJsonRequestAsync < JToken > ( "/trade/cancel-order" , BaseUrlV5 , payload , "POST" ) ;
304
+ }
305
+
306
+ protected override async Task < ExchangeOrderResult > OnPlaceOrderAsync ( ExchangeOrderRequest order )
307
+ {
308
+ if ( string . IsNullOrEmpty ( order . MarketSymbol ) )
309
+ {
310
+ throw new ArgumentNullException ( nameof ( order . MarketSymbol ) , "Okex place order request requires symbol" ) ;
311
+ }
312
+
313
+ var payload = await GetNoncePayloadAsync ( ) ;
314
+ payload [ "instId" ] = order . MarketSymbol ;
315
+ payload [ "tdMode" ] = order . IsMargin ? "isolated" : "cash" ;
316
+ if ( ! string . IsNullOrEmpty ( order . ClientOrderId ) )
317
+ {
318
+ payload [ "clOrdId" ] = order . ClientOrderId ;
319
+ }
320
+ payload [ "side" ] = order . IsBuy ? "buy" : "sell" ;
321
+ payload [ "posSide" ] = "net" ;
322
+ payload [ "ordType" ] = order . OrderType switch
323
+ {
324
+ OrderType . Limit => "limit" ,
325
+ OrderType . Market => "market" ,
326
+ OrderType . Stop => throw new ArgumentException ( "Okex does not support stop order" ,
327
+ nameof ( order . OrderType ) ) ,
328
+ _ => throw new ArgumentOutOfRangeException ( nameof ( order . OrderType ) , "Invalid order type." )
329
+ } ;
330
+ payload [ "sz" ] = order . Amount . ToStringInvariant ( ) ;
331
+ if ( order . OrderType != OrderType . Market )
332
+ {
333
+ if ( ! order . Price . HasValue ) throw new ArgumentNullException ( nameof ( order . Price ) , "Okex place order request requires price" ) ;
334
+ payload [ "px" ] = order . Price . ToStringInvariant ( ) ;
335
+ }
336
+
337
+ var token = await MakeJsonRequestAsync < JToken > ( "/trade/order" , BaseUrlV5 , payload , "POST" ) ;
338
+ return new ExchangeOrderResult ( )
339
+ {
340
+ MarketSymbol = order . MarketSymbol ,
341
+ Amount = order . Amount ,
342
+ Price = order . Price ,
343
+ OrderDate = DateTime . UtcNow ,
344
+ OrderId = token [ 0 ] [ "ordId" ] . Value < string > ( ) ,
345
+ ClientOrderId = token [ 0 ] [ "clOrdId" ] . Value < string > ( ) ,
346
+ Result = ExchangeAPIOrderResult . Open ,
347
+ IsBuy = order . IsBuy
348
+ } ;
349
+ }
350
+
351
+ protected override async Task ProcessRequestAsync ( IHttpWebRequest request , Dictionary < string , object > payload )
352
+ {
353
+ if ( ! CanMakeAuthenticatedRequest ( payload ) ) return ;
354
+ // We don't need nonce in the request. Using it only to not break CanMakeAuthenticatedRequest.
355
+ payload . Remove ( "nonce" ) ;
356
+
357
+ var method = request . Method ;
358
+ var now = DateTime . Now ;
359
+ var timeStamp = TimeZoneInfo . ConvertTimeToUtc ( now ) . ToString ( "yyyy-MM-ddTHH:mm:ss.fffZ" ) ;
360
+ var requestUrl = request . RequestUri . PathAndQuery ;
361
+ var body = payload . Any ( ) ? JsonConvert . SerializeObject ( payload ) : string . Empty ;
362
+
363
+ var sign = string . IsNullOrEmpty ( body )
364
+ ? CryptoUtility . SHA256SignBase64 ( $ "{ timeStamp } { method } { requestUrl } ",
365
+ PrivateApiKey ! . ToUnsecureString ( ) . ToBytesUTF8 ( ) )
366
+ : CryptoUtility . SHA256SignBase64 ( $ "{ timeStamp } { method } { requestUrl } { body } ",
367
+ PrivateApiKey ! . ToUnsecureString ( ) . ToBytesUTF8 ( ) ) ;
368
+
369
+ request . AddHeader ( "OK-ACCESS-KEY" , PublicApiKey ! . ToUnsecureString ( ) ) ;
370
+ request . AddHeader ( "OK-ACCESS-SIGN" , sign ) ;
371
+ request . AddHeader ( "OK-ACCESS-TIMESTAMP" , timeStamp ) ;
372
+ request . AddHeader ( "OK-ACCESS-PASSPHRASE" , Passphrase ! . ToUnsecureString ( ) ) ;
373
+ request . AddHeader ( "x-simulated-trading" , "0" ) ;
374
+ request . AddHeader ( "content-type" , "application/json" ) ;
375
+
376
+ if ( request . Method == "POST" )
377
+ {
378
+ await request . WritePayloadJsonToRequestAsync ( payload ) ;
379
+ }
380
+ }
381
+
382
+ private async Task < JToken > GetBalance ( )
383
+ {
384
+ return await MakeJsonRequestAsync < JToken > ( "/account/balance" , BaseUrlV5 , await GetNoncePayloadAsync ( ) ) ;
385
+ }
386
+
387
+ private IEnumerable < ExchangeOrderResult > ParseOrders ( JToken token )
388
+ => token . Select ( x =>
389
+ new ExchangeOrderResult ( )
390
+ {
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
+ } ) ;
406
+
210
407
private async Task < ExchangeTicker > ParseTickerV5Async ( JToken t , string symbol )
211
408
{
212
409
return await this . ParseTickerAsync (
0 commit comments