diff --git a/Binance.Net.UnitTests/TestImplementations/BinanceRestApiClient.cs b/Binance.Net.UnitTests/TestImplementations/BinanceRestApiClient.cs
index 74859e5c5..258566df2 100644
--- a/Binance.Net.UnitTests/TestImplementations/BinanceRestApiClient.cs
+++ b/Binance.Net.UnitTests/TestImplementations/BinanceRestApiClient.cs
@@ -2,6 +2,7 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects;
+using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using System;
@@ -14,7 +15,7 @@ public BinanceRestApiClient(ILogger logger, BinanceRestOptions options, BinanceR
}
///
- public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
+ public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
diff --git a/Binance.Net.UnitTests/TestImplementations/TestSocket.cs b/Binance.Net.UnitTests/TestImplementations/TestSocket.cs
index b803555ac..60e2f82c0 100644
--- a/Binance.Net.UnitTests/TestImplementations/TestSocket.cs
+++ b/Binance.Net.UnitTests/TestImplementations/TestSocket.cs
@@ -20,6 +20,7 @@ public class TestSocket: IWebsocket
public event Func OnReconnected;
public event Func OnReconnecting;
public event Func OnRequestRateLimited;
+ public event Func OnConnectRateLimited;
public event Func OnError;
#pragma warning restore 0067
public event Func OnRequestSent;
diff --git a/Binance.Net/Binance.Net.csproj b/Binance.Net/Binance.Net.csproj
index 958a51388..97a1a3e8b 100644
--- a/Binance.Net/Binance.Net.csproj
+++ b/Binance.Net/Binance.Net.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.0;netstandard2.1
10.0
@@ -31,7 +31,7 @@
true
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -48,10 +48,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
\ No newline at end of file
diff --git a/Binance.Net/Binance.Net.xml b/Binance.Net/Binance.Net.xml
index de91990c8..9ca9d8b05 100644
--- a/Binance.Net/Binance.Net.xml
+++ b/Binance.Net/Binance.Net.xml
@@ -265,7 +265,7 @@
-
+
@@ -469,7 +469,7 @@
-
+
@@ -628,7 +628,7 @@
-
+
@@ -1133,7 +1133,7 @@
-
+
@@ -1673,7 +1673,7 @@
-
+
@@ -1908,7 +1908,7 @@
Event triggered when an order is canceled via this client. Note that this does not trigger when using CancelAllOrdersAsync. Only available for Spot orders
-
+
@@ -2224,7 +2224,7 @@
-
+
@@ -5494,9 +5494,13 @@
- Get the IFuturesClient for this client. This is a common interface which allows for some basic operations without knowing any details of the exchange.
+ DEPRECATED; use instead for common/shared functionality. See for more info.
+
+
+
+
+ Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges.
-
@@ -5981,6 +5985,11 @@
Cancellation token
List of prices
+
+
+ Shared interface for COIN-M Futures rest API usage
+
+
Binance COIN-M futures trading endpoints, placing and mananging orders.
@@ -6149,6 +6158,11 @@
Binance Coin futures streams
+
+
+ Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges.
+
+
Subscribes to the aggregated trades update stream for the provided symbol
@@ -6539,6 +6553,11 @@
Cancellation token for closing this subscription
A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected
+
+
+ Shared interface for COIN-M Futures socket API usage
+
+
Binance general API endpoints
@@ -8479,9 +8498,13 @@
- Get the ISpotClient for this client. This is a common interface which allows for some basic operations without knowing any details of the exchange.
+ DEPRECATED; use instead for common/shared functionality. See for more info.
+
+
+
+
+ Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges.
-
@@ -9745,6 +9768,11 @@
Cancellation token
+
+
+ Shared interface for Spot rest API usage
+
+
Binance Spot trading endpoints, placing and mananging orders.
@@ -10479,6 +10507,11 @@
Trading data and queries
+
+
+ Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges.
+
+
Binance Spot Account socket requests and subscriptions
@@ -10949,6 +10982,11 @@
Cancellation token for closing this subscription
A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected
+
+
+ Shared interface for Spot socket API usage
+
+
Binance Spot Trading socket requests
@@ -11227,9 +11265,13 @@
- Get the IFuturesClient for this client. This is a common interface which allows for some basic operations without knowing any details of the exchange.
+ DEPRECATED; use instead for common/shared functionality. See for more info.
+
+
+
+
+ Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges.
-
@@ -11897,6 +11939,11 @@
To asset
Cancellation token
+
+
+ Shared interface for USD-M Futures rest API usage
+
+
Binance USD-M futures trading endpoints, placing and mananging orders.
@@ -12225,6 +12272,11 @@
Binance USD futures streams
+
+
+ Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges.
+
+
Subscribes to the aggregated trades update stream for the provided symbol
@@ -12582,6 +12634,11 @@
Cancellation token for closing this subscription
+
+
+ Shared interface for USD-M Futures socket API usage
+
+
24 hour price stats
diff --git a/Binance.Net/Clients/CoinFuturesApi/BinanceRestClientCoinFuturesApi.cs b/Binance.Net/Clients/CoinFuturesApi/BinanceRestClientCoinFuturesApi.cs
index 1335faf7c..36b164cc7 100644
--- a/Binance.Net/Clients/CoinFuturesApi/BinanceRestClientCoinFuturesApi.cs
+++ b/Binance.Net/Clients/CoinFuturesApi/BinanceRestClientCoinFuturesApi.cs
@@ -9,11 +9,12 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.RateLimiting.Interfaces;
+using CryptoExchange.Net.SharedApis;
namespace Binance.Net.Clients.CoinFuturesApi
{
///
- internal class BinanceRestClientCoinFuturesApi : RestApiClient, IBinanceRestClientCoinFuturesApi, IFuturesClient
+ internal partial class BinanceRestClientCoinFuturesApi : RestApiClient, IBinanceRestClientCoinFuturesApi, IFuturesClient
{
#region fields
///
@@ -75,7 +76,10 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer();
///
- public override string FormatSymbol(string baseAsset, string quoteAsset) => baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant();
+ public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null)
+ {
+ return baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant() + (deliverTime == null ? "_PERP" : "_" + deliverTime.Value.ToString("yyMMdd"));
+ }
internal Uri GetUrl(string endpoint, string api, string? version = null)
{
@@ -256,6 +260,7 @@ protected override Task> GetServerTimestampAsync()
///
public IFuturesClient CommonFuturesClient => this;
+ public IBinanceRestClientCoinFuturesApiShared SharedClient => this;
///
public string GetSymbolName(string baseAsset, string quoteAsset) =>
diff --git a/Binance.Net/Clients/CoinFuturesApi/BinanceRestClientCoinFuturesApiShared.cs b/Binance.Net/Clients/CoinFuturesApi/BinanceRestClientCoinFuturesApiShared.cs
new file mode 100644
index 000000000..8a562cc61
--- /dev/null
+++ b/Binance.Net/Clients/CoinFuturesApi/BinanceRestClientCoinFuturesApiShared.cs
@@ -0,0 +1,895 @@
+using Binance.Net.Interfaces.Clients.CoinFuturesApi;
+using Binance.Net.Enums;
+using CryptoExchange.Net.SharedApis;
+using System.Linq.Expressions;
+
+namespace Binance.Net.Clients.CoinFuturesApi
+{
+ internal partial class BinanceRestClientCoinFuturesApi : IBinanceRestClientCoinFuturesApiShared
+ {
+ public string Exchange => BinanceExchange.ExchangeName;
+
+ public TradingMode[] SupportedTradingModes => new[] { TradingMode.DeliveryInverse, TradingMode.PerpetualInverse };
+
+ public void SetDefaultExchangeParameter(string key, object value) => ExchangeParameters.SetStaticParameter(Exchange, key, value);
+ public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticParameters();
+
+ #region Klines client
+
+ GetKlinesOptions IKlineRestClient.GetKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false)
+ {
+ MaxRequestDataPoints = 1000
+ };
+
+ async Task>> IKlineRestClient.GetKlinesAsync(GetKlinesRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var interval = (Enums.KlineInterval)request.Interval;
+ if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval))
+ return new ExchangeWebResult>(Exchange, new ArgumentError("Interval not supported"));
+
+ var validationError = ((IKlineRestClient)this).GetKlinesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ // Determine pagination
+ // Data is normally returned oldest first, so to do newest first pagination we have to do some calc
+ DateTime endTime = request.EndTime ?? DateTime.UtcNow;
+ DateTime? startTime = request.StartTime;
+ if (pageToken is DateTimeToken dateTimeToken)
+ endTime = dateTimeToken.LastTime;
+
+ var limit = request.Limit ?? 1000;
+ if (startTime == null || startTime < endTime)
+ {
+ var offset = (int)interval * limit;
+ startTime = endTime.AddSeconds(-offset);
+ }
+
+ if (startTime < request.StartTime)
+ startTime = request.StartTime;
+
+ var result = await ExchangeData.GetKlinesAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ interval,
+ startTime,
+ endTime,
+ limit,
+ ct: ct
+ ).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ // Get next token
+ DateTimeToken? nextToken = null;
+ if (result.Data.Count() == limit)
+ {
+ var minOpenTime = result.Data.Min(x => x.OpenTime);
+ if (request.StartTime == null || minOpenTime > request.StartTime.Value)
+ nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)interval));
+ }
+
+ return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedKline(x.OpenTime, x.ClosePrice, x.HighPrice, x.LowPrice, x.OpenPrice, x.Volume)).ToArray(), nextToken);
+ }
+
+ #endregion
+
+ #region Futures Symbol client
+
+ EndpointOptions IFuturesSymbolRestClient.GetFuturesSymbolsOptions { get; } = new EndpointOptions(false);
+ async Task>> IFuturesSymbolRestClient.GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesSymbolRestClient)this).GetFuturesSymbolsOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var result = await ExchangeData.GetExchangeInfoAsync(ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ var data = result.Data.Symbols.Where(x => x.ContractType != null);
+ if (request.TradingMode != null)
+ data = data.Where(x => request.TradingMode == TradingMode.PerpetualInverse ? x.ContractType == ContractType.Perpetual : (x.ContractType != ContractType.Perpetual && x.ContractType != ContractType.PerpetualDelivering));
+ return result.AsExchangeResult>(Exchange, request.TradingMode == null ? SupportedTradingModes : new[] { request.TradingMode.Value }, data.Select(s => new SharedFuturesSymbol(s.ContractType == ContractType.Perpetual ? SharedSymbolType.PerpetualInverse : SharedSymbolType.DeliveryInverse, s.BaseAsset, s.QuoteAsset, s.Name, s.Status == SymbolStatus.Trading)
+ {
+ MinTradeQuantity = s.LotSizeFilter?.MinQuantity,
+ MaxTradeQuantity = s.LotSizeFilter?.MaxQuantity,
+ QuantityStep = s.LotSizeFilter?.StepSize,
+ PriceStep = s.PriceFilter?.TickSize,
+ ContractSize = s.ContractSize,
+ DeliveryTime = s.DeliveryDate.Year == 2100 ? null: s.DeliveryDate
+ }).ToArray());
+ }
+
+ #endregion
+
+ #region Ticker client
+
+ EndpointOptions IFuturesTickerRestClient.GetFuturesTickerOptions { get; } = new EndpointOptions(false);
+ async Task> IFuturesTickerRestClient.GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesTickerRestClient)this).GetFuturesTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var resultTicker = ExchangeData.GetTickersAsync(request.Symbol.GetSymbol(FormatSymbol), ct: ct);
+ var resultMarkPrice = ExchangeData.GetMarkPricesAsync(request.Symbol.GetSymbol(FormatSymbol), ct: ct);
+ await Task.WhenAll(resultTicker, resultMarkPrice).ConfigureAwait(false);
+ if (!resultTicker.Result)
+ return resultTicker.Result.AsExchangeResult(Exchange, null, default);
+ if (!resultMarkPrice.Result)
+ return resultMarkPrice.Result.AsExchangeResult(Exchange, null, default);
+
+ var ticker = resultTicker.Result.Data.SingleOrDefault();
+ var mark = resultMarkPrice.Result.Data.SingleOrDefault();
+
+ if (ticker == null || mark == null)
+ return resultTicker.Result.AsExchangeError(Exchange, new ServerError("Not found"));
+
+ return resultTicker.Result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedFuturesTicker(ticker.Symbol, ticker.LastPrice, ticker.HighPrice, ticker.LowPrice, ticker.Volume, ticker.PriceChangePercent)
+ {
+ IndexPrice = mark.IndexPrice,
+ MarkPrice = mark.MarkPrice,
+ FundingRate = mark.FundingRate,
+ NextFundingTime = mark.NextFundingTime == default ? null: mark.NextFundingTime
+ });
+ }
+
+ EndpointOptions IFuturesTickerRestClient.GetFuturesTickersOptions { get; } = new EndpointOptions(false);
+ async Task>> IFuturesTickerRestClient.GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesTickerRestClient)this).GetFuturesTickersOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var resultTickers = ExchangeData.GetTickersAsync(ct: ct);
+ var resultMarkPrices = ExchangeData.GetMarkPricesAsync(ct: ct);
+ await Task.WhenAll(resultTickers, resultMarkPrices).ConfigureAwait(false);
+ if (!resultTickers.Result)
+ return resultTickers.Result.AsExchangeResult>(Exchange, null, default);
+ if (!resultMarkPrices.Result)
+ return resultMarkPrices.Result.AsExchangeResult>(Exchange, null, default);
+
+ var data = resultTickers.Result.Data;
+ if (request.TradingMode != null)
+ data = data.Where(x => request.TradingMode == TradingMode.PerpetualInverse ? x.Symbol.Contains("_PERP") : !x.Symbol.Contains("_PERP"));
+
+ return resultTickers.Result.AsExchangeResult>(Exchange, SupportedTradingModes, data.Select(x =>
+ {
+ var markPrice = resultMarkPrices.Result.Data.Single(p => p.Symbol == x.Symbol);
+ return new SharedFuturesTicker(x.Symbol, x.LastPrice, x.HighPrice, x.LowPrice, x.Volume, x.PriceChangePercent)
+ {
+ IndexPrice = markPrice.IndexPrice,
+ MarkPrice = markPrice.MarkPrice,
+ FundingRate = markPrice.FundingRate,
+ NextFundingTime = markPrice.NextFundingTime
+ };
+ }).ToArray());
+ }
+
+ #endregion
+
+ #region Recent Trade client
+
+ GetRecentTradesOptions IRecentTradeRestClient.GetRecentTradesOptions { get; } = new GetRecentTradesOptions(1000, false);
+ async Task>> IRecentTradeRestClient.GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct)
+ {
+ var validationError = ((IRecentTradeRestClient)this).GetRecentTradesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var result = await ExchangeData.GetRecentTradesAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ limit: request.Limit,
+ ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Select(x => new SharedTrade(x.BaseQuantity, x.Price, x.TradeTime)).ToArray());
+ }
+
+ #endregion
+
+ #region Futures Order Client
+
+
+ SharedFeeDeductionType IFuturesOrderRestClient.FuturesFeeDeductionType => SharedFeeDeductionType.AddToCost;
+ SharedFeeAssetType IFuturesOrderRestClient.FuturesFeeAssetType => SharedFeeAssetType.BaseAsset;
+ IEnumerable IFuturesOrderRestClient.FuturesSupportedOrderTypes { get; } = new[] { SharedOrderType.Limit, SharedOrderType.Market };
+ IEnumerable IFuturesOrderRestClient.FuturesSupportedTimeInForce { get; } = new[] { SharedTimeInForce.GoodTillCanceled, SharedTimeInForce.ImmediateOrCancel, SharedTimeInForce.FillOrKill };
+ SharedQuantitySupport IFuturesOrderRestClient.FuturesSupportedOrderQuantity { get; } = new SharedQuantitySupport(
+ SharedQuantityType.BaseAsset,
+ SharedQuantityType.BaseAsset,
+ SharedQuantityType.BaseAsset,
+ SharedQuantityType.BaseAsset);
+
+ PlaceFuturesOrderOptions IFuturesOrderRestClient.PlaceFuturesOrderOptions { get; } = new PlaceFuturesOrderOptions();
+ async Task> IFuturesOrderRestClient.PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).PlaceFuturesOrderOptions.ValidateRequest(
+ Exchange,
+ request,
+ request.Symbol.TradingMode,
+ SupportedTradingModes,
+ ((IFuturesOrderRestClient)this).FuturesSupportedOrderTypes,
+ ((IFuturesOrderRestClient)this).FuturesSupportedTimeInForce,
+ ((IFuturesOrderRestClient)this).FuturesSupportedOrderQuantity);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var result = await Trading.PlaceOrderAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ request.Side == SharedOrderSide.Buy ? Enums.OrderSide.Buy : Enums.OrderSide.Sell,
+ request.OrderType == SharedOrderType.Limit ? Enums.FuturesOrderType.Limit : Enums.FuturesOrderType.Market,
+ quantity: request.Quantity,
+ price: request.Price,
+ positionSide: request.PositionSide == null ? null : request.PositionSide == SharedPositionSide.Long ? PositionSide.Long: PositionSide.Short,
+ reduceOnly: request.ReduceOnly,
+ timeInForce: GetTimeInForce(request.OrderType, request.TimeInForce),
+ newClientOrderId: request.ClientOrderId,
+ ct: ct).ConfigureAwait(false);
+
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(result.Data.Id.ToString()));
+ }
+
+ EndpointOptions IFuturesOrderRestClient.GetFuturesOrderOptions { get; } = new EndpointOptions(true);
+ async Task> IFuturesOrderRestClient.GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).GetFuturesOrderOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ if (!long.TryParse(request.OrderId, out var orderId))
+ return new ExchangeWebResult(Exchange, new ArgumentError("Invalid order id"));
+
+ var order = await Trading.GetOrderAsync(request.Symbol.GetSymbol(FormatSymbol), orderId, ct: ct).ConfigureAwait(false);
+ if (!order)
+ return order.AsExchangeResult(Exchange, null, default);
+
+ return order.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedFuturesOrder(
+ order.Data.Symbol,
+ order.Data.Id.ToString(),
+ ParseOrderType(order.Data.Type),
+ order.Data.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell,
+ ParseOrderStatus(order.Data.Status),
+ order.Data.CreateTime)
+ {
+ ClientOrderId = order.Data.ClientOrderId,
+ AveragePrice = order.Data.AveragePrice == 0 ? null : order.Data.AveragePrice,
+ OrderPrice = order.Data.Price == 0 ? null : order.Data.Price,
+ Quantity = order.Data.Quantity,
+ QuantityFilled = order.Data.QuantityFilled,
+ QuoteQuantityFilled = order.Data.QuoteQuantityFilled,
+ TimeInForce = ParseTimeInForce(order.Data.TimeInForce),
+ UpdateTime = order.Data.UpdateTime,
+ PositionSide = order.Data.PositionSide == PositionSide.Both ? null : order.Data.PositionSide == PositionSide.Long ? SharedPositionSide.Long : SharedPositionSide.Short,
+ ReduceOnly = order.Data.ReduceOnly
+ });
+ }
+
+ EndpointOptions IFuturesOrderRestClient.GetOpenFuturesOrdersOptions { get; } = new EndpointOptions(true);
+ async Task>> IFuturesOrderRestClient.GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).GetOpenFuturesOrdersOptions.ValidateRequest(Exchange, request, request.Symbol?.TradingMode ?? request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var symbol = request.Symbol?.GetSymbol(FormatSymbol);
+ var orders = await Trading.GetOpenOrdersAsync(symbol, ct: ct).ConfigureAwait(false);
+ if (!orders)
+ return orders.AsExchangeResult>(Exchange, null, default);
+
+ return orders.AsExchangeResult>(Exchange, request.Symbol == null ? SupportedTradingModes : new[] { request.Symbol.TradingMode }, orders.Data.Select(x => new SharedFuturesOrder(
+ x.Symbol,
+ x.Id.ToString(),
+ ParseOrderType(x.Type),
+ x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell,
+ ParseOrderStatus(x.Status),
+ x.CreateTime)
+ {
+ ClientOrderId = x.ClientOrderId,
+ AveragePrice = x.AveragePrice == 0 ? null : x.AveragePrice,
+ OrderPrice = x.Price == 0 ? null : x.Price,
+ Quantity = x.Quantity,
+ QuantityFilled = x.QuantityFilled,
+ QuoteQuantityFilled = x.QuoteQuantityFilled,
+ TimeInForce = ParseTimeInForce(x.TimeInForce),
+ UpdateTime = x.UpdateTime,
+ PositionSide = x.PositionSide == PositionSide.Both ? null : x.PositionSide == PositionSide.Long ? SharedPositionSide.Long : SharedPositionSide.Short,
+ ReduceOnly = x.ReduceOnly
+ }).ToArray());
+ }
+
+ PaginatedEndpointOptions IFuturesOrderRestClient.GetClosedFuturesOrdersOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Descending, true);
+ async Task>> IFuturesOrderRestClient.GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).GetClosedFuturesOrdersOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ // Determine page token
+ DateTime? fromTimestamp = null;
+ if (pageToken is DateTimeToken dateTimeToken)
+ fromTimestamp = dateTimeToken.LastTime;
+
+ // Get data
+ var orders = await Trading.GetOrdersAsync(request.Symbol.GetSymbol(FormatSymbol),
+ startTime: request.StartTime,
+ endTime: fromTimestamp ?? request.EndTime,
+ limit: request.Limit ?? 100,
+ ct: ct).ConfigureAwait(false);
+ if (!orders)
+ return orders.AsExchangeResult>(Exchange, null, default);
+
+ // Get next token
+ DateTimeToken? nextToken = null;
+ if (orders.Data.Count() == (request.Limit ?? 100))
+ nextToken = new DateTimeToken(orders.Data.Min(o => o.CreateTime).AddMilliseconds(-1));
+
+ return orders.AsExchangeResult>(Exchange, SupportedTradingModes, orders.Data.Where(x => x.Status == OrderStatus.Filled || x.Status == OrderStatus.Canceled || x.Status == OrderStatus.Expired).Select(x => new SharedFuturesOrder(
+ x.Symbol,
+ x.Id.ToString(),
+ ParseOrderType(x.Type),
+ x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell,
+ ParseOrderStatus(x.Status),
+ x.CreateTime)
+ {
+ ClientOrderId = x.ClientOrderId,
+ AveragePrice = x.AveragePrice == 0 ? null : x.AveragePrice,
+ OrderPrice = x.Price == 0 ? null : x.Price,
+ Quantity = x.Quantity,
+ QuantityFilled = x.QuantityFilled,
+ QuoteQuantityFilled = x.QuoteQuantityFilled,
+ TimeInForce = ParseTimeInForce(x.TimeInForce),
+ UpdateTime = x.UpdateTime,
+ PositionSide = x.PositionSide == PositionSide.Both ? null : x.PositionSide == PositionSide.Long ? SharedPositionSide.Long : SharedPositionSide.Short,
+ ReduceOnly = x.ReduceOnly
+ }).ToArray());
+ }
+
+ EndpointOptions IFuturesOrderRestClient.GetFuturesOrderTradesOptions { get; } = new EndpointOptions(true);
+ async Task>> IFuturesOrderRestClient.GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).GetFuturesOrderTradesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ if (!long.TryParse(request.OrderId, out var orderId))
+ return new ExchangeWebResult>(Exchange, new ArgumentError("Invalid order id"));
+
+ var orders = await Trading.GetUserTradesAsync(request.Symbol.GetSymbol(FormatSymbol), orderId: orderId, ct: ct).ConfigureAwait(false);
+ if (!orders)
+ return orders.AsExchangeResult>(Exchange, null, default);
+
+ return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode, orders.Data.Select(x => new SharedUserTrade(
+ x.Symbol,
+ x.OrderId.ToString(),
+ x.Id.ToString(),
+ x.Buyer ? SharedOrderSide.Buy : SharedOrderSide.Sell,
+ x.Quantity,
+ x.Price,
+ x.Timestamp)
+ {
+ Price = x.Price,
+ Quantity = x.Quantity,
+ Fee = x.Fee,
+ FeeAsset = x.FeeAsset,
+ Role = x.Maker ? SharedRole.Maker : SharedRole.Taker
+ }).ToArray());
+ }
+
+ PaginatedEndpointOptions IFuturesOrderRestClient.GetFuturesUserTradesOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Descending, true);
+ async Task>> IFuturesOrderRestClient.GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).GetFuturesUserTradesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ // Determine page token
+ long? fromId = null;
+ if (pageToken is FromIdToken fromIdToken)
+ fromId = long.Parse(fromIdToken.FromToken);
+
+ // Get data
+ var orders = await Trading.GetUserTradesAsync(request.Symbol.GetSymbol(FormatSymbol),
+ startTime: request.StartTime,
+ endTime: request.EndTime,
+ limit: request.Limit ?? 500,
+ fromId: fromId,
+ ct: ct
+ ).ConfigureAwait(false);
+ if (!orders)
+ return orders.AsExchangeResult>(Exchange, null, default);
+
+ // Get next token
+ FromIdToken? nextToken = null;
+ if (orders.Data.Count() == (request.Limit ?? 500))
+ nextToken = new FromIdToken(orders.Data.Max(o => o.Id).ToString());
+
+ return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode, orders.Data.Select(x => new SharedUserTrade(
+ x.Symbol,
+ x.OrderId.ToString(),
+ x.Id.ToString(),
+ x.Buyer ? SharedOrderSide.Buy : SharedOrderSide.Sell,
+ x.Quantity,
+ x.Price,
+ x.Timestamp)
+ {
+ Price = x.Price,
+ Quantity = x.Quantity,
+ Fee = x.Fee,
+ FeeAsset = x.FeeAsset,
+ Role = x.Maker ? SharedRole.Maker : SharedRole.Taker
+ }).ToArray(), nextToken);
+ }
+
+ EndpointOptions IFuturesOrderRestClient.CancelFuturesOrderOptions { get; } = new EndpointOptions(true);
+ async Task> IFuturesOrderRestClient.CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).CancelFuturesOrderOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ if (!long.TryParse(request.OrderId, out var orderId))
+ return new ExchangeWebResult(Exchange, new ArgumentError("Invalid order id"));
+
+ var order = await Trading.CancelOrderAsync(request.Symbol.GetSymbol(FormatSymbol), orderId).ConfigureAwait(false);
+ if (!order)
+ return order.AsExchangeResult(Exchange, null, default);
+
+ return order.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(order.Data.Id.ToString()));
+ }
+
+ EndpointOptions IFuturesOrderRestClient.GetPositionsOptions { get; } = new EndpointOptions(true);
+ async Task>> IFuturesOrderRestClient.GetPositionsAsync(GetPositionsRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).GetPositionsOptions.ValidateRequest(Exchange, request, request.Symbol?.TradingMode ?? request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var symbol = request.Symbol?.GetSymbol(FormatSymbol);
+ var result = await Account.GetPositionInformationAsync(pair: symbol?.Split('_')[0], ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ return result.AsExchangeResult>(Exchange, request.Symbol == null ? SupportedTradingModes : new[] { request.Symbol.TradingMode }, result.Data.Select(x => new SharedPosition(x.Symbol, Math.Abs(x.Quantity), x.UpdateTime)
+ {
+ UnrealizedPnl = x.UnrealizedPnl,
+ LiquidationPrice = x.LiquidationPrice == 0 ? null : x.LiquidationPrice,
+ Leverage = x.Leverage,
+ AverageOpenPrice = x.EntryPrice,
+ PositionSide = x.PositionSide == PositionSide.Both ? (x.Quantity >= 0 ? SharedPositionSide.Long : SharedPositionSide.Short) : x.PositionSide == PositionSide.Short ? SharedPositionSide.Short : SharedPositionSide.Long
+ }).ToArray());
+ }
+
+ EndpointOptions IFuturesOrderRestClient.ClosePositionOptions { get; } = new EndpointOptions(true)
+ {
+ RequiredOptionalParameters = new List
+ {
+ new ParameterDescription(nameof(ClosePositionRequest.PositionSide), typeof(SharedPositionSide), "The position side to close", SharedPositionSide.Long),
+ new ParameterDescription(nameof(ClosePositionRequest.Quantity), typeof(decimal), "Quantity of the position is required", 0.1m)
+ }
+ };
+ async Task> IFuturesOrderRestClient.ClosePositionAsync(ClosePositionRequest request, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderRestClient)this).ClosePositionOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var symbol = request.Symbol.GetSymbol(FormatSymbol);
+ var positionMode = await Account.GetPositionModeAsync().ConfigureAwait(false);
+ if (!positionMode)
+ return positionMode.AsExchangeResult(Exchange, null, default);
+
+ var result = await Trading.PlaceOrderAsync(
+ symbol,
+ request.PositionSide == SharedPositionSide.Long ? OrderSide.Sell : OrderSide.Buy,
+ FuturesOrderType.Market,
+ request.Quantity,
+ positionSide: !positionMode.Data.IsHedgeMode ? null : request.PositionSide == SharedPositionSide.Short ? PositionSide.Short : PositionSide.Long,
+ reduceOnly: positionMode.Data.IsHedgeMode ? null : true,
+ ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(result.Data.Id.ToString()));
+ }
+
+ private TimeInForce? GetTimeInForce(SharedOrderType type, SharedTimeInForce? tif)
+ {
+ if (tif == SharedTimeInForce.ImmediateOrCancel) return TimeInForce.ImmediateOrCancel;
+ if (tif == SharedTimeInForce.FillOrKill) return TimeInForce.FillOrKill;
+ if (tif == SharedTimeInForce.GoodTillCanceled) return TimeInForce.GoodTillCanceled;
+ if (type == SharedOrderType.Limit) return TimeInForce.GoodTillCanceled; // Limit order always needs tif
+
+ return null;
+ }
+
+ private SharedOrderStatus ParseOrderStatus(OrderStatus status)
+ {
+ if (status == OrderStatus.PendingNew || status == OrderStatus.New || status == OrderStatus.PartiallyFilled || status == OrderStatus.PendingCancel) return SharedOrderStatus.Open;
+ if (status == OrderStatus.Canceled || status == OrderStatus.Rejected || status == OrderStatus.Expired) return SharedOrderStatus.Canceled;
+ return SharedOrderStatus.Filled;
+ }
+
+ private SharedOrderType ParseOrderType(FuturesOrderType type)
+ {
+ if (type == FuturesOrderType.Market) return SharedOrderType.Market;
+ if (type == FuturesOrderType.Limit) return SharedOrderType.Limit;
+
+ return SharedOrderType.Other;
+ }
+
+ private SharedTimeInForce? ParseTimeInForce(TimeInForce tif)
+ {
+ if (tif == TimeInForce.GoodTillCanceled) return SharedTimeInForce.GoodTillCanceled;
+ if (tif == TimeInForce.ImmediateOrCancel) return SharedTimeInForce.ImmediateOrCancel;
+ if (tif == TimeInForce.FillOrKill) return SharedTimeInForce.FillOrKill;
+
+ return null;
+ }
+
+ #endregion
+
+ #region Leverage client
+ SharedLeverageSettingMode ILeverageRestClient.LeverageSettingType => SharedLeverageSettingMode.PerSymbol;
+
+ EndpointOptions ILeverageRestClient.GetLeverageOptions { get; } = new EndpointOptions(true);
+ async Task> ILeverageRestClient.GetLeverageAsync(GetLeverageRequest request, CancellationToken ct)
+ {
+ var validationError = ((ILeverageRestClient)this).GetLeverageOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var symbol = request.Symbol.GetSymbol(FormatSymbol);
+ var result = await Account.GetPositionInformationAsync(pair: symbol.Split('_')[0], ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ if (!result.Data.Any())
+ return result.AsExchangeError(Exchange, new ServerError("Not found"));
+
+ var data = result.Data.Where(x => x.Symbol == symbol).ToList();
+ return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedLeverage(data.First().Leverage)
+ {
+ Side = request.PositionSide
+ });
+ }
+
+ SetLeverageOptions ILeverageRestClient.SetLeverageOptions { get; } = new SetLeverageOptions();
+ async Task> ILeverageRestClient.SetLeverageAsync(SetLeverageRequest request, CancellationToken ct)
+ {
+ var validationError = ((ILeverageRestClient)this).SetLeverageOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var result = await Account.ChangeInitialLeverageAsync(symbol: request.Symbol.GetSymbol(FormatSymbol), (int)request.Leverage ,ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedLeverage(result.Data.Leverage));
+ }
+ #endregion
+
+ #region Mark Klines client
+
+ GetKlinesOptions IMarkPriceKlineRestClient.GetMarkPriceKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false)
+ {
+ MaxRequestDataPoints = 1000
+ };
+
+ async Task>> IMarkPriceKlineRestClient.GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var interval = (Enums.KlineInterval)request.Interval;
+ if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval))
+ return new ExchangeWebResult>(Exchange, new ArgumentError("Interval not supported"));
+
+ var validationError = ((IMarkPriceKlineRestClient)this).GetMarkPriceKlinesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ // Determine pagination
+ // Data is normally returned oldest first, so to do newest first pagination we have to do some calc
+ DateTime endTime = request.EndTime ?? DateTime.UtcNow;
+ DateTime? startTime = request.StartTime;
+ if (pageToken is DateTimeToken dateTimeToken)
+ endTime = dateTimeToken.LastTime;
+
+ var limit = request.Limit ?? 1000;
+ if (startTime == null || startTime < endTime)
+ {
+ var offset = (int)interval * limit;
+ startTime = endTime.AddSeconds(-offset);
+ }
+
+ if (startTime < request.StartTime)
+ startTime = request.StartTime;
+
+ var result = await ExchangeData.GetMarkPriceKlinesAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ interval,
+ limit,
+ startTime,
+ endTime,
+ ct: ct
+ ).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ // Get next token
+ DateTimeToken? nextToken = null;
+ if (result.Data.Count() == limit)
+ {
+ var minOpenTime = result.Data.Min(x => x.OpenTime);
+ if (request.StartTime == null || minOpenTime > request.StartTime.Value)
+ nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)(interval - 1)));
+ }
+
+ return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedFuturesKline(x.OpenTime, x.ClosePrice, x.HighPrice, x.LowPrice, x.OpenPrice)).ToArray(), nextToken);
+ }
+
+ #endregion
+
+ #region Order Book client
+ GetOrderBookOptions IOrderBookRestClient.GetOrderBookOptions { get; } = new GetOrderBookOptions(new[] { 5, 10, 20, 50, 100, 500, 1000 }, false);
+ async Task> IOrderBookRestClient.GetOrderBookAsync(GetOrderBookRequest request, CancellationToken ct)
+ {
+ var validationError = ((IOrderBookRestClient)this).GetOrderBookOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var result = await ExchangeData.GetOrderBookAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ limit: request.Limit,
+ ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedOrderBook(result.Data.Asks, result.Data.Bids));
+ }
+
+ #endregion
+
+ #region Trade History client
+ GetTradeHistoryOptions ITradeHistoryRestClient.GetTradeHistoryOptions { get; } = new GetTradeHistoryOptions(SharedPaginationSupport.Ascending, false);
+
+ async Task>> ITradeHistoryRestClient.GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var validationError = ((ITradeHistoryRestClient)this).GetTradeHistoryOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ long? fromId = null;
+ if (pageToken is FromIdToken token)
+ fromId = long.Parse(token.FromToken);
+
+ // Get data
+ var result = await ExchangeData.GetAggregatedTradeHistoryAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ startTime: fromId != null ? null : request.StartTime,
+ endTime: fromId != null ? null : request.EndTime,
+ limit: request.Limit ?? 1000,
+ fromId: fromId,
+ ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ FromIdToken? nextToken = null;
+ if (result.Data.Any() && result.Data.Last().TradeTime < request.EndTime)
+ nextToken = new FromIdToken(result.Data.Max(x => x.Id).ToString());
+
+ // Return
+ return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Where(x => x.TradeTime < request.EndTime).Select(x => new SharedTrade(x.Quantity, x.Price, x.TradeTime)).ToArray(), nextToken);
+ }
+ #endregion
+
+ #region Index Klines client
+
+ GetKlinesOptions IIndexPriceKlineRestClient.GetIndexPriceKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false)
+ {
+ MaxRequestDataPoints = 1000
+ };
+
+ async Task>> IIndexPriceKlineRestClient.GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var interval = (Enums.KlineInterval)request.Interval;
+ if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval))
+ return new ExchangeWebResult>(Exchange, new ArgumentError("Interval not supported"));
+
+ var validationError = ((IIndexPriceKlineRestClient)this).GetIndexPriceKlinesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ // Determine pagination
+ // Data is normally returned oldest first, so to do newest first pagination we have to do some calc
+ DateTime endTime = request.EndTime ?? DateTime.UtcNow;
+ DateTime? startTime = request.StartTime;
+ if (pageToken is DateTimeToken dateTimeToken)
+ endTime = dateTimeToken.LastTime;
+
+ var limit = request.Limit ?? 1000;
+ if (startTime == null || startTime < endTime)
+ {
+ var offset = (int)interval * limit;
+ startTime = endTime.AddSeconds(-offset);
+ }
+
+ if (startTime < request.StartTime)
+ startTime = request.StartTime;
+
+ var result = await ExchangeData.GetMarkPriceKlinesAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ interval,
+ limit,
+ startTime,
+ endTime,
+ ct: ct
+ ).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ // Get next token
+ DateTimeToken? nextToken = null;
+ if (result.Data.Count() == limit)
+ {
+ var minOpenTime = result.Data.Min(x => x.OpenTime);
+ if (request.StartTime == null || minOpenTime > request.StartTime.Value)
+ nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)(interval - 1)));
+ }
+
+ return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedFuturesKline(x.OpenTime, x.ClosePrice, x.HighPrice, x.LowPrice, x.OpenPrice)).ToArray(), nextToken);
+ }
+
+ #endregion
+
+ #region Open Interest client
+
+ EndpointOptions IOpenInterestRestClient.GetOpenInterestOptions { get; } = new EndpointOptions(false);
+ async Task> IOpenInterestRestClient.GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct)
+ {
+ var validationError = ((IOpenInterestRestClient)this).GetOpenInterestOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var result = await ExchangeData.GetOpenInterestAsync(request.Symbol.GetSymbol(FormatSymbol), ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedOpenInterest(result.Data.OpenInterest));
+ }
+
+ #endregion
+
+ #region Funding Rate client
+ GetFundingRateHistoryOptions IFundingRateRestClient.GetFundingRateHistoryOptions { get; } = new GetFundingRateHistoryOptions(SharedPaginationSupport.Ascending, false);
+
+ async Task>> IFundingRateRestClient.GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var validationError = ((IFundingRateRestClient)this).GetFundingRateHistoryOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ DateTime? fromTime = null;
+ if (pageToken is DateTimeToken token)
+ fromTime = token.LastTime;
+
+ // Get data
+ var result = await ExchangeData.GetFundingRatesAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ startTime: fromTime ?? request.StartTime,
+ endTime: request.EndTime,
+ limit: request.Limit ?? 1000,
+ ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ DateTimeToken? nextToken = null;
+ if (result.Data.Count() == (request.Limit ?? 1000))
+ nextToken = new DateTimeToken(result.Data.Max(x => x.FundingTime).AddSeconds(1));
+
+ // Return
+ return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Select(x => new SharedFundingRate(x.FundingRate, x.FundingTime)).ToArray(), nextToken);
+ }
+ #endregion
+
+ #region Balance Client
+ EndpointOptions IBalanceRestClient.GetBalancesOptions { get; } = new EndpointOptions(true);
+
+ async Task>> IBalanceRestClient.GetBalancesAsync(GetBalancesRequest request, CancellationToken ct)
+ {
+ var validationError = ((IBalanceRestClient)this).GetBalancesOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var result = await Account.GetBalancesAsync(ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ return result.AsExchangeResult>(Exchange, SupportedTradingModes, result.Data.Select(x => new SharedBalance(x.Asset, x.AvailableBalance, x.WalletBalance)).ToArray());
+ }
+
+ #endregion
+
+ #region Position Mode client
+
+ SharedPositionModeSelection IPositionModeRestClient.PositionModeSettingType => SharedPositionModeSelection.PerAccount;
+
+ GetPositionModeOptions IPositionModeRestClient.GetPositionModeOptions { get; } = new GetPositionModeOptions();
+ async Task> IPositionModeRestClient.GetPositionModeAsync(GetPositionModeRequest request, CancellationToken ct)
+ {
+ var validationError = ((IPositionModeRestClient)this).GetPositionModeOptions.ValidateRequest(Exchange, request, request.Symbol?.TradingMode ?? request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var result = await Account.GetPositionModeAsync(ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, SupportedTradingModes, new SharedPositionModeResult(result.Data.IsHedgeMode ? SharedPositionMode.HedgeMode : SharedPositionMode.OneWay));
+ }
+
+ SetPositionModeOptions IPositionModeRestClient.SetPositionModeOptions { get; } = new SetPositionModeOptions();
+ async Task> IPositionModeRestClient.SetPositionModeAsync(SetPositionModeRequest request, CancellationToken ct)
+ {
+ var validationError = ((IPositionModeRestClient)this).SetPositionModeOptions.ValidateRequest(Exchange, request, request.Symbol?.TradingMode ?? request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var result = await Account.ModifyPositionModeAsync(request.PositionMode == SharedPositionMode.HedgeMode, ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, SupportedTradingModes, new SharedPositionModeResult(request.PositionMode));
+ }
+ #endregion
+
+ #region Listen Key client
+
+ EndpointOptions IListenKeyRestClient.StartOptions { get; } = new EndpointOptions(true);
+ async Task> IListenKeyRestClient.StartListenKeyAsync(StartListenKeyRequest request, CancellationToken ct)
+ {
+ var validationError = ((IListenKeyRestClient)this).StartOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ // Get data
+ var result = await Account.StartUserStreamAsync(ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, SupportedTradingModes, result.Data);
+ }
+ EndpointOptions IListenKeyRestClient.KeepAliveOptions { get; } = new EndpointOptions(true);
+ async Task> IListenKeyRestClient.KeepAliveListenKeyAsync(KeepAliveListenKeyRequest request, CancellationToken ct)
+ {
+ var validationError = ((IListenKeyRestClient)this).KeepAliveOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ // Get data
+ var result = await Account.KeepAliveUserStreamAsync(request.ListenKey, ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, SupportedTradingModes, request.ListenKey);
+ }
+
+ EndpointOptions IListenKeyRestClient.StopOptions { get; } = new EndpointOptions(true);
+ async Task> IListenKeyRestClient.StopListenKeyAsync(StopListenKeyRequest request, CancellationToken ct)
+ {
+ var validationError = ((IListenKeyRestClient)this).StopOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ // Get data
+ var result = await Account.StopUserStreamAsync(request.ListenKey, ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, SupportedTradingModes, request.ListenKey);
+ }
+ #endregion
+ }
+}
diff --git a/Binance.Net/Clients/CoinFuturesApi/BinanceSocketClientCoinFuturesApi.cs b/Binance.Net/Clients/CoinFuturesApi/BinanceSocketClientCoinFuturesApi.cs
index 89de866a9..525391fef 100644
--- a/Binance.Net/Clients/CoinFuturesApi/BinanceSocketClientCoinFuturesApi.cs
+++ b/Binance.Net/Clients/CoinFuturesApi/BinanceSocketClientCoinFuturesApi.cs
@@ -12,12 +12,13 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Objects.Sockets;
+using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Sockets;
namespace Binance.Net.Clients.CoinFuturesApi
{
///
- internal class BinanceSocketClientCoinFuturesApi : SocketApiClient, IBinanceSocketClientCoinFuturesApi
+ internal partial class BinanceSocketClientCoinFuturesApi : SocketApiClient, IBinanceSocketClientCoinFuturesApi
{
#region fields
private const string _klineStreamEndpoint = "@kline";
@@ -66,9 +67,13 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer();
protected override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor();
+ public IBinanceSocketClientCoinFuturesApiShared SharedClient => this;
///
- public override string FormatSymbol(string baseAsset, string quoteAsset) => baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant();
+ public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null)
+ {
+ return baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant() + (deliverTime == null ? "_PERP" : "_" + deliverTime.Value.ToString("yyMMdd"));
+ }
#region methods
diff --git a/Binance.Net/Clients/CoinFuturesApi/BinanceSocketClientCoinFuturesApiShared.cs b/Binance.Net/Clients/CoinFuturesApi/BinanceSocketClientCoinFuturesApiShared.cs
new file mode 100644
index 000000000..d7754da10
--- /dev/null
+++ b/Binance.Net/Clients/CoinFuturesApi/BinanceSocketClientCoinFuturesApiShared.cs
@@ -0,0 +1,226 @@
+using Binance.Net.Interfaces.Clients.CoinFuturesApi;
+using CryptoExchange.Net.Objects.Sockets;
+using CryptoExchange.Net.SharedApis;
+
+namespace Binance.Net.Clients.CoinFuturesApi
+{
+ internal partial class BinanceSocketClientCoinFuturesApi : IBinanceSocketClientCoinFuturesApiShared
+ {
+ public string Exchange => BinanceExchange.ExchangeName;
+ public TradingMode[] SupportedTradingModes => new[] { TradingMode.DeliveryInverse, TradingMode.PerpetualInverse };
+
+ public void SetDefaultExchangeParameter(string key, object value) => ExchangeParameters.SetStaticParameter(Exchange, key, value);
+ public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticParameters();
+
+ #region Ticker client
+
+ EndpointOptions ITickerSocketClient.SubscribeTickerOptions { get; } = new EndpointOptions(false);
+ async Task> ITickerSocketClient.SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct)
+ {
+ var validationError = ((ITickerSocketClient)this).SubscribeTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var symbol = request.Symbol.GetSymbol(FormatSymbol);
+ var result = await SubscribeToTickerUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, new SharedSpotTicker(symbol, update.Data.LastPrice, update.Data.LowPrice, update.Data.HighPrice, update.Data.Volume, update.Data.PriceChangePercent))), ct: ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+
+ #endregion
+
+ #region Tickers client
+
+ EndpointOptions ITickersSocketClient.SubscribeAllTickersOptions { get; } = new EndpointOptions(false);
+ async Task> ITickersSocketClient.SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action>> handler, CancellationToken ct)
+ {
+ var validationError = ((ITickersSocketClient)this).SubscribeAllTickersOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var result = await SubscribeToAllTickerUpdatesAsync(update =>
+ {
+ var data = update.Data;
+ if (request.TradingMode != null)
+ data = update.Data.Where(x => request.TradingMode == TradingMode.PerpetualInverse ? x.Symbol.EndsWith("_PERP") : !x.Symbol.Contains("_PERP"));
+
+ if (!data.Any())
+ return;
+
+ handler(update.AsExchangeEvent>(Exchange, data.Select(x => new SharedSpotTicker(x.Symbol, x.LastPrice, x.LowPrice, x.HighPrice, x.Volume, x.PriceChangePercent)).ToArray()));
+ }, ct: ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+
+ #endregion
+
+ #region Trade client
+
+ EndpointOptions ITradeSocketClient.SubscribeTradeOptions { get; } = new EndpointOptions(false);
+ async Task> ITradeSocketClient.SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action>> handler, CancellationToken ct)
+ {
+ var validationError = ((ITradeSocketClient)this).SubscribeTradeOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var symbol = request.Symbol.GetSymbol(FormatSymbol);
+ var result = await SubscribeToAggregatedTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent>(Exchange, new[] { new SharedTrade(update.Data.Quantity, update.Data.Price, update.Data.TradeTime) })), ct:ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+
+ #endregion
+
+ #region Book Ticker client
+
+ EndpointOptions IBookTickerSocketClient.SubscribeBookTickerOptions { get; } = new EndpointOptions(false);
+ async Task> IBookTickerSocketClient.SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct)
+ {
+ var validationError = ((IBookTickerSocketClient)this).SubscribeBookTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var symbol = request.Symbol.GetSymbol(FormatSymbol);
+ var result = await SubscribeToBookTickerUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, new SharedBookTicker(update.Data.BestAskPrice, update.Data.BestAskQuantity, update.Data.BestBidPrice, update.Data.BestBidQuantity))), ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+
+ #endregion
+
+ #region Kline client
+ SubscribeKlineOptions IKlineSocketClient.SubscribeKlineOptions { get; } = new SubscribeKlineOptions(false);
+ async Task> IKlineSocketClient.SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct)
+ {
+ var interval = (Enums.KlineInterval)request.Interval;
+ if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval))
+ return new ExchangeResult(Exchange, new ArgumentError("Interval not supported"));
+
+ var validationError = ((IKlineSocketClient)this).SubscribeKlineOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var symbol = request.Symbol.GetSymbol(FormatSymbol);
+ var result = await SubscribeToKlineUpdatesAsync(symbol, interval, update => handler(update.AsExchangeEvent(Exchange, new SharedKline(update.Data.Data.OpenTime, update.Data.Data.ClosePrice, update.Data.Data.HighPrice, update.Data.Data.LowPrice, update.Data.Data.OpenPrice, update.Data.Data.Volume))), ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+ #endregion
+
+ #region Order Book client
+ SubscribeOrderBookOptions IOrderBookSocketClient.SubscribeOrderBookOptions { get; } = new SubscribeOrderBookOptions(false, new[] { 5, 10, 20 });
+ async Task> IOrderBookSocketClient.SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct)
+ {
+ var validationError = ((IOrderBookSocketClient)this).SubscribeOrderBookOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var symbol = request.Symbol.GetSymbol(FormatSymbol);
+ var result = await SubscribeToPartialOrderBookUpdatesAsync(symbol, request.Limit ?? 20, 100, update => handler(update.AsExchangeEvent(Exchange, new SharedOrderBook(update.Data.Asks, update.Data.Bids))), ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+ #endregion
+
+ #region Balance client
+ EndpointOptions IBalanceSocketClient.SubscribeBalanceOptions { get; } = new EndpointOptions(false)
+ {
+ RequiredOptionalParameters = new List
+ {
+ new ParameterDescription(nameof(SubscribeBalancesRequest.ListenKey), typeof(string), "The listenkey for starting the user stream", "123123123")
+ }
+ };
+ async Task> IBalanceSocketClient.SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action>> handler, CancellationToken ct)
+ {
+ var validationError = ((IBalanceSocketClient)this).SubscribeBalanceOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var result = await SubscribeToUserDataUpdatesAsync(request.ListenKey!,
+ onAccountUpdate: update => handler(update.AsExchangeEvent>(Exchange, update.Data.UpdateData.Balances.Select(x => new SharedBalance(x.Asset, x.WalletBalance, x.WalletBalance)).ToArray())),
+ ct: ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+
+ #endregion
+
+ #region Position client
+ EndpointOptions IPositionSocketClient.SubscribePositionOptions { get; } = new EndpointOptions(false)
+ {
+ RequiredOptionalParameters = new List
+ {
+ new ParameterDescription(nameof(SubscribePositionRequest.ListenKey), typeof(string), "The listenkey for starting the user stream", "123123123")
+ }
+ };
+ async Task> IPositionSocketClient.SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action>> handler, CancellationToken ct)
+ {
+ var validationError = ((IPositionSocketClient)this).SubscribePositionOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var result = await SubscribeToUserDataUpdatesAsync(request.ListenKey!,
+ onAccountUpdate: update => handler(update.AsExchangeEvent>(Exchange, update.Data.UpdateData.Positions.Select(x => new SharedPosition(x.Symbol, x.Quantity, update.Data.EventTime)
+ {
+ AverageOpenPrice = x.EntryPrice,
+ PositionSide = x.PositionSide == Enums.PositionSide.Both ? (x.Quantity >= 0 ? SharedPositionSide.Long : SharedPositionSide.Short) : x.PositionSide == Enums.PositionSide.Short ? SharedPositionSide.Short : SharedPositionSide.Long,
+ UnrealizedPnl = x.UnrealizedPnl
+ }).ToArray())),
+ ct: ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+
+ #endregion
+
+ #region Futures Order client
+
+ EndpointOptions IFuturesOrderSocketClient.SubscribeFuturesOrderOptions { get; } = new EndpointOptions(false)
+ {
+ RequiredOptionalParameters = new List
+ {
+ new ParameterDescription(nameof(SubscribeFuturesOrderRequest.ListenKey), typeof(string), "The listenkey for starting the user stream", "123123123")
+ }
+ };
+
+ async Task> IFuturesOrderSocketClient.SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action>> handler, CancellationToken ct)
+ {
+ var validationError = ((IFuturesOrderSocketClient)this).SubscribeFuturesOrderOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeResult(Exchange, validationError);
+
+ var result = await SubscribeToUserDataUpdatesAsync(request.ListenKey!,
+ onOrderUpdate: update => handler(update.AsExchangeEvent>(Exchange, new[] {
+ new SharedFuturesOrder(
+ update.Data.UpdateData.Symbol,
+ update.Data.UpdateData.OrderId.ToString(),
+ update.Data.UpdateData.Type == Enums.FuturesOrderType.Limit ? SharedOrderType.Limit : update.Data.UpdateData.Type == Enums.FuturesOrderType.Market ? SharedOrderType.Market : SharedOrderType.Other,
+ update.Data.UpdateData.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell,
+ update.Data.UpdateData.Status == Enums.OrderStatus.Canceled ? SharedOrderStatus.Canceled : (update.Data.UpdateData.Status == Enums.OrderStatus.New || update.Data.UpdateData.Status == Enums.OrderStatus.PartiallyFilled) ? SharedOrderStatus.Open : SharedOrderStatus.Filled,
+ update.Data.UpdateData.UpdateTime)
+ {
+ ClientOrderId = update.Data.UpdateData.ClientOrderId,
+ OrderPrice = update.Data.UpdateData.Price,
+ Quantity = update.Data.UpdateData.Quantity,
+ QuantityFilled = update.Data.UpdateData.AccumulatedQuantityOfFilledTrades,
+ UpdateTime = update.Data.UpdateData.UpdateTime,
+ Fee = update.Data.UpdateData.Fee,
+ FeeAsset = update.Data.UpdateData.FeeAsset,
+ AveragePrice = update.Data.UpdateData.AveragePrice,
+ PositionSide = update.Data.UpdateData.PositionSide == Enums.PositionSide.Long ? SharedPositionSide.Long : update.Data.UpdateData.PositionSide == Enums.PositionSide.Short ? SharedPositionSide.Short : null,
+ ReduceOnly = update.Data.UpdateData.IsReduce,
+ TimeInForce = update.Data.UpdateData.TimeInForce == Enums.TimeInForce.ImmediateOrCancel ? SharedTimeInForce.ImmediateOrCancel : update.Data.UpdateData.TimeInForce == Enums.TimeInForce.FillOrKill ? SharedTimeInForce.FillOrKill : SharedTimeInForce.GoodTillCanceled,
+ LastTrade = update.Data.UpdateData.QuantityOfLastFilledTrade == 0 ? null : new SharedUserTrade(update.Data.UpdateData.Symbol, update.Data.UpdateData.OrderId.ToString(), update.Data.UpdateData.TradeId.ToString(), update.Data.UpdateData.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, update.Data.UpdateData.QuantityOfLastFilledTrade, update.Data.UpdateData.PriceLastFilledTrade, update.Data.UpdateData.UpdateTime)
+ {
+ Role = update.Data.UpdateData.BuyerIsMaker ? SharedRole.Maker : SharedRole.Taker
+ }
+ }
+ })),
+ ct: ct).ConfigureAwait(false);
+
+ return new ExchangeResult(Exchange, result);
+ }
+ #endregion
+ }
+}
diff --git a/Binance.Net/Clients/GeneralApi/BinanceRestClientGeneralApi.cs b/Binance.Net/Clients/GeneralApi/BinanceRestClientGeneralApi.cs
index 69ba2cd72..ac5c00bd9 100644
--- a/Binance.Net/Clients/GeneralApi/BinanceRestClientGeneralApi.cs
+++ b/Binance.Net/Clients/GeneralApi/BinanceRestClientGeneralApi.cs
@@ -5,6 +5,7 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.RateLimiting.Interfaces;
+using CryptoExchange.Net.SharedApis;
namespace Binance.Net.Clients.GeneralApi
{
@@ -74,7 +75,7 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer();
///
- public override string FormatSymbol(string baseAsset, string quoteAsset) => baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant();
+ public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) => baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant();
internal Uri GetUrl(string endpoint) => new Uri(BaseAddress.AppendPath(endpoint));
diff --git a/Binance.Net/Clients/SpotApi/BinanceRestClientSpotApi.cs b/Binance.Net/Clients/SpotApi/BinanceRestClientSpotApi.cs
index 26905afba..07dd2c24e 100644
--- a/Binance.Net/Clients/SpotApi/BinanceRestClientSpotApi.cs
+++ b/Binance.Net/Clients/SpotApi/BinanceRestClientSpotApi.cs
@@ -10,11 +10,12 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.RateLimiting.Interfaces;
+using CryptoExchange.Net.SharedApis;
namespace Binance.Net.Clients.SpotApi
{
///
- internal class BinanceRestClientSpotApi : RestApiClient, IBinanceRestClientSpotApi, ISpotClient
+ internal partial class BinanceRestClientSpotApi : RestApiClient, IBinanceRestClientSpotApi, ISpotClient
{
#region fields
///
@@ -76,7 +77,7 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer();
///
- public override string FormatSymbol(string baseAsset, string quoteAsset) => baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant();
+ public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime) => baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant();
#region helpers
@@ -231,6 +232,8 @@ protected override Task> GetServerTimestampAsync()
///
public ISpotClient CommonSpotClient => this;
+ public IBinanceRestClientSpotApiShared SharedClient => this;
+
///
public string GetSymbolName(string baseAsset, string quoteAsset) =>
(baseAsset + quoteAsset).ToUpper(CultureInfo.InvariantCulture);
diff --git a/Binance.Net/Clients/SpotApi/BinanceRestClientSpotApiShared.cs b/Binance.Net/Clients/SpotApi/BinanceRestClientSpotApiShared.cs
new file mode 100644
index 000000000..109a58bdf
--- /dev/null
+++ b/Binance.Net/Clients/SpotApi/BinanceRestClientSpotApiShared.cs
@@ -0,0 +1,743 @@
+using Binance.Net.Interfaces.Clients.SpotApi;
+using Binance.Net.Enums;
+using CryptoExchange.Net.SharedApis;
+
+namespace Binance.Net.Clients.SpotApi
+{
+ internal partial class BinanceRestClientSpotApi : IBinanceRestClientSpotApiShared
+ {
+ public string Exchange => BinanceExchange.ExchangeName;
+ public TradingMode[] SupportedTradingModes => new[] { TradingMode.Spot };
+
+ public void SetDefaultExchangeParameter(string key, object value) => ExchangeParameters.SetStaticParameter(Exchange, key, value);
+ public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticParameters();
+
+ #region Klines Client
+
+ GetKlinesOptions IKlineRestClient.GetKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false)
+ {
+ MaxRequestDataPoints = 1000
+ };
+
+ async Task>> IKlineRestClient.GetKlinesAsync(GetKlinesRequest request, INextPageToken? pageToken, CancellationToken ct)
+ {
+ var interval = (Enums.KlineInterval)request.Interval;
+ if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval))
+ return new ExchangeWebResult>(Exchange, new ArgumentError("Interval not supported"));
+
+ var validationError = ((IKlineRestClient)this).GetKlinesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ // Determine pagination
+ // Data is normally returned oldest first, so to do newest first pagination we have to do some calc
+ DateTime endTime = request.EndTime ?? DateTime.UtcNow;
+ DateTime? startTime = request.StartTime;
+ if (pageToken is DateTimeToken dateTimeToken)
+ endTime = dateTimeToken.LastTime;
+
+ var limit = request.Limit ?? 1000;
+ if (startTime == null || startTime < endTime)
+ {
+ var offset = (int)interval * limit;
+ startTime = endTime.AddSeconds(-offset);
+ }
+
+ if (startTime < request.StartTime)
+ startTime = request.StartTime;
+
+ // Get data
+ var result = await ExchangeData.GetKlinesAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ interval,
+ startTime,
+ endTime,
+ limit,
+ ct: ct
+ ).ConfigureAwait(false);
+ if (!result)
+ return new ExchangeWebResult>(Exchange, TradingMode.Spot, result.As>(default));
+
+ // Get next token
+ DateTimeToken? nextToken = null;
+ if (result.Data.Count() == limit)
+ {
+ var minOpenTime = result.Data.Min(x => x.OpenTime);
+ if (request.StartTime == null || minOpenTime > request.StartTime.Value)
+ nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)(interval - 1)));
+ }
+
+ return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedKline(x.OpenTime, x.ClosePrice, x.HighPrice, x.LowPrice, x.OpenPrice, x.Volume)).ToArray(), nextToken);
+ }
+
+ #endregion
+
+ #region Spot Symbol client
+ EndpointOptions ISpotSymbolRestClient.GetSpotSymbolsOptions { get; } = new EndpointOptions(false);
+
+ async Task>> ISpotSymbolRestClient.GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct)
+ {
+ var validationError = ((ISpotSymbolRestClient)this).GetSpotSymbolsOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var result = await ExchangeData.GetExchangeInfoAsync(ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ return result.AsExchangeResult>(Exchange, TradingMode.Spot, result.Data.Symbols.Select(s => new SharedSpotSymbol(s.BaseAsset, s.QuoteAsset, s.Name, s.Status == SymbolStatus.Trading && s.IsSpotTradingAllowed)
+ {
+ MinTradeQuantity = s.LotSizeFilter?.MinQuantity,
+ MaxTradeQuantity = s.LotSizeFilter?.MaxQuantity,
+ MinNotionalValue = s.MinNotionalFilter?.MinNotional ?? s.NotionalFilter?.MinNotional,
+ QuantityStep = s.LotSizeFilter?.StepSize,
+ PriceStep = s.PriceFilter?.TickSize
+ }).ToArray());
+ }
+
+ #endregion
+
+ #region Ticker client
+
+ EndpointOptions ISpotTickerRestClient.GetSpotTickerOptions { get; } = new EndpointOptions(false);
+ async Task> ISpotTickerRestClient.GetSpotTickerAsync(GetTickerRequest request, CancellationToken ct)
+ {
+ var validationError = ((ISpotTickerRestClient)this).GetSpotTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult(Exchange, validationError);
+
+ var result = await ExchangeData.GetTickerAsync(request.Symbol.GetSymbol(FormatSymbol), ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult(Exchange, null, default);
+
+ return result.AsExchangeResult(Exchange, TradingMode.Spot, new SharedSpotTicker(result.Data.Symbol, result.Data.LastPrice, result.Data.HighPrice, result.Data.LowPrice, result.Data.Volume, result.Data.PriceChangePercent));
+ }
+
+ EndpointOptions ISpotTickerRestClient.GetSpotTickersOptions { get; } = new EndpointOptions(false);
+ async Task>> ISpotTickerRestClient.GetSpotTickersAsync(GetTickersRequest request, CancellationToken ct)
+ {
+ var validationError = ((ISpotTickerRestClient)this).GetSpotTickersOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ var result = await ExchangeData.GetTickersAsync(ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ return result.AsExchangeResult>(Exchange, TradingMode.Spot, result.Data.Select(x => new SharedSpotTicker(x.Symbol, x.LastPrice, x.HighPrice, x.LowPrice, x.Volume, x.PriceChangePercent)).ToArray());
+ }
+
+ #endregion
+
+ #region Recent Trades client
+ GetRecentTradesOptions IRecentTradeRestClient.GetRecentTradesOptions { get; } = new GetRecentTradesOptions(1000, false);
+
+ async Task>> IRecentTradeRestClient.GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct)
+ {
+ var validationError = ((IRecentTradeRestClient)this).GetRecentTradesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
+ if (validationError != null)
+ return new ExchangeWebResult>(Exchange, validationError);
+
+ // Get data
+ var result = await ExchangeData.GetRecentTradesAsync(
+ request.Symbol.GetSymbol(FormatSymbol),
+ limit: request.Limit,
+ ct: ct).ConfigureAwait(false);
+ if (!result)
+ return result.AsExchangeResult>(Exchange, null, default);
+
+ // Return
+ return result.AsExchangeResult