@@ -10,11 +10,15 @@ The above copyright notice and this permission notice shall be included in all c
1010THE 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.
1111*/
1212
13+ #nullable enable
1314using System ;
1415using System . Collections . Generic ;
1516using System . Linq ;
17+ using System . Security . Cryptography ;
18+ using System . Text ;
1619using System . Threading . Tasks ;
1720using ExchangeSharp . OKGroup ;
21+ using Newtonsoft . Json ;
1822using Newtonsoft . Json . Linq ;
1923
2024namespace ExchangeSharp
@@ -25,7 +29,7 @@ public sealed partial class ExchangeOKExAPI : OKGroupCommon
2529 public override string BaseUrlV2 { get ; set ; } = "https://www.okex.com/v2/spot" ;
2630 public override string BaseUrlV3 { get ; set ; } = "https://www.okex.com/api" ;
2731 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" ;
2933 protected override bool IsFuturesAndSwapEnabled { get ; } = true ;
3034
3135 public override string PeriodSecondsToString ( int seconds )
@@ -68,33 +72,39 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS
6872 }
6973 */
7074 var markets = new List < ExchangeMarket > ( ) ;
71- parseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
75+ ParseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
7276 "/public/instruments?instType=SPOT" , BaseUrlV5 ) ) ;
7377 if ( ! IsFuturesAndSwapEnabled )
7478 return markets ;
75- parseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
79+ ParseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
7680 "/public/instruments?instType=FUTURES" , BaseUrlV5 ) ) ;
77- parseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
81+ ParseMarketSymbolTokens ( await MakeJsonRequestAsync < JToken > (
7882 "/public/instruments?instType=SWAP" , BaseUrlV5 ) ) ;
7983 return markets ;
8084
81- void parseMarketSymbolTokens ( JToken allMarketSymbolTokens )
85+ void ParseMarketSymbolTokens ( JToken allMarketSymbolTokens )
8286 {
8387 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+ } ) ;
98108 }
99109 }
100110
@@ -108,14 +118,14 @@ protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymb
108118 protected override async Task < IEnumerable < KeyValuePair < string , ExchangeTicker > > > OnGetTickersAsync ( )
109119 {
110120 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 ) ) ;
112122 if ( ! IsFuturesAndSwapEnabled )
113123 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 ) ) ;
116126 return tickers ;
117127
118- async Task parseData ( JToken tickerResponse )
128+ async Task ParseData ( JToken tickerResponse )
119129 {
120130 /*{
121131 "code":"0",
@@ -167,11 +177,13 @@ protected override async Task<IEnumerable<ExchangeTrade>> OnGetRecentTradesAsync
167177
168178 protected override async Task < ExchangeOrderBook > OnGetOrderBookAsync ( string marketSymbol , int maxCount = 100 )
169179 {
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 ) ;
171182 return token [ 0 ] . ParseOrderBookFromJTokenArrays ( maxCount : maxCount ) ;
172183 }
173184
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 )
175187 {
176188 /*
177189 {
@@ -203,10 +215,195 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
203215 url += $ "&bar={ periodString } ";
204216 var obj = await MakeJsonRequestAsync < JToken > ( url , BaseUrlV5 ) ;
205217 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 ) ) ;
207220 return candles ;
208221 }
209222
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+
210407 private async Task < ExchangeTicker > ParseTickerV5Async ( JToken t , string symbol )
211408 {
212409 return await this . ParseTickerAsync (
0 commit comments