Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving to OKEx V5 API part 3 #679

Merged
merged 8 commits into from
Oct 16, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 223 additions & 26 deletions src/ExchangeSharp/API/Exchanges/OKGroup/ExchangeOKExAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ The above copyright notice and this permission notice shall be included in all c
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.
*/

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using ExchangeSharp.OKGroup;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace ExchangeSharp
Expand All @@ -25,7 +29,7 @@ public sealed partial class ExchangeOKExAPI : OKGroupCommon
public override string BaseUrlV2 { get; set; } = "https://www.okex.com/v2/spot";
public override string BaseUrlV3 { get; set; } = "https://www.okex.com/api";
public override string BaseUrlWebSocket { get; set; } = "wss://real.okex.com:8443/ws/v3";
public string BaseUrlV5 { get; set; } = "https://okex.com/api/v5";
public string BaseUrlV5 { get; set; } = "https://www.okex.com/api/v5";
protected override bool IsFuturesAndSwapEnabled { get; } = true;

public override string PeriodSecondsToString(int seconds)
Expand Down Expand Up @@ -68,33 +72,39 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS
}
*/
var markets = new List<ExchangeMarket>();
parseMarketSymbolTokens(await MakeJsonRequestAsync<JToken>(
ParseMarketSymbolTokens(await MakeJsonRequestAsync<JToken>(
"/public/instruments?instType=SPOT", BaseUrlV5));
if (!IsFuturesAndSwapEnabled)
return markets;
parseMarketSymbolTokens(await MakeJsonRequestAsync<JToken>(
ParseMarketSymbolTokens(await MakeJsonRequestAsync<JToken>(
"/public/instruments?instType=FUTURES", BaseUrlV5));
parseMarketSymbolTokens(await MakeJsonRequestAsync<JToken>(
ParseMarketSymbolTokens(await MakeJsonRequestAsync<JToken>(
"/public/instruments?instType=SWAP", BaseUrlV5));
return markets;

void parseMarketSymbolTokens(JToken allMarketSymbolTokens)
void ParseMarketSymbolTokens(JToken allMarketSymbolTokens)
{
markets.AddRange(from marketSymbolToken in allMarketSymbolTokens
let isSpot = marketSymbolToken["instType"].Value<string>() == "SPOT"
let baseCurrency = isSpot ? marketSymbolToken["baseCcy"].Value<string>() : marketSymbolToken["settleCcy"].Value<string>()
let quoteCurrency = isSpot ? marketSymbolToken["quoteCcy"].Value<string>() : marketSymbolToken["ctValCcy"].Value<string>()
select new ExchangeMarket
{
MarketSymbol = marketSymbolToken["instId"].Value<string>(),
IsActive = marketSymbolToken["state"].Value<string>() == "live",
QuoteCurrency = quoteCurrency,
BaseCurrency = baseCurrency,
PriceStepSize = marketSymbolToken["tickSz"].ConvertInvariant<decimal>(),
MinPrice = marketSymbolToken["tickSz"].ConvertInvariant<decimal>(), // assuming that this is also the min price since it isn't provided explicitly by the exchange
MinTradeSize = marketSymbolToken["minSz"].ConvertInvariant<decimal>(),
QuantityStepSize = marketSymbolToken["lotSz"].ConvertInvariant<decimal>()
});
let isSpot = marketSymbolToken["instType"].Value<string>() == "SPOT"
let baseCurrency = isSpot
? marketSymbolToken["baseCcy"].Value<string>()
: marketSymbolToken["settleCcy"].Value<string>()
let quoteCurrency = isSpot
? marketSymbolToken["quoteCcy"].Value<string>()
: marketSymbolToken["ctValCcy"].Value<string>()
select new ExchangeMarket
{
MarketSymbol = marketSymbolToken["instId"].Value<string>(),
IsActive = marketSymbolToken["state"].Value<string>() == "live",
QuoteCurrency = quoteCurrency,
BaseCurrency = baseCurrency,
PriceStepSize = marketSymbolToken["tickSz"].ConvertInvariant<decimal>(),
MinPrice = marketSymbolToken["tickSz"]
.ConvertInvariant<
decimal>(), // assuming that this is also the min price since it isn't provided explicitly by the exchange
MinTradeSize = marketSymbolToken["minSz"].ConvertInvariant<decimal>(),
QuantityStepSize = marketSymbolToken["lotSz"].ConvertInvariant<decimal>()
});
}
}

Expand All @@ -108,14 +118,14 @@ protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymb
protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>> OnGetTickersAsync()
{
var tickers = new List<KeyValuePair<string, ExchangeTicker>>();
await parseData(await MakeJsonRequestAsync<JToken>("/market/tickers?instType=SPOT", BaseUrlV5));
await ParseData(await MakeJsonRequestAsync<JToken>("/market/tickers?instType=SPOT", BaseUrlV5));
if (!IsFuturesAndSwapEnabled)
return tickers;
await parseData(await MakeJsonRequestAsync<JToken>("/market/tickers?instType=FUTURES", BaseUrlV5));
await parseData(await MakeJsonRequestAsync<JToken>("/market/tickers?instType=SWAP", BaseUrlV5));
await ParseData(await MakeJsonRequestAsync<JToken>("/market/tickers?instType=FUTURES", BaseUrlV5));
await ParseData(await MakeJsonRequestAsync<JToken>("/market/tickers?instType=SWAP", BaseUrlV5));
return tickers;

async Task parseData(JToken tickerResponse)
async Task ParseData(JToken tickerResponse)
{
/*{
"code":"0",
Expand Down Expand Up @@ -167,11 +177,13 @@ protected override async Task<IEnumerable<ExchangeTrade>> OnGetRecentTradesAsync

protected override async Task<ExchangeOrderBook> OnGetOrderBookAsync(string marketSymbol, int maxCount = 100)
{
var token = await MakeJsonRequestAsync<JToken>($"/market/books?instId={marketSymbol}&sz={maxCount}", BaseUrlV5);
var token = await MakeJsonRequestAsync<JToken>($"/market/books?instId={marketSymbol}&sz={maxCount}",
BaseUrlV5);
return token[0].ParseOrderBookFromJTokenArrays(maxCount: maxCount);
}

protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null)
protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(string marketSymbol,
int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null)
{
/*
{
Expand Down Expand Up @@ -203,10 +215,195 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
url += $"&bar={periodString}";
var obj = await MakeJsonRequestAsync<JToken>(url, BaseUrlV5);
foreach (JArray token in obj)
candles.Add(this.ParseCandle(token, marketSymbol, periodSeconds, 1, 2, 3, 4, 0, TimestampType.UnixMilliseconds, 5, 6));
candles.Add(this.ParseCandle(token, marketSymbol, periodSeconds, 1, 2, 3, 4, 0,
TimestampType.UnixMilliseconds, 5, 6));
return candles;
}

protected override async Task<Dictionary<string, decimal>> OnGetAmountsAsync()
{
var token = await GetBalance();
return token[0]["details"]
.Select(x => new { Currency = x["ccy"].Value<string>(), TotalBalance = x["cashBal"].Value<decimal>() })
.ToDictionary(k => k.Currency, v => v.TotalBalance);
}

protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailableToTradeAsync()
{
var token = await GetBalance();
return token[0]["details"]
.Select(x => new
{ Currency = x["ccy"].Value<string>(), AvailableBalance = x["availBal"].Value<decimal>() })
.ToDictionary(k => k.Currency, v => v.AvailableBalance);
}

protected override async Task<Dictionary<string, decimal>> OnGetMarginAmountsAvailableToTradeAsync(
bool includeZeroBalances)
{
var token = await GetBalance();
var availableEquity = token[0]["details"]
.Select(x => new
{
Currency = x["ccy"].Value<string>(),
AvailableEquity = x["availEq"].Value<string>() == string.Empty ? 0 : x["availEq"].Value<decimal>()
})
.ToDictionary(k => k.Currency, v => v.AvailableEquity);

return includeZeroBalances
? availableEquity
: availableEquity
.Where(x => x.Value > 0)
.ToDictionary(k => k.Key, v => v.Value);
}

protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDetailsAsync(string marketSymbol)
{
var token = await MakeJsonRequestAsync<JToken>("/trade/orders-pending", BaseUrlV5,
await GetNoncePayloadAsync());
return ParseOrders(token);
}

protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string orderId,
string marketSymbol, bool isClientOrderId = false)
{
if (string.IsNullOrEmpty(marketSymbol))
{
throw new ArgumentNullException(nameof(marketSymbol),
"Okex single order details request requires symbol");
}

if (string.IsNullOrEmpty(orderId))
{
throw new ArgumentNullException(nameof(orderId),
"Okex single order details request requires order ID or client-supplied order ID");
}

var param = isClientOrderId ? $"clOrdId={orderId}" : $"ordId={orderId}";
var token = await MakeJsonRequestAsync<JToken>($"/trade/order?{param}&instId={marketSymbol}", BaseUrlV5,
await GetNoncePayloadAsync());

return ParseOrders(token).First();
}

protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol)
{
if (string.IsNullOrEmpty(orderId))
{
throw new ArgumentNullException(nameof(orderId), "Okex cancel order request requires order ID");
}

if (string.IsNullOrEmpty(marketSymbol))
{
throw new ArgumentNullException(nameof(marketSymbol), "Okex cancel order request requires symbol");
}

var payload = await GetNoncePayloadAsync();
payload["ordId"] = orderId;
payload["instId"] = marketSymbol;
await MakeJsonRequestAsync<JToken>("/trade/cancel-order", BaseUrlV5, payload, "POST");
}

protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrderRequest order)
{
if (string.IsNullOrEmpty(order.MarketSymbol))
{
throw new ArgumentNullException(nameof(order.MarketSymbol), "Okex place order request requires symbol");
}

var payload = await GetNoncePayloadAsync();
payload["instId"] = order.MarketSymbol;
payload["tdMode"] = order.IsMargin ? "isolated" : "cash";
if (!string.IsNullOrEmpty(order.ClientOrderId))
{
payload["clOrdId"] = order.ClientOrderId;
}
payload["side"] = order.IsBuy ? "buy" : "sell";
payload["posSide"] = "net";
payload["ordType"] = order.OrderType switch
{
OrderType.Limit => "limit",
OrderType.Market => "market",
OrderType.Stop => throw new ArgumentException("Okex does not support stop order",
nameof(order.OrderType)),
_ => throw new ArgumentOutOfRangeException(nameof(order.OrderType), "Invalid order type.")
};
payload["sz"] = order.Amount.ToStringInvariant();
if (order.OrderType != OrderType.Market)
{
if (!order.Price.HasValue) throw new ArgumentNullException(nameof(order.Price), "Okex place order request requires price");
payload["px"] = order.Price.ToStringInvariant();
}

var token = await MakeJsonRequestAsync<JToken>("/trade/order", BaseUrlV5, payload, "POST");
return new ExchangeOrderResult()
{
MarketSymbol = order.MarketSymbol,
Amount = order.Amount,
Price = order.Price,
OrderDate = DateTime.UtcNow,
OrderId = token[0]["ordId"].Value<string>(),
ClientOrderId = token[0]["clOrdId"].Value<string>(),
Result = ExchangeAPIOrderResult.Open,
IsBuy = order.IsBuy
};
}

protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary<string, object> payload)
{
if (!CanMakeAuthenticatedRequest(payload)) return;
// We don't need nonce in the request. Using it only to not break CanMakeAuthenticatedRequest.
payload.Remove("nonce");

var method = request.Method;
var now = DateTime.Now;
var timeStamp = TimeZoneInfo.ConvertTimeToUtc(now).ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
var requestUrl = request.RequestUri.PathAndQuery;
var body = payload.Any() ? JsonConvert.SerializeObject(payload) : string.Empty;

var sign = string.IsNullOrEmpty(body)
? CryptoUtility.SHA256SignBase64($"{timeStamp}{method}{requestUrl}",
PrivateApiKey!.ToUnsecureString().ToBytesUTF8())
: CryptoUtility.SHA256SignBase64($"{timeStamp}{method}{requestUrl}{body}",
PrivateApiKey!.ToUnsecureString().ToBytesUTF8());

request.AddHeader("OK-ACCESS-KEY", PublicApiKey!.ToUnsecureString());
request.AddHeader("OK-ACCESS-SIGN", sign);
request.AddHeader("OK-ACCESS-TIMESTAMP", timeStamp);
request.AddHeader("OK-ACCESS-PASSPHRASE", Passphrase!.ToUnsecureString());
request.AddHeader("x-simulated-trading", "0");
request.AddHeader("content-type", "application/json");

if (request.Method == "POST")
{
await request.WritePayloadJsonToRequestAsync(payload);
}
}

private async Task<JToken> GetBalance()
{
return await MakeJsonRequestAsync<JToken>("/account/balance", BaseUrlV5, await GetNoncePayloadAsync());
}

private IEnumerable<ExchangeOrderResult> ParseOrders(JToken token)
=> token.Select(x =>
new ExchangeOrderResult()
{
OrderId = x["ordId"].Value<string>(),
OrderDate = DateTimeOffset.FromUnixTimeMilliseconds(x["cTime"].Value<long>()).DateTime,
Result = x["state"].Value<string>() == "live"
? ExchangeAPIOrderResult.Open
: ExchangeAPIOrderResult.FilledPartially,
IsBuy = x["side"].Value<string>() == "buy",
IsAmountFilledReversed = false,
Amount = x["sz"].Value<decimal>(),
AmountFilled = x["accFillSz"].Value<decimal>(),
AveragePrice = x["avgPx"].Value<string>() == string.Empty ? default : x["avgPx"].Value<decimal>(),
Price = x["px"].Value<decimal>(),
ClientOrderId = x["clOrdId"].Value<string>(),
FeesCurrency = x["feeCcy"].Value<string>(),
MarketSymbol = x["instId"].Value<string>()
});

private async Task<ExchangeTicker> ParseTickerV5Async(JToken t, string symbol)
{
return await this.ParseTickerAsync(
Expand Down