diff --git a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetCrossMarginPositionMode.txt b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetCrossMarginPositionMode.txt index 34b6f0b1..be926dc2 100644 --- a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetCrossMarginPositionMode.txt +++ b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetCrossMarginPositionMode.txt @@ -3,11 +3,11 @@ GET true { "status": "ok", - "data": [ + "data": { "margin_account": "USDT", "position_mode": "single_side" } - ], + , "ts": 1566899973811 } \ No newline at end of file diff --git a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetIsolatedMarginPositionMode.txt b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetIsolatedMarginPositionMode.txt index 1b206ae2..d51e119b 100644 --- a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetIsolatedMarginPositionMode.txt +++ b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/GetIsolatedMarginPositionMode.txt @@ -3,11 +3,11 @@ GET true { "status": "ok", - "data": [ + "data": { "margin_account": "USDT", "position_mode": "single_side" } - ], + , "ts": 1566899973811 } \ No newline at end of file diff --git a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetCrossMarginPositionMode.txt b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetCrossMarginPositionMode.txt index 42e061b8..ad9a350e 100644 --- a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetCrossMarginPositionMode.txt +++ b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetCrossMarginPositionMode.txt @@ -3,11 +3,11 @@ POST true { "status": "ok", - "data": [ + "data": { "margin_account": "USDT", "position_mode": "single_side" } - ], + , "ts": 1566899973811 } \ No newline at end of file diff --git a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetIsolatedMarginPositionMode.txt b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetIsolatedMarginPositionMode.txt index 71ea96b3..03ba68d6 100644 --- a/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetIsolatedMarginPositionMode.txt +++ b/HTX.Net.UnitTests/Endpoints/UsdtMarginSwap/Account/SetIsolatedMarginPositionMode.txt @@ -3,11 +3,11 @@ POST true { "status": "ok", - "data": [ + "data": { "margin_account": "BTC-USDT", "position_mode": "single_side" } - ], + , "ts": 1566899973811 } \ No newline at end of file diff --git a/HTX.Net.UnitTests/TestImplementations/TestSocket.cs b/HTX.Net.UnitTests/TestImplementations/TestSocket.cs index e1fc7dd1..9a14bca6 100644 --- a/HTX.Net.UnitTests/TestImplementations/TestSocket.cs +++ b/HTX.Net.UnitTests/TestImplementations/TestSocket.cs @@ -19,6 +19,7 @@ public class TestSocket: IWebsocket public event Func OnReconnected; public event Func OnReconnecting; public event Func OnRequestRateLimited; + public event Func OnConnectRateLimited; #pragma warning restore 0067 public event Func OnRequestSent; public event Func, Task> OnStreamMessage; diff --git a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs index 138214fb..07327b44 100644 --- a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs +++ b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.CommonObjects; using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Interfaces.CommonClients; +using CryptoExchange.Net.SharedApis; using HTX.Net.Enums; using HTX.Net.Interfaces.Clients.SpotApi; using HTX.Net.Objects.Internal; @@ -10,7 +11,7 @@ namespace HTX.Net.Clients.SpotApi { /// - internal class HTXRestClientSpotApi : RestApiClient, IHTXRestClientSpotApi, ISpotClient + internal partial class HTXRestClientSpotApi : RestApiClient, IHTXRestClientSpotApi, ISpotClient { /// public new HTXRestOptions ClientOptions => (HTXRestOptions)base.ClientOptions; @@ -64,8 +65,12 @@ internal HTXRestClientSpotApi(ILogger logger, HttpClient? httpClient, HTXRestOpt protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); + /// - public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToLowerInvariant()}{quoteAsset.ToLowerInvariant()}"; + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + { + return $"{baseAsset.ToLowerInvariant()}{quoteAsset.ToLowerInvariant()}"; + } /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) @@ -529,5 +534,6 @@ protected override Task> GetServerTimestampAsync() /// /// TODO make this take an accountId param so we don't need it in the interface? public ISpotClient CommonSpotClient => this; + public IHTXRestClientSpotApiShared SharedClient => this; } } diff --git a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiAccount.cs b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiAccount.cs index 26229f3d..11fefde3 100644 --- a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiAccount.cs +++ b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiAccount.cs @@ -371,7 +371,7 @@ public async Task> CancelWithdrawalAsync(long id, Cancellati #region Get Withdraw Deposit History /// - public async Task>> GetWithdrawDepositHistoryAsync(WithdrawDepositType type, string? asset = null, int? from = null, int? size = null, FilterDirection? direction = null, CancellationToken ct = default) + public async Task>> GetWithdrawDepositHistoryAsync(WithdrawDepositType type, string? asset = null, long? from = null, int? size = null, FilterDirection? direction = null, CancellationToken ct = default) { asset = asset?.ToLowerInvariant(); diff --git a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiShared.cs b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiShared.cs new file mode 100644 index 00000000..0919a5a5 --- /dev/null +++ b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiShared.cs @@ -0,0 +1,715 @@ +using HTX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.SharedApis; +using HTX.Net.Enums; + +namespace HTX.Net.Clients.SpotApi +{ + internal partial class HTXRestClientSpotApi : IHTXRestClientSpotApiShared + { + public string Exchange => HTXExchange.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 Kline client + GetKlinesOptions IKlineRestClient.GetKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.NotSupported, false) + { + MaxTotalDataPoints = 2000 + }; + + 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 the amount of data points we need to match the requested time + var apiLimit = 2000; + int limit = request.Limit ?? apiLimit; + if (request.StartTime.HasValue == true) + limit = (int)Math.Ceiling((DateTime.UtcNow - request.StartTime!.Value).TotalSeconds / (int)request.Interval); + + if (limit > apiLimit) + { + // Not available via the API + var cutoff = DateTime.UtcNow.AddSeconds(-(int)request.Interval * apiLimit); + return new ExchangeWebResult>(Exchange, new ArgumentError($"Time filter outside of supported range. Can only request the most recent {apiLimit} klines i.e. data later than {cutoff} at this interval")); + } + + // Pagination not supported, no time filter available + + // Get data + var result = await ExchangeData.GetKlinesAsync( + request.Symbol.GetSymbol(FormatSymbol), + interval, + limit, + ct: ct + ).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + // Filter the data based on requested timestamps + var data = result.Data; + if (request.StartTime.HasValue == true) + data = data.Where(d => d.OpenTime >= request.StartTime.Value); + if (request.EndTime.HasValue == true) + data = data.Where(d => d.OpenTime < request.EndTime.Value); + data = data.Reverse(); + if (request.Limit.HasValue == true) + data = data.Take(request.Limit.Value); + + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, data.Reverse().Select(x => new SharedKline(x.OpenTime, x.ClosePrice!.Value, x.HighPrice!.Value, x.LowPrice!.Value, x.OpenPrice!.Value, x.Volume!.Value)).ToArray()); + } + #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.GetSymbolConfigAsync(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.BaseAsset.ToUpperInvariant(), + s.QuoteAsset.ToUpperInvariant(), + s.Symbol, + s.Status == SymbolStatus.Online) + { + QuantityDecimals = s.QuantityPrecision, + PriceDecimals = s.PricePrecision, + MinNotionalValue = s.MinOrderValue, + MinTradeQuantity = s.MinOrderQuantity, + MaxTradeQuantity = s.MaxOrderQuantity + }).ToArray()); + } + + #endregion + + #region Ticker client + + 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).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + return result.AsExchangeResult>(Exchange, TradingMode.Spot, result.Data.Ticks.Select(x => new SharedSpotTicker(x.Symbol, x.LastTradePrice, x.HighPrice, x.LowPrice, x.Volume ?? 0, x.OpenPrice == null || x.OpenPrice == 0 ? null : Math.Round(((x.ClosePrice ?? 0) / x.OpenPrice.Value) * 100 - 100, 2))).ToArray()); + } + + 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 symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await ExchangeData.GetTickerAsync( + symbol, + ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, TradingMode.Spot, new SharedSpotTicker(symbol, result.Data.ClosePrice, result.Data.HighPrice, result.Data.LowPrice, result.Data.Volume ?? 0, result.Data.OpenPrice == null || result.Data.OpenPrice == 0 ? null : Math.Round((result.Data.ClosePrice ?? 0) / result.Data.OpenPrice.Value * 100 - 100, 2))); + } + + #endregion + + #region Recent Trade client + + GetRecentTradesOptions IRecentTradeRestClient.GetRecentTradesOptions { get; } = new GetRecentTradesOptions(2000, 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 ?? 100, + ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.SelectMany(x => x.Details.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp))).ToArray()); + } + + #endregion + + #region Balance client + EndpointOptions IBalanceRestClient.GetBalancesOptions { get; } = new EndpointOptions(true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("AccountId", typeof(long), "Account id of the user", 123123123L) + } + }; + + 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 accountId = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "AccountId"); + var result = await Account.GetBalancesAsync(accountId, ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + var resp = new List(); + foreach(var balance in result.Data) + { + var asset = resp.SingleOrDefault(x => x.Asset == balance.Asset.ToUpperInvariant()); + if (asset == null) + { + asset = new SharedBalance(balance.Asset.ToUpperInvariant(), 0, 0); + resp.Add(asset); + } + + if (balance.Type == Enums.BalanceType.Trade) + { + asset.Available = balance.Balance; + asset.Total += balance.Balance; + } + else if (balance.Type == Enums.BalanceType.Frozen) + { + asset.Total += balance.Balance; + } + } + + return result.AsExchangeResult>(Exchange, TradingMode.Spot, resp); + } + + #endregion + + #region Spot Order client + + + SharedFeeDeductionType ISpotOrderRestClient.SpotFeeDeductionType => SharedFeeDeductionType.DeductFromOutput; + SharedFeeAssetType ISpotOrderRestClient.SpotFeeAssetType => SharedFeeAssetType.OutputAsset; + IEnumerable ISpotOrderRestClient.SpotSupportedOrderTypes { get; } = new[] { SharedOrderType.Limit, SharedOrderType.Market, SharedOrderType.LimitMaker }; + IEnumerable ISpotOrderRestClient.SpotSupportedTimeInForce { get; } = new[] { SharedTimeInForce.GoodTillCanceled, SharedTimeInForce.ImmediateOrCancel, SharedTimeInForce.FillOrKill }; + + SharedQuantitySupport ISpotOrderRestClient.SpotSupportedOrderQuantity { get; } = new SharedQuantitySupport( + SharedQuantityType.BaseAsset, + SharedQuantityType.BaseAsset, + SharedQuantityType.QuoteAsset, + SharedQuantityType.BaseAsset); + + PlaceSpotOrderOptions ISpotOrderRestClient.PlaceSpotOrderOptions { get; } = new PlaceSpotOrderOptions() + { + RequiredExchangeParameters = new List + { + new ParameterDescription("AccountId", typeof(long), "The id of the account", 123123123L) + } + }; + 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 accountId = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "AccountId"); + var quantity = request.Quantity ?? 0; + if (request.OrderType == SharedOrderType.Market && request.Side == SharedOrderSide.Buy) + quantity = request.QuoteQuantity ?? 0; + + var result = await Trading.PlaceOrderAsync( + accountId, + request.Symbol.GetSymbol(FormatSymbol), + request.Side == SharedOrderSide.Buy ? Enums.OrderSide.Buy : Enums.OrderSide.Sell, + GetPlaceOrderType(request.OrderType, request.TimeInForce), + quantity, + request.Price, + request.ClientOrderId).ConfigureAwait(false); + + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(result.Data.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(orderId).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult(Exchange, null, default); + + return order.AsExchangeResult(Exchange, TradingMode.Spot, new SharedSpotOrder( + order.Data.Symbol, + order.Data.Id.ToString(), + ParseOrderType(order.Data.Type), + order.Data.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(order.Data.Status), + order.Data.CreateTime) + { + ClientOrderId = order.Data.ClientOrderId, + Fee = order.Data.Fee, + OrderPrice = order.Data.Price, + Quantity = order.Data.Type == OrderType.Market && order.Data.Side == OrderSide.Buy ? null : order.Data.Quantity, + QuantityFilled = order.Data.QuantityFilled, + QuoteQuantity = order.Data.Type == OrderType.Market && order.Data.Side == OrderSide.Buy ? order.Data.Quantity : null, + QuoteQuantityFilled = order.Data.QuoteQuantityFilled, + TimeInForce = ParseTimeInForce(order.Data.Type) + }); + } + + 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 order = await Trading.GetOpenOrdersAsync(symbol: symbol).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult>(Exchange, null, default); + + return order.AsExchangeResult>(Exchange, TradingMode.Spot, order.Data.Select(x => new SharedSpotOrder( + x.Symbol, + x.Id.ToString(), + ParseOrderType(x.Type), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId, + Fee = x.Fee, + OrderPrice = x.Price, + Quantity = x.Type == OrderType.Market && x.Side == OrderSide.Buy ? null : x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantity = x.Type == OrderType.Market && x.Side == OrderSide.Buy ? x.Quantity : null, + QuoteQuantityFilled = x.QuoteQuantityFilled, + TimeInForce = ParseTimeInForce(x.Type) + }).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); + + long? fromId = null; + if (pageToken is FromIdToken fromToken) + fromId = long.Parse(fromToken.FromToken); + + var limit = request.Limit ?? 100; + var order = await Trading.GetClosedOrdersAsync( + request.Symbol.GetSymbol(FormatSymbol), + startTime: request.StartTime, + endTime: request.EndTime, + direction: FilterDirection.Next, + fromId: fromId, + limit: limit).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult>(Exchange, null, default); + + var data = order.Data.Where(x => x.Id != long.Parse((pageToken as FromIdToken)?.FromToken ?? "0")); + FromIdToken? nextToken = null; + if (order.Data.Count() == limit) + nextToken = new FromIdToken(data.Min(x => x.Id).ToString()); + + return order.AsExchangeResult>(Exchange, TradingMode.Spot, data.Select(x => new SharedSpotOrder( + x.Symbol, + x.Id.ToString(), + ParseOrderType(x.Type), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId, + Fee = x.Fee, + OrderPrice = x.Price, + Quantity = x.Type == OrderType.Market && x.Side == OrderSide.Buy ? null : x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantity = x.Type == OrderType.Market && x.Side == OrderSide.Buy ? x.Quantity : null, + QuoteQuantityFilled = x.QuoteQuantityFilled, + TimeInForce = ParseTimeInForce(x.Type) + }).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 order = await Trading.GetOrderTradesAsync(orderId).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult>(Exchange, null, default); + + return order.AsExchangeResult>(Exchange, TradingMode.Spot, order.Data.Select(x => new SharedUserTrade( + x.Symbol, + x.OrderId.ToString(), + x.Id.ToString(), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.Timestamp) + { + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.Role == OrderRole.Taker ? SharedRole.Taker : SharedRole.Maker + }).ToArray()); + } + + PaginatedEndpointOptions ISpotOrderRestClient.GetSpotUserTradesOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Descending, true); + async Task>> ISpotOrderRestClient.GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? pageToken, CancellationToken ct) + { + var validationError = ((ISpotOrderRestClient)this).GetSpotUserTradesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + // Determine page token + long? fromId = null; + if (pageToken is FromIdToken fromIdToken) + fromId = long.Parse(fromIdToken.FromToken); + + // Get data + var limit = request.Limit ?? 100; + var order = await Trading.GetUserTradesAsync( + request.Symbol.GetSymbol(FormatSymbol), + startTime: request.StartTime, + endTime: request.EndTime, + fromId: fromId, + direction: FilterDirection.Next, + limit: limit).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult>(Exchange, null, default); + + // Get next token + var data = order.Data.Where(x => x.Id != long.Parse((pageToken as FromIdToken)?.FromToken ?? "0")); + FromIdToken? nextToken = null; + if (order.Data.Count() == limit) + nextToken = new FromIdToken(data.Min(o => o.Id).ToString()); + + return order.AsExchangeResult>(Exchange, TradingMode.Spot, 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) + { + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.Role == OrderRole.Taker ? SharedRole.Taker: SharedRole.Maker + }).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(orderId).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult(Exchange, null, default); + + return order.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(order.Data.ToString())); + } + + private SharedOrderStatus ParseOrderStatus(OrderStatus status) + { + if (status == OrderStatus.Submitted || status == OrderStatus.PreSubmitted || status == OrderStatus.Created || status == OrderStatus.PartiallyFilled) return SharedOrderStatus.Open; + if (status == OrderStatus.Canceled || status == OrderStatus.PartiallyCanceled || status == OrderStatus.Rejected) return SharedOrderStatus.Canceled; + return SharedOrderStatus.Filled; + } + + private SharedOrderType ParseOrderType(OrderType type) + { + if (type == OrderType.Market) return SharedOrderType.Market; + if (type == OrderType.LimitMaker) return SharedOrderType.LimitMaker; + if (type == OrderType.Limit || type == OrderType.FillOrKillLimit || type == OrderType.IOC) return SharedOrderType.Limit; + + return SharedOrderType.Other; + } + + private SharedTimeInForce? ParseTimeInForce(OrderType tif) + { + if (tif == OrderType.IOC) return SharedTimeInForce.ImmediateOrCancel; + if (tif == OrderType.FillOrKillLimit) return SharedTimeInForce.FillOrKill; + if (tif == OrderType.Limit) return SharedTimeInForce.GoodTillCanceled; + if (tif == OrderType.LimitMaker) return SharedTimeInForce.GoodTillCanceled; + + return null; + } + + private OrderType GetPlaceOrderType(SharedOrderType type, SharedTimeInForce? tif) + { + if (type == SharedOrderType.Limit && (tif == null || tif == SharedTimeInForce.GoodTillCanceled)) return OrderType.Limit; + if (type == SharedOrderType.Limit && tif == SharedTimeInForce.ImmediateOrCancel) return OrderType.IOC; + if (type == SharedOrderType.Limit && tif == SharedTimeInForce.FillOrKill) return OrderType.FillOrKillLimit; + if (type == SharedOrderType.LimitMaker) return OrderType.LimitMaker; + if (type == SharedOrderType.Market) return OrderType.Market; + + throw new ArgumentException($"The combination of order type `{type}` and time in force `{tif}` in invalid"); + } + + #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 assets = await ExchangeData.GetAssetsAndNetworksAsync(request.Asset, ct: ct).ConfigureAwait(false); + if (!assets) + return assets.AsExchangeResult(Exchange, null, default); + + var asset = assets.Data.SingleOrDefault(); + if (asset == null) + return assets.AsExchangeError(Exchange, new ServerError("Asset not found")); + + return assets.AsExchangeResult(Exchange, TradingMode.Spot, new SharedAsset(asset.Asset.ToUpperInvariant()) + { + Networks = asset.Networks.Select(x => new SharedAssetNetwork(x.Network) + { + FullName = x.DisplayName, + MinConfirmations = x.NumOfConfirmations, + DepositEnabled = x.DepositStatus == NetworkStatus.Allowed, + MinWithdrawQuantity = x.MinWithdrawQuantity, + MaxWithdrawQuantity = x.MaxWithdrawQuantity, + WithdrawEnabled = x.WithdrawStatus == NetworkStatus.Allowed, + WithdrawFee = x.FixedWithdrawFee + }).ToArray() + }); + } + + EndpointOptions IAssetsRestClient.GetAssetsOptions { get; } = new EndpointOptions(false); + async Task>> IAssetsRestClient.GetAssetsAsync(GetAssetsRequest request, CancellationToken ct) + { + var validationError = ((IAssetsRestClient)this).GetAssetsOptions.ValidateRequest(Exchange, request, TradingMode.Spot, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + var assets = await ExchangeData.GetAssetsAndNetworksAsync(ct: ct).ConfigureAwait(false); + if (!assets) + return assets.AsExchangeResult>(Exchange, null, default); + + return assets.AsExchangeResult>(Exchange, TradingMode.Spot, assets.Data.Select(x => new SharedAsset(x.Asset.ToUpperInvariant()) + { + Networks = x.Networks.Select(x => new SharedAssetNetwork(x.Network) + { + FullName = x.DisplayName, + MinConfirmations = x.NumOfConfirmations, + DepositEnabled = x.DepositStatus == NetworkStatus.Allowed, + MinWithdrawQuantity = x.MinWithdrawQuantity, + MaxWithdrawQuantity = x.MaxWithdrawQuantity, + WithdrawEnabled = x.WithdrawStatus == NetworkStatus.Allowed, + WithdrawFee = x.FixedWithdrawFee + }).ToArray() + }).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.GetDepositAddressesAsync(request.Asset).ConfigureAwait(false); + if (!depositAddresses) + return depositAddresses.AsExchangeResult>(Exchange, null, default); + + return depositAddresses.AsExchangeResult>(Exchange, TradingMode.Spot, depositAddresses.Data.Where(x => request.Network == null ? true : x.Network == request.Network).Select(x => new SharedDepositAddress(x.Asset.ToUpperInvariant(), x.Address) + { + Network = x.Network, + TagOrMemo = x.AddressTag + } + ).ToArray()); + } + + GetDepositsOptions IDepositRestClient.GetDepositsOptions { get; } = new GetDepositsOptions(SharedPaginationSupport.Descending, false); + 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 + long? from = null; + if (pageToken is FromIdToken idToken) + from = long.Parse(idToken.FromToken); + + // Get data + var deposits = await Account.GetWithdrawDepositHistoryAsync( + WithdrawDepositType.Deposit, + request.Asset, + from: from, + size: request.Limit ?? 100, + direction: FilterDirection.Next, + ct: ct).ConfigureAwait(false); + if (!deposits) + return deposits.AsExchangeResult>(Exchange, null, default); + + // Determine next token + FromIdToken? nextToken = null; + if (deposits.Data.Count() == (request.Limit ?? 100)) + nextToken = new FromIdToken(deposits.Data.Min(x => x.Id - 1).ToString()); + + return deposits.AsExchangeResult>(Exchange, TradingMode.Spot, deposits.Data.Select(x => new SharedDeposit(x.Asset!.ToUpperInvariant(), x.Quantity, x.Status == WithdrawDepositStatus.Confirmed, x.CreateTime) + { + Id = x.Id.ToString(), + Network = x.Network, + TransactionId = x.TransactionHash, + Tag = x.AddressTag + }).ToArray(), nextToken); + } + + #endregion + + #region Order Book client + GetOrderBookOptions IOrderBookRestClient.GetOrderBookOptions { get; } = new GetOrderBookOptions(new[] { 5, 10, 20 }, 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), + 0, + 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, false); + 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 + long? from = null; + if (pageToken is FromIdToken idToken) + from = long.Parse(idToken.FromToken); + + // Get data + var deposits = await Account.GetWithdrawDepositHistoryAsync( + WithdrawDepositType.Withdraw, + request.Asset, + from: from, + size: request.Limit ?? 100, + direction: FilterDirection.Next, + ct: ct).ConfigureAwait(false); + if (!deposits) + return deposits.AsExchangeResult>(Exchange, null, default); + + // Determine next token + FromIdToken? nextToken = null; + if (deposits.Data.Count() == (request.Limit ?? 100)) + nextToken = new FromIdToken(deposits.Data.Min(x => x.Id - 1).ToString()); + + return deposits.AsExchangeResult>(Exchange, TradingMode.Spot, deposits.Data.Select(x => new SharedWithdrawal(x.Asset!.ToUpperInvariant(), x.Address!, x.Quantity, x.Status == WithdrawDepositStatus.Confirmed, x.CreateTime) + { + Id = x.Id.ToString(), + Network = x.Network, + TransactionId = x.TransactionHash, + Tag = x.AddressTag, + Fee = x.Fee + }).ToArray(), nextToken); + } + + #endregion + + #region Withdraw client + + WithdrawOptions IWithdrawRestClient.WithdrawOptions { get; } = new WithdrawOptions() + { + RequiredExchangeParameters = new List + { + new ParameterDescription("withdrawFee", typeof(decimal), "Fee to use for the withdrawal", 0.001m) + } + }; + + 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); + + var fee = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "withdrawFee"); + if (fee == null) + return new ExchangeWebResult(Exchange, new ArgumentError("HTX requires withdrawal fee parameter. Please pass it as exchangeParameter `fee`")); + + // Get data + var withdrawal = await Account.WithdrawAsync( + asset: request.Asset, + fee: fee.Value, + address: request.Address, + quantity: request.Quantity, + network: request.Network, + addressTag: request.AddressTag, + ct: ct).ConfigureAwait(false); + if (!withdrawal) + return withdrawal.AsExchangeResult(Exchange, null, default); + + return withdrawal.AsExchangeResult(Exchange, TradingMode.Spot, new SharedId(withdrawal.Data.ToString())); + } + + #endregion + } +} diff --git a/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs index 8d4b7ea8..534c244f 100644 --- a/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs +++ b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.Clients; using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.Sockets; using HTX.Net.Enums; using HTX.Net.Interfaces.Clients.SpotApi; @@ -17,7 +18,7 @@ namespace HTX.Net.Clients.SpotApi { /// - internal class HTXSocketClientSpotApi : SocketApiClient, IHTXSocketClientSpotApi + internal partial class HTXSocketClientSpotApi : SocketApiClient, IHTXSocketClientSpotApi { private static readonly MessagePath _idPath = MessagePath.Get().Property("id"); private static readonly MessagePath _actionPath = MessagePath.Get().Property("action"); @@ -45,8 +46,10 @@ internal HTXSocketClientSpotApi(ILogger logger, HTXSocketOptions options) protected override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(); + public IHTXSocketClientSpotApiShared SharedClient => this; + /// - public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToLowerInvariant()}{quoteAsset.ToLowerInvariant()}"; + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) => $"{baseAsset.ToLowerInvariant()}{quoteAsset.ToLowerInvariant()}"; /// public override string? GetListenerIdentifier(IMessageAccessor message) @@ -79,6 +82,7 @@ public override ReadOnlyMemory PreprocessStreamMessage(SocketConnection co return data.DecompressGzip(); } + /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => new HTXAuthenticationProvider(credentials, false); diff --git a/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApiShared.cs b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApiShared.cs new file mode 100644 index 00000000..5417d93f --- /dev/null +++ b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApiShared.cs @@ -0,0 +1,246 @@ +using HTX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.Objects.Sockets; +using HTX.Net.Objects.Models.Socket; +using HTX.Net.Enums; + +namespace HTX.Net.Clients.SpotApi +{ + internal partial class HTXSocketClientSpotApi : IHTXSocketClientSpotApiShared + { + public string Exchange => HTXExchange.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 Tickers client + EndpointOptions ITickersSocketClient.SubscribeAllTickersOptions { get; } = new EndpointOptions(false); + async Task> ITickersSocketClient.SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action>> handler, CancellationToken ct) + { + var validationError = ((ITickersSocketClient)this).SubscribeAllTickersOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var result = await SubscribeToTickerUpdatesAsync(update => handler(update.AsExchangeEvent>(Exchange, update.Data.Select(x => new SharedSpotTicker(x.Symbol, x.ClosePrice ?? 0, x.HighPrice ?? 0, x.LowPrice ?? 0, x.Volume ?? 0, x.OpenPrice == null ? null : Math.Round((x.ClosePrice ?? 0) / x.OpenPrice.Value * 100 - 100, 2))).ToArray()))).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Ticker client + EndpointOptions ITickerSocketClient.SubscribeTickerOptions { get; } = new EndpointOptions(false); + async Task> ITickerSocketClient.SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct) + { + var validationError = ((ITickerSocketClient)this).SubscribeTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToTickerUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, new SharedSpotTicker(symbol, update.Data.LastTradePrice, update.Data.HighPrice ?? 0, update.Data.LowPrice ?? 0, update.Data.Volume ?? 0, update.Data.OpenPrice == null ? null : Math.Round((update.Data.ClosePrice ?? 0) / update.Data.OpenPrice.Value * 100 - 100, 2))))).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.Details.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Book Ticker client + + EndpointOptions IBookTickerSocketClient.SubscribeBookTickerOptions { get; } = new EndpointOptions(false); + async Task> IBookTickerSocketClient.SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct) + { + var validationError = ((IBookTickerSocketClient)this).SubscribeBookTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToBookTickerUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, new SharedBookTicker(update.Data.BestAskPrice, update.Data.BestAskQuantity, update.Data.BestBidPrice, update.Data.BestBidQuantity))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Kline client + SubscribeKlineOptions IKlineSocketClient.SubscribeKlineOptions { get; } = new SubscribeKlineOptions(false); + async Task> IKlineSocketClient.SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct) + { + var interval = (Enums.KlineInterval)request.Interval; + if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval)) + return new ExchangeResult(Exchange, new ArgumentError("Interval not supported")); + + var validationError = ((IKlineSocketClient)this).SubscribeKlineOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToKlineUpdatesAsync(symbol, interval, update => handler(update.AsExchangeEvent(Exchange, new SharedKline(update.Data.OpenTime, update.Data.ClosePrice ?? 0, update.Data.HighPrice ?? 0, update.Data.LowPrice ?? 0, update.Data.OpenPrice ?? 0, update.Data.Volume ?? 0))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Order Book client + SubscribeOrderBookOptions IOrderBookSocketClient.SubscribeOrderBookOptions { get; } = new SubscribeOrderBookOptions(false, new[] { 5, 10, 20 }); + async Task> IOrderBookSocketClient.SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct) + { + var validationError = ((IOrderBookSocketClient)this).SubscribeOrderBookOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToPartialOrderBookUpdates100MilisecondAsync(symbol, request.Limit ?? 20, update => handler(update.AsExchangeEvent(Exchange, new SharedOrderBook(update.Data.Asks, update.Data.Bids))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Balance client + EndpointOptions IBalanceSocketClient.SubscribeBalanceOptions { get; } = new EndpointOptions(false); + 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 SubscribeToAccountUpdatesAsync( + update => handler(update.AsExchangeEvent>(Exchange, new[] { new SharedBalance(update.Data.Asset, update.Data.Available ?? 0, update.Data.Balance ?? update.Data.Available ?? 0) })), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Spot Order client + + EndpointOptions ISpotOrderSocketClient.SubscribeSpotOrderOptions { get; } = new EndpointOptions(false); + 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(null, + update => handler(update.AsExchangeEvent>(Exchange, new[] { ParseOrder(update.Data) })), + update => handler(update.AsExchangeEvent>(Exchange, new[] { ParseOrder(update.Data) })), + update => handler(update.AsExchangeEvent>(Exchange, new[] { ParseOrder(update.Data) })), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region User Trade client + EndpointOptions IUserTradeSocketClient.SubscribeUserTradeOptions { get; } = new EndpointOptions(false); + async Task> IUserTradeSocketClient.SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action>> handler, CancellationToken ct) + { + var validationError = ((IUserTradeSocketClient)this).SubscribeUserTradeOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var result = await SubscribeToOrderDetailsUpdatesAsync( + null, + update => handler(update.AsExchangeEvent>(Exchange, new[] { + new SharedUserTrade( + update.Data.Symbol, + update.Data.OrderId.ToString(), + update.Data.Id.ToString(), + update.Data.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + update.Data.Quantity, + update.Data.Price, + update.Data.Timestamp) + { + Role = update.Data.IsTaker ? SharedRole.Taker : SharedRole.Maker, + Fee = update.Data.TransactionFee, + FeeAsset = update.Data.FeeAsset + } + })), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + public SharedSpotOrder ParseOrder(HTXOrderUpdate orderUpdate) + { + if (orderUpdate is HTXSubmittedOrderUpdate update) + { + return new SharedSpotOrder( + update.Symbol, + update.OrderId.ToString(), + update.Type == Enums.OrderType.Limit ? SharedOrderType.Limit : update.Type == Enums.OrderType.Market ? SharedOrderType.Market : SharedOrderType.Other, + update.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + SharedOrderStatus.Open, + update.CreateTime) + { + ClientOrderId = update.ClientOrderId, + Quantity = update.Quantity, + QuantityFilled = 0, + QuoteQuantity = update.QuoteQuantity, + QuoteQuantityFilled = 0, + UpdateTime = update.UpdateTime, + OrderPrice = update.Price, + Fee = 0 + }; + } + if (orderUpdate is HTXMatchedOrderUpdate matchUpdate) + { + return new SharedSpotOrder( + matchUpdate.Symbol, + matchUpdate.OrderId.ToString(), + matchUpdate.Type == Enums.OrderType.Limit ? SharedOrderType.Limit : matchUpdate.Type == Enums.OrderType.Market ? SharedOrderType.Market : SharedOrderType.Other, + matchUpdate.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + matchUpdate.QuantityRemaining == 0 ? SharedOrderStatus.Filled : SharedOrderStatus.Open, + null) + { + ClientOrderId = matchUpdate.ClientOrderId, + Quantity = matchUpdate.Type == Enums.OrderType.Market && matchUpdate.Side == Enums.OrderSide.Buy ? null : matchUpdate.Quantity, + QuantityFilled = matchUpdate.Type == Enums.OrderType.Market && matchUpdate.Side == Enums.OrderSide.Buy ? null : matchUpdate.QuantityFilled, + QuoteQuantity = matchUpdate.QuoteQuantity, + QuoteQuantityFilled = matchUpdate.Type == Enums.OrderType.Market && matchUpdate.Side == Enums.OrderSide.Buy ? matchUpdate.QuantityFilled : null, + UpdateTime = matchUpdate.UpdateTime, + OrderPrice = matchUpdate.Price, + LastTrade = new SharedUserTrade(matchUpdate.Symbol, matchUpdate.OrderId.ToString(), matchUpdate.TradeId.ToString(), matchUpdate.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, matchUpdate.TradeQuantity, matchUpdate.TradePrice, matchUpdate.TradeTime) + { + Role = matchUpdate.IsTaker ? SharedRole.Taker : SharedRole.Maker + } + }; + } + + if (orderUpdate is HTXCanceledOrderUpdate cancelUpdate) + { + return new SharedSpotOrder( + cancelUpdate.Symbol, + cancelUpdate.OrderId.ToString(), + cancelUpdate.Type == Enums.OrderType.Limit ? SharedOrderType.Limit : cancelUpdate.Type == Enums.OrderType.Market ? SharedOrderType.Market : SharedOrderType.Other, + cancelUpdate.Side == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + SharedOrderStatus.Canceled, + null) + { + ClientOrderId = cancelUpdate.ClientOrderId, + Quantity = cancelUpdate.Type == Enums.OrderType.Market && cancelUpdate.Side == Enums.OrderSide.Buy ? null : cancelUpdate.Quantity, + QuantityFilled = cancelUpdate.Type == Enums.OrderType.Market && cancelUpdate.Side == Enums.OrderSide.Buy ? null : cancelUpdate.Quantity - cancelUpdate.QuantityRemaining, + QuoteQuantity = cancelUpdate.Type == Enums.OrderType.Market && cancelUpdate.Side == Enums.OrderSide.Buy ? cancelUpdate.Quantity : null, + QuoteQuantityFilled = cancelUpdate.Type == Enums.OrderType.Market && cancelUpdate.Side == Enums.OrderSide.Buy ? cancelUpdate.Quantity - cancelUpdate.QuantityRemaining : null, + UpdateTime = cancelUpdate.UpdateTime, + OrderPrice = cancelUpdate.Price + }; + } + + throw new Exception("Unknown order update type"); + } + } +} diff --git a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs index 1bdf5ee7..66e84c43 100644 --- a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs +++ b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs @@ -1,5 +1,7 @@ using CryptoExchange.Net.Clients; using CryptoExchange.Net.Converters.MessageParsing; +using CryptoExchange.Net.SharedApis; +using HTX.Net.Interfaces.Clients.SpotApi; using HTX.Net.Interfaces.Clients.UsdtFuturesApi; using HTX.Net.Objects.Internal; using HTX.Net.Objects.Options; @@ -7,7 +9,7 @@ namespace HTX.Net.Clients.UsdtFutures { /// - internal class HTXRestClientUsdtFuturesApi : RestApiClient, IHTXRestClientUsdtFuturesApi + internal partial class HTXRestClientUsdtFuturesApi : RestApiClient, IHTXRestClientUsdtFuturesApi { /// public new HTXRestOptions ClientOptions => (HTXRestOptions)base.ClientOptions; @@ -49,8 +51,12 @@ internal HTXRestClientUsdtFuturesApi(ILogger log, HttpClient? httpClient, HTXRes protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); + public IHTXRestClientUsdtFuturesApiShared SharedClient => this; /// - public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}"; + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + { + return $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}" + (!deliverTime.HasValue ? string.Empty: ("-" + deliverTime.Value.ToString("yyMMdd"))); + } /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) diff --git a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiAccount.cs b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiAccount.cs index 965896f2..866dbb23 100644 --- a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiAccount.cs +++ b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiAccount.cs @@ -326,8 +326,7 @@ public async Task> SetIsolatedMarginPositionModeA { "position_mode", EnumConverter.GetString(positionMode) }, }; var request = _definitions.GetOrCreate(HttpMethod.Post, "linear-swap-api/v1/swap_switch_position_mode", HTXExchange.RateLimiter.EndpointLimit, 1, true); - var result = await _baseClient.SendBasicAsync>(request, parameters, ct).ConfigureAwait(false); - return result.As(result.Data?.First()); + return await _baseClient.SendBasicAsync(request, parameters, ct).ConfigureAwait(false); } #endregion @@ -343,8 +342,7 @@ public async Task> SetCrossMarginPositionModeAsyn { "position_mode", EnumConverter.GetString(positionMode) }, }; var request = _definitions.GetOrCreate(HttpMethod.Post, "linear-swap-api/v1/swap_cross_switch_position_mode", HTXExchange.RateLimiter.EndpointLimit, 1, true); - var result = await _baseClient.SendBasicAsync>(request, parameters, ct).ConfigureAwait(false); - return result.As(result.Data?.First()); + return await _baseClient.SendBasicAsync(request, parameters, ct).ConfigureAwait(false); } #endregion @@ -352,12 +350,12 @@ public async Task> SetCrossMarginPositionModeAsyn #region Get Isolated Margin Position Mode /// - public async Task>> GetIsolatedMarginPositionModeAsync(string marginAccount, CancellationToken ct = default) + public async Task> GetIsolatedMarginPositionModeAsync(string marginAccount, CancellationToken ct = default) { var parameters = new ParameterCollection(); parameters.Add("margin_account", marginAccount); var request = _definitions.GetOrCreate(HttpMethod.Get, "/linear-swap-api/v1/swap_position_side", HTXExchange.RateLimiter.EndpointLimit, 1, true); - var result = await _baseClient.SendBasicAsync>(request, parameters, ct).ConfigureAwait(false); + var result = await _baseClient.SendBasicAsync(request, parameters, ct).ConfigureAwait(false); return result; } @@ -366,12 +364,12 @@ public async Task>> GetIsolatedMargin #region Get Cross Margin Position Mode /// - public async Task>> GetCrossMarginPositionModeAsync(string marginAccount, CancellationToken ct = default) + public async Task> GetCrossMarginPositionModeAsync(string marginAccount, CancellationToken ct = default) { var parameters = new ParameterCollection(); parameters.Add("margin_account", marginAccount); var request = _definitions.GetOrCreate(HttpMethod.Get, "/linear-swap-api/v1/swap_cross_position_side", HTXExchange.RateLimiter.EndpointLimit, 1, true); - var result = await _baseClient.SendBasicAsync>(request, parameters, ct).ConfigureAwait(false); + var result = await _baseClient.SendBasicAsync(request, parameters, ct).ConfigureAwait(false); return result; } diff --git a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiShared.cs b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiShared.cs new file mode 100644 index 00000000..cc65e685 --- /dev/null +++ b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiShared.cs @@ -0,0 +1,1113 @@ +using HTX.Net.Interfaces.Clients.SpotApi; +using CryptoExchange.Net.SharedApis; +using HTX.Net.Enums; + +namespace HTX.Net.Clients.UsdtFutures +{ + internal partial class HTXRestClientUsdtFuturesApi : IHTXRestClientUsdtFuturesApiShared + { + public string Exchange => HTXExchange.ExchangeName; + public TradingMode[] SupportedTradingModes { get; } = new[] { TradingMode.PerpetualLinear, TradingMode.DeliveryLinear }; + + public void SetDefaultExchangeParameter(string key, object value) => ExchangeParameters.SetStaticParameter(Exchange, key, value); + public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticParameters(); + + #region Balance Client + EndpointOptions IBalanceRestClient.GetBalancesOptions { get; } = new EndpointOptions(true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await Account.GetCrossMarginAccountInfoAsync(ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + return result.AsExchangeResult>(Exchange, SupportedTradingModes, result.Data.Select(x => new SharedBalance(x.MarginAsset, x.MarginBalance, x.MarginFrozen + x.MarginBalance)).ToArray()); + } + else + { + var result = await Account.GetIsolatedMarginAccountInfoAsync(ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + return result.AsExchangeResult>(Exchange, SupportedTradingModes, result.Data.Select(x => new SharedBalance(x.MarginAsset, x.MarginBalance, x.MarginFrozen + x.MarginBalance) + { + IsolatedMarginSymbol = x.ContractCode + }).ToArray()); + } + } + + #endregion + + #region Ticker client + + EndpointOptions IFuturesTickerRestClient.GetFuturesTickerOptions { get; } = new EndpointOptions(false); + async Task> IFuturesTickerRestClient.GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct) + { + var validationError = ((IFuturesTickerRestClient)this).GetFuturesTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var resultTicker = ExchangeData.GetTickerAsync(symbol, ct); + var resultIndex = ExchangeData.GetSwapIndexPriceAsync(symbol, ct); + var resultFunding = ExchangeData.GetFundingRateAsync(request.Symbol.GetSymbol(FormatSymbol), ct); + await Task.WhenAll(resultTicker, resultFunding, resultIndex).ConfigureAwait(false); + + if (!resultTicker.Result) + return resultTicker.Result.AsExchangeResult(Exchange, null, default); + if (!resultFunding.Result) + return resultFunding.Result.AsExchangeResult(Exchange, null, default); + if (!resultIndex.Result) + return resultIndex.Result.AsExchangeResult(Exchange, null, default); + + return resultTicker.Result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedFuturesTicker(symbol, resultTicker.Result.Data.ClosePrice ?? 0, resultTicker.Result.Data.HighPrice ?? 0, resultTicker.Result.Data.LowPrice ?? 0, resultTicker.Result.Data.Volume ?? 0, resultTicker.Result.Data.OpenPrice == null ? null : Math.Round((resultTicker.Result.Data.ClosePrice ?? 0) / resultTicker.Result.Data.OpenPrice.Value * 100 - 100, 2)) + { + IndexPrice = resultIndex.Result.Data.Single().IndexPrice, + FundingRate = resultFunding.Result.Data.FundingRate, + NextFundingTime = resultFunding.Result.Data.FundingTime + }); + } + + 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); + + var data = resultTickers.Result.Data; + if (request.TradingMode != null) + data = data.Where(x => request.TradingMode == TradingMode.PerpetualLinear ? x.ContractCode.Count(x => x == '-') == 1 : x.ContractCode.Count(x => x == '-') == 2); + + return resultTickers.Result.AsExchangeResult>(Exchange, SupportedTradingModes, data.Select(x => + { + var funding = resultFunding.Result.Data.SingleOrDefault(p => p.ContractCode == x.ContractCode); + return new SharedFuturesTicker(x.ContractCode!, x.ClosePrice ?? 0, x.HighPrice ?? 0, x.LowPrice ?? 0, x.Volume ?? 0, x.OpenPrice == null ? null : Math.Round((x.ClosePrice ?? 0) / x.OpenPrice.Value * 100 - 100, 2)) + { + FundingRate = funding?.FundingRate, + NextFundingTime = funding?.FundingTime + }; + }).ToArray()); + } + + #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); + + var data = result.Data; + if (request.TradingMode.HasValue) + data = data.Where(x => request.TradingMode == TradingMode.PerpetualLinear ? x.BusinessType == BusinessType.Swap : x.BusinessType == BusinessType.Futures); + + return result.AsExchangeResult>(Exchange, + request.TradingMode == null ? SupportedTradingModes : new[] { request.TradingMode.Value }, + data.Select(s => new SharedFuturesSymbol( + s.BusinessType == BusinessType.Futures ? SharedSymbolType.DeliveryLinear : SharedSymbolType.PerpetualLinear, + s.Asset, + "USDT", + s.Symbol, + s.Status == ContractStatus.Listing) + { + PriceStep = s.PriceTick, + ContractSize = s.ContractSize, + DeliveryTime = s.DeliveryDate, + QuantityStep = 1 + }).ToArray()); + } + + #endregion + + #region Futures Order Client + + SharedFeeDeductionType IFuturesOrderRestClient.FuturesFeeDeductionType => SharedFeeDeductionType.AddToCost; + SharedFeeAssetType IFuturesOrderRestClient.FuturesFeeAssetType => SharedFeeAssetType.QuoteAsset; + IEnumerable IFuturesOrderRestClient.FuturesSupportedOrderTypes { get; } = new[] { SharedOrderType.Limit, SharedOrderType.Market }; + IEnumerable IFuturesOrderRestClient.FuturesSupportedTimeInForce { get; } = new[] { SharedTimeInForce.GoodTillCanceled, SharedTimeInForce.ImmediateOrCancel, SharedTimeInForce.FillOrKill }; + SharedQuantitySupport IFuturesOrderRestClient.FuturesSupportedOrderQuantity { get; } = new SharedQuantitySupport( + SharedQuantityType.Contracts, + SharedQuantityType.Contracts, + SharedQuantityType.Contracts, + SharedQuantityType.Contracts); + + PlaceFuturesOrderOptions IFuturesOrderRestClient.PlaceFuturesOrderOptions { get; } = new PlaceFuturesOrderOptions() + { + RequestNotes = "ClientOrderId can only be an integer", + RequiredExchangeParameters = new List + { + new ParameterDescription(nameof(PlaceFuturesOrderRequest.MarginMode), typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + }, + RequiredOptionalParameters = new List + { + new ParameterDescription(nameof(PlaceFuturesOrderRequest.Leverage), typeof(int), "The leverage to use", 3) + } + }; + + 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 marginMode = request.MarginMode ?? ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await Trading.PlaceCrossMarginOrderAsync( + contractCode: request.Symbol.GetSymbol(FormatSymbol), + quantity: (long)(request.Quantity ?? 0), + side: request.Side == SharedOrderSide.Buy ? Enums.OrderSide.Buy : Enums.OrderSide.Sell, + leverageRate: (int)(request.Leverage ?? 0), + orderPriceType: GetOrderPriceType(request.OrderType, request.TimeInForce), + price: request.Price, + offset: GetOffset(request.Side, request.PositionSide), + reduceOnly: request.ReduceOnly, + clientOrderId: request.ClientOrderId == null ? null : long.Parse(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())); + } + else + { + var result = await Trading.PlaceIsolatedMarginOrderAsync( + request.Symbol.GetSymbol(FormatSymbol), + quantity: (long)(request.Quantity ?? 0), + side: request.Side == SharedOrderSide.Buy ? Enums.OrderSide.Buy : Enums.OrderSide.Sell, + leverageRate: (int)(request.Leverage ?? 0), + orderPriceType: GetOrderPriceType(request.OrderType, request.TimeInForce), + price: request.Price, + offset: GetOffset(request.Side, request.PositionSide), + reduceOnly: request.ReduceOnly, + clientOrderId: request.ClientOrderId == null ? null :long.Parse(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) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var orders = await Trading.GetCrossMarginOrderAsync(request.Symbol.GetSymbol(FormatSymbol), orderId: orderId).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult(Exchange, null, default); + + var order = orders.Data.Single(); + return orders.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedFuturesOrder( + order.ContractCode, + order.OrderId.ToString(), + ParseOrderType(order.OrderPriceType), + order.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(order.Status), + order.CreateTime) + { + ClientOrderId = order.ClientOrderId.ToString(), + AveragePrice = order.AverageFillPrice, + OrderPrice = order.Price, + Quantity = order.Quantity, + QuantityFilled = order.QuantityFilled, + QuoteQuantityFilled = order.ValueFilled, + TimeInForce = ParseTimeInForce(order.OrderPriceType), + UpdateTime = order.UpdateTime, + PositionSide = ParsePositionSide(order.Offset, order.Side), + ReduceOnly = order.ReduceOnly, + Leverage = order.LeverageRate + }); + } + else + { + var orders = await Trading.GetIsolatedMarginOrderAsync(request.Symbol.GetSymbol(FormatSymbol), orderId: orderId).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult(Exchange, null, default); + + var order = orders.Data.Single(); + return orders.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedFuturesOrder( + order.ContractCode, + order.OrderId.ToString(), + ParseOrderType(order.OrderPriceType), + order.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(order.Status), + order.CreateTime) + { + ClientOrderId = order.ClientOrderId.ToString(), + AveragePrice = order.AverageFillPrice, + OrderPrice = order.Price, + Quantity = order.Quantity, + QuantityFilled = order.QuantityFilled, + QuoteQuantityFilled = order.ValueFilled, + TimeInForce = ParseTimeInForce(order.OrderPriceType), + UpdateTime = order.UpdateTime, + PositionSide = ParsePositionSide(order.Offset, order.Side), + ReduceOnly = order.ReduceOnly, + Leverage = order.LeverageRate + }); + } + } + + EndpointOptions IFuturesOrderRestClient.GetOpenFuturesOrdersOptions { get; } = new EndpointOptions(true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var symbol = request.Symbol?.GetSymbol(FormatSymbol); + var orders = await Trading.GetCrossMarginOpenOrdersAsync(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.Orders.Select(x => new SharedFuturesOrder( + x.ContractCode, + x.OrderId.ToString(), + ParseOrderType(x.OrderPriceType), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId.ToString(), + AveragePrice = x.AverageFillPrice, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantityFilled = x.ValueFilled, + TimeInForce = ParseTimeInForce(x.OrderPriceType), + UpdateTime = x.UpdateTime, + PositionSide = ParsePositionSide(x.Offset, x.Side), + ReduceOnly = x.ReduceOnly, + Leverage = x.LeverageRate + }).ToArray()); + } + else + { + if (request.Symbol == null) + return new ExchangeWebResult>(Exchange, new ArgumentError("Symbol parameter required for isolated margin request")); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var orders = await Trading.GetIsolatedMarginOpenOrdersAsync(symbol, ct: ct).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + return orders.AsExchangeResult>(Exchange, SupportedTradingModes ,orders.Data.Orders.Select(x => new SharedFuturesOrder( + x.ContractCode, + x.OrderId.ToString(), + ParseOrderType(x.OrderPriceType), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId.ToString(), + AveragePrice = x.AverageFillPrice, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantityFilled = x.ValueFilled, + TimeInForce = ParseTimeInForce(x.OrderPriceType), + UpdateTime = x.UpdateTime, + PositionSide = ParsePositionSide(x.Offset, x.Side), + ReduceOnly = x.ReduceOnly, + Leverage = x.LeverageRate + }).ToArray()); + } + } + + PaginatedEndpointOptions IFuturesOrderRestClient.GetClosedFuturesOrdersOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Descending, true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 + long? fromId = null; + if (pageToken is FromIdToken fromToken) + fromId = long.Parse(fromToken.FromToken); + + // Get data + var marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var orders = await Trading.GetCrossMarginClosedOrdersAsync( + symbol, + MarginTradeType.All, + allOrders: false, + new[] { OrderStatusFilter.All }, + startTime: request.StartTime, + endTime: request.EndTime, + fromId: fromId, + direction: FilterDirection.Previous, + ct: ct).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + var result = orders.Data.Select(x => new SharedFuturesOrder( + x.ContractCode, + x.OrderId.ToString(), + ParseOrderType(x.OrderPriceType), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId.ToString(), + AveragePrice = x.AverageFillPrice, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantityFilled = x.ValueFilled, + TimeInForce = ParseTimeInForce(x.OrderPriceType), + UpdateTime = x.UpdateTime, + PositionSide = ParsePositionSide(x.Offset, x.Side), + ReduceOnly = x.ReduceOnly, + Leverage = x.LeverageRate + }).ToArray(); + + // Get next token + FromIdToken? nextToken = null; + if (orders.Data.Any()) + nextToken = new FromIdToken(orders.Data.Min(x => x.OrderIdStr)); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result, nextToken); + } + else + { + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var orders = await Trading.GetIsolatedMarginClosedOrdersAsync( + symbol, + MarginTradeType.All, + allOrders: false, + new[] { OrderStatusFilter.All }, + startTime: request.StartTime, + endTime: request.EndTime, + direction: FilterDirection.Previous, + fromId: fromId, + ct: ct).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + var result = orders.Data.Select(x => new SharedFuturesOrder( + x.ContractCode, + x.OrderId.ToString(), + ParseOrderType(x.OrderPriceType), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(x.Status), + x.CreateTime) + { + ClientOrderId = x.ClientOrderId.ToString(), + AveragePrice = x.AverageFillPrice, + OrderPrice = x.Price, + Quantity = x.Quantity, + QuantityFilled = x.QuantityFilled, + QuoteQuantityFilled = x.ValueFilled, + TimeInForce = ParseTimeInForce(x.OrderPriceType), + UpdateTime = x.UpdateTime, + PositionSide = ParsePositionSide(x.Offset, x.Side), + ReduceOnly = x.ReduceOnly, + Leverage = x.LeverageRate + }).ToArray(); + + // Get next token + FromIdToken? nextToken = null; + if (orders.Data.Any()) + nextToken = new FromIdToken(orders.Data.Max(x => x.OrderIdStr)); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result, nextToken); + } + + } + + EndpointOptions IFuturesOrderRestClient.GetFuturesOrderTradesOptions { get; } = new EndpointOptions(true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 symbol = request.Symbol.GetSymbol(FormatSymbol); + var marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var orders = await Trading.GetCrossMarginOrderDetailsAsync(symbol, orderId: orderId).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode,orders.Data.Trades.Select(x => new SharedUserTrade( + symbol, + request.OrderId, + x.Id.ToString(), + orders.Data.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.CreateTime) + { + Price = x.Price, + Quantity = x.Quantity, + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.Role == OrderRole.Maker ? SharedRole.Maker : SharedRole.Taker + }).ToArray()); + } + else + { + var orders = await Trading.GetIsolatedMarginOrderDetailsAsync(symbol, orderId: orderId).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode,orders.Data.Trades.Select(x => new SharedUserTrade( + symbol, + request.OrderId, + x.Id.ToString(), + orders.Data.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.CreateTime) + { + Price = x.Price, + Quantity = x.Quantity, + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.Role == OrderRole.Maker ? SharedRole.Maker : SharedRole.Taker + }).ToArray()); + } + } + + PaginatedEndpointOptions IFuturesOrderRestClient.GetFuturesUserTradesOptions { get; } = new PaginatedEndpointOptions(SharedPaginationSupport.Descending, true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var orders = await Trading.GetCrossMarginUserTradesAsync( + symbol, + MarginTradeType.All, + startTime: request.StartTime, + endTime: request.EndTime, + filterDirection: FilterDirection.Previous, + fromId: fromId).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + // Get next token + FromIdToken? nextToken = null; + if (orders.Data.Any()) + nextToken = new FromIdToken(orders.Data.Min(o => o.Id).ToString()); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode,orders.Data.Select(x => new SharedUserTrade( + symbol, + x.OrderIdStr, + x.Id.ToString(), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.CreateTime) + { + Price = x.Price, + Quantity = x.Quantity, + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.Role == OrderRole.Maker ? SharedRole.Maker : SharedRole.Taker + }).ToArray(), nextToken); + } + else + { + var orders = await Trading.GetIsolatedMarginUserTradesAsync(symbol, + MarginTradeType.All, + startTime: request.StartTime, + endTime: request.EndTime, + filterDirection: FilterDirection.Previous, + fromId: fromId).ConfigureAwait(false); + if (!orders) + return orders.AsExchangeResult>(Exchange, null, default); + + // Get next token + FromIdToken? nextToken = null; + if (orders.Data.Any()) + nextToken = new FromIdToken(orders.Data.Max(o => o.Id).ToString()); + + return orders.AsExchangeResult>(Exchange, request.Symbol.TradingMode,orders.Data.Select(x => new SharedUserTrade( + symbol, + x.OrderIdStr, + x.Id.ToString(), + x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.CreateTime) + { + Price = x.Price, + Quantity = x.Quantity, + Fee = x.Fee, + FeeAsset = x.FeeAsset, + Role = x.Role == OrderRole.Maker ? SharedRole.Maker : SharedRole.Taker + }).ToArray(), nextToken); + } + } + + EndpointOptions IFuturesOrderRestClient.CancelFuturesOrderOptions { get; } = new EndpointOptions(true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var order = await Trading.CancelCrossMarginOrderAsync(contractCode: request.Symbol.GetSymbol(FormatSymbol), orderId: orderId).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult(Exchange, null, default); + + return order.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(request.OrderId)); + } + else + { + var order = await Trading.CancelIsolatedMarginOrderAsync(contractCode: request.Symbol.GetSymbol(FormatSymbol), orderId: orderId).ConfigureAwait(false); + if (!order) + return order.AsExchangeResult(Exchange, null, default); + + return order.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedId(request.OrderId)); + } + } + + EndpointOptions IFuturesOrderRestClient.GetPositionsOptions { get; } = new EndpointOptions(true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await Account.GetCrossMarginPositionsAsync(contractCode: 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.ContractCode, x.Quantity, default) + { + UnrealizedPnl = x.UnrealizedPnl, + AverageOpenPrice = x.CostOpen, + Leverage = x.LeverageRate, + PositionSide = x.Side == OrderSide.Sell ? SharedPositionSide.Short : SharedPositionSide.Long + }).ToArray()); + } + else + { + var result = await Account.GetIsolatedMarginPositionsAsync(contractCode: 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.ContractCode, x.Quantity, default) + { + UnrealizedPnl = x.UnrealizedPnl, + AverageOpenPrice = x.CostOpen, + Leverage = x.LeverageRate, + PositionSide = x.Side == OrderSide.Sell ? SharedPositionSide.Short : SharedPositionSide.Long + }).ToArray()); + } + } + + EndpointOptions IFuturesOrderRestClient.ClosePositionOptions { get; } = new EndpointOptions(true) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await Trading.CloseCrossMarginPositionAsync( + request.PositionSide == SharedPositionSide.Short ? OrderSide.Buy : OrderSide.Sell, + contractCode: request.Symbol.GetSymbol(FormatSymbol), + 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())); + } + else + { + var result = await Trading.CloseIsolatedMarginPositionAsync( + direction: request.PositionSide == SharedPositionSide.Short ? OrderSide.Buy : OrderSide.Sell, + contractCode: request.Symbol.GetSymbol(FormatSymbol), + 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())); + } + } + + private OrderPriceType GetOrderPriceType(SharedOrderType type, SharedTimeInForce? tif) + { + if (type == SharedOrderType.LimitMaker) return OrderPriceType.PostOnly; + if (type == SharedOrderType.Market) return OrderPriceType.Market; + + if (tif == SharedTimeInForce.ImmediateOrCancel) return OrderPriceType.ImmediateOrCancel; + if (tif == SharedTimeInForce.FillOrKill) return OrderPriceType.FillOrKill; + + return OrderPriceType.Limit; + } + + private Offset? GetOffset(SharedOrderSide side, SharedPositionSide? posSide) + { + if (posSide == null) + return null; + + if (posSide == SharedPositionSide.Long) + { + if (side == SharedOrderSide.Buy) return Offset.Open; + return Offset.Close; + } + + if (side == SharedOrderSide.Sell) return Offset.Open; + return Offset.Close; + } + + private SharedOrderStatus ParseOrderStatus(SwapMarginOrderStatus status) + { + if (status == SwapMarginOrderStatus.Submitting || status == SwapMarginOrderStatus.Submitted || status == SwapMarginOrderStatus.ReadyToSubmit || status == SwapMarginOrderStatus.PartiallyFilled) return SharedOrderStatus.Open; + if (status == SwapMarginOrderStatus.Cancelled || status == SwapMarginOrderStatus.Cancelling || status == SwapMarginOrderStatus.PartiallyCancelled) return SharedOrderStatus.Canceled; + return SharedOrderStatus.Filled; + } + + private SharedOrderType ParseOrderType(OrderPriceType type) + { + if (type == OrderPriceType.Market) return SharedOrderType.Market; + if (type == OrderPriceType.Limit) return SharedOrderType.Limit; + if (type == OrderPriceType.PostOnly) return SharedOrderType.LimitMaker; + + return SharedOrderType.Other; + } + + private SharedTimeInForce? ParseTimeInForce(OrderPriceType tif) + { + if (tif == OrderPriceType.ImmediateOrCancel) return SharedTimeInForce.ImmediateOrCancel; + if (tif == OrderPriceType.FillOrKill) return SharedTimeInForce.FillOrKill; + + return null; + } + + private SharedPositionSide? ParsePositionSide(Offset offset, OrderSide side) + { + if (offset == Offset.Both) + return null; + + if (offset == Offset.Open) + { + if (side == OrderSide.Buy) return SharedPositionSide.Long; + return SharedPositionSide.Short; + } + + if (side == OrderSide.Sell) return SharedPositionSide.Long; + return SharedPositionSide.Short; + } + #endregion + + #region Klines client + + GetKlinesOptions IKlineRestClient.GetKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false) + { + MaxRequestDataPoints = 1000 + }; + + async Task>> IKlineRestClient.GetKlinesAsync(GetKlinesRequest request, INextPageToken? pageToken, CancellationToken ct) + { + var interval = (Enums.KlineInterval)request.Interval; + if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval)) + return new ExchangeWebResult>(Exchange, new ArgumentError("Interval not supported")); + + var validationError = ((IKlineRestClient)this).GetKlinesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeWebResult>(Exchange, validationError); + + // Determine pagination + // Data is normally returned oldest first, so to do newest first pagination we have to do some calc + DateTime endTime = request.EndTime ?? DateTime.UtcNow; + DateTime? startTime = request.StartTime; + if (pageToken is DateTimeToken dateTimeToken) + endTime = dateTimeToken.LastTime; + + var limit = request.Limit ?? 1000; + if (startTime == null || startTime < endTime) + { + var offset = (int)interval * (limit - 1); + startTime = endTime.AddSeconds(-offset); + } + + if (startTime < request.StartTime) + startTime = request.StartTime; + + var result = await ExchangeData.GetKlinesAsync( + request.Symbol.GetSymbol(FormatSymbol), + interval, + startTime, + endTime, + //limit, + ct: ct + ).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + // Get next token + DateTimeToken? nextToken = null; + if (result.Data.Count() == limit) + { + var minOpenTime = result.Data.Min(x => x.OpenTime); + if (request.StartTime == null || minOpenTime > request.StartTime.Value) + nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)(interval - 1))); + } + + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedKline(x.OpenTime, x.ClosePrice ?? 0, x.HighPrice ?? 0, x.LowPrice ?? 0, x.OpenPrice ?? 0, x.Volume ?? 0)).ToArray(), nextToken); + } + + #endregion + + #region Mark Klines client + + GetKlinesOptions IMarkPriceKlineRestClient.GetMarkPriceKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.NotSupported, false) + { + MaxTotalDataPoints = 2000 + }; + + 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); + + var result = await ExchangeData.GetMarkPriceKlinesAsync( + request.Symbol.GetSymbol(FormatSymbol), + interval, + request.Limit ?? 2000, + 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 SharedFuturesKline(x.OpenTime, x.ClosePrice ?? 0, x.HighPrice ?? 0, x.LowPrice ?? 0, x.OpenPrice ?? 0)).ToArray()); + } + + #endregion + + #region Index Klines client + + GetKlinesOptions IIndexPriceKlineRestClient.GetIndexPriceKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false) + { + MaxTotalDataPoints = 2000 + }; + + 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); + + var result = await ExchangeData.GetMarkPriceKlinesAsync( + request.Symbol.GetSymbol(FormatSymbol), + interval, + request.Limit ?? 2000, + 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 SharedFuturesKline(x.OpenTime, x.ClosePrice ?? 0, x.HighPrice ?? 0, x.LowPrice ?? 0, x.OpenPrice ?? 0)).ToArray()); + } + + #endregion + + #region Order Book client + GetOrderBookOptions IOrderBookRestClient.GetOrderBookOptions { get; } = new GetOrderBookOptions(new[] { 150 }, 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), + 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 Recent Trade client + + GetRecentTradesOptions IRecentTradeRestClient.GetRecentTradesOptions { get; } = new GetRecentTradesOptions(2000, 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 ?? 1000, + 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 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); + + int page = 1; + int pageSize = 50; + if (pageToken is PageToken token) + { + page = token.Page; + pageSize = token.PageSize; + } + + // Get data + var result = await ExchangeData.GetHistoricalFundingRatesAsync( + request.Symbol.GetSymbol(FormatSymbol), + page: page, + pageSize: pageSize, + ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult>(Exchange, null, default); + + PageToken? nextToken = null; + if (result.Data.Rates.Any()) + nextToken = new PageToken(page + 1, pageSize); + + // Return + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode,result.Data.Rates.Select(x => new SharedFundingRate(x.FundingRate, x.FundingTime)).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.GetSwapOpenInterestAsync(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.Single().Volume)); + } + + #endregion + + #region Position Mode client + + SharedPositionModeSelection IPositionModeRestClient.PositionModeSettingType => SharedPositionModeSelection.PerAccount; + GetPositionModeOptions IPositionModeRestClient.GetPositionModeOptions { get; } = new GetPositionModeOptions() + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "Margin mode to get position mode for", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await Account.GetCrossMarginPositionModeAsync("USDT", ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, SupportedTradingModes, new SharedPositionModeResult(result.Data.PositionMode == PositionMode.DualSide ? SharedPositionMode.HedgeMode : SharedPositionMode.OneWay)); + } + else + { + if (request.Symbol == null) + return new ExchangeWebResult(Exchange, new ArgumentError("Symbol parameter required for isolated mode")); + + var result = await Account.GetIsolatedMarginPositionModeAsync(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.DualSide ? SharedPositionMode.HedgeMode : SharedPositionMode.OneWay)); + } + } + + SetPositionModeOptions IPositionModeRestClient.SetPositionModeOptions { get; } = new SetPositionModeOptions() + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "Margin mode to get position mode for", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await Account.SetCrossMarginPositionModeAsync("USDT", request.PositionMode == SharedPositionMode.HedgeMode ? PositionMode.DualSide : PositionMode.SingleSide, ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, SupportedTradingModes, new SharedPositionModeResult(request.PositionMode)); + } + else + { + if (request.Symbol == null) + return new ExchangeWebResult(Exchange, new ArgumentError("Symbol parameter required for isolated mode")); + + var result = await Account.SetIsolatedMarginPositionModeAsync(request.Symbol.GetSymbol(FormatSymbol), request.PositionMode == SharedPositionMode.HedgeMode ? PositionMode.DualSide : PositionMode.SingleSide, ct: ct).ConfigureAwait(false); + if (!result) + return result.AsExchangeResult(Exchange, null, default); + + return result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedPositionModeResult(request.PositionMode)); + } + } + #endregion + + } +} diff --git a/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs b/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs index 88282c8c..25b9957d 100644 --- a/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs +++ b/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.Clients; using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.Sockets; using HTX.Net.Enums; using HTX.Net.Interfaces.Clients.UsdtFuturesApi; @@ -15,7 +16,7 @@ namespace HTX.Net.Clients.UsdtFutures { /// - internal class HTXSocketClientUsdtFuturesApi : SocketApiClient, IHTXSocketClientUsdtFuturesApi + internal partial class HTXSocketClientUsdtFuturesApi : SocketApiClient, IHTXSocketClientUsdtFuturesApi { private static readonly MessagePath _idPath = MessagePath.Get().Property("id"); private static readonly MessagePath _actionPath = MessagePath.Get().Property("action"); @@ -45,8 +46,14 @@ internal HTXSocketClientUsdtFuturesApi(ILogger logger, HTXSocketOptions options) protected override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(); + public IHTXSocketClientUsdtFuturesApiShared SharedClient => this; + /// - public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}"; + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + { + return $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}" + (!deliverTime.HasValue ? string.Empty : ("-" + deliverTime.Value.ToString("yyMMdd"))); + } + /// public override ReadOnlyMemory PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory data) diff --git a/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApiShared.cs b/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApiShared.cs new file mode 100644 index 00000000..de932c1e --- /dev/null +++ b/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApiShared.cs @@ -0,0 +1,324 @@ +using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.Objects.Sockets; +using HTX.Net.Interfaces.Clients.UsdtFuturesApi; +using HTX.Net.Enums; + +namespace HTX.Net.Clients.UsdtFutures +{ + internal partial class HTXSocketClientUsdtFuturesApi : IHTXSocketClientUsdtFuturesApiShared + { + public string Exchange => HTXExchange.ExchangeName; + public TradingMode[] SupportedTradingModes { get; } = new[] { TradingMode.PerpetualLinear, TradingMode.DeliveryLinear }; + + public void SetDefaultExchangeParameter(string key, object value) => ExchangeParameters.SetStaticParameter(Exchange, key, value); + public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticParameters(); + + #region Ticker client + EndpointOptions ITickerSocketClient.SubscribeTickerOptions { get; } = new EndpointOptions(false); + async Task> ITickerSocketClient.SubscribeToTickerUpdatesAsync(SubscribeTickerRequest request, Action> handler, CancellationToken ct) + { + var validationError = ((ITickerSocketClient)this).SubscribeTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToTickerUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, new SharedSpotTicker(symbol, update.Data.ClosePrice, update.Data.HighPrice ?? 0, update.Data.LowPrice ?? 0, update.Data.Volume ?? 0, update.Data.OpenPrice == null ? null : Math.Round((update.Data.ClosePrice ?? 0) / update.Data.OpenPrice.Value * 100 - 100, 2))))).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.Trades.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Book Ticker client + + EndpointOptions IBookTickerSocketClient.SubscribeBookTickerOptions { get; } = new EndpointOptions(false); + async Task> IBookTickerSocketClient.SubscribeToBookTickerUpdatesAsync(SubscribeBookTickerRequest request, Action> handler, CancellationToken ct) + { + var validationError = ((IBookTickerSocketClient)this).SubscribeBookTickerOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToBookTickerUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, new SharedBookTicker(update.Data.Ask.Price, update.Data.Ask.Quantity, update.Data.Bid.Price, update.Data.Bid.Quantity))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Kline client + SubscribeKlineOptions IKlineSocketClient.SubscribeKlineOptions { get; } = new SubscribeKlineOptions(false); + async Task> IKlineSocketClient.SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action> handler, CancellationToken ct) + { + var interval = (Enums.KlineInterval)request.Interval; + if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval)) + return new ExchangeResult(Exchange, new ArgumentError("Interval not supported")); + + var validationError = ((IKlineSocketClient)this).SubscribeKlineOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToKlineUpdatesAsync(symbol, interval, update => handler(update.AsExchangeEvent(Exchange, new SharedKline(update.Data.OpenTime, update.Data.ClosePrice ?? 0, update.Data.HighPrice ?? 0, update.Data.LowPrice ?? 0, update.Data.OpenPrice ?? 0, update.Data.Volume ?? 0))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Order Book client + SubscribeOrderBookOptions IOrderBookSocketClient.SubscribeOrderBookOptions { get; } = new SubscribeOrderBookOptions(false, new[] { 150 }); + async Task> IOrderBookSocketClient.SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action> handler, CancellationToken ct) + { + var validationError = ((IOrderBookSocketClient)this).SubscribeOrderBookOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToOrderBookUpdatesAsync(symbol, 0, update => handler(update.AsExchangeEvent(Exchange, new SharedOrderBook(update.Data.Asks, update.Data.Bids))), ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + #endregion + + #region Balance client + EndpointOptions IBalanceSocketClient.SubscribeBalanceOptions { get; } = new EndpointOptions(false) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await SubscribeToCrossMarginBalanceUpdatesAsync( + update => handler(update.AsExchangeEvent>(Exchange, update.Data.Data.Select(x => new SharedBalance(x.MarginAsset, x.MarginBalance - x.MarginFrozen, x.MarginBalance) ).ToArray())), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + else + { + var result = await SubscribeToIsolatedMarginBalanceUpdatesAsync( + update => handler(update.AsExchangeEvent>(Exchange, update.Data.Data.Select(x => new SharedBalance(x.Asset, x.MarginBalance - x.MarginFrozen, x.MarginBalance) { IsolatedMarginSymbol = x.MarginAccount }).ToArray())), + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + + } + #endregion + + #region Futures Order client + + EndpointOptions IFuturesOrderSocketClient.SubscribeFuturesOrderOptions { get; } = new EndpointOptions(false) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + var result = await SubscribeToOrderUpdatesAsync(marginMode == SharedMarginMode.Cross ? MarginMode.Cross : MarginMode.Isolated, + update => { + var lastTrade = update.Data.Trade?.OrderByDescending(x => x.TradeId).FirstOrDefault(); + handler(update.AsExchangeEvent>(Exchange, new[] { + new SharedFuturesOrder( + update.Data.ContractCode, + update.Data.OrderId.ToString(), + ParseOrderType(update.Data.OrderPriceType), + update.Data.OrderSide == Enums.OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + ParseOrderStatus(update.Data.OrderStatus), + update.Data.CreatedAt) + { + ClientOrderId = update.Data.ClientOrderId.ToString(), + AveragePrice = update.Data.AveragePrice, + OrderPrice = update.Data.Price, + Quantity = update.Data.Quantity, + QuantityFilled = update.Data.QuantityFilled, + QuoteQuantityFilled = update.Data.ValueFilled, + TimeInForce = ParseTimeInForce(update.Data.OrderPriceType), + UpdateTime = update.Data.Timestamp, + PositionSide = ParsePositionSide(update.Data.Offset, update.Data.OrderSide), + ReduceOnly = update.Data.ReduceOnly, + Fee = update.Data.Fee, + FeeAsset = update.Data.FeeAsset, + LastTrade = update.Data.Trade?.Any() != true ? null : new SharedUserTrade(update.Data.Symbol, update.Data.OrderIdStr, lastTrade!.TradeId.ToString(), update.Data.OrderSide == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, lastTrade.Quantity, lastTrade.Price, update.Data.Timestamp) + { + Fee = lastTrade.Fee, + FeeAsset = lastTrade.FeeAsset, + Role = lastTrade.Role == OrderRole.Maker ? SharedRole.Maker : SharedRole.Taker + } + } })); + }, + ct: ct).ConfigureAwait(false); + + return new ExchangeResult(Exchange, result); + } + + private SharedOrderStatus ParseOrderStatus(OrderStatusFilter status) + { + if (status == OrderStatusFilter.Submitted || status == OrderStatusFilter.ReadyToPlace || status == OrderStatusFilter.PartiallyMatched) return SharedOrderStatus.Open; + if (status == OrderStatusFilter.Canceled || status == OrderStatusFilter.Canceling || status == OrderStatusFilter.PartiallyCanceled) return SharedOrderStatus.Canceled; + return SharedOrderStatus.Filled; + } + + private SharedOrderType ParseOrderType(OrderPriceType type) + { + if (type == OrderPriceType.Market) return SharedOrderType.Market; + if (type == OrderPriceType.Limit) return SharedOrderType.Limit; + if (type == OrderPriceType.PostOnly) return SharedOrderType.LimitMaker; + + return SharedOrderType.Other; + } + + private SharedTimeInForce? ParseTimeInForce(OrderPriceType tif) + { + if (tif == OrderPriceType.ImmediateOrCancel) return SharedTimeInForce.ImmediateOrCancel; + if (tif == OrderPriceType.FillOrKill) return SharedTimeInForce.FillOrKill; + + return null; + } + + private SharedPositionSide? ParsePositionSide(Offset offset, OrderSide side) + { + if (offset == Offset.Both) + return null; + + if (offset == Offset.Open) + { + if (side == OrderSide.Buy) return SharedPositionSide.Long; + return SharedPositionSide.Short; + } + + if (side == OrderSide.Sell) return SharedPositionSide.Long; + return SharedPositionSide.Short; + } + #endregion + + #region User Trade client + EndpointOptions IUserTradeSocketClient.SubscribeUserTradeOptions { get; } = new EndpointOptions(false) + { + RequiredExchangeParameters = new List + { + new ParameterDescription("MarginMode", typeof(SharedMarginMode), "The margin mode", SharedMarginMode.Cross) + } + }; + async Task> IUserTradeSocketClient.SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action>> handler, CancellationToken ct) + { + var validationError = ((IUserTradeSocketClient)this).SubscribeUserTradeOptions.ValidateRequest(Exchange, request, request.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult(Exchange, validationError); + + var marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await SubscribeToCrossMarginUserTradeUpdatesAsync( + update => { + handler(update.AsExchangeEvent>(Exchange, update.Data.Trades.Select(x => + new SharedUserTrade( + update.Data.Symbol, + update.Data.OrderId.ToString(), + x.ToString(), + update.Data.OrderSide == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.CreateTime) + { + Role = x.Role == Enums.OrderRole.Taker ? SharedRole.Taker : SharedRole.Maker, + } + ).ToArray())); + }, + ct: ct).ConfigureAwait(false); + return new ExchangeResult(Exchange, result); + } + else + { + var result = await SubscribeToIsolatedMarginUserTradeUpdatesAsync( + update => { + handler(update.AsExchangeEvent>(Exchange, update.Data.Trades.Select(x => + new SharedUserTrade( + update.Data.Symbol, + update.Data.OrderId.ToString(), + x.ToString(), + update.Data.OrderSide == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell, + x.Quantity, + x.Price, + x.CreateTime) + { + Role = x.Role == Enums.OrderRole.Taker ? SharedRole.Taker : SharedRole.Maker, + } + ).ToArray())); + }, + ct: ct).ConfigureAwait(false); + return new ExchangeResult(Exchange, result); + } + } + #endregion + + #region Position client + EndpointOptions IPositionSocketClient.SubscribePositionOptions { get; } = new EndpointOptions(true); + 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 marginMode = ExchangeParameters.GetValue(request.ExchangeParameters, Exchange, "MarginMode"); + if (marginMode == SharedMarginMode.Cross) + { + var result = await SubscribeToCrossMarginPositionUpdatesAsync( + update => handler(update.AsExchangeEvent(Exchange, update.Data.Data.Select(x => new SharedPosition(x.Symbol, x.Quantity, update.Data.Timestamp) + { + AverageOpenPrice = x.PositionPrice, + PositionSide = x.OrderSide == Enums.OrderSide.Sell ? SharedPositionSide.Short : SharedPositionSide.Long, + Leverage = x.LeverageRate, + UnrealizedPnl = x.UnrealizedPnl + }))), + ct: ct).ConfigureAwait(false); + return new ExchangeResult(Exchange, result); + } + else + { + var result = await SubscribeToIsolatedMarginPositionUpdatesAsync( + update => handler(update.AsExchangeEvent(Exchange, update.Data.Data.Select(x => new SharedPosition(x.Symbol, x.Quantity, update.Data.Timestamp) + { + AverageOpenPrice = x.PositionPrice, + PositionSide = x.OrderSide == Enums.OrderSide.Sell ? SharedPositionSide.Short : SharedPositionSide.Long, + Leverage = x.LeverageRate, + UnrealizedPnl = x.UnrealizedPnl + }))), + ct: ct).ConfigureAwait(false); + return new ExchangeResult(Exchange, result); + } + } + + #endregion + } +} diff --git a/HTX.Net/Enums/OrderStatusFilter.cs b/HTX.Net/Enums/OrderStatusFilter.cs index a0ac9a09..c0bed7d4 100644 --- a/HTX.Net/Enums/OrderStatusFilter.cs +++ b/HTX.Net/Enums/OrderStatusFilter.cs @@ -7,6 +7,11 @@ namespace HTX.Net.Enums /// public enum OrderStatusFilter { + /// + /// Order statuses + /// + [Map("0")] + All, /// /// Placing in book /// diff --git a/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs b/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs index cb4355a2..3b4b1737 100644 --- a/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs +++ b/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs @@ -58,6 +58,12 @@ public static IServiceCollection AddHTX( services.AddTransient(); services.AddTransient(); services.AddTransient(); + + services.RegisterSharedRestInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); + services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); + services.RegisterSharedRestInterfaces(x => x.GetRequiredService().UsdtFuturesApi.SharedClient); + services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().UsdtFuturesApi.SharedClient); + services.AddTransient(x => x.GetRequiredService().SpotApi.CommonSpotClient); if (socketClientLifeTime == null) services.AddSingleton(); diff --git a/HTX.Net/HTX.Net.csproj b/HTX.Net/HTX.Net.csproj index 190bec14..8ae4ef76 100644 --- a/HTX.Net/HTX.Net.csproj +++ b/HTX.Net/HTX.Net.csproj @@ -34,7 +34,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/HTX.Net/HTX.Net.xml b/HTX.Net/HTX.Net.xml index 22b3cff9..75c3f02b 100644 --- a/HTX.Net/HTX.Net.xml +++ b/HTX.Net/HTX.Net.xml @@ -107,7 +107,7 @@ - + @@ -200,7 +200,7 @@ - + @@ -425,7 +425,7 @@ - + @@ -512,7 +512,7 @@ - + @@ -932,7 +932,7 @@ - + @@ -2479,6 +2479,11 @@ Order status filter + + + Order statuses + + Placing in book @@ -3484,9 +3489,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. - @@ -3606,7 +3615,7 @@ Cancellation token - + Get withdrawal/deposit history @@ -4048,6 +4057,11 @@ Cancellation token + + + Shared interface for Spot rest API usage + + HTX sub-account endpoints. @@ -4458,6 +4472,11 @@ Spot streams + + + Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + + Get kline/candlestick data for a symbol @@ -4629,6 +4648,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 Spot socket API usage + + + + + Shared interface for Usdt futures rest API usage + + Usdt futures api endpoints @@ -4654,6 +4683,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. + + HTX usdt futures endpoints @@ -6151,6 +6185,11 @@ Usdt 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 basis updates @@ -6399,6 +6438,11 @@ Cancellation token + + + Shared interface for Usdt futures socket API usage + + HTX order book factory @@ -10145,6 +10189,11 @@ Order source + + + Value of the order + + Canceled order update @@ -14930,7 +14979,7 @@ Trade role - + Fee asset diff --git a/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApi.cs b/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApi.cs index 0b25ddd3..08d9386b 100644 --- a/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApi.cs +++ b/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApi.cs @@ -31,9 +31,13 @@ public interface IHTXRestClientSpotApi : IRestApiClient, IDisposable IHTXRestClientSpotApiTrading 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 IHTXRestClientSpotApiShared SharedClient { get; } } } \ No newline at end of file diff --git a/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApiAccount.cs b/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApiAccount.cs index 3299e713..6b6249c6 100644 --- a/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApiAccount.cs +++ b/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApiAccount.cs @@ -132,7 +132,7 @@ public interface IHTXRestClientSpotApiAccount /// the order of response /// Cancellation token /// - Task>> GetWithdrawDepositHistoryAsync(WithdrawDepositType type, string? asset = null, int? from = null, int? size = null, FilterDirection? direction = null, CancellationToken ct = default); + Task>> GetWithdrawDepositHistoryAsync(WithdrawDepositType type, string? asset = null, long? from = null, int? size = null, FilterDirection? direction = null, CancellationToken ct = default); /// /// Get current trading fees for symbols diff --git a/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApiShared.cs b/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApiShared.cs new file mode 100644 index 00000000..58bb4c7c --- /dev/null +++ b/HTX.Net/Interfaces/Clients/SpotApi/IHTXRestClientSpotApiShared.cs @@ -0,0 +1,23 @@ +using CryptoExchange.Net.SharedApis; + +namespace HTX.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Spot rest API usage + /// + public interface IHTXRestClientSpotApiShared : + IAssetsRestClient, + IBalanceRestClient, + IDepositRestClient, + IKlineRestClient, + IOrderBookRestClient, + IRecentTradeRestClient, + ISpotOrderRestClient, + ISpotSymbolRestClient, + ISpotTickerRestClient, + //ITradeHistoryRestClient + IWithdrawalRestClient, + IWithdrawRestClient + { + } +} diff --git a/HTX.Net/Interfaces/Clients/SpotApi/IHTXSocketClientSpotApi.cs b/HTX.Net/Interfaces/Clients/SpotApi/IHTXSocketClientSpotApi.cs index 92459ae6..8436501d 100644 --- a/HTX.Net/Interfaces/Clients/SpotApi/IHTXSocketClientSpotApi.cs +++ b/HTX.Net/Interfaces/Clients/SpotApi/IHTXSocketClientSpotApi.cs @@ -10,6 +10,11 @@ namespace HTX.Net.Interfaces.Clients.SpotApi /// public interface IHTXSocketClientSpotApi : ISocketApiClient, IDisposable { + /// + /// Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + public IHTXSocketClientSpotApiShared SharedClient { get; } + /// /// Get kline/candlestick data for a symbol /// @@ -188,5 +193,6 @@ Task> SubscribeToOrderUpdatesAsync( /// A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected Task> SubscribeToOrderDetailsUpdatesAsync(string? symbol = null, Action>? onOrderMatch = null, Action>? onOrderCancel = null, CancellationToken ct = default); + } } \ No newline at end of file diff --git a/HTX.Net/Interfaces/Clients/SpotApi/IHTXSocketClientSpotApiShared.cs b/HTX.Net/Interfaces/Clients/SpotApi/IHTXSocketClientSpotApiShared.cs new file mode 100644 index 00000000..e62e4120 --- /dev/null +++ b/HTX.Net/Interfaces/Clients/SpotApi/IHTXSocketClientSpotApiShared.cs @@ -0,0 +1,20 @@ +using CryptoExchange.Net.SharedApis; + +namespace HTX.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Spot socket API usage + /// + public interface IHTXSocketClientSpotApiShared : + ITickerSocketClient, + ITickersSocketClient, + ITradeSocketClient, + IBookTickerSocketClient, + IKlineSocketClient, + IOrderBookSocketClient, + IBalanceSocketClient, + ISpotOrderSocketClient, + IUserTradeSocketClient + { + } +} diff --git a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApi.cs b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApi.cs index e230d8c9..518d29b6 100644 --- a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApi.cs +++ b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApi.cs @@ -1,4 +1,6 @@ -namespace HTX.Net.Interfaces.Clients.UsdtFuturesApi +using HTX.Net.Interfaces.Clients.SpotApi; + +namespace HTX.Net.Interfaces.Clients.UsdtFuturesApi { /// /// Usdt futures api endpoints @@ -21,5 +23,10 @@ public interface IHTXRestClientUsdtFuturesApi : IRestApiClient /// Endpoints related to orders and trades /// IHTXRestClientUsdtFuturesApiTrading 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 IHTXRestClientUsdtFuturesApiShared SharedClient { get; } } } \ No newline at end of file diff --git a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApiAccount.cs b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApiAccount.cs index e3b88b0b..fced050e 100644 --- a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApiAccount.cs +++ b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApiAccount.cs @@ -242,7 +242,7 @@ public interface IHTXRestClientUsdtFuturesApiAccount /// /// Margin account, for example `USDT` /// Cancellation token - Task>> GetIsolatedMarginPositionModeAsync(string marginAccount, CancellationToken ct = default); + Task> GetIsolatedMarginPositionModeAsync(string marginAccount, CancellationToken ct = default); /// /// Get cross margin position mode @@ -250,7 +250,7 @@ public interface IHTXRestClientUsdtFuturesApiAccount /// /// Margin account, for example `USDT` /// Cancellation token - Task>> GetCrossMarginPositionModeAsync(string marginAccount, CancellationToken ct = default); + Task> GetCrossMarginPositionModeAsync(string marginAccount, CancellationToken ct = default); } } \ No newline at end of file diff --git a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApiShared.cs b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApiShared.cs new file mode 100644 index 00000000..9d70e5d7 --- /dev/null +++ b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXRestClientUsdtFuturesApiShared.cs @@ -0,0 +1,23 @@ +using CryptoExchange.Net.SharedApis; + +namespace HTX.Net.Interfaces.Clients.SpotApi +{ + /// + /// Shared interface for Usdt futures rest API usage + /// + public interface IHTXRestClientUsdtFuturesApiShared : + IBalanceRestClient, + IFuturesTickerRestClient, + IFuturesSymbolRestClient, + IFuturesOrderRestClient, + IKlineRestClient, + IMarkPriceKlineRestClient, + IIndexPriceKlineRestClient, + IOrderBookRestClient, + IRecentTradeRestClient, + IFundingRateRestClient, + IOpenInterestRestClient, + IPositionModeRestClient + { + } +} diff --git a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXSocketClientUsdtFuturesApi.cs b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXSocketClientUsdtFuturesApi.cs index fa16e61e..5e1276fd 100644 --- a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXSocketClientUsdtFuturesApi.cs +++ b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXSocketClientUsdtFuturesApi.cs @@ -10,6 +10,11 @@ namespace HTX.Net.Interfaces.Clients.UsdtFuturesApi /// public interface IHTXSocketClientUsdtFuturesApi : ISocketApiClient { + /// + /// Get the shared socket subscription client. This interface is shared with other exhanges to allow for a common implementation for different exchanges. + /// + IHTXSocketClientUsdtFuturesApiShared SharedClient { get; } + /// /// Subscribe to basis updates /// diff --git a/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXSocketClientUsdtFuturesApiShared.cs b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXSocketClientUsdtFuturesApiShared.cs new file mode 100644 index 00000000..977fae5b --- /dev/null +++ b/HTX.Net/Interfaces/Clients/UsdtFuturesApi/IHTXSocketClientUsdtFuturesApiShared.cs @@ -0,0 +1,20 @@ +using CryptoExchange.Net.SharedApis; + +namespace HTX.Net.Interfaces.Clients.UsdtFuturesApi +{ + /// + /// Shared interface for Usdt futures socket API usage + /// + public interface IHTXSocketClientUsdtFuturesApiShared : + ITickerSocketClient, + ITradeSocketClient, + IBookTickerSocketClient, + IKlineSocketClient, + IOrderBookSocketClient, + IBalanceSocketClient, + IFuturesOrderSocketClient, + IUserTradeSocketClient, + IPositionSocketClient + { + } +} diff --git a/HTX.Net/Objects/Models/HTXSymbolConfig.cs b/HTX.Net/Objects/Models/HTXSymbolConfig.cs index b1b81a83..fd3356dc 100644 --- a/HTX.Net/Objects/Models/HTXSymbolConfig.cs +++ b/HTX.Net/Objects/Models/HTXSymbolConfig.cs @@ -1,4 +1,6 @@ -namespace HTX.Net.Objects.Models +using HTX.Net.Enums; + +namespace HTX.Net.Objects.Models { /// /// Symbol configuration @@ -14,7 +16,7 @@ public record HTXSymbolConfig /// Status /// [JsonPropertyName("state")] - public string Status { get; set; } = string.Empty; + public SymbolStatus Status { get; set; } /// /// Base asset /// diff --git a/HTX.Net/Objects/Models/Socket/HTXOrderUpdate.cs b/HTX.Net/Objects/Models/Socket/HTXOrderUpdate.cs index c81e3eef..c6d66ffe 100644 --- a/HTX.Net/Objects/Models/Socket/HTXOrderUpdate.cs +++ b/HTX.Net/Objects/Models/Socket/HTXOrderUpdate.cs @@ -176,6 +176,11 @@ public record HTXMatchedOrderUpdate : HTXOrderUpdate /// [JsonPropertyName("orderSource")] public string OrderSource { get; set; } = string.Empty; + /// + /// Value of the order + /// + [JsonPropertyName("orderValue")] + public decimal? QuoteQuantity { get; set; } } /// diff --git a/HTX.Net/Objects/Models/Socket/HTXUsdtMarginSwapOrderUpdate.cs b/HTX.Net/Objects/Models/Socket/HTXUsdtMarginSwapOrderUpdate.cs index db3cd943..2d285358 100644 --- a/HTX.Net/Objects/Models/Socket/HTXUsdtMarginSwapOrderUpdate.cs +++ b/HTX.Net/Objects/Models/Socket/HTXUsdtMarginSwapOrderUpdate.cs @@ -47,7 +47,7 @@ public record HTXUsdtMarginSwapOrderUpdate : HTXOpMessage /// Order side /// [JsonPropertyName("direction")] - public string OrderSide { get; set; } = string.Empty; + public OrderSide OrderSide { get; set; } /// /// Offset /// diff --git a/HTX.Net/Objects/Models/UsdtMarginSwap/HTXMarginTrade.cs b/HTX.Net/Objects/Models/UsdtMarginSwap/HTXMarginTrade.cs index c447ea4c..37e724f5 100644 --- a/HTX.Net/Objects/Models/UsdtMarginSwap/HTXMarginTrade.cs +++ b/HTX.Net/Objects/Models/UsdtMarginSwap/HTXMarginTrade.cs @@ -53,7 +53,7 @@ public record HTXMarginTrade /// Fee asset /// [JsonPropertyName("fee_asset")] - public string FeeAssset { get; set; } = string.Empty; + public string FeeAsset { get; set; } = string.Empty; /// /// Profit ///