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