diff --git a/BingX.Net.UnitTests/Subscriptions/Spot/Ticker.txt b/BingX.Net.UnitTests/Subscriptions/Spot/Ticker.txt index 7b2e69a..ad72a30 100644 --- a/BingX.Net.UnitTests/Subscriptions/Spot/Ticker.txt +++ b/BingX.Net.UnitTests/Subscriptions/Spot/Ticker.txt @@ -1,4 +1,4 @@ > { "id": "|1|", "reqType": "sub", "dataType": "BTC-USDT@ticker" } < { "id": "|1|", "code": 0, "msg": "" } = -{"code":0,"timestamp":1714641292823,"data":{"e":"24hTicker","E":1714641292821,"s":"BTC-USDT","p":458.94,"P":"0.80%","o":57341.78,"h":59464.03,"l":56633.32,"c":57800.72,"v":2012.11,"q":115994192.64,"O":1714554892821,"C":1714641292821,"A":57800.78,"a":1.62801,"B":57800.27,"b":1.59514}} \ No newline at end of file +{"code":0,"timestamp":1727163468055,"dataType":"BTC-USDT@ticker","data":{"e":"24hTicker","E":1727163468053,"s":"ETH-USDT","p":-2.29,"P":"-0.09%","o":2644.56,"h":2702.79,"l":2609.68,"c":2642.27,"v":13865.37,"q":36747069.40,"O":1727077068053,"C":1727163468053,"A":2642.30,"a":21.552,"B":2642.23,"b":3.136416}} \ No newline at end of file diff --git a/BingX.Net/BingX.Net.csproj b/BingX.Net/BingX.Net.csproj index b56a98b..866272d 100644 --- a/BingX.Net/BingX.Net.csproj +++ b/BingX.Net/BingX.Net.csproj @@ -31,7 +31,7 @@ true - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,6 +52,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + \ No newline at end of file diff --git a/BingX.Net/BingX.Net.xml b/BingX.Net/BingX.Net.xml index bf6276d..100f515 100644 --- a/BingX.Net/BingX.Net.xml +++ b/BingX.Net/BingX.Net.xml @@ -158,7 +158,7 @@ - + @@ -320,6 +320,9 @@ + + + @@ -342,7 +345,7 @@ ctor - + @@ -421,7 +424,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 - + @@ -544,7 +547,7 @@ - + @@ -571,7 +574,7 @@ - + @@ -602,7 +605,7 @@ - + @@ -1124,6 +1127,21 @@ Both + + + Role + + + + + Maker + + + + + Taker + + Status of a symbol @@ -1356,6 +1374,11 @@ Endpoints related to orders and trades + + + Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + + BingX futures account endpoints. Account endpoints include balance info, withdraw/deposit info and requesting and account settings @@ -1437,7 +1460,7 @@ - Get the current leverage setrings for a symbol + Get the current leverage settings for a symbol Symbol, for example `ETH-USDT` @@ -1831,6 +1854,20 @@ Cancellation token + + + Get user trade history + + + The symbol + Filter by order id + Filter by start time + Filter by end time + Return results after this id + Max number of results + Cancellation token + + Cancel all order after a set period. Can be called contineously to maintain a rolling timeout @@ -1862,7 +1899,7 @@ Get position close history - + The symbol, for example `ETH-USDT` Filter by position id @@ -1877,6 +1914,11 @@ BingX futures streams + + + Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + + Subscribe to live trade updates @@ -1993,6 +2035,16 @@ 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 Perpetual futures rest API usage + + + + + Shared interface for Perpetual futures socket API usage + + BingX Spot API endpoints @@ -2015,9 +2067,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. - @@ -2316,12 +2372,17 @@ Cancellation token + + + Shared interface for Spot rest API usage + + BingX Spot trading endpoints, placing and managing orders. - + Place a new order @@ -2334,6 +2395,7 @@ Order quantity in quote asset Stop price Client order id + Time in force Cancellation token @@ -2424,7 +2486,7 @@ Cancellation token - + Get user trade history @@ -2497,6 +2559,11 @@ BingX spot streams + + + Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + + Subscribe to live trade updates @@ -2579,6 +2646,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 + + BingX local order book factory @@ -3152,7 +3224,7 @@ - 0: Online, 1:Offline. Different unknown values have been observed. + 1: Online, 25:Suspended. Different unknown values have been observed. @@ -4510,6 +4582,71 @@ Position side + + + User trade info + + + + + Symbol + + + + + Order id + + + + + Trade id + + + + + Trade price + + + + + Quantity + + + + + Value + + + + + Fee paid + + + + + Fee asset + + + + + Trade time + + + + + Trade side + + + + + Position side + + + + + Trade role + + Id result diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs index 1aca7fc..b663068 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs @@ -14,11 +14,13 @@ using BingX.Net.Objects.Internal; using CryptoExchange.Net.Converters.MessageParsing; using BingX.Net.Interfaces.Clients.PerpetualFuturesApi; +using BingX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.SharedApis; namespace BingX.Net.Clients.PerpetualFuturesApi { /// - internal class BingXRestClientPerpetualFuturesApi : RestApiClient, IBingXRestClientPerpetualFuturesApi + internal partial class BingXRestClientPerpetualFuturesApi : RestApiClient, IBingXRestClientPerpetualFuturesApi { #region fields internal static TimeSyncState _timeSyncState = new TimeSyncState("Perpetual Futures Api"); @@ -53,7 +55,8 @@ internal BingXRestClientPerpetualFuturesApi(ILogger logger, HttpClient? httpClie #endregion /// - 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(); + public IBingXRestClientPerpetualFuturesApiShared SharedClient => this; /// protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiAccount.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiAccount.cs index 0cda509..f9e6165 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiAccount.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiAccount.cs @@ -78,6 +78,9 @@ public async Task> GetTradingFeesAsync(Ca /// public async Task> StartUserStreamAsync(CancellationToken ct = default) { + if (_baseClient.AuthenticationProvider == null) + return new WebCallResult(new NoApiCredentialsError()); + var request = _definitions.GetOrCreate(HttpMethod.Post, "/openApi/user/auth/userDataStream", BingXExchange.RateLimiter.RestAccount1, 1, false, limitGuard: new SingleLimitGuard(5, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding, keySelector: SingleLimitGuard.PerApiKey)); var result = await _baseClient.SendRawAsync(request, null, ct).ConfigureAwait(false); @@ -91,6 +94,9 @@ public async Task> StartUserStreamAsync(CancellationToken /// public async Task KeepAliveUserStreamAsync(string listenKey, CancellationToken ct = default) { + if (_baseClient.AuthenticationProvider == null) + return new WebCallResult(new NoApiCredentialsError()); + var parameters = new ParameterCollection { { "listenKey", listenKey } @@ -107,6 +113,9 @@ public async Task KeepAliveUserStreamAsync(string listenKey, Canc /// public async Task StopUserStreamAsync(string listenKey, CancellationToken ct = default) { + if (_baseClient.AuthenticationProvider == null) + return new WebCallResult(new NoApiCredentialsError()); + var parameters = new ParameterCollection { { "listenKey", listenKey } diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiShared.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiShared.cs new file mode 100644 index 0000000..c91f1a3 --- /dev/null +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiShared.cs @@ -0,0 +1,917 @@ +using BingX.Net; +using BingX.Net.Enums; +using BingX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.SharedApis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BingX.Net.Clients.PerpetualFuturesApi +{ + internal partial class BingXRestClientPerpetualFuturesApi : IBingXRestClientPerpetualFuturesApiShared + { + public string Exchange => BingXExchange.ExchangeName; + public TradingMode[] SupportedTradingModes => new[] { 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 ?? RoundDown(DateTime.UtcNow, TimeSpan.FromSeconds((int)interval)); + DateTime? startTime = request.StartTime; + if (pageToken is DateTimeToken dateTimeToken) + endTime = dateTimeToken.LastTime; + + var limit = request.Limit ?? 1440; + 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); + + DateTimeToken? nextToken = null; + if (result.Data.Count() == limit) + { + var minOpenTime = result.Data.Min(x => x.Timestamp); + if (request.StartTime == null || minOpenTime > request.StartTime.Value) + nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)interval)); + } + + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Select(x => new SharedKline(x.Timestamp, 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.GetContractsAsync(ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + return result.AsExchangeResult>(Exchange, SupportedTradingModes, result.Data.Where(x => !string.IsNullOrEmpty(x.Asset)).Select(s => new SharedFuturesSymbol(SharedSymbolType.PerpetualLinear, s.Asset, s.Currency, s.Symbol, s.Status == 1) + { + MinTradeQuantity = s.MinOrderQuantity, + MinNotionalValue = s.MinOrderValue, + PriceDecimals = s.PricePrecision, + QuantityDecimals = s.QuantityPrecision, + ContractSize = 1 + }).ToArray()); + } + + #endregion + + #region Futures 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 resultFunding = ExchangeData.GetFundingRateAsync(request.Symbol.GetSymbol(FormatSymbol), ct); + await Task.WhenAll(resultTicker, resultFunding).ConfigureAwait(false); + if (!resultTicker.Result) + return resultTicker.Result.AsExchangeResult(Exchange, null, default); + if (!resultFunding.Result) + return resultFunding.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) + { + IndexPrice = resultFunding.Result.Data.IndexPrice, + MarkPrice = resultFunding.Result.Data.MarkPrice, + FundingRate = resultFunding.Result.Data.LastFundingRate, + NextFundingTime = resultFunding.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 resultFunding = ExchangeData.GetFundingRatesAsync(ct: ct); + await Task.WhenAll(resultTickers, resultFunding).ConfigureAwait(false); + if (!resultTickers.Result) + return resultTickers.Result.AsExchangeResult>(Exchange, null, default); + if (!resultFunding.Result) + return resultFunding.Result.AsExchangeResult>(Exchange, null, default); + + return resultTickers.Result.AsExchangeResult>(Exchange, SupportedTradingModes, resultTickers.Result.Data.Select(x => + { + var markPrice = resultFunding.Result.Data.Single(p => p.Symbol == x.Symbol); + return new SharedFuturesTicker(x.Symbol, x.LastPrice, x.HighPrice, x.LowPrice, x.Volume, x.PriceChangePercent) + { + MarkPrice = markPrice.MarkPrice, + IndexPrice = markPrice.IndexPrice, + FundingRate = markPrice.LastFundingRate, + 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.Reverse().Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)).ToArray()); + } + + #endregion + + #region Leverage client + SharedLeverageSettingMode ILeverageRestClient.LeverageSettingType => SharedLeverageSettingMode.PerSide; + + 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.GetLeverageAsync(symbol: request.Symbol.GetSymbol(FormatSymbol), ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedLeverage(request.PositionSide == SharedPositionSide.Short ? result.Data.ShortLeverage : result.Data.LongLeverage) { + 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.SetLeverageAsync(symbol: request.Symbol.GetSymbol(FormatSymbol), + request.Side == null ? PositionSide.Both : request.Side == SharedPositionSide.Long ? PositionSide.Long : PositionSide.Short, + (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) + { + Side = request.Side + }); + } + #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 Index Klines client + + GetKlinesOptions IIndexPriceKlineRestClient.GetIndexPriceKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false) + { + MaxRequestDataPoints = 1440 + }; + + 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 ?? RoundDown(DateTime.UtcNow, TimeSpan.FromSeconds((int)interval)); + DateTime? startTime = request.StartTime; + if (pageToken is DateTimeToken dateTimeToken) + endTime = dateTimeToken.LastTime; + + var limit = request.Limit ?? 1440; + 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, + 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.CloseTime); + if (request.StartTime == null || minOpenTime.AddSeconds(-(int)interval) > request.StartTime.Value) + nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)interval)); + } + + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Select(x => new SharedFuturesKline(x.CloseTime.AddSeconds(-(int)interval), x.ClosePrice, x.HighPrice, x.LowPrice, x.OpenPrice)).ToArray(), nextToken); + } + + #endregion + + #region Mark Klines client + + GetKlinesOptions IMarkPriceKlineRestClient.GetMarkPriceKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false) + { + MaxRequestDataPoints = 1440 + }; + + 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 ?? RoundDown(DateTime.UtcNow, TimeSpan.FromSeconds((int)interval)); + DateTime? startTime = request.StartTime; + if (pageToken is DateTimeToken dateTimeToken) + endTime = dateTimeToken.LastTime; + + var limit = request.Limit ?? 1440; + 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, + 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.CloseTime); + if (request.StartTime == null || minOpenTime.AddSeconds(-(int)interval) > request.StartTime.Value) + nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)interval)); + } + + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Select(x => new SharedFuturesKline(x.CloseTime.AddSeconds(-(int)interval), 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.Descending, 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; + + var limit = request.Limit ?? 1000; + // Get data + var result = await ExchangeData.GetFundingRateHistoryAsync( + request.Symbol.GetSymbol(FormatSymbol), + startTime: request.StartTime, + endTime: fromTime ?? request.EndTime, + limit: limit, + ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + DateTimeToken? nextToken = null; + if (result.Data.Count() == limit) + nextToken = new DateTimeToken(result.Data.Min(x => x.FundingTime).AddSeconds(-1)); + + // Return + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode,result.Data.Reverse().Select(x => new SharedFundingRate(x.FundingRate, x.FundingTime)).ToArray(), nextToken); + } + #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 ? PositionSide.Both : request.PositionSide == SharedPositionSide.Long ? PositionSide.Long : PositionSide.Short, + reduceOnly: request.ReduceOnly, + timeInForce: GetTimeInForce(request.TimeInForce), + clientOrderId: 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.OrderId.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.OrderId.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, + Leverage = order.Data.Leverage?.EndsWith("X") == true ? decimal.Parse(order.Data.Leverage.Substring(0, order.Data.Leverage.Length - 1)) : null, + Quantity = order.Data.Quantity, + QuantityFilled = order.Data.QuantityFilled, + QuoteQuantityFilled = order.Data.ValueFilled, + 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.OrderId.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, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantityFilled = x.ValueFilled, + Leverage = x.Leverage?.EndsWith("X") == true ? decimal.Parse(x.Leverage.Substring(0, x.Leverage.Length - 1)) : null, + 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.GetClosedOrdersAsync(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, SupportedTradingModes ,orders.Data.Select(x => new SharedFuturesOrder( + x.Symbol, + x.OrderId.ToString(), + ParseOrderType(x.Type), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId, + AveragePrice = x.AveragePrice, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantityFilled = x.ValueFilled, + Leverage = x.Leverage?.EndsWith("X") == true ? decimal.Parse(x.Leverage.Substring(0, x.Leverage.Length - 1)) : null, + 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(), nextToken); + } + + 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.TradeId, + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.Timestamp) + { + Price = x.Price, + Quantity = x.Quantity, + Fee = Math.Abs(x.Fee), + Role = x.Role == Role.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.TradeId).ToString()); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode,orders.Data.Select(x => new SharedUserTrade( + x.Symbol, + x.OrderId.ToString(), + x.TradeId.ToString(), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.Timestamp) + { + Price = x.Price, + Quantity = x.Quantity, + Fee = Math.Abs(x.Fee), + FeeAsset = x.FeeAsset, + Role = x.Role == Role.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.OrderId.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 Trading.GetPositionsAsync(symbol: request.Symbol?.GetSymbol(FormatSymbol), 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, x.Size, x.UpdateTime) + { + UnrealizedPnl = x.UnrealizedProfit, + LiquidationPrice = x.LiquidationPrice, + Leverage = x.Leverage, + AverageOpenPrice = x.AveragePrice, + PositionSide = x.Side == TradeSide.Short ? SharedPositionSide.Short : SharedPositionSide.Long + }).ToArray()); + } + + EndpointOptions IFuturesOrderRestClient.ClosePositionOptions { get; } = new EndpointOptions(true); + 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 positions = await Trading.GetPositionsAsync(request.Symbol.GetSymbol(FormatSymbol), ct: ct).ConfigureAwait(false); + if (!positions) + return positions.AsExchangeResult(Exchange, null, default); + + var position = positions.Data.FirstOrDefault(x => request.PositionSide == null ? x.Size != 0 : x.Side == (request.PositionSide == SharedPositionSide.Short ? TradeSide.Short : TradeSide.Long)); + if (position == null) + return positions.AsExchangeError(Exchange, new ServerError("Position not found")); + + var result = await Trading.ClosePositionAsync(position.PositionId).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(result.Data.OrderId.ToString())); + } + + private TimeInForce? GetTimeInForce(SharedTimeInForce? tif) + { + if (tif == null) + return null; + + if (tif == SharedTimeInForce.ImmediateOrCancel) return TimeInForce.ImmediateOrCancel; + if (tif == SharedTimeInForce.FillOrKill) return TimeInForce.FillOrKill; + if (tif == SharedTimeInForce.GoodTillCanceled) return TimeInForce.GoodTillCanceled; + + return null; + } + + private SharedOrderStatus ParseOrderStatus(OrderStatus status) + { + if (status == OrderStatus.New || status == OrderStatus.PartiallyFilled || status == OrderStatus.Pending) return SharedOrderStatus.Open; + if (status == OrderStatus.Canceled || status == OrderStatus.Failed) 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 == null) + return null; + + 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 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.Balance, x.Equity)).ToArray()); + } + + #endregion + + #region Position Mode client + + SharedPositionModeSelection IPositionModeRestClient.PositionModeSettingType => SharedPositionModeSelection.PerSymbol; + + 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(request.Symbol!.GetSymbol(FormatSymbol), ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedPositionModeResult(result.Data.PositionMode == PositionMode.DualPositionMode ? 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.SetPositionModeAsync(request.Symbol!.GetSymbol(FormatSymbol), request.PositionMode == SharedPositionMode.HedgeMode ? PositionMode.DualPositionMode : PositionMode.SinglePositionMode, ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedPositionModeResult(request.PositionMode)); + } + #endregion + + #region Position History client + + GetPositionHistoryOptions IPositionHistoryRestClient.GetPositionHistoryOptions { get; } = new GetPositionHistoryOptions(SharedPaginationSupport.Descending) + { + RequiredOptionalParameters = new List + { + new ParameterDescription(nameof(GetPositionHistoryRequest.Symbol), typeof(SharedSymbol), "The symbol to get position history for", "ETH-USDT") + } + }; + async Task>> IPositionHistoryRestClient.GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? pageToken, CancellationToken ct) + { + var validationError = ((IPositionHistoryRestClient)this).GetPositionHistoryOptions.ValidateRequest(Exchange, request, request.Symbol?.TradingMode ?? request.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + // Determine page token + int page = 0; + int pageSize = request.Limit ?? 20; + if (pageToken is PageToken token) + { + page = token.Page; + pageSize = token.PageSize; + } + + // Get data + var orders = await Trading.GetPositionHistoryAsync( + symbol: request.Symbol!.GetSymbol(FormatSymbol), + startTime: request.StartTime, + endTime: request.EndTime, + page: page, + pageSize: pageSize, + ct: ct + ).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + // Get next token + PageToken? nextToken = null; + if (orders.Data.Count() == pageSize) + nextToken = new PageToken(page + 1, pageSize); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode, orders.Data.Select(x => new SharedPositionHistory( + x.Symbol, + x.PositionSide == PositionSide.Long ? SharedPositionSide.Long : SharedPositionSide.Short, + x.AveragePrice, + x.AverageClosePrice, + x.ClosePositionQuantity, + x.RealisedPnl, + x.UpdateTime) + { + Leverage = x.Leverage, + PositionId = x.PositionId + }).ToArray(), nextToken); + } + #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 + + private DateTime RoundDown(DateTime dt, TimeSpan d) + { + return new DateTime(dt.Ticks - (dt.Ticks % d.Ticks), dt.Kind); + } + } +} diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiTrading.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiTrading.cs index 9bb443e..57e716d 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiTrading.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiTrading.cs @@ -403,6 +403,27 @@ public async Task>> GetUserTrad #endregion + + #region Get User Trades + + /// + public async Task>> GetUserTradesAsync(string symbol, long? orderId = null, DateTime? startTime = null, DateTime? endTime = null, long? fromId = null, int? limit = null, CancellationToken ct = default) + { + var parameters = new ParameterCollection(); + parameters.Add("symbol", symbol); + parameters.AddOptional("orderId", orderId); + parameters.AddOptional("lastFillId", fromId); + parameters.AddOptional("pageSize", limit); + parameters.AddOptionalMilliseconds("startTs", startTime); + parameters.AddOptionalMilliseconds("endTs", endTime); + var request = _definitions.GetOrCreate(HttpMethod.Get, "/openApi/swap/v1/trade/fillHistory", BingXExchange.RateLimiter.RestAccount1, 1, true, + limitGuard: new SingleLimitGuard(5, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding, keySelector: SingleLimitGuard.PerApiKey)); + var result = await _baseClient.SendAsync(request, parameters, ct).ConfigureAwait(false); + return result.As>(result.Data?.Trades); + } + + #endregion + #region Cancel All Orders After /// @@ -458,7 +479,7 @@ public async Task>> GetPositi public async Task>> GetPositionHistoryAsync(string symbol, long? positionId = null, DateTime? startTime = null, DateTime? endTime = null, int? page = null, int? pageSize = null, CancellationToken ct = default) { var parameters = new ParameterCollection(); - parameters.AddOptional("symbol", symbol); + parameters.Add("symbol", symbol); parameters.AddOptional("positionId", positionId); parameters.AddOptionalMillisecondsString("startTs", startTime); parameters.AddOptionalMillisecondsString("endTs", endTime); diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs index 6bb4373..c1124c5 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs @@ -21,13 +21,15 @@ using System.IO; using System.Runtime.InteropServices.ComTypes; using System.Linq; +using BingX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.SharedApis; namespace BingX.Net.Clients.PerpetualFuturesApi { /// /// Client providing access to the BingX futures websocket Api /// - internal class BingXSocketClientPerpetualFuturesApi : SocketApiClient, IBingXSocketClientPerpetualFuturesApi + internal partial class BingXSocketClientPerpetualFuturesApi : SocketApiClient, IBingXSocketClientPerpetualFuturesApi { #region fields private static readonly MessagePath _idPath = MessagePath.Get().Property("id"); @@ -48,7 +50,7 @@ internal BingXSocketClientPerpetualFuturesApi(ILogger logger, BingXSocketOptions #endregion /// - 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) @@ -59,6 +61,8 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden /// protected override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(); + public IBingXSocketClientPerpetualFuturesApiShared SharedClient => this; + /// public async Task> SubscribeToTradeUpdatesAsync(string symbol, Action>> onMessage, CancellationToken ct = default) { diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApiShared.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApiShared.cs new file mode 100644 index 0000000..54617ac --- /dev/null +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApiShared.cs @@ -0,0 +1,165 @@ +using BingX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.SharedApis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BingX.Net.Clients.PerpetualFuturesApi +{ + internal partial class BingXSocketClientPerpetualFuturesApi : IBingXSocketClientPerpetualFuturesApiShared + { + public string Exchange => BingXExchange.ExchangeName; + + public TradingMode[] SupportedTradingModes { get; } = new[] { 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(update.Data.Symbol, update.Data.LastPrice, update.Data.HighPrice, update.Data.LowPrice, update.Data.Volume, update.Data.PriceChangePercentage))), 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 SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent>(Exchange, update.Data.Select(x => new SharedTrade(x.Quantity, x.Price, x.TradeTime)).ToArray())), 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 SubscribeToBookPriceUpdatesAsync(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.Update.Balances.Select(x => new SharedBalance(x.Asset, x.BalanceExIsolatedMargin, x.Balance)).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.Symbol, + update.Data.OrderId.ToString(), + update.Data.Type == Enums.FuturesOrderType.Limit ? SharedOrderType.Limit : update.Data.Type == Enums.FuturesOrderType.Market ? SharedOrderType.Market : 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.UpdateTime) + { + ClientOrderId = update.Data.ClientOrderId, + OrderPrice = update.Data.Price, + Quantity = update.Data.Quantity, + QuantityFilled = update.Data.QuantityFilled, + QuoteQuantityFilled = update.Data.VolumeFilled, + Fee = update.Data.Fee == null ? null :Math.Abs(update.Data.Fee.Value), + AveragePrice = update.Data.AveragePrice == 0 ? null : update.Data.AveragePrice, + PositionSide = update.Data.PositionSide == Enums.PositionSide.Long ? SharedPositionSide.Long : update.Data.PositionSide == Enums.PositionSide.Short ? SharedPositionSide.Short : null, + FeeAsset = update.Data.FeeAsset, + UpdateTime = update.Data.UpdateTime + } + })), + 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.Update.Positions.Select(x => new SharedPosition(x.Symbol, x.Size, update.Data.EventTime) + { + AverageOpenPrice = x.EntryPrice, + PositionSide = x.Side == Enums.TradeSide.Short ? SharedPositionSide.Short : SharedPositionSide.Long, + UnrealizedPnl = x.UnrealizedPnl + }).ToArray())), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + + #endregion + } +} diff --git a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs index dd7d7b0..fbce924 100644 --- a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs +++ b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs @@ -19,11 +19,12 @@ using System.Linq; using System.Globalization; using BingX.Net.Enums; +using CryptoExchange.Net.SharedApis; namespace BingX.Net.Clients.SpotApi { /// - internal class BingXRestClientSpotApi : RestApiClient, IBingXRestClientSpotApi, ISpotClient + internal partial class BingXRestClientSpotApi : RestApiClient, IBingXRestClientSpotApi, ISpotClient { #region fields internal static TimeSyncState _timeSyncState = new TimeSyncState("Spot Api"); @@ -65,7 +66,7 @@ internal BingXRestClientSpotApi(ILogger logger, HttpClient? httpClient, BingXRes #endregion /// - 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 IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); @@ -134,6 +135,7 @@ protected override Task> GetServerTimestampAsync() /// public ISpotClient CommonSpotClient => this; + public IBingXRestClientSpotApiShared SharedClient => this; /// protected override void WriteParamBody(IRequest request, IDictionary parameters, string contentType) diff --git a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiAccount.cs b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiAccount.cs index 26186c6..0dbbcc2 100644 --- a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiAccount.cs +++ b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiAccount.cs @@ -229,6 +229,9 @@ public async Task> GetInternalTransfersAsy /// public async Task> StartUserStreamAsync(CancellationToken ct = default) { + if (_baseClient.AuthenticationProvider == null) + return new WebCallResult(new NoApiCredentialsError()); + var request = _definitions.GetOrCreate(HttpMethod.Post, "/openApi/user/auth/userDataStream", BingXExchange.RateLimiter.RestAccount1, 1, false, limitGuard: new SingleLimitGuard(5, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding, keySelector: SingleLimitGuard.PerApiKey)); var result = await _baseClient.SendRawAsync(request, null, ct).ConfigureAwait(false); @@ -242,6 +245,9 @@ public async Task> StartUserStreamAsync(CancellationToken /// public async Task KeepAliveUserStreamAsync(string listenKey, CancellationToken ct = default) { + if (_baseClient.AuthenticationProvider == null) + return new WebCallResult(new NoApiCredentialsError()); + var parameters = new ParameterCollection { { "listenKey", listenKey } @@ -258,6 +264,9 @@ public async Task KeepAliveUserStreamAsync(string listenKey, Canc /// public async Task StopUserStreamAsync(string listenKey, CancellationToken ct = default) { + if (_baseClient.AuthenticationProvider == null) + return new WebCallResult(new NoApiCredentialsError()); + var parameters = new ParameterCollection { { "listenKey", listenKey } diff --git a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiShared.cs b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiShared.cs new file mode 100644 index 0000000..96baadd --- /dev/null +++ b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiShared.cs @@ -0,0 +1,723 @@ +using BingX.Net.Enums; +using BingX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.SharedApis; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BingX.Net.Clients.SpotApi +{ + internal partial class BingXRestClientSpotApi : IBingXRestClientSpotApiShared + { + public string Exchange => BingXExchange.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 Kline 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 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.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.GetSymbolsAsync(ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + return result.AsExchangeResult>(Exchange, TradingMode.Spot, result.Data.Select(s => new SharedSpotSymbol(s.Name.Split(new[] { '-' })[0], s.Name.Split(new[] { '-' })[1], s.Name, s.Status == SymbolStatus.Online) + { + MinTradeQuantity = s.MinOrderQuantity, + MinNotionalValue = s.MinNotional, + MaxTradeQuantity = s.MaxOrderQuantity, + QuantityStep = s.StepSize, + PriceStep = s.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.GetTickersAsync(request.Symbol.GetSymbol(FormatSymbol), ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + var ticker = result.Data.Single(); + return result.AsExchangeResult(Exchange, TradingMode.Spot, new SharedSpotTicker(ticker.Symbol, ticker.LastPrice, ticker.HighPrice, ticker.LowPrice, ticker.Volume, decimal.Parse(ticker.PriceChangePercent.Substring(0, ticker.PriceChangePercent.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture))); + } + + 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, decimal.Parse(x.PriceChangePercent.Substring(0, x.PriceChangePercent.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture))).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.GetTradeHistoryAsync( + 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.Quantity, x.Price, x.Timestamp)).ToArray()); + } + + #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.Free, x.Total)).ToArray()); + } + + #endregion + + #region Spot Order Client + + PlaceSpotOrderOptions ISpotOrderRestClient.PlaceSpotOrderOptions { get; } = new PlaceSpotOrderOptions(); + + SharedFeeDeductionType ISpotOrderRestClient.SpotFeeDeductionType => SharedFeeDeductionType.DeductFromOutput; + SharedFeeAssetType ISpotOrderRestClient.SpotFeeAssetType => SharedFeeAssetType.OutputAsset; + IEnumerable ISpotOrderRestClient.SpotSupportedOrderTypes { get; } = new[] { SharedOrderType.Limit, SharedOrderType.Market }; + IEnumerable ISpotOrderRestClient.SpotSupportedTimeInForce { get; } = new[] { SharedTimeInForce.GoodTillCanceled, SharedTimeInForce.ImmediateOrCancel, SharedTimeInForce.FillOrKill }; + SharedQuantitySupport ISpotOrderRestClient.SpotSupportedOrderQuantity { get; } = new SharedQuantitySupport( + SharedQuantityType.BaseAndQuoteAsset, + SharedQuantityType.BaseAndQuoteAsset, + SharedQuantityType.QuoteAsset, + SharedQuantityType.BaseAsset); + + 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 || request.OrderType == SharedOrderType.LimitMaker) ? Enums.OrderType.Limit : Enums.OrderType.Market, + quantity: request.Quantity, + quoteQuantity: request.QuoteQuantity, + price: request.Price, + timeInForce: GetTimeInForce(request.OrderType, request.TimeInForce), + clientOrderId: 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.OrderId.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.OrderId.ToString(), + ParseOrderType(order.Data.Type, null), + order.Data.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(order.Data.Status), + order.Data.CreateTime) + { + ClientOrderId = order.Data.ClientOrderId, + OrderPrice = order.Data.Price, + Quantity = order.Data.Quantity, + QuantityFilled = order.Data.QuantityFilled, + QuoteQuantity = order.Data.QuoteQuantity, + QuoteQuantityFilled = order.Data.ValueFilled, + Fee = Math.Abs(order.Data.Fee), + FeeAsset = order.Data.FeeAsset, + 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.OrderId.ToString(), + ParseOrderType(x.Type, null), + x.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantity = x.QuoteQuantity, + QuoteQuantityFilled = x.ValueFilled, + Fee = Math.Abs(x.Fee), + FeeAsset = x.FeeAsset, + UpdateTime = x.UpdateTime, + }).ToArray()); + } + + PaginatedEndpointOptions ISpotOrderRestClient.GetClosedSpotOrdersOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Descending, 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 + int page = 1; + int pageSize = request.Limit ?? 100; + if (pageToken is PageToken token) + { + page = token.Page; + pageSize = token.PageSize; + } + + // Get data + var orders = await Trading.GetOrdersAsync(request.Symbol.GetSymbol(FormatSymbol), + startTime: request.StartTime, + endTime: request.EndTime, + page: page, + pageSize: pageSize, + ct: ct).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + // Get next token + PageToken? nextToken = null; + if (orders.Data.Count() == pageSize) + nextToken = new PageToken(page + 1, pageSize); + + return orders.AsExchangeResult>(Exchange, TradingMode.Spot, orders.Data.Select(x => new SharedSpotOrder( + x.Symbol, + x.OrderId.ToString(), + ParseOrderType(x.Type, null), + x.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantity = x.QuoteQuantity, + QuoteQuantityFilled = x.ValueFilled, + Fee = Math.Abs(x.Fee), + FeeAsset = x.FeeAsset, + 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, request.Symbol.TradingMode,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) + { + Role = x.IsMaker ? SharedRole.Maker : SharedRole.Taker, + Fee = Math.Abs(x.Fee), + FeeAsset = x.FeeAsset, + }).ToArray()); + } + + PaginatedEndpointOptions ISpotOrderRestClient.GetSpotUserTradesOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Ascending, 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 + 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: fromId != null ? null: request.StartTime ?? DateTime.UtcNow.AddDays(-7), + endTime: fromId != null ? null : request.EndTime ?? DateTime.UtcNow, + 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 + 1).ToString()); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode,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) + { + Role = x.IsMaker ? SharedRole.Maker : SharedRole.Taker, + Fee = Math.Abs(x.Fee), + FeeAsset = x.FeeAsset, + }).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, request.Symbol.TradingMode, new SharedId(order.Data.OrderId.ToString())); + } + + private SharedOrderStatus ParseOrderStatus(OrderStatus status) + { + if (status == OrderStatus.Pending || status == OrderStatus.PartiallyFilled || status == OrderStatus.New) return SharedOrderStatus.Open; + if (status == OrderStatus.Canceled || status == OrderStatus.Failed) return SharedOrderStatus.Canceled; + return SharedOrderStatus.Filled; + } + + private SharedOrderType ParseOrderType(OrderType type, TimeInForce? tif) + { + if (type == OrderType.Market) return SharedOrderType.Market; + if (type == OrderType.Limit && tif == TimeInForce.PostOnly) return SharedOrderType.LimitMaker; + if (type == OrderType.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; + } + + private Enums.TimeInForce? GetTimeInForce(SharedOrderType type, SharedTimeInForce? tif) + { + if (type == SharedOrderType.LimitMaker) return Enums.TimeInForce.PostOnly; + if (tif == SharedTimeInForce.ImmediateOrCancel) return Enums.TimeInForce.ImmediateOrCancel; + if (tif == SharedTimeInForce.GoodTillCanceled) return Enums.TimeInForce.GoodTillCanceled; + + return null; + } + + #endregion + + #region Asset client + + 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 result = await Account.GetAssetsAsync(request.Asset, ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, TradingMode.Spot, default); + + var asset = result.Data.SingleOrDefault(); + if (asset == null) + return result.AsExchangeError(Exchange, new ServerError("Asset not found")); + + return result.AsExchangeResult(Exchange, TradingMode.Spot, new SharedAsset(asset.Asset) + { + FullName = asset.Name, + Networks = asset.Networks.Select(x => new SharedAssetNetwork(x.Network) + { + DepositEnabled = x.DepositEnabled, + MaxWithdrawQuantity = x.MaxWithdraw, + MinConfirmations = x.MinConfirmations, + MinWithdrawQuantity = x.MinWithdraw, + WithdrawEnabled = x.WithdrawEnabled, + WithdrawFee = x.WithdrawFee + }) + }); + } + + 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 result = await Account.GetAssetsAsync(ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + return result.AsExchangeResult>(Exchange, TradingMode.Spot, result.Data.Select(x => new SharedAsset(x.Asset) + { + FullName = x.Name, + Networks = x.Networks.Select(x => new SharedAssetNetwork(x.Network) + { + DepositEnabled = x.DepositEnabled, + MaxWithdrawQuantity = x.MaxWithdraw, + MinConfirmations = x.MinConfirmations, + MinWithdrawQuantity = x.MinWithdraw, + WithdrawEnabled = x.WithdrawEnabled, + WithdrawFee = x.WithdrawFee + }) + }).ToArray()); + } + + #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, ct: ct).ConfigureAwait(false); + if (!depositAddresses) + return depositAddresses.AsExchangeResult>(Exchange, null, default); + + var result = depositAddresses.Data.Data; + if (request.Network != null) + result = result.Where(r => r.Network == request.Network); + + return depositAddresses.AsExchangeResult>(Exchange, TradingMode.Spot, result.Select(x => new SharedDepositAddress(x.Asset, x.Address) + { + Network = x.Network + } + ).ToArray()); + } + + 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 result = await Account.GetDepositHistoryAsync(request.Asset, + startTime: request.StartTime, + endTime: request.EndTime, + limit: request.Limit ?? 100, + offset: offset, + ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + // Determine next token + OffsetToken? nextToken = null; + if (result.Data.Count() == (request.Limit ?? 100)) + nextToken = new OffsetToken((offset ?? 0) + result.Data.Count()); + + return result.AsExchangeResult>(Exchange, TradingMode.Spot, result.Data.Select(x => new SharedDeposit(x.Asset, x.Quantity, x.Status == DepositStatus.Completed, x.InsertTime) + { + Confirmations = x.ConfirmedTimes.Contains("/") ? int.Parse(x.ConfirmedTimes.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries)[0]) : null, + Tag = x.AddressTag, + TransactionId = x.TransactionId, + Network = x.Network, + }).ToArray(), nextToken); + } + + #endregion + + #region Order Book client + GetOrderBookOptions IOrderBookRestClient.GetOrderBookOptions { get; } = new GetOrderBookOptions(1, 2000, 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 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 = null; + 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) + { + Id = x.Id, + Confirmations = x.Confirmations, + Network = x.Network, + Tag = x.AddressTag, + TransactionId = x.TransactionId, + Fee = x.Fee + }).ToArray(), nextToken); + } + + #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, + AccountType.Funding, + 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/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiTrading.cs b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiTrading.cs index ccb64c9..428ca1d 100644 --- a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiTrading.cs +++ b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiTrading.cs @@ -35,7 +35,7 @@ internal BingXRestClientSpotApiTrading(ILogger logger, BingXRestClientSpotApi ba #region Place Order /// - public async Task> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal? quantity = null, decimal? price = null, decimal? quoteQuantity = null, decimal? stopPrice = null, string? clientOrderId = null, CancellationToken ct = default) + public async Task> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal? quantity = null, decimal? price = null, decimal? quoteQuantity = null, decimal? stopPrice = null, string? clientOrderId = null, TimeInForce? timeInForce = null, CancellationToken ct = default) { var parameters = new ParameterCollection() { @@ -48,6 +48,7 @@ public async Task> PlaceOrderAsync(string symbol, Orde parameters.AddOptional("quoteOrderQty", quoteQuantity); parameters.AddOptional("stopPrice", stopPrice); parameters.AddOptional("newClientOrderId", clientOrderId); + parameters.AddOptionalEnum("timeInForce", timeInForce); var request = _definitions.GetOrCreate(HttpMethod.Post, "/openApi/spot/v1/trade/order", BingXExchange.RateLimiter.RestAccount2, 1, true, limitGuard: new SingleLimitGuard(5, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding, keySelector: SingleLimitGuard.PerApiKey)); @@ -224,7 +225,7 @@ public async Task>> GetOrdersAsync( #region Get User Trades /// - public async Task>> GetUserTradesAsync(string symbol, long? orderId = null, OrderStatus? status = null, OrderType? type = null, DateTime? startTime = null, DateTime? endTime = null, int? fromId = null, int? limit = null, CancellationToken ct = default) + public async Task>> GetUserTradesAsync(string symbol, long? orderId = null, OrderStatus? status = null, OrderType? type = null, DateTime? startTime = null, DateTime? endTime = null, long? fromId = null, int? limit = null, CancellationToken ct = default) { var parameters = new ParameterCollection(); parameters.AddOptional("symbol", symbol); diff --git a/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs index 636f8f4..5278363 100644 --- a/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs +++ b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs @@ -19,13 +19,14 @@ using CryptoExchange.Net.Converters.SystemTextJson; using BingX.Net.Enums; using System.Runtime.InteropServices.ComTypes; +using CryptoExchange.Net.SharedApis; namespace BingX.Net.Clients.SpotApi { /// /// Client providing access to the BingX spot websocket Api /// - internal class BingXSocketClientSpotApi : SocketApiClient, IBingXSocketClientSpotApi + internal partial class BingXSocketClientSpotApi : SocketApiClient, IBingXSocketClientSpotApi { #region fields private static readonly MessagePath _idPath = MessagePath.Get().Property("id"); @@ -53,7 +54,9 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden => new BingXAuthenticationProvider(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) => baseAsset.ToUpperInvariant() + "-" + quoteAsset.ToUpperInvariant(); + + public IBingXSocketClientSpotApiShared SharedClient => this; /// protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); @@ -90,7 +93,7 @@ public async Task> SubscribeToPartialOrderBookUpd public async Task> SubscribeToTickerUpdatesAsync(string symbol, Action> onMessage, CancellationToken ct = default) { var stream = symbol + "@ticker"; - var subscription = new BingXSubscription(_logger, stream, "24hTicker" + symbol, x => onMessage(x.WithStreamId(stream).WithSymbol(x.Data.Symbol)), false); + var subscription = new BingXSubscription(_logger, stream, stream, x => onMessage(x.WithStreamId(stream).WithSymbol(x.Data.Symbol)), false); return await SubscribeAsync(BaseAddress.AppendPath("market"), subscription, ct).ConfigureAwait(false); } diff --git a/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApiShared.cs b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApiShared.cs new file mode 100644 index 0000000..ae48e49 --- /dev/null +++ b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApiShared.cs @@ -0,0 +1,139 @@ +using BingX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.SharedApis; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BingX.Net.Clients.SpotApi +{ + internal partial class BingXSocketClientSpotApi : IBingXSocketClientSpotApiShared + { + public string Exchange => BingXExchange.ExchangeName; + + public TradingMode[] SupportedTradingModes { get; } = new[] { TradingMode.Spot }; + 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(update.Data.Symbol, update.Data.LastPrice, update.Data.HighPrice, update.Data.LowPrice, update.Data.Volume, decimal.Parse(update.Data.PriceChangePercentage.Substring(0, update.Data.PriceChangePercentage.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture)))), 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 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 SubscribeToBookPriceUpdatesAsync(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 SubscribeToBalanceUpdatesAsync(request.ListenKey!, + update => handler(update.AsExchangeEvent>(Exchange, update.Data.EventData.Balances.Select(x => new SharedBalance(x.Asset, x.Total, x.Total + x.Locked)).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), "The listenkey for starting 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 SubscribeToOrderUpdatesAsync(request.ListenKey!, + update => handler(update.AsExchangeEvent>(Exchange, new[] { + new SharedSpotOrder( + update.Data.Symbol, + update.Data.OrderId.ToString(), + update.Data.Type == Enums.OrderType.Limit ? SharedOrderType.Limit : update.Data.Type == Enums.OrderType.Market ? SharedOrderType.Market : 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.Pending || 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.QuoteOrderQuantity, + QuoteQuantityFilled = update.Data.VolumeFilled, + Fee = update.Data.Fee.HasValue ? Math.Abs(update.Data.Fee.Value) : null, + FeeAsset = update.Data.FeeAsset, + UpdateTime = update.Data.UpdateTime, + LastTrade = update.Data.LastFillQuantity > 0 ? new SharedUserTrade(update.Data.Symbol, update.Data.OrderId.ToString(), update.Data.TradeId.ToString(), update.Data.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, update.Data.LastFillQuantity!.Value, update.Data.LastFillPrice!.Value, update.Data.UpdateTime!.Value) : null + } + })), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + + #endregion + + } +} diff --git a/BingX.Net/Enums/KlineInterval.cs b/BingX.Net/Enums/KlineInterval.cs index 5f6164d..8b13671 100644 --- a/BingX.Net/Enums/KlineInterval.cs +++ b/BingX.Net/Enums/KlineInterval.cs @@ -11,76 +11,76 @@ public enum KlineInterval /// One minute /// [Map("1m")] - OneMinute, + OneMinute = 60, /// /// Three minutes /// [Map("3m")] - ThreeMinutes, + ThreeMinutes = 60 * 3, /// /// Five minutes /// [Map("5m")] - FiveMinutes, + FiveMinutes = 60 * 5, /// /// Fifteen minutes /// [Map("15m")] - FifteenMinutes, + FifteenMinutes = 60 * 15, /// /// Thirty minutes /// [Map("30m")] - ThirtyMinutes, + ThirtyMinutes = 60 * 30, /// /// One hour /// [Map("1h")] - OneHour, + OneHour = 60 * 60, /// /// Two hours /// [Map("2h")] - TwoHours, + TwoHours = 60 * 60 * 2, /// /// Four hours /// [Map("4h")] - FourHours, + FourHours = 60 * 60 * 4, /// /// Six hours /// [Map("6h")] - SixHours, + SixHours = 60 * 60 * 6, /// /// Eight hours /// [Map("8h")] - EightHours, + EightHours = 60 * 60 * 8, /// /// Twelve hours /// [Map("12h")] - TwelveHours, + TwelveHours = 60 * 60 * 12, /// /// One day /// [Map("1d")] - OneDay, + OneDay = 60 * 60 * 24, /// /// Three days /// [Map("3d")] - ThreeDay, + ThreeDay = 60 * 60 * 24 * 3, /// /// One week /// [Map("1w")] - OneWeek, + OneWeek = 60 * 60 * 24 * 7, /// /// One month /// [Map("1M")] - OneMonth + OneMonth = 60 * 60 * 24 * 30 } } diff --git a/BingX.Net/Enums/Role.cs b/BingX.Net/Enums/Role.cs new file mode 100644 index 0000000..8027556 --- /dev/null +++ b/BingX.Net/Enums/Role.cs @@ -0,0 +1,24 @@ +using CryptoExchange.Net.Attributes; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BingX.Net.Enums +{ + /// + /// Role + /// + public enum Role + { + /// + /// Maker + /// + [Map("maker")] + Maker, + /// + /// Taker + /// + [Map("taker")] + Taker + } +} diff --git a/BingX.Net/Enums/TransferStatus.cs b/BingX.Net/Enums/TransferStatus.cs deleted file mode 100644 index 7c1bab4..0000000 --- a/BingX.Net/Enums/TransferStatus.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BingX.Net.Enums -{ - internal class TransferStatus - { - } -} diff --git a/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs b/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs index 2986b2d..161553c 100644 --- a/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs +++ b/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using BingX.Net.Interfaces.Clients; using BingX.Net.Objects.Options; using BingX.Net.SymbolOrderBooks; +using CryptoExchange.Net; namespace Microsoft.Extensions.DependencyInjection { @@ -62,6 +63,12 @@ public static IServiceCollection AddBingX( services.AddSingleton(); services.AddTransient(); services.AddTransient(x => x.GetRequiredService().SpotApi.CommonSpotClient); + + services.RegisterSharedRestInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); + services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); + services.RegisterSharedRestInterfaces(x => x.GetRequiredService().PerpetualFuturesApi.SharedClient); + services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().PerpetualFuturesApi.SharedClient); + if (socketClientLifeTime == null) services.AddSingleton(); else diff --git a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApi.cs b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApi.cs index 95eaa04..6ff0b89 100644 --- a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApi.cs +++ b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApi.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Interfaces; +using BingX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.Interfaces; using System; namespace BingX.Net.Interfaces.Clients.PerpetualFuturesApi @@ -22,5 +23,11 @@ public interface IBingXRestClientPerpetualFuturesApi : IRestApiClient, IDisposab /// Endpoints related to orders and trades /// public IBingXRestClientPerpetualFuturesApiTrading Trading { get; } + + /// + /// Get the shared rest requests client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + public IBingXRestClientPerpetualFuturesApiShared SharedClient { get; } + } } diff --git a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiAccount.cs b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiAccount.cs index f98d32c..ab89fb7 100644 --- a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiAccount.cs +++ b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiAccount.cs @@ -88,7 +88,7 @@ public interface IBingXRestClientPerpetualFuturesApiAccount Task SetMarginModeAsync(string symbol, MarginMode marginMode, CancellationToken ct = default); /// - /// Get the current leverage setrings for a symbol + /// Get the current leverage settings for a symbol /// /// /// Symbol, for example `ETH-USDT` diff --git a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiShared.cs b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiShared.cs new file mode 100644 index 0000000..f2f5cab --- /dev/null +++ b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiShared.cs @@ -0,0 +1,27 @@ +using CryptoExchange.Net.SharedApis; + +namespace BingX.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Perpetual futures rest API usage + /// + public interface IBingXRestClientPerpetualFuturesApiShared : + IBalanceRestClient, + IFuturesTickerRestClient, + IFuturesSymbolRestClient, + IFuturesOrderRestClient, + IKlineRestClient, + IRecentTradeRestClient, + //ITradeHistoryRestClient, + ILeverageRestClient, + IMarkPriceKlineRestClient, + IIndexPriceKlineRestClient, + IOrderBookRestClient, + IOpenInterestRestClient, + IFundingRateRestClient, + IPositionModeRestClient, + IPositionHistoryRestClient, + IListenKeyRestClient + { + } +} diff --git a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiTrading.cs b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiTrading.cs index be98148..7278ee4 100644 --- a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiTrading.cs +++ b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXRestClientPerpetualFuturesApiTrading.cs @@ -253,6 +253,20 @@ Task>> PlaceMultipleOrderAsync( /// Task>> GetUserTradesAsync(long? orderId = null, DateTime? startTime = null, DateTime? endTime = null, CancellationToken ct = default); + /// + /// Get user trade history + /// + /// + /// The symbol + /// Filter by order id + /// Filter by start time + /// Filter by end time + /// Return results after this id + /// Max number of results + /// Cancellation token + /// + Task>> GetUserTradesAsync(string symbol, long? orderId = null, DateTime? startTime = null, DateTime? endTime = null, long? fromId = null, int? limit = null, CancellationToken ct = default); + /// /// Cancel all order after a set period. Can be called contineously to maintain a rolling timeout /// @@ -283,7 +297,7 @@ Task>> PlaceMultipleOrderAsync( /// /// Get position close history - /// + /// /// /// The symbol, for example `ETH-USDT` /// Filter by position id diff --git a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApi.cs b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApi.cs index 20c07ff..58995d1 100644 --- a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApi.cs +++ b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApi.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using BingX.Net.Enums; +using BingX.Net.Interfaces.Clients.SpotApi; using BingX.Net.Objects.Models; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; @@ -15,6 +16,10 @@ namespace BingX.Net.Interfaces.Clients.PerpetualFuturesApi /// public interface IBingXSocketClientPerpetualFuturesApi : ISocketApiClient, IDisposable { + /// + /// Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + IBingXSocketClientPerpetualFuturesApiShared SharedClient { get; } /// /// Subscribe to live trade updates diff --git a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApiShared.cs b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApiShared.cs new file mode 100644 index 0000000..2471092 --- /dev/null +++ b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApiShared.cs @@ -0,0 +1,17 @@ +using CryptoExchange.Net.SharedApis; + +namespace BingX.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Perpetual futures socket API usage + /// + public interface IBingXSocketClientPerpetualFuturesApiShared : + ITickerSocketClient, + ITradeSocketClient, + IBookTickerSocketClient, + IBalanceSocketClient, + IPositionSocketClient, + IFuturesOrderSocketClient + { + } +} diff --git a/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApi.cs b/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApi.cs index aa4f3b5..4664196 100644 --- a/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApi.cs +++ b/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApi.cs @@ -25,9 +25,14 @@ public interface IBingXRestClientSpotApi : IRestApiClient, IDisposable public IBingXRestClientSpotApiTrading 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 IBingXRestClientSpotApiShared SharedClient { get; } + } } diff --git a/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApiShared.cs b/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApiShared.cs new file mode 100644 index 0000000..ee51790 --- /dev/null +++ b/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApiShared.cs @@ -0,0 +1,24 @@ +using CryptoExchange.Net.SharedApis; + +namespace BingX.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Spot rest API usage + /// + public interface IBingXRestClientSpotApiShared : + IAssetsRestClient, + IBalanceRestClient, + IDepositRestClient, + IKlineRestClient, + IOrderBookRestClient, + IRecentTradeRestClient, + ISpotOrderRestClient, + ISpotSymbolRestClient, + ISpotTickerRestClient, + //ITradeHistoryRestClient + IWithdrawalRestClient, + IWithdrawRestClient, + IListenKeyRestClient + { + } +} diff --git a/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApiTrading.cs b/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApiTrading.cs index ee45853..1d248e5 100644 --- a/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApiTrading.cs +++ b/BingX.Net/Interfaces/Clients/SpotApi/IBingXRestClientSpotApiTrading.cs @@ -26,9 +26,10 @@ public interface IBingXRestClientSpotApiTrading /// Order quantity in quote asset /// Stop price /// Client order id + /// Time in force /// Cancellation token /// - Task> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal? quantity = null, decimal? price = null, decimal? quoteQuantity = null, decimal? stopPrice = null, string? clientOrderId = null, CancellationToken ct = default); + Task> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal? quantity = null, decimal? price = null, decimal? quoteQuantity = null, decimal? stopPrice = null, string? clientOrderId = null, TimeInForce? timeInForce = null, CancellationToken ct = default); /// /// Place multiple orders @@ -131,8 +132,8 @@ public interface IBingXRestClientSpotApiTrading /// Max amount of results /// Cancellation token /// - Task>> GetUserTradesAsync(string symbol, long? orderId = null, OrderStatus? status = null, OrderType? type = null, DateTime? startTime = null, DateTime? endTime = null, int? fromId = null, int? limit = null, CancellationToken ct = default); - + Task>> GetUserTradesAsync(string symbol, long? orderId = null, OrderStatus? status = null, OrderType? type = null, DateTime? startTime = null, DateTime? endTime = null, long? fromId = null, int? limit = null, CancellationToken ct = default); + /// /// Place a new OCO order /// @@ -184,6 +185,5 @@ public interface IBingXRestClientSpotApiTrading /// Page size /// Cancellation token Task>> GetClosedOcoOrdersAsync(int page, int pageSize, CancellationToken ct = default); - } } diff --git a/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApi.cs b/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApi.cs index bd55131..69af93a 100644 --- a/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApi.cs +++ b/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApi.cs @@ -6,7 +6,6 @@ using CryptoExchange.Net.Objects.Sockets; using BingX.Net.Enums; using BingX.Net.Objects.Models; -using BingX.Net.Interfaces.Clients.PerpetualFuturesApi; namespace BingX.Net.Interfaces.Clients.SpotApi { @@ -15,6 +14,11 @@ namespace BingX.Net.Interfaces.Clients.SpotApi /// public interface IBingXSocketClientSpotApi : ISocketApiClient, IDisposable { + /// + /// Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + IBingXSocketClientSpotApiShared SharedClient { get; } + /// /// Subscribe to live trade updates /// diff --git a/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApiShared.cs b/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApiShared.cs new file mode 100644 index 0000000..c5efcb9 --- /dev/null +++ b/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApiShared.cs @@ -0,0 +1,16 @@ +using CryptoExchange.Net.SharedApis; + +namespace BingX.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Spot socket API usage + /// + public interface IBingXSocketClientSpotApiShared : + ITickerSocketClient, + ITradeSocketClient, + IBookTickerSocketClient, + IBalanceSocketClient, + ISpotOrderSocketClient + { + } +} diff --git a/BingX.Net/Objects/Models/BingXContract.cs b/BingX.Net/Objects/Models/BingXContract.cs index 356530d..32a799e 100644 --- a/BingX.Net/Objects/Models/BingXContract.cs +++ b/BingX.Net/Objects/Models/BingXContract.cs @@ -78,7 +78,7 @@ public record BingXContract [JsonPropertyName("asset")] public string Asset { get; set; } = string.Empty; /// - /// 0: Online, 1:Offline. Different unknown values have been observed. + /// 1: Online, 25:Suspended. Different unknown values have been observed. /// [JsonPropertyName("status")] public int Status { get; set; } diff --git a/BingX.Net/Objects/Models/BingXFuturesUserTradeDetails.cs b/BingX.Net/Objects/Models/BingXFuturesUserTradeDetails.cs new file mode 100644 index 0000000..efd86d4 --- /dev/null +++ b/BingX.Net/Objects/Models/BingXFuturesUserTradeDetails.cs @@ -0,0 +1,82 @@ +using BingX.Net.Enums; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace BingX.Net.Objects.Models +{ + internal record BingXFuturesUserTradeDetailsWrapper + { + [JsonPropertyName("fill_history_orders")] + public IEnumerable Trades { get; set; } = Array.Empty(); + //[JsonInclude, JsonPropertyName("fill_history_orders")] + //internal IEnumerable TradeHistory { set => Trades = value; } + } + + /// + /// User trade info + /// + public record BingXFuturesUserTradeDetails + { + /// + /// Symbol + /// + [JsonPropertyName("symbol")] + public string Symbol { get; set; } = string.Empty; + /// + /// Order id + /// + [JsonPropertyName("orderId")] + public long OrderId { get; set; } + /// + /// Trade id + /// + [JsonPropertyName("tradeId")] + public string TradeId { get; set; } = string.Empty; + /// + /// Trade price + /// + [JsonPropertyName("price")] + public decimal Price { get; set; } + /// + /// Quantity + /// + [JsonPropertyName("qty")] + public decimal Quantity { get; set; } + /// + /// Value + /// + [JsonPropertyName("quoteQty")] + public decimal Value { get; set; } + /// + /// Fee paid + /// + [JsonPropertyName("commission")] + public decimal Fee { get; set; } + /// + /// Fee asset + /// + [JsonPropertyName("commissionAsset")] + public string FeeAsset { get; set; } = string.Empty; + /// + /// Trade time + /// + [JsonPropertyName("filledTime")] + public DateTime Timestamp { get; set; } + /// + /// Trade side + /// + [JsonPropertyName("side")] + public OrderSide Side { get; set; } + /// + /// Position side + /// + [JsonPropertyName("positionSide")] + public PositionSide? PositionSide { get; set; } + /// + /// Trade role + /// + [JsonPropertyName("role")] + public Role? Role { get; set; } + } +}