Skip to content

Commit 1b1a0a8

Browse files
authored
Moving to OKEx V5 API part 3 (#679)
* Update OnGetAmountsAsync Migration to V5 API * Update OnGetAmountsAvailableToTradeAsync Migration to V5 API * Update OnGetMarginAmountsAvailableToTradeAsync Migration to V5 API * Update OnGetOpenOrderDetailsAsync Migration to V5 API * Update OnGetOrderDetailsAsync Migration to V5 API * Update OnCancelOrderAsync Migration to V5 API * Cleanup * Update OnPlaceOrderAsync Migration to V5 API
1 parent 2acf765 commit 1b1a0a8

File tree

1 file changed

+223
-26
lines changed

1 file changed

+223
-26
lines changed

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

Lines changed: 223 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ The above copyright notice and this permission notice shall be included in all c
1010
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.
1111
*/
1212

13+
#nullable enable
1314
using System;
1415
using System.Collections.Generic;
1516
using System.Linq;
17+
using System.Security.Cryptography;
18+
using System.Text;
1619
using System.Threading.Tasks;
1720
using ExchangeSharp.OKGroup;
21+
using Newtonsoft.Json;
1822
using Newtonsoft.Json.Linq;
1923

2024
namespace 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

Comments
 (0)