diff --git a/Bitget.Net/Bitget.Net.csproj b/Bitget.Net/Bitget.Net.csproj index f592ce7..7bc9e0a 100644 --- a/Bitget.Net/Bitget.Net.csproj +++ b/Bitget.Net/Bitget.Net.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1 latest @@ -56,7 +56,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApi.cs b/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApi.cs index ea776ea..5fab620 100644 --- a/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApi.cs +++ b/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApi.cs @@ -32,11 +32,15 @@ internal partial class BitgetRestClientFuturesApi : RestApiClient public string ExchangeName => "Bitget"; + internal BitgetRestClient _baseClient; + protected override IRestMessageHandler MessageHandler { get; } = new BitgetRestMessageHandler(BitgetErrors.RestErrors); internal BitgetRestClientFuturesApi(ILoggerFactory? loggerFactory, HttpClient? httpClient, BitgetRestClient baseClient, BitgetRestOptions options) : base(loggerFactory, BitgetExchange.Metadata.Id, httpClient, options.Environment.RestBaseAddress, options, options.FuturesOptions) { + _baseClient = baseClient; + Account = new BitgetRestClientFuturesApiAccount(this); ExchangeData = new BitgetRestClientFuturesApiExchangeData(this); Trading = new BitgetRestClientFuturesApiTrading(this); diff --git a/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApiShared.cs b/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApiShared.cs index f19b1aa..585cbeb 100644 --- a/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApiShared.cs +++ b/Bitget.Net/Clients/FuturesApiV2/BitgetRestClientFuturesApiShared.cs @@ -1,8 +1,11 @@ using Bitget.Net.Enums; +using Bitget.Net.Enums.Uta; using Bitget.Net.Enums.V2; using Bitget.Net.Interfaces.Clients.FuturesApiV2; +using Bitget.Net.Objects.Models; using Bitget.Net.Objects.Models.V2; using CryptoExchange.Net; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.SharedApis; @@ -171,6 +174,7 @@ async Task> IBookTickerRestClient.GetBookTickerAsyn #region Futures Symbol client + SharedSymbolCatalog? IFuturesSymbolRestClient.FuturesSymbolCatalog => ExchangeSymbolCache.GetSymbolCatalog(_exchangeName, _topicId, EnvironmentName, null); GetFuturesSymbolsOptions IFuturesSymbolRestClient.GetFuturesSymbolsOptions { get; } = new GetFuturesSymbolsOptions(_exchangeName, false) { RequiredExchangeParameters = new List @@ -184,46 +188,71 @@ async Task> IFuturesSymbolRestClient.GetFuture if (validationError != null) return HttpResult.Fail(Exchange, validationError); - var productType = GetProductType(request.TradingMode, request.ExchangeParameters); - var result = await ExchangeData.GetContractsAsync( - productType, + var productCategory = GetProductCategory(request.TradingMode, request.ExchangeParameters); + var result = await _baseClient.UnifiedApi.ExchangeData.GetFuturesSymbolsAsync( + productCategory, ct: ct).ConfigureAwait(false); if (!result.Success) return HttpResult.Fail(result); - IEnumerable data = result.Data; - if (request.TradingMode != null) - { - data = data - .Where(x => ((request.TradingMode == TradingMode.PerpetualInverse || request.TradingMode == TradingMode.PerpetualLinear) && x.ContractType == ContractType.Perpetual) - || ((request.TradingMode == TradingMode.DeliveryLinear || request.TradingMode == TradingMode.DeliveryInverse) && x.ContractType == ContractType.Delivery)); - } + var data = result.Data + .Select(x => ParseSymbol(x, productCategory)) + .ToArray(); - var response = HttpResult.Ok(result, data.Select(s => - new SharedFuturesSymbol( - productType == BitgetProductTypeV2.CoinFutures && s.ContractType == ContractType.Delivery ? TradingMode.DeliveryInverse : - productType == BitgetProductTypeV2.CoinFutures && s.ContractType == ContractType.Perpetual ? TradingMode.PerpetualInverse : - s.DeliveryPeriod.HasValue ? TradingMode.DeliveryLinear : - TradingMode.PerpetualLinear, - s.BaseAsset, - s.QuoteAsset, - s.Symbol, - s.Status == Enums.V2.FuturesSymbolStatus.Normal) + ExchangeSymbolCache.UpdateSymbolInfo(_topicId, EnvironmentName, productCategory.ToString(), data); + return HttpResult.Ok(result, SharedUtils.ApplySymbolFilter(data, request)); + } + + private SharedFuturesSymbol ParseSymbol(BitgetUaFuturesSymbol s, ProductCategory productCategory) + { + var result = new SharedFuturesSymbol( + productCategory == ProductCategory.CoinFutures && s.Type == ContractType.Delivery ? TradingMode.DeliveryInverse : + productCategory == ProductCategory.CoinFutures && s.Type == ContractType.Perpetual ? TradingMode.PerpetualInverse : + s.DeliveryPeriod.HasValue ? TradingMode.DeliveryLinear : + TradingMode.PerpetualLinear, + s.BaseAsset, + s.QuoteAsset, + s.Symbol, + s.Status == InstrumentStatus.Online) { MinTradeQuantity = s.MinOrderQuantity, - PriceDecimals = s.PriceDecimals, - QuantityDecimals = s.QuantityDecimals, + PriceDecimals = s.PricePrecision, + QuantityDecimals = s.QuantityPrecision, DeliveryTime = s.DeliveryTime, - PriceStep = s.PriceStep, - QuantityStep = s.QuantityStep, + PriceStep = s.PriceMultiplier, + QuantityStep = s.QuantityMultiplier, ContractSize = 1, MaxShortLeverage = s.MaxLeverage, MaxLongLeverage = s.MaxLeverage, - MaxTradeQuantity = s.MaxLimitOrderQuantity == null && s.MaxLimitOrderQuantity == null ? null : Math.Min(s.MaxLimitOrderQuantity ?? decimal.MaxValue, s.MaxMarketOrderQuantity ?? decimal.MaxValue) - }).ToArray()); + MaxTradeQuantity = Math.Min(s.MaxOrderQuantity, s.MaxMarketOrderQuantity), + DisplayName = s.Symbol, + }; + + if (productCategory != ProductCategory.CoinFutures) + { + result.BaseAssetType = s.IsRwa ? SharedAssetType.TradFi : SharedAssetType.Crypto; + result.BaseAssetSubType = ParseSymbolSubType(s); + result.QuoteAssetType = SharedAssetType.Crypto; + result.QuoteAssetSubType = SharedAssetSubType.StableCoin; + } + else + { + result.BaseAssetType = SharedAssetType.Crypto; + result.QuoteAssetType = SharedAssetType.Fiat; + } - ExchangeSymbolCache.UpdateSymbolInfo(_topicId, EnvironmentName, null, response.Data!); - return response; + return result; + } + + private SharedAssetSubType? ParseSymbolSubType(BitgetUaFuturesSymbol s) + { + if (s.SymbolType == SymbolType.Commodity || s.SymbolType == SymbolType.Metal) + return SharedAssetSubType.Commodity; + + if (s.SymbolType == SymbolType.Stock) + return SharedAssetSubType.Equity; + + return null; } async Task> IFuturesSymbolRestClient.GetFuturesSymbolsForBaseAssetAsync(string baseAsset) @@ -1490,11 +1519,18 @@ private BitgetProductTypeV2 GetProductType(TradingMode? tradingMode, ExchangePar return (BitgetProductTypeV2)Enum.Parse(typeof(BitgetProductTypeV2), productTypeStr!); } - public static DateTime RoundUp(DateTime dt, TimeSpan d) + private ProductCategory GetProductCategory(TradingMode? tradingMode, ExchangeParameters? exchangeParameters) { - var modTicks = dt.Ticks % d.Ticks; - var delta = modTicks != 0 ? d.Ticks - modTicks : 0; - return new DateTime(dt.Ticks + delta, dt.Kind); + var productType = GetProductType(tradingMode, exchangeParameters); + if (productType == BitgetProductTypeV2.CoinFutures) + return ProductCategory.CoinFutures; + else if(productType == BitgetProductTypeV2.UsdtFutures) + return ProductCategory.UsdtFutures; + else if (productType == BitgetProductTypeV2.UsdcFutures) + return ProductCategory.UsdcFutures; + + throw new ArgumentException("ProductType", $"Invalid product type {productType} for futures"); } + } } diff --git a/Bitget.Net/Clients/SpotApiV2/BitgetRestClientSpotApi.cs b/Bitget.Net/Clients/SpotApiV2/BitgetRestClientSpotApi.cs index eeaf886..b7beb09 100644 --- a/Bitget.Net/Clients/SpotApiV2/BitgetRestClientSpotApi.cs +++ b/Bitget.Net/Clients/SpotApiV2/BitgetRestClientSpotApi.cs @@ -38,9 +38,13 @@ internal partial class BitgetRestClientSpotApi : RestApiClient ExchangeParameters.SetStaticParameter(Exchange, key, value); public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticParameters(); public SharedClientInfo Discover() => SharedUtils.GetClientInfo(BitgetExchange.Metadata, this); + private static HashSet _exchangeSupportedFiatCurrencies = ["EUR", "USD", "BRL"]; #region Kline client @@ -83,6 +85,7 @@ async Task> IKlineRestClient.GetKlinesAsync(GetKlinesR #endregion #region Spot Symbol client + SharedSymbolCatalog? ISpotSymbolRestClient.SpotSymbolCatalog => ExchangeSymbolCache.GetSymbolCatalog(_exchangeName, _topicId, EnvironmentName, null); GetSpotSymbolsOptions ISpotSymbolRestClient.GetSpotSymbolsOptions { get; } = new GetSpotSymbolsOptions(_exchangeName, false); async Task> ISpotSymbolRestClient.GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct) @@ -91,21 +94,70 @@ async Task> ISpotSymbolRestClient.GetSpotSymbolsA if (validationError != null) return HttpResult.Fail(Exchange, validationError); - var result = await ExchangeData.GetSymbolsAsync(ct: ct).ConfigureAwait(false); + var result = await _baseClient.UnifiedApi.ExchangeData.GetSpotSymbolsAsync(ct: ct).ConfigureAwait(false); if (!result.Success) return HttpResult.Fail(result); - var response = HttpResult.Ok(result, result.Data.Select(s => new SharedSpotSymbol(s.BaseAsset, s.QuoteAsset, s.Symbol, s.Status == Enums.SymbolStatus.Online) + var data = result.Data + .Select(x => ParseSymbol(x)!) + .Where(x => x != null) + .ToArray(); + + ExchangeSymbolCache.UpdateSymbolInfo(_topicId, EnvironmentName, null, data); + return HttpResult.Ok(result, SharedUtils.ApplySymbolFilter(data, request)); + } + + private SharedSpotSymbol ParseSymbol(BitgetUaSpotSymbol s) + { + var result = new SharedSpotSymbol(s.BaseAsset, s.QuoteAsset, s.Symbol, s.Status == Enums.Uta.InstrumentStatus.Online) { MinTradeQuantity = s.MinOrderQuantity, MinNotionalValue = s.MinOrderValue, MaxTradeQuantity = s.MaxOrderQuantity, QuantityDecimals = s.QuantityPrecision, - PriceDecimals = s.PricePrecision - }).ToArray()); + PriceDecimals = s.PricePrecision, + DisplayName = s.Symbol + }; + + if (s.SymbolType == Enums.Uta.SymbolType.Commodity || s.SymbolType == Enums.Uta.SymbolType.Metal) + { + result.BaseAssetType = SharedAssetType.TradFi; + result.BaseAssetSubType = SharedAssetSubType.Commodity; + } + else if (s.SymbolType == Enums.Uta.SymbolType.Stock) + { + result.BaseAssetType = SharedAssetType.TradFi; + result.BaseAssetSubType = SharedAssetSubType.Equity; + } + else if (LibraryHelpers.IsStableCoin(s.BaseAsset)) + { + result.BaseAssetType = SharedAssetType.Crypto; + result.BaseAssetSubType = SharedAssetSubType.StableCoin; + } + else if (_exchangeSupportedFiatCurrencies.Contains(s.BaseAsset)) + { + result.BaseAssetType = SharedAssetType.Fiat; + } + else + { + result.BaseAssetType = SharedAssetType.Crypto; + } + + if (LibraryHelpers.IsStableCoin(s.QuoteAsset)) + { + result.QuoteAssetType = SharedAssetType.Crypto; + result.QuoteAssetSubType = SharedAssetSubType.StableCoin; + } + else if (_exchangeSupportedFiatCurrencies.Contains(s.QuoteAsset)) + { + result.QuoteAssetType = SharedAssetType.Fiat; + } + else + { + result.QuoteAssetType = SharedAssetType.Crypto; + } - ExchangeSymbolCache.UpdateSymbolInfo(_topicId, EnvironmentName, null, response.Data!); - return response; + return result; } async Task> ISpotSymbolRestClient.GetSpotSymbolsForBaseAssetAsync(string baseAsset)