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>(Exchange, TradingMode.Spot, result.Data.Select(x => new SharedTrade(x.BaseQuantity, x.Price, x.TradeTime)).ToArray()); + } + #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, TradingMode.Spot, result.Data.Where(x => x.TradeTime < request.EndTime).Select(x => new SharedTrade(x.Quantity, x.Price, x.TradeTime)).ToArray(), nextToken); + } + #endregion + + #region Order Book client + GetOrderBookOptions IOrderBookRestClient.GetOrderBookOptions { get; } = new GetOrderBookOptions(1, 5000, 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, TradingMode.Spot, new SharedOrderBook(result.Data.Asks, result.Data.Bids)); + } + + #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, TradingMode.Spot, result.Data.Select(x => new SharedBalance(x.Asset, x.Available, x.Total)).ToArray()); + } + + #endregion + + #region Spot Order Client + + + SharedFeeDeductionType ISpotOrderRestClient.SpotFeeDeductionType => SharedFeeDeductionType.DeductFromOutput; + SharedFeeAssetType ISpotOrderRestClient.SpotFeeAssetType => SharedFeeAssetType.OutputAsset; + IEnumerable ISpotOrderRestClient.SpotSupportedOrderTypes { get; } = new[] { SharedOrderType.Limit, SharedOrderType.Market, SharedOrderType.LimitMaker }; + IEnumerable ISpotOrderRestClient.SpotSupportedTimeInForce { get; } = new[] { SharedTimeInForce.GoodTillCanceled, SharedTimeInForce.ImmediateOrCancel, SharedTimeInForce.FillOrKill }; + SharedQuantitySupport ISpotOrderRestClient.SpotSupportedOrderQuantity { get; } = new SharedQuantitySupport( + SharedQuantityType.BaseAsset, + SharedQuantityType.BaseAsset, + SharedQuantityType.BaseAndQuoteAsset, + SharedQuantityType.BaseAndQuoteAsset); + + PlaceSpotOrderOptions ISpotOrderRestClient.PlaceSpotOrderOptions { get; } = new PlaceSpotOrderOptions(); + async Task> ISpotOrderRestClient.PlaceSpotOrderAsync(PlaceSpotOrderRequest request, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).PlaceSpotOrderOptions.ValidateRequest( + Exchange, + request, + request.Symbol.TradingMode, + SupportedTradingModes, + ((ISpotOrderRestClient)this).SpotSupportedOrderTypes, + ((ISpotOrderRestClient)this).SpotSupportedTimeInForce, + ((ISpotOrderRestClient)this).SpotSupportedOrderQuantity); + 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.SpotOrderType.Limit : request.OrderType == SharedOrderType.Market ? Enums.SpotOrderType.Market: Enums.SpotOrderType.LimitMaker, + quantity: request.Quantity, + quoteQuantity: request.QuoteQuantity, + price: request.Price, + timeInForce: GetTimeInForce(request.TimeInForce, request.OrderType), + newClientOrderId: request.ClientOrderId, + ct: ct).ConfigureAwait(false); + + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, TradingMode.Spot, new SharedId(result.Data.Id.ToString())); + } + + EndpointOptions ISpotOrderRestClient.GetSpotOrderOptions { get; } = new EndpointOptions(true); + async Task> ISpotOrderRestClient.GetSpotOrderAsync(GetOrderRequest request, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).GetSpotOrderOptions.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, TradingMode.Spot, new SharedSpotOrder( + 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.AverageFillPrice, + OrderPrice = order.Data.Price, + Quantity = order.Data.Quantity, + QuantityFilled = order.Data.QuantityFilled, + QuoteQuantity = order.Data.QuoteQuantity, + QuoteQuantityFilled = order.Data.QuoteQuantityFilled, + TimeInForce = ParseTimeInForce(order.Data.TimeInForce), + UpdateTime = order.Data.UpdateTime + }); + } + + EndpointOptions ISpotOrderRestClient.GetOpenSpotOrdersOptions { get; } = new EndpointOptions(true); + async Task>> ISpotOrderRestClient.GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).GetOpenSpotOrdersOptions.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, TradingMode.Spot, orders.Data.Select(x => new SharedSpotOrder( + 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.AverageFillPrice, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantity = x.QuoteQuantity, + QuoteQuantityFilled = x.QuoteQuantityFilled, + TimeInForce = ParseTimeInForce(x.TimeInForce), + UpdateTime = x.UpdateTime + }).ToArray()); + } + + PaginatedEndpointOptions ISpotOrderRestClient.GetClosedSpotOrdersOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Ascending, true); + async Task>> ISpotOrderRestClient.GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? pageToken, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).GetClosedSpotOrdersOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + // Determine page token + DateTime? fromTime = null; + if (pageToken is DateTimeToken dateTimeToken) + fromTime = dateTimeToken.LastTime; + + // Get data + var orders = await Trading.GetOrdersAsync(request.Symbol.GetSymbol(FormatSymbol), + startTime: request.StartTime, + endTime: fromTime ?? request.EndTime, + limit: request.Limit ?? 1000, + ct: ct).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + // Get next token + DateTimeToken? nextToken = null; + if (orders.Data.Any()) + nextToken = new DateTimeToken(orders.Data.Min(o => o.CreateTime).AddMilliseconds(-1)); + + return orders.AsExchangeResult>(Exchange, TradingMode.Spot, orders.Data.Where(x => x.Status == OrderStatus.Filled || x.Status == OrderStatus.Canceled || x.Status == OrderStatus.Expired).Select(x => new SharedSpotOrder( + 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.AverageFillPrice, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantity = x.QuoteQuantity, + QuoteQuantityFilled = x.QuoteQuantityFilled, + TimeInForce = ParseTimeInForce(x.TimeInForce), + UpdateTime = x.UpdateTime + }).ToArray(), nextToken); + } + + EndpointOptions ISpotOrderRestClient.GetSpotOrderTradesOptions { get; } = new EndpointOptions(true); + async Task>> ISpotOrderRestClient.GetSpotOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).GetSpotOrderTradesOptions.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, TradingMode.Spot, orders.Data.Select(x => new SharedUserTrade( + x.Symbol, + x.OrderId.ToString(), + x.Id.ToString(), + x.IsBuyer ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.Timestamp) + { + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.IsMaker ? SharedRole.Maker : SharedRole.Taker + }).ToArray()); + } + + PaginatedEndpointOptions ISpotOrderRestClient.GetSpotUserTradesOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Descending, true); + async Task>> ISpotOrderRestClient.GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? pageToken, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).GetSpotUserTradesOptions.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.GetUserTradesAsync(request.Symbol.GetSymbol(FormatSymbol), + startTime: request.StartTime, + endTime: fromTimestamp ?? request.EndTime, + limit: request.Limit ?? 500, + ct: ct + ).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + // Get next token + DateTimeToken? nextToken = null; + if (orders.Data.Count() == (request.Limit ?? 500)) + nextToken = new DateTimeToken(orders.Data.Min(o => o.Timestamp).AddMilliseconds(-1)); + + return orders.AsExchangeResult>(Exchange, TradingMode.Spot, orders.Data.Select(x => new SharedUserTrade( + x.Symbol, + x.OrderId.ToString(), + x.Id.ToString(), + x.IsBuyer ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.Timestamp) + { + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.IsMaker ? SharedRole.Maker : SharedRole.Taker + }).ToArray(), nextToken); + } + + EndpointOptions ISpotOrderRestClient.CancelSpotOrderOptions { get; } = new EndpointOptions(true); + async Task> ISpotOrderRestClient.CancelSpotOrderAsync(CancelOrderRequest request, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).CancelSpotOrderOptions.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, ct: ct).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult(Exchange, null, default); + + return order.AsExchangeResult(Exchange, TradingMode.Spot, new SharedId(order.Data.Id.ToString())); + } + + private Enums.TimeInForce? GetTimeInForce(SharedTimeInForce? tif, SharedOrderType type) + { + if (tif == SharedTimeInForce.FillOrKill) return TimeInForce.FillOrKill; + if (tif == SharedTimeInForce.ImmediateOrCancel) return TimeInForce.ImmediateOrCancel; + if (tif == SharedTimeInForce.GoodTillCanceled) return TimeInForce.GoodTillCanceled; + if (type == SharedOrderType.Limit) return TimeInForce.GoodTillCanceled; // Limit orders 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(SpotOrderType type) + { + if (type == SpotOrderType.Market) return SharedOrderType.Market; + if (type == SpotOrderType.LimitMaker) return SharedOrderType.LimitMaker; + if (type == SpotOrderType.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 Asset client + EndpointOptions IAssetsRestClient.GetAssetsOptions { get; } = new EndpointOptions(true); + + async Task>> IAssetsRestClient.GetAssetsAsync(GetAssetsRequest request, CancellationToken ct) + { + var validationError = ((IAssetsRestClient)this).GetAssetsOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + var assets = await Account.GetUserAssetsAsync(ct: ct).ConfigureAwait(false); + if (!assets) + return assets.AsExchangeResult>(Exchange, null, default); + + return assets.AsExchangeResult>(Exchange, TradingMode.Spot, assets.Data.Select(x => new SharedAsset(x.Asset) + { + FullName = x.Name, + Networks = x.NetworkList.Select(x => new SharedAssetNetwork(x.Network) + { + FullName = x.Name, + MinConfirmations = x.MinConfirmations, + DepositEnabled = x.DepositEnabled, + MinWithdrawQuantity = x.WithdrawMin, + MaxWithdrawQuantity = x.WithdrawMax, + WithdrawEnabled = x.WithdrawEnabled, + WithdrawFee = x.WithdrawFee, + }) + }).ToArray()); + } + + EndpointOptions IAssetsRestClient.GetAssetOptions { get; } = new EndpointOptions(false); + async Task> IAssetsRestClient.GetAssetAsync(GetAssetRequest request, CancellationToken ct) + { + var validationError = ((IAssetsRestClient)this).GetAssetOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult(Exchange, validationError); + + var assets = await Account.GetUserAssetsAsync(ct: ct).ConfigureAwait(false); + if (!assets) + return assets.AsExchangeResult(Exchange, null, default); + + var asset = assets.Data.SingleOrDefault(x => x.Asset.Equals(request.Asset, StringComparison.InvariantCultureIgnoreCase)); + if (asset == null) + return assets.AsExchangeError(Exchange, new ServerError("Asset not found")); + + return assets.AsExchangeResult(Exchange, TradingMode.Spot, new SharedAsset(asset.Asset) + { + FullName = asset.Name, + Networks = asset.NetworkList.Select(x => new SharedAssetNetwork(x.Network) + { + FullName = x.Name, + MinConfirmations = x.MinConfirmations, + DepositEnabled = x.DepositEnabled, + MinWithdrawQuantity = x.WithdrawMin, + MaxWithdrawQuantity = x.WithdrawMax, + WithdrawEnabled = x.WithdrawEnabled, + WithdrawFee = x.WithdrawFee + }) + }); + } + + #endregion + + #region Deposit client + + EndpointOptions IDepositRestClient.GetDepositAddressesOptions { get; } = new EndpointOptions(true); + async Task>> IDepositRestClient.GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct) + { + var validationError = ((IDepositRestClient)this).GetDepositAddressesOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + var depositAddresses = await Account.GetDepositAddressAsync(request.Asset, request.Network, ct: ct).ConfigureAwait(false); + if (!depositAddresses) + return depositAddresses.AsExchangeResult>(Exchange, null, default); + + return depositAddresses.AsExchangeResult>(Exchange, TradingMode.Spot, new[] { new SharedDepositAddress(depositAddresses.Data.Asset, depositAddresses.Data.Address) + { + TagOrMemo = depositAddresses.Data.Tag + } + }); + } + + GetDepositsOptions IDepositRestClient.GetDepositsOptions { get; } = new GetDepositsOptions(SharedPaginationSupport.Descending, true); + async Task>> IDepositRestClient.GetDepositsAsync(GetDepositsRequest request, INextPageToken? pageToken, CancellationToken ct) + { + var validationError = ((IDepositRestClient)this).GetDepositsOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + // Determine page token + int? offset = null; + if (pageToken is OffsetToken offsetToken) + offset = offsetToken.Offset; + + // Get data + var deposits = await Account.GetDepositHistoryAsync( + request.Asset, + startTime: request.StartTime, + endTime: request.EndTime, + limit: request.Limit ?? 100, + offset: offset, + ct: ct).ConfigureAwait(false); + if (!deposits) + return deposits.AsExchangeResult>(Exchange, null, default); + + // Determine next token + OffsetToken? nextToken = null; + if (deposits.Data.Count() == (request.Limit ?? 100)) + nextToken = new OffsetToken((offset ?? 0) + deposits.Data.Count()); + + return deposits.AsExchangeResult>(Exchange, TradingMode.Spot, deposits.Data.Select(x => new SharedDeposit(x.Asset, x.Quantity, x.Status == DepositStatus.Success, x.InsertTime) + { + Confirmations = x.Confirmations.Contains("/") ? int.Parse(x.Confirmations.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries)[0]) : null, + Network = x.Network, + TransactionId = x.TransactionId, + Tag = x.AddressTag, + Id = x.Id + }).ToArray(), nextToken); + } + + #endregion + + #region Withdrawal client + + GetWithdrawalsOptions IWithdrawalRestClient.GetWithdrawalsOptions { get; } = new GetWithdrawalsOptions(SharedPaginationSupport.Descending, true); + async Task>> IWithdrawalRestClient.GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? pageToken, CancellationToken ct) + { + var validationError = ((IWithdrawalRestClient)this).GetWithdrawalsOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + // Determine page token + int? offset = null; + if (pageToken is OffsetToken offsetToken) + offset = offsetToken.Offset; + + // Get data + var withdrawals = await Account.GetWithdrawalHistoryAsync( + request.Asset, + startTime: request.StartTime, + endTime: request.EndTime, + limit: request.Limit ?? 100, + offset: offset, + ct: ct).ConfigureAwait(false); + if (!withdrawals) + return withdrawals.AsExchangeResult>(Exchange, null, default); + + // Determine next token + OffsetToken nextToken; + if (withdrawals.Data.Count() == (request.Limit ?? 100)) + nextToken = new OffsetToken((offset ?? 0) + withdrawals.Data.Count()); + + return withdrawals.AsExchangeResult>(Exchange, TradingMode.Spot, withdrawals.Data.Select(x => new SharedWithdrawal(x.Asset, x.Address, x.Quantity, x.Status == WithdrawalStatus.Completed, x.ApplyTime) + { + Confirmations = x.ConfirmTimes, + Network = x.Network, + Tag = x.AddressTag, + TransactionId = x.TransactionId, + Fee = x.TransactionFee, + Id = x.Id + }).ToArray()); + } + + #endregion + + #region Withdraw client + + WithdrawOptions IWithdrawRestClient.WithdrawOptions { get; } = new WithdrawOptions(); + async Task > IWithdrawRestClient.WithdrawAsync(WithdrawRequest request, CancellationToken ct) + { + var validationError = ((IWithdrawRestClient)this).WithdrawOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult(Exchange, validationError); + + // Get data + var withdrawal = await Account.WithdrawAsync( + request.Asset, + request.Address, + request.Quantity, + network: request.Network, + addressTag: request.AddressTag, + ct: ct).ConfigureAwait(false); + if (!withdrawal) + return withdrawal.AsExchangeResult(Exchange, null, default); + + return withdrawal.AsExchangeResult(Exchange, TradingMode.Spot, new SharedId(withdrawal.Data.Id)); + } + + #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, TradingMode.Spot, 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, TradingMode.Spot, 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, TradingMode.Spot, request.ListenKey); + } + #endregion + } +} diff --git a/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApi.cs b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApi.cs index 679c57681..77d7e18ff 100644 --- a/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApi.cs +++ b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApi.cs @@ -9,12 +9,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.SpotApi { /// - internal class BinanceSocketClientSpotApi : SocketApiClient, IBinanceSocketClientSpotApi + internal partial class BinanceSocketClientSpotApi : SocketApiClient, IBinanceSocketClientSpotApi { #region fields /// @@ -59,8 +60,10 @@ internal BinanceSocketClientSpotApi(ILogger logger, BinanceSocketOptions options } #endregion + public IBinanceSocketClientSpotApiShared 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) => baseAsset.ToUpperInvariant() + quoteAsset.ToUpperInvariant(); /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) diff --git a/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiAccount.cs b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiAccount.cs index a3f7e078a..3920aca6f 100644 --- a/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiAccount.cs +++ b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiAccount.cs @@ -118,3 +118,4 @@ public async Task> SubscribeToUserDataUpdatesAsyn #endregion } } + \ No newline at end of file diff --git a/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiExchangeData.cs b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiExchangeData.cs index c432c9d7d..f93a358ed 100644 --- a/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiExchangeData.cs +++ b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiExchangeData.cs @@ -239,7 +239,7 @@ public async Task> SubscribeToTradeUpdatesAsync(I return await _client.SubscribeAsync(_client.BaseAddress, symbols, handler, ct).ConfigureAwait(false); } - #endregion + #endregion #region Aggregate Trade Streams diff --git a/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiShared.cs b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiShared.cs new file mode 100644 index 000000000..4ff04b215 --- /dev/null +++ b/Binance.Net/Clients/SpotApi/BinanceSocketClientSpotApiShared.cs @@ -0,0 +1,183 @@ +using Binance.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Clients.SpotApi +{ + internal partial class BinanceSocketClientSpotApi : IBinanceSocketClientSpotApiShared + { + 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 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 ExchangeData.SubscribeToAllTickerUpdatesAsync(update => handler(update.AsExchangeEvent>(Exchange, update.Data.Select(x => new SharedSpotTicker(x.Symbol, x.LastPrice, x.HighPrice, x.LowPrice, x.Volume, x.PriceChangePercent)).ToArray())), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + + #endregion + + #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 ExchangeData.SubscribeToTickerUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, new SharedSpotTicker(update.Data.Symbol, update.Data.LastPrice, update.Data.HighPrice, update.Data.LowPrice, update.Data.Volume, update.Data.PriceChangePercent))), 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 ExchangeData.SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent>(Exchange, new[] { new SharedTrade(update.Data.Quantity, update.Data.Price, update.Data.TradeTime) })), 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 ExchangeData.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 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 Account.SubscribeToUserDataUpdatesAsync(request.ListenKey!, + onAccountPositionMessage: update => handler(update.AsExchangeEvent>(Exchange, update.Data.Balances.Select(x => new SharedBalance(x.Asset, x.Available, x.Total)).ToArray())), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + + #endregion + + #region Spot Order client + + EndpointOptions ISpotOrderSocketClient.SubscribeSpotOrderOptions { get; } = new EndpointOptions(false) + { + RequiredOptionalParameters = new List + { + new ParameterDescription(nameof(SubscribeSpotOrderRequest.ListenKey), typeof(string), "Listenkey for the user stream", "123123123") + } + }; + async Task> ISpotOrderSocketClient.SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action>> handler, CancellationToken ct) + { + var validationError = ((ISpotOrderSocketClient)this).SubscribeSpotOrderOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var result = await Account.SubscribeToUserDataUpdatesAsync(request.ListenKey!, + onOrderUpdateMessage: update => handler(update.AsExchangeEvent>(Exchange, new[] { + new SharedSpotOrder( + update.Data.Symbol, + update.Data.Id.ToString(), + update.Data.Type == Enums.SpotOrderType.Limit ? SharedOrderType.Limit : update.Data.Type == Enums.SpotOrderType.Market ? SharedOrderType.Market : update.Data.Type == Enums.SpotOrderType.LimitMaker ? SharedOrderType.LimitMaker : SharedOrderType.Other, + update.Data.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + update.Data.Status == Enums.OrderStatus.Canceled ? SharedOrderStatus.Canceled : (update.Data.Status == Enums.OrderStatus.New || update.Data.Status == Enums.OrderStatus.PartiallyFilled) ? SharedOrderStatus.Open : SharedOrderStatus.Filled, + update.Data.CreateTime) + { + ClientOrderId = update.Data.ClientOrderId, + OrderPrice = update.Data.Price, + Quantity = update.Data.Quantity, + QuantityFilled = update.Data.QuantityFilled, + QuoteQuantity = update.Data.QuoteQuantity, + QuoteQuantityFilled = update.Data.QuoteQuantityFilled, + UpdateTime = update.Data.UpdateTime, + Fee = update.Data.Fee, + FeeAsset = update.Data.FeeAsset, + TimeInForce = update.Data.TimeInForce == Enums.TimeInForce.ImmediateOrCancel ? SharedTimeInForce.ImmediateOrCancel : update.Data.TimeInForce == Enums.TimeInForce.FillOrKill ? SharedTimeInForce.FillOrKill : SharedTimeInForce.GoodTillCanceled, + LastTrade = update.Data.LastQuantityFilled == 0 ? null : new SharedUserTrade(update.Data.Symbol, update.Data.Id.ToString(), update.Data.TradeId.ToString(), update.Data.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, update.Data.LastQuantityFilled, update.Data.LastPriceFilled, update.Data.UpdateTime) + { + Role = update.Data.BuyerIsMaker ? SharedRole.Maker : SharedRole.Taker + } + } + })), + ct: 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 ExchangeData.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 ExchangeData.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 + } +} diff --git a/Binance.Net/Clients/UsdFuturesApi/BinanceRestClientUsdFuturesApi.cs b/Binance.Net/Clients/UsdFuturesApi/BinanceRestClientUsdFuturesApi.cs index c57c0a34a..d7749bb67 100644 --- a/Binance.Net/Clients/UsdFuturesApi/BinanceRestClientUsdFuturesApi.cs +++ b/Binance.Net/Clients/UsdFuturesApi/BinanceRestClientUsdFuturesApi.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.UsdFuturesApi { /// - internal class BinanceRestClientUsdFuturesApi : RestApiClient, IBinanceRestClientUsdFuturesApi, IFuturesClient + internal partial class BinanceRestClientUsdFuturesApi : RestApiClient, IBinanceRestClientUsdFuturesApi, IFuturesClient { #region fields /// @@ -49,7 +50,10 @@ internal class BinanceRestClientUsdFuturesApi : RestApiClient, IBinanceRestClien public event Action? OnOrderCanceled; /// - 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 ? string.Empty : "_" + deliverTime.Value.ToString("yyMMdd")); + } #region constructor/destructor internal BinanceRestClientUsdFuturesApi(ILogger logger, HttpClient? httpClient, BinanceRestOptions options) @@ -279,6 +283,7 @@ protected override Task> GetServerTimestampAsync() /// public IFuturesClient CommonFuturesClient => this; + public IBinanceRestClientUsdFuturesApiShared SharedClient => this; internal void InvokeOrderPlaced(OrderId id) { @@ -291,8 +296,10 @@ internal void InvokeOrderCanceled(OrderId id) } /// - public string GetSymbolName(string baseAsset, string quoteAsset) => - (baseAsset + quoteAsset).ToUpper(CultureInfo.InvariantCulture); + public string GetSymbolName(string baseAsset, string quoteAsset) + { + return (baseAsset + quoteAsset).ToUpper(CultureInfo.InvariantCulture); + } async Task> IFuturesClient.PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price, int? leverage, string? accountId, string? clientOrderId, CancellationToken ct) { diff --git a/Binance.Net/Clients/UsdFuturesApi/BinanceRestClientUsdFuturesApiShared.cs b/Binance.Net/Clients/UsdFuturesApi/BinanceRestClientUsdFuturesApiShared.cs new file mode 100644 index 000000000..f241094f3 --- /dev/null +++ b/Binance.Net/Clients/UsdFuturesApi/BinanceRestClientUsdFuturesApiShared.cs @@ -0,0 +1,890 @@ +using Binance.Net.Enums; +using Binance.Net.Interfaces.Clients.UsdFuturesApi; +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Clients.UsdFuturesApi +{ + internal partial class BinanceRestClientUsdFuturesApi : IBinanceRestClientUsdFuturesApiShared + { + public string Exchange => BinanceExchange.ExchangeName; + + public TradingMode[] SupportedTradingModes => new[] { TradingMode.DeliveryLinear, TradingMode.PerpetualLinear }; + 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 - 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 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 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.PerpetualLinear ? 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.PerpetualLinear : SharedSymbolType.DeliveryLinear, 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, + MinNotionalValue = s.MinNotionalFilter?.MinNotional, + ContractSize = 1, + 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.GetTickerAsync(request.Symbol.GetSymbol(FormatSymbol), ct); + var resultMarkPrice = ExchangeData.GetMarkPriceAsync(request.Symbol.GetSymbol(FormatSymbol), 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); + + return resultTicker.Result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedFuturesTicker(resultTicker.Result.Data.Symbol, resultTicker.Result.Data.LastPrice, resultTicker.Result.Data.HighPrice, resultTicker.Result.Data.LowPrice, resultTicker.Result.Data.Volume, resultTicker.Result.Data.PriceChangePercent) + { + MarkPrice = resultMarkPrice.Result.Data.MarkPrice, + IndexPrice = resultMarkPrice.Result.Data.IndexPrice, + FundingRate = resultMarkPrice.Result.Data.FundingRate, + NextFundingTime = resultMarkPrice.Result.Data.NextFundingTime == default ? null : resultMarkPrice.Result.Data.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.HasValue) + data = data.Where(x => (request.TradingMode == TradingMode.DeliveryLinear ? x.Symbol.Contains("_") : !x.Symbol.Contains("_"))); + + return resultTickers.Result.AsExchangeResult>(Exchange, request.TradingMode == null ? SupportedTradingModes : new[] { request.TradingMode.Value }, 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 == default ? null : 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.QuoteAsset; + + 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: fromTimestamp ?? request.StartTime, + endTime: request.EndTime, + limit: request.Limit ?? 1000, + ct: ct).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + // Get next token + DateTimeToken? nextToken = null; + if (orders.Data.Count() == (request.Limit ?? 1000)) + nextToken = new DateTimeToken(orders.Data.Max(o => o.CreateTime)); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode, 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, ct: ct).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 result = await Account.GetPositionInformationAsync(symbol: request.Symbol?.GetSymbol(FormatSymbol), ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + var data = result.Data; + if (request.TradingMode.HasValue) + data = data.Where(x => request.TradingMode == TradingMode.DeliveryLinear ? x.Symbol.Contains("_") : !x.Symbol.Contains("_")); + + var resultTypes = request.Symbol == null && request.TradingMode == null ? SupportedTradingModes : request.Symbol != null ? new[] { request.Symbol.TradingMode } : new[] { request.TradingMode!.Value }; + return result.AsExchangeResult>(Exchange, resultTypes, 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 result = await Account.GetPositionInformationAsync(symbol: request.Symbol.GetSymbol(FormatSymbol), ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + if (!result.Data.Any()) + return result.AsExchangeError(Exchange, new ServerError("Not found")); + + return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedLeverage(result.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 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(true); + 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/UsdFuturesApi/BinanceSocketClientUsdFuturesApi.cs b/Binance.Net/Clients/UsdFuturesApi/BinanceSocketClientUsdFuturesApi.cs index 6191d4f84..58134f350 100644 --- a/Binance.Net/Clients/UsdFuturesApi/BinanceSocketClientUsdFuturesApi.cs +++ b/Binance.Net/Clients/UsdFuturesApi/BinanceSocketClientUsdFuturesApi.cs @@ -11,6 +11,7 @@ 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.UsdFuturesApi @@ -18,7 +19,7 @@ namespace Binance.Net.Clients.UsdFuturesApi /// /// Client providing access to the Binance Usd futures websocket Api /// - internal class BinanceSocketClientUsdFuturesApi : SocketApiClient, IBinanceSocketClientUsdFuturesApi + internal partial class BinanceSocketClientUsdFuturesApi : SocketApiClient, IBinanceSocketClientUsdFuturesApi { #region fields private const string _klineStreamEndpoint = "@kline"; @@ -66,11 +67,14 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden => new BinanceAuthenticationProvider(credentials); /// - 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 ? string.Empty: "_" + deliverTime.Value.ToString("yyMMdd")); + } protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); - protected override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(); + public IBinanceSocketClientUsdFuturesApiShared SharedClient => this; #region Mark Price Stream diff --git a/Binance.Net/Clients/UsdFuturesApi/BinanceSocketClientUsdFuturesApiShared.cs b/Binance.Net/Clients/UsdFuturesApi/BinanceSocketClientUsdFuturesApiShared.cs new file mode 100644 index 000000000..87f9f3177 --- /dev/null +++ b/Binance.Net/Clients/UsdFuturesApi/BinanceSocketClientUsdFuturesApiShared.cs @@ -0,0 +1,226 @@ +using Binance.Net.Interfaces.Clients.SpotApi; +using Binance.Net.Interfaces.Clients.UsdFuturesApi; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Clients.UsdFuturesApi +{ + internal partial class BinanceSocketClientUsdFuturesApi : IBinanceSocketClientUsdFuturesApiShared + { + public string Exchange => BinanceExchange.ExchangeName; + public TradingMode[] SupportedTradingModes => new[] { TradingMode.DeliveryLinear, TradingMode.PerpetualLinear }; + + 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.HighPrice, update.Data.LowPrice, 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 = data.Where(x => request.TradingMode == TradingMode.PerpetualLinear ? !x.Symbol.Contains("_") : x.Symbol.Contains("_")); + + if (!data.Any()) + return; + + handler(update.AsExchangeEvent>(Exchange, data.Select(x => new SharedSpotTicker(x.Symbol, x.LastPrice, x.HighPrice, x.LowPrice, 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 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 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 + + #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 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 + } +} diff --git a/Binance.Net/ExtensionMethods/ServiceCollectionExtensions.cs b/Binance.Net/ExtensionMethods/ServiceCollectionExtensions.cs index a1dec4c8a..1bca891a7 100644 --- a/Binance.Net/ExtensionMethods/ServiceCollectionExtensions.cs +++ b/Binance.Net/ExtensionMethods/ServiceCollectionExtensions.cs @@ -61,6 +61,14 @@ public static IServiceCollection AddBinance( services.AddTransient(x => x.GetRequiredService().SpotApi.CommonSpotClient); services.AddTransient(x => x.GetRequiredService().UsdFuturesApi.CommonFuturesClient); services.AddTransient(x => x.GetRequiredService().CoinFuturesApi.CommonFuturesClient); + + services.RegisterSharedRestInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); + services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); + services.RegisterSharedRestInterfaces(x => x.GetRequiredService().UsdFuturesApi.SharedClient); + services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().UsdFuturesApi.SharedClient); + services.RegisterSharedRestInterfaces(x => x.GetRequiredService().CoinFuturesApi.SharedClient); + services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().CoinFuturesApi.SharedClient); + if (socketClientLifeTime == null) services.AddSingleton(); else diff --git a/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceRestClientCoinFuturesApi.cs b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceRestClientCoinFuturesApi.cs index 6cd97fc8b..3f9336248 100644 --- a/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceRestClientCoinFuturesApi.cs +++ b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceRestClientCoinFuturesApi.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Interfaces.CommonClients; +using Binance.Net.Interfaces.Clients.UsdFuturesApi; +using CryptoExchange.Net.Interfaces.CommonClients; namespace Binance.Net.Interfaces.Clients.CoinFuturesApi { @@ -23,9 +24,14 @@ public interface IBinanceRestClientCoinFuturesApi : IRestApiClient, IDisposable public IBinanceRestClientCoinFuturesApiTrading Trading { get; } /// - /// 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. /// - /// public IFuturesClient CommonFuturesClient { get; } + + /// + /// Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + public IBinanceRestClientCoinFuturesApiShared SharedClient { get; } + } } diff --git a/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceRestClientCoinFuturesApiShared.cs b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceRestClientCoinFuturesApiShared.cs new file mode 100644 index 000000000..e0816db13 --- /dev/null +++ b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceRestClientCoinFuturesApiShared.cs @@ -0,0 +1,26 @@ +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Interfaces.Clients.CoinFuturesApi +{ + /// + /// Shared interface for COIN-M Futures rest API usage + /// + public interface IBinanceRestClientCoinFuturesApiShared : + IFuturesTickerRestClient, + IFuturesSymbolRestClient, + IFuturesOrderRestClient, + IKlineRestClient, + IRecentTradeRestClient, + ITradeHistoryRestClient, + ILeverageRestClient, + IMarkPriceKlineRestClient, + IIndexPriceKlineRestClient, + IOrderBookRestClient, + IOpenInterestRestClient, + IFundingRateRestClient, + IBalanceRestClient, + IPositionModeRestClient, + IListenKeyRestClient + { + } +} diff --git a/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceSocketClientCoinFuturesApi.cs b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceSocketClientCoinFuturesApi.cs index c6141f449..5874fb35d 100644 --- a/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceSocketClientCoinFuturesApi.cs +++ b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceSocketClientCoinFuturesApi.cs @@ -11,6 +11,11 @@ namespace Binance.Net.Interfaces.Clients.CoinFuturesApi /// public interface IBinanceSocketClientCoinFuturesApi : ISocketApiClient, IDisposable { + /// + /// Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + IBinanceSocketClientCoinFuturesApiShared SharedClient { get; } + /// /// Subscribes to the aggregated trades update stream for the provided symbol /// diff --git a/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceSocketClientCoinFuturesApiShared.cs b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceSocketClientCoinFuturesApiShared.cs new file mode 100644 index 000000000..19fcbc3de --- /dev/null +++ b/Binance.Net/Interfaces/Clients/CoinFuturesApi/IBinanceSocketClientCoinFuturesApiShared.cs @@ -0,0 +1,20 @@ +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Interfaces.Clients.CoinFuturesApi +{ + /// + /// Shared interface for COIN-M Futures socket API usage + /// + public interface IBinanceSocketClientCoinFuturesApiShared : + ITickerSocketClient, + ITickersSocketClient, + ITradeSocketClient, + IBookTickerSocketClient, + IOrderBookSocketClient, + IKlineSocketClient, + IFuturesOrderSocketClient, + IBalanceSocketClient, + IPositionSocketClient + { + } +} diff --git a/Binance.Net/Interfaces/Clients/SpotApi/IBinanceRestClientSpotApi.cs b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceRestClientSpotApi.cs index dc271add3..87e8f180a 100644 --- a/Binance.Net/Interfaces/Clients/SpotApi/IBinanceRestClientSpotApi.cs +++ b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceRestClientSpotApi.cs @@ -23,9 +23,13 @@ public interface IBinanceRestClientSpotApi : IRestApiClient, IDisposable public IBinanceRestClientSpotApiTrading Trading { get; } /// - /// 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. /// - /// public ISpotClient CommonSpotClient { get; } + + /// + /// Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + public IBinanceRestClientSpotApiShared SharedClient { get; } } } diff --git a/Binance.Net/Interfaces/Clients/SpotApi/IBinanceRestClientSpotApiShared.cs b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceRestClientSpotApiShared.cs new file mode 100644 index 000000000..941b9d5f8 --- /dev/null +++ b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceRestClientSpotApiShared.cs @@ -0,0 +1,24 @@ +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Spot rest API usage + /// + public interface IBinanceRestClientSpotApiShared: + IAssetsRestClient, + IBalanceRestClient, + IDepositRestClient, + IKlineRestClient, + IOrderBookRestClient, + IRecentTradeRestClient, + ISpotOrderRestClient, + ISpotSymbolRestClient, + ISpotTickerRestClient, + ITradeHistoryRestClient, + IWithdrawalRestClient, + IWithdrawRestClient, + IListenKeyRestClient + { + } +} diff --git a/Binance.Net/Interfaces/Clients/SpotApi/IBinanceSocketClientSpotApi.cs b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceSocketClientSpotApi.cs index 9dfddd4d0..c90ac9f69 100644 --- a/Binance.Net/Interfaces/Clients/SpotApi/IBinanceSocketClientSpotApi.cs +++ b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceSocketClientSpotApi.cs @@ -17,5 +17,10 @@ public interface IBinanceSocketClientSpotApi : ISocketApiClient /// Trading data and queries /// IBinanceSocketClientSpotApiTrading Trading { get; } + + /// + /// Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + IBinanceSocketClientSpotApiShared SharedClient { get; } } } \ No newline at end of file diff --git a/Binance.Net/Interfaces/Clients/SpotApi/IBinanceSocketClientSpotApiShared.cs b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceSocketClientSpotApiShared.cs new file mode 100644 index 000000000..54bc39410 --- /dev/null +++ b/Binance.Net/Interfaces/Clients/SpotApi/IBinanceSocketClientSpotApiShared.cs @@ -0,0 +1,19 @@ +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Spot socket API usage + /// + public interface IBinanceSocketClientSpotApiShared : + ITickerSocketClient, + ITickersSocketClient, + ISpotOrderSocketClient, + ITradeSocketClient, + IBookTickerSocketClient, + IBalanceSocketClient, + IKlineSocketClient, + IOrderBookSocketClient + { + } +} diff --git a/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceRestClientUsdFuturesApi.cs b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceRestClientUsdFuturesApi.cs index b32f9c7c9..7f01588a7 100644 --- a/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceRestClientUsdFuturesApi.cs +++ b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceRestClientUsdFuturesApi.cs @@ -23,9 +23,13 @@ public interface IBinanceRestClientUsdFuturesApi : IRestApiClient, IDisposable public IBinanceRestClientUsdFuturesApiTrading Trading { get; } /// - /// 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. /// - /// public IFuturesClient CommonFuturesClient { get; } + + /// + /// Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + public IBinanceRestClientUsdFuturesApiShared SharedClient { get; } } } diff --git a/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceRestClientUsdFuturesApiShared.cs b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceRestClientUsdFuturesApiShared.cs new file mode 100644 index 000000000..00f53974f --- /dev/null +++ b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceRestClientUsdFuturesApiShared.cs @@ -0,0 +1,26 @@ +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Interfaces.Clients.UsdFuturesApi +{ + /// + /// Shared interface for USD-M Futures rest API usage + /// + public interface IBinanceRestClientUsdFuturesApiShared : + IBalanceRestClient, + IFuturesTickerRestClient, + IFuturesSymbolRestClient, + IFuturesOrderRestClient, + IKlineRestClient, + IRecentTradeRestClient, + ITradeHistoryRestClient, + ILeverageRestClient, + IMarkPriceKlineRestClient, + IIndexPriceKlineRestClient, + IOrderBookRestClient, + IOpenInterestRestClient, + IFundingRateRestClient, + IPositionModeRestClient, + IListenKeyRestClient + { + } +} diff --git a/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceSocketClientUsdFuturesApi.cs b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceSocketClientUsdFuturesApi.cs index e43b4617a..deb74659d 100644 --- a/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceSocketClientUsdFuturesApi.cs +++ b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceSocketClientUsdFuturesApi.cs @@ -11,6 +11,11 @@ namespace Binance.Net.Interfaces.Clients.UsdFuturesApi /// public interface IBinanceSocketClientUsdFuturesApi : ISocketApiClient, IDisposable { + /// + /// Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + IBinanceSocketClientUsdFuturesApiShared SharedClient { get; } + /// /// Subscribes to the aggregated trades update stream for the provided symbol /// diff --git a/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceSocketClientUsdFuturesApiShared.cs b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceSocketClientUsdFuturesApiShared.cs new file mode 100644 index 000000000..a5e364d0a --- /dev/null +++ b/Binance.Net/Interfaces/Clients/UsdFuturesApi/IBinanceSocketClientUsdFuturesApiShared.cs @@ -0,0 +1,20 @@ +using CryptoExchange.Net.SharedApis; + +namespace Binance.Net.Interfaces.Clients.UsdFuturesApi +{ + /// + /// Shared interface for USD-M Futures socket API usage + /// + public interface IBinanceSocketClientUsdFuturesApiShared: + ITickerSocketClient, + ITickersSocketClient, + ITradeSocketClient, + IBookTickerSocketClient, + IOrderBookSocketClient, + IKlineSocketClient, + IBalanceSocketClient, + IPositionSocketClient, + IFuturesOrderSocketClient + { + } +} diff --git a/Binance.Net/Objects/Models/Spot/BinanceWithdrawalPlaced.cs b/Binance.Net/Objects/Models/Spot/BinanceWithdrawalPlaced.cs index 8c03bb43b..a00b89b84 100644 --- a/Binance.Net/Objects/Models/Spot/BinanceWithdrawalPlaced.cs +++ b/Binance.Net/Objects/Models/Spot/BinanceWithdrawalPlaced.cs @@ -9,6 +9,6 @@ public record BinanceWithdrawalPlaced /// The id /// [JsonPropertyName("id")] - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; } }