diff --git a/BingX.Net/BingX.Net.csproj b/BingX.Net/BingX.Net.csproj index c3c6906..0ac4bbe 100644 --- a/BingX.Net/BingX.Net.csproj +++ b/BingX.Net/BingX.Net.csproj @@ -48,10 +48,10 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> + <PackageReference Include="CryptoExchange.Net" Version="8.1.0" /> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="CryptoExchange.Net" Version="8.0.3" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/BingX.Net/BingX.Net.xml b/BingX.Net/BingX.Net.xml index 12b76b8..1d1b85e 100644 --- a/BingX.Net/BingX.Net.xml +++ b/BingX.Net/BingX.Net.xml @@ -59,6 +59,16 @@ Urls to the API documentation </summary> </member> + <member name="M:BingX.Net.BingXExchange.FormatSymbol(System.String,System.String,CryptoExchange.Net.SharedApis.TradingMode,System.Nullable{System.DateTime})"> + <summary> + Format a base and quote asset to a BingX recognized symbol + </summary> + <param name="baseAsset">Base asset</param> + <param name="quoteAsset">Quote asset</param> + <param name="tradingMode">Trading mode</param> + <param name="deliverTime">Delivery time for delivery futures</param> + <returns></returns> + </member> <member name="P:BingX.Net.BingXExchange.RateLimiter"> <summary> Rate limiter configuration for the BingX API @@ -74,6 +84,26 @@ Event for when a rate limit is triggered </summary> </member> + <member name="T:BingX.Net.BingXTrackerFactory"> + <inheritdoc /> + </member> + <member name="M:BingX.Net.BingXTrackerFactory.#ctor"> + <summary> + ctor + </summary> + </member> + <member name="M:BingX.Net.BingXTrackerFactory.#ctor(System.IServiceProvider)"> + <summary> + ctor + </summary> + <param name="serviceProvider">Service provider for resolving logging and clients</param> + </member> + <member name="M:BingX.Net.BingXTrackerFactory.CreateKlineTracker(CryptoExchange.Net.SharedApis.SharedSymbol,CryptoExchange.Net.SharedApis.SharedKlineInterval,System.Nullable{System.Int32},System.Nullable{System.TimeSpan})"> + <inheritdoc /> + </member> + <member name="M:BingX.Net.BingXTrackerFactory.CreateTradeTracker(CryptoExchange.Net.SharedApis.SharedSymbol,System.Nullable{System.Int32},System.Nullable{System.TimeSpan})"> + <inheritdoc /> + </member> <member name="T:BingX.Net.Clients.BingXRestClient"> <inheritdoc cref="T:BingX.Net.Interfaces.Clients.IBingXRestClient" /> </member> @@ -2666,6 +2696,14 @@ Perpetual Futures order book factory methods </summary> </member> + <member name="M:BingX.Net.Interfaces.IBingXOrderBookFactory.Create(CryptoExchange.Net.SharedApis.SharedSymbol,System.Action{BingX.Net.Objects.Options.BingXOrderBookOptions})"> + <summary> + Create a SymbolOrderBook for the symbol + </summary> + <param name="symbol">The symbol</param> + <param name="options">Book options</param> + <returns></returns> + </member> <member name="M:BingX.Net.Interfaces.IBingXOrderBookFactory.CreatePerpetualFutures(System.String,System.Action{BingX.Net.Objects.Options.BingXOrderBookOptions})"> <summary> Create a new futures local order book instance @@ -2682,6 +2720,30 @@ <param name="options"></param> <returns></returns> </member> + <member name="T:BingX.Net.Interfaces.IBingXTrackerFactory"> + <summary> + Tracker factory + </summary> + </member> + <member name="M:BingX.Net.Interfaces.IBingXTrackerFactory.CreateKlineTracker(CryptoExchange.Net.SharedApis.SharedSymbol,CryptoExchange.Net.SharedApis.SharedKlineInterval,System.Nullable{System.Int32},System.Nullable{System.TimeSpan})"> + <summary> + Create a new kline tracker + </summary> + <param name="symbol">The symbol</param> + <param name="interval">Kline interval</param> + <param name="limit">The max amount of klines to retain</param> + <param name="period">The max period the data should be retained</param> + <returns></returns> + </member> + <member name="M:BingX.Net.Interfaces.IBingXTrackerFactory.CreateTradeTracker(CryptoExchange.Net.SharedApis.SharedSymbol,System.Nullable{System.Int32},System.Nullable{System.TimeSpan})"> + <summary> + Create a new trade tracker for a symbol + </summary> + <param name="symbol">The symbol</param> + <param name="limit">The max amount of klines to retain</param> + <param name="period">The max period the data should be retained</param> + <returns></returns> + </member> <member name="T:BingX.Net.Objects.BingXApiAddresses"> <summary> Api addresses @@ -6477,6 +6539,9 @@ <member name="P:BingX.Net.SymbolOrderBooks.BingXOrderBookFactory.PerpetualFutures"> <inheritdoc /> </member> + <member name="M:BingX.Net.SymbolOrderBooks.BingXOrderBookFactory.Create(CryptoExchange.Net.SharedApis.SharedSymbol,System.Action{BingX.Net.Objects.Options.BingXOrderBookOptions})"> + <inheritdoc /> + </member> <member name="M:BingX.Net.SymbolOrderBooks.BingXOrderBookFactory.CreateSpot(System.String,System.Action{BingX.Net.Objects.Options.BingXOrderBookOptions})"> <inheritdoc /> </member> diff --git a/BingX.Net/BingXExchange.cs b/BingX.Net/BingXExchange.cs index c4a518a..ffa71fd 100644 --- a/BingX.Net/BingXExchange.cs +++ b/BingX.Net/BingXExchange.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.SharedApis; using System; using System.Collections.Generic; @@ -29,6 +30,19 @@ public static class BingXExchange "https://bingx-api.github.io/docs" }; + /// <summary> + /// Format a base and quote asset to a BingX recognized symbol + /// </summary> + /// <param name="baseAsset">Base asset</param> + /// <param name="quoteAsset">Quote asset</param> + /// <param name="tradingMode">Trading mode</param> + /// <param name="deliverTime">Delivery time for delivery futures</param> + /// <returns></returns> + public static string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + { + return baseAsset.ToUpperInvariant() + "-" + quoteAsset.ToUpperInvariant(); + } + /// <summary> /// Rate limiter configuration for the BingX API /// </summary> diff --git a/BingX.Net/BingXTrackerFactory.cs b/BingX.Net/BingXTrackerFactory.cs new file mode 100644 index 0000000..4a03bc2 --- /dev/null +++ b/BingX.Net/BingXTrackerFactory.cs @@ -0,0 +1,92 @@ +using BingX.Net.Clients; +using BingX.Net.Interfaces; +using BingX.Net.Interfaces.Clients; +using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.Trackers.Klines; +using CryptoExchange.Net.Trackers.Trades; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +namespace BingX.Net +{ + /// <inheritdoc /> + public class BingXTrackerFactory : IBingXTrackerFactory + { + private readonly IServiceProvider? _serviceProvider; + + /// <summary> + /// ctor + /// </summary> + public BingXTrackerFactory() + { + } + + /// <summary> + /// ctor + /// </summary> + /// <param name="serviceProvider">Service provider for resolving logging and clients</param> + public BingXTrackerFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// <inheritdoc /> + public IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null) + { + var restClient = _serviceProvider?.GetRequiredService<IBingXRestClient>() ?? new BingXRestClient(); + var socketClient = _serviceProvider?.GetRequiredService<IBingXSocketClient>() ?? new BingXSocketClient(); + + IKlineRestClient sharedRestClient; + IKlineSocketClient sharedSocketClient; + if (symbol.TradingMode == TradingMode.Spot) + { + sharedRestClient = restClient.SpotApi.SharedClient; + sharedSocketClient = socketClient.SpotApi.SharedClient; + } + else + { + sharedRestClient = restClient.PerpetualFuturesApi.SharedClient; + sharedSocketClient = socketClient.PerpetualFuturesApi.SharedClient; + } + + return new KlineTracker( + _serviceProvider?.GetRequiredService<ILoggerFactory>().CreateLogger(restClient.Exchange), + sharedRestClient, + sharedSocketClient, + symbol, + interval, + limit, + period + ); + } + + /// <inheritdoc /> + public ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null) + { + var restClient = _serviceProvider?.GetRequiredService<IBingXRestClient>() ?? new BingXRestClient(); + var socketClient = _serviceProvider?.GetRequiredService<IBingXSocketClient>() ?? new BingXSocketClient(); + + IRecentTradeRestClient sharedRestClient; + ITradeSocketClient sharedSocketClient; + if (symbol.TradingMode == TradingMode.Spot) { + sharedRestClient = restClient.SpotApi.SharedClient; + sharedSocketClient = socketClient.SpotApi.SharedClient; + } + else { + sharedRestClient = restClient.PerpetualFuturesApi.SharedClient; + sharedSocketClient = socketClient.PerpetualFuturesApi.SharedClient; + } + + return new TradeTracker( + _serviceProvider?.GetRequiredService<ILoggerFactory>().CreateLogger(restClient.Exchange), + sharedRestClient, + null, + sharedSocketClient, + symbol, + limit, + period + ); + } + } +} diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs index b663068..11129f3 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApi.cs @@ -55,7 +55,9 @@ internal BingXRestClientPerpetualFuturesApi(ILogger logger, HttpClient? httpClie #endregion /// <inheritdoc /> - public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) => baseAsset.ToUpperInvariant() + "-" + quoteAsset.ToUpperInvariant(); + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + => BingXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); + public IBingXRestClientPerpetualFuturesApiShared SharedClient => this; /// <inheritdoc /> diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiShared.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiShared.cs index c91f1a3..8ccaa47 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiShared.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXRestClientPerpetualFuturesApiShared.cs @@ -181,7 +181,10 @@ async Task<ExchangeWebResult<IEnumerable<SharedTrade>>> IRecentTradeRestClient.G if (!result) return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, null, default); - return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)).ToArray()); + return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp) + { + Side = x.BuyerIsMaker ? SharedOrderSide.Buy : SharedOrderSide.Sell + }).ToArray()); } #endregion diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs index c1124c5..758c309 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApi.cs @@ -50,7 +50,8 @@ internal BingXSocketClientPerpetualFuturesApi(ILogger logger, BingXSocketOptions #endregion /// <inheritdoc /> - public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) => baseAsset.ToUpperInvariant() + "-" + quoteAsset.ToUpperInvariant(); + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + => BingXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); /// <inheritdoc /> protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) diff --git a/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApiShared.cs b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApiShared.cs index 7c6b5e4..c19b147 100644 --- a/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApiShared.cs +++ b/BingX.Net/Clients/PerpetualFuturesApi/BingXSocketClientPerpetualFuturesApiShared.cs @@ -44,13 +44,42 @@ async Task<ExchangeResult<UpdateSubscription>> ITradeSocketClient.SubscribeToTra return new ExchangeResult<UpdateSubscription>(Exchange, validationError); var symbol = request.Symbol.GetSymbol(FormatSymbol); - var result = await SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent<IEnumerable<SharedTrade>>(Exchange, update.Data.Select(x => new SharedTrade(x.Quantity, x.Price, x.TradeTime)).ToArray())), ct).ConfigureAwait(false); + var result = await SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent<IEnumerable<SharedTrade>>(Exchange, update.Data.Select(x => new SharedTrade(x.Quantity, x.Price, x.TradeTime) + { + Side = x.BuyerIsMaker ? SharedOrderSide.Buy : SharedOrderSide.Sell + }).ToArray())), ct).ConfigureAwait(false); return new ExchangeResult<UpdateSubscription>(Exchange, result); } #endregion + #region Kline client + SubscribeKlineOptions IKlineSocketClient.SubscribeKlineOptions { get; } = new SubscribeKlineOptions(false); + async Task<ExchangeResult<UpdateSubscription>> IKlineSocketClient.SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action<ExchangeEvent<SharedKline>> handler, CancellationToken ct) + { + var interval = (Enums.KlineInterval)request.Interval; + if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval)) + return new ExchangeResult<UpdateSubscription>(Exchange, new ArgumentError("Interval not supported")); + + var validationError = ((IKlineSocketClient)this).SubscribeKlineOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult<UpdateSubscription>(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToKlineUpdatesAsync(symbol, interval, update => + { + if (update.UpdateType == SocketUpdateType.Snapshot) + return; + + foreach (var item in update.Data) + handler(update.AsExchangeEvent(Exchange, new SharedKline(item.Timestamp, item.ClosePrice, item.HighPrice, item.LowPrice, item.OpenPrice, item.Volume))); + }, ct).ConfigureAwait(false); + + return new ExchangeResult<UpdateSubscription>(Exchange, result); + } + #endregion + #region Book Ticker client EndpointOptions<SubscribeBookTickerRequest> IBookTickerSocketClient.SubscribeBookTickerOptions { get; } = new EndpointOptions<SubscribeBookTickerRequest>(false); diff --git a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs index fbce924..76f600b 100644 --- a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs +++ b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApi.cs @@ -66,7 +66,8 @@ internal BingXRestClientSpotApi(ILogger logger, HttpClient? httpClient, BingXRes #endregion /// <inheritdoc /> - public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) => baseAsset.ToUpperInvariant() + "-" + quoteAsset.ToUpperInvariant(); + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + => BingXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); /// <inheritdoc /> protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); diff --git a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiShared.cs b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiShared.cs index 96baadd..9170062 100644 --- a/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiShared.cs +++ b/BingX.Net/Clients/SpotApi/BingXRestClientSpotApiShared.cs @@ -152,7 +152,10 @@ async Task<ExchangeWebResult<IEnumerable<SharedTrade>>> IRecentTradeRestClient.G if (!result) return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, null, default); - return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, request.Symbol.TradingMode, result.Data.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)).ToArray()); + return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, request.Symbol.TradingMode, result.Data.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp) + { + Side = x.BuyerIsMaker ? SharedOrderSide.Buy : SharedOrderSide.Sell + }).ToArray()); } #endregion diff --git a/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs index 5278363..44833fc 100644 --- a/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs +++ b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApi.cs @@ -54,7 +54,8 @@ protected override AuthenticationProvider CreateAuthenticationProvider(ApiCreden => new BingXAuthenticationProvider(credentials); /// <inheritdoc /> - public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) => baseAsset.ToUpperInvariant() + "-" + quoteAsset.ToUpperInvariant(); + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + => BingXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); public IBingXSocketClientSpotApiShared SharedClient => this; diff --git a/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApiShared.cs b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApiShared.cs index ae48e49..f831205 100644 --- a/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApiShared.cs +++ b/BingX.Net/Clients/SpotApi/BingXSocketClientSpotApiShared.cs @@ -45,13 +45,35 @@ async Task<ExchangeResult<UpdateSubscription>> ITradeSocketClient.SubscribeToTra return new ExchangeResult<UpdateSubscription>(Exchange, validationError); var symbol = request.Symbol.GetSymbol(FormatSymbol); - var result = await SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent<IEnumerable<SharedTrade>>(Exchange, new[] { new SharedTrade(update.Data.Quantity, update.Data.Price, update.Data.TradeTime) })), ct).ConfigureAwait(false); + var result = await SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent<IEnumerable<SharedTrade>>(Exchange, new[] { new SharedTrade(update.Data.Quantity, update.Data.Price, update.Data.TradeTime) + { + Side = update.Data.BuyerIsMaker ? SharedOrderSide.Buy : SharedOrderSide.Sell + } })), ct).ConfigureAwait(false); return new ExchangeResult<UpdateSubscription>(Exchange, result); } #endregion + #region Kline client + SubscribeKlineOptions IKlineSocketClient.SubscribeKlineOptions { get; } = new SubscribeKlineOptions(false); + async Task<ExchangeResult<UpdateSubscription>> IKlineSocketClient.SubscribeToKlineUpdatesAsync(SubscribeKlineRequest request, Action<ExchangeEvent<SharedKline>> handler, CancellationToken ct) + { + var interval = (Enums.KlineInterval)request.Interval; + if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval)) + return new ExchangeResult<UpdateSubscription>(Exchange, new ArgumentError("Interval not supported")); + + var validationError = ((IKlineSocketClient)this).SubscribeKlineOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes); + if (validationError != null) + return new ExchangeResult<UpdateSubscription>(Exchange, validationError); + + var symbol = request.Symbol.GetSymbol(FormatSymbol); + var result = await SubscribeToKlineUpdatesAsync(symbol, interval, update => handler(update.AsExchangeEvent(Exchange, new SharedKline(update.Data.Kline.OpenTime, update.Data.Kline.ClosePrice, update.Data.Kline.HighPrice, update.Data.Kline.LowPrice, update.Data.Kline.OpenPrice, update.Data.Kline.Volume))), ct).ConfigureAwait(false); + + return new ExchangeResult<UpdateSubscription>(Exchange, result); + } + #endregion + #region Book Ticker client EndpointOptions<SubscribeBookTickerRequest> IBookTickerSocketClient.SubscribeBookTickerOptions { get; } = new EndpointOptions<SubscribeBookTickerRequest>(false); diff --git a/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs b/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs index 161553c..5c14ae1 100644 --- a/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs +++ b/BingX.Net/ExtensionMethods/ServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using BingX.Net.Objects.Options; using BingX.Net.SymbolOrderBooks; using CryptoExchange.Net; +using BingX.Net; namespace Microsoft.Extensions.DependencyInjection { @@ -62,6 +63,7 @@ public static IServiceCollection AddBingX( services.AddTransient<ICryptoRestClient, CryptoRestClient>(); services.AddSingleton<ICryptoSocketClient, CryptoSocketClient>(); services.AddTransient<IBingXOrderBookFactory, BingXOrderBookFactory>(); + services.AddTransient<IBingXTrackerFactory, BingXTrackerFactory>(); services.AddTransient(x => x.GetRequiredService<IBingXRestClient>().SpotApi.CommonSpotClient); services.RegisterSharedRestInterfaces(x => x.GetRequiredService<IBingXRestClient>().SpotApi.SharedClient); diff --git a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApiShared.cs b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApiShared.cs index 2471092..89928be 100644 --- a/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApiShared.cs +++ b/BingX.Net/Interfaces/Clients/PerpetualFuturesApi/IBingXSocketClientPerpetualFuturesApiShared.cs @@ -11,7 +11,8 @@ public interface IBingXSocketClientPerpetualFuturesApiShared : IBookTickerSocketClient, IBalanceSocketClient, IPositionSocketClient, - IFuturesOrderSocketClient + IFuturesOrderSocketClient, + IKlineSocketClient { } } diff --git a/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApiShared.cs b/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApiShared.cs index c5efcb9..ebfb9a3 100644 --- a/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApiShared.cs +++ b/BingX.Net/Interfaces/Clients/SpotApi/IBingXSocketClientSpotApiShared.cs @@ -10,7 +10,8 @@ public interface IBingXSocketClientSpotApiShared : ITradeSocketClient, IBookTickerSocketClient, IBalanceSocketClient, - ISpotOrderSocketClient + ISpotOrderSocketClient, + IKlineSocketClient { } } diff --git a/BingX.Net/Interfaces/IBingXOrderBookFactory.cs b/BingX.Net/Interfaces/IBingXOrderBookFactory.cs index cb64e7d..0c8e544 100644 --- a/BingX.Net/Interfaces/IBingXOrderBookFactory.cs +++ b/BingX.Net/Interfaces/IBingXOrderBookFactory.cs @@ -1,6 +1,7 @@ using CryptoExchange.Net.Interfaces; using System; using BingX.Net.Objects.Options; +using CryptoExchange.Net.SharedApis; namespace BingX.Net.Interfaces { @@ -18,6 +19,14 @@ public interface IBingXOrderBookFactory /// </summary> public IOrderBookFactory<BingXOrderBookOptions> PerpetualFutures { get; } + /// <summary> + /// Create a SymbolOrderBook for the symbol + /// </summary> + /// <param name="symbol">The symbol</param> + /// <param name="options">Book options</param> + /// <returns></returns> + ISymbolOrderBook Create(SharedSymbol symbol, Action<BingXOrderBookOptions>? options = null); + /// <summary> /// Create a new futures local order book instance /// </summary> diff --git a/BingX.Net/Interfaces/IBingXTrackerFactory.cs b/BingX.Net/Interfaces/IBingXTrackerFactory.cs new file mode 100644 index 0000000..fc481ae --- /dev/null +++ b/BingX.Net/Interfaces/IBingXTrackerFactory.cs @@ -0,0 +1,34 @@ +using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.Trackers.Klines; +using CryptoExchange.Net.Trackers.Trades; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BingX.Net.Interfaces +{ + /// <summary> + /// Tracker factory + /// </summary> + public interface IBingXTrackerFactory + { + /// <summary> + /// Create a new kline tracker + /// </summary> + /// <param name="symbol">The symbol</param> + /// <param name="interval">Kline interval</param> + /// <param name="limit">The max amount of klines to retain</param> + /// <param name="period">The max period the data should be retained</param> + /// <returns></returns> + IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null); + + /// <summary> + /// Create a new trade tracker for a symbol + /// </summary> + /// <param name="symbol">The symbol</param> + /// <param name="limit">The max amount of klines to retain</param> + /// <param name="period">The max period the data should be retained</param> + /// <returns></returns> + ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null); + } +} diff --git a/BingX.Net/SymbolOrderBooks/BingXOrderBookFactory.cs b/BingX.Net/SymbolOrderBooks/BingXOrderBookFactory.cs index e57a474..0bf8d08 100644 --- a/BingX.Net/SymbolOrderBooks/BingXOrderBookFactory.cs +++ b/BingX.Net/SymbolOrderBooks/BingXOrderBookFactory.cs @@ -6,6 +6,7 @@ using BingX.Net.Interfaces.Clients; using BingX.Net.Objects.Options; using CryptoExchange.Net.OrderBook; +using CryptoExchange.Net.SharedApis; namespace BingX.Net.SymbolOrderBooks { @@ -24,8 +25,13 @@ public BingXOrderBookFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - Spot = new OrderBookFactory<BingXOrderBookOptions>((symbol, options) => CreateSpot(symbol, options), (baseAsset, quoteAsset, options) => CreateSpot(baseAsset + "-" + quoteAsset, options)); - PerpetualFutures = new OrderBookFactory<BingXOrderBookOptions>((symbol, options) => CreatePerpetualFutures(symbol, options), (baseAsset, quoteAsset, options) => CreatePerpetualFutures(baseAsset + quoteAsset, options)); + Spot = new OrderBookFactory<BingXOrderBookOptions>( + CreateSpot, + (sharedSymbol, options) => CreateSpot(BingXExchange.FormatSymbol(sharedSymbol.BaseAsset, sharedSymbol.QuoteAsset, sharedSymbol.TradingMode, sharedSymbol.DeliverTime), options)); + + PerpetualFutures = new OrderBookFactory<BingXOrderBookOptions>( + CreatePerpetualFutures, + (sharedSymbol, options) => CreatePerpetualFutures(BingXExchange.FormatSymbol(sharedSymbol.BaseAsset, sharedSymbol.QuoteAsset, sharedSymbol.TradingMode, sharedSymbol.DeliverTime), options)); } /// <inheritdoc /> @@ -33,6 +39,16 @@ public BingXOrderBookFactory(IServiceProvider serviceProvider) /// <inheritdoc /> public IOrderBookFactory<BingXOrderBookOptions> PerpetualFutures { get; } + /// <inheritdoc /> + public ISymbolOrderBook Create(SharedSymbol symbol, Action<BingXOrderBookOptions>? options = null) + { + var symbolName = BingXExchange.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime); + if (symbol.TradingMode == TradingMode.Spot) + return CreateSpot(symbolName, options); + + return CreatePerpetualFutures(symbolName, options); + } + /// <inheritdoc /> public ISymbolOrderBook CreateSpot(string symbol, Action<BingXOrderBookOptions>? options = null) => new BingXSpotSymbolOrderBook(symbol, diff --git a/Examples/BingX.Examples.Api/BingX.Examples.Api.csproj b/Examples/BingX.Examples.Api/BingX.Examples.Api.csproj index 093e78f..da64028 100644 --- a/Examples/BingX.Examples.Api/BingX.Examples.Api.csproj +++ b/Examples/BingX.Examples.Api/BingX.Examples.Api.csproj @@ -1,16 +1,19 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <InvariantGlobalization>true</InvariantGlobalization> </PropertyGroup> <ItemGroup> - <PackageReference Include="JK.BingX.Net" Version="1.0.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\..\BingX.Net\BingX.Net.csproj" /> + </ItemGroup> + </Project> diff --git a/Examples/BingX.Examples.Console/BingX.Examples.Console.csproj b/Examples/BingX.Examples.Console/BingX.Examples.Console.csproj index 3351840..bf23bce 100644 --- a/Examples/BingX.Examples.Console/BingX.Examples.Console.csproj +++ b/Examples/BingX.Examples.Console/BingX.Examples.Console.csproj @@ -2,13 +2,13 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> - <PackageReference Include="JK.BingX.Net" Version="1.0.0" /> + <ProjectReference Include="..\..\BingX.Net\BingX.Net.csproj" /> </ItemGroup> </Project> diff --git a/Examples/BingX.Examples.OrderBook/BingX.Examples.OrderBook.csproj b/Examples/BingX.Examples.OrderBook/BingX.Examples.OrderBook.csproj new file mode 100644 index 0000000..5a74ae1 --- /dev/null +++ b/Examples/BingX.Examples.OrderBook/BingX.Examples.OrderBook.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Spectre.Console.Cli" Version="0.49.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\BingX.Net\BingX.Net.csproj" /> + </ItemGroup> + +</Project> diff --git a/Examples/BingX.Examples.OrderBook/Program.cs b/Examples/BingX.Examples.OrderBook/Program.cs new file mode 100644 index 0000000..8b21c1e --- /dev/null +++ b/Examples/BingX.Examples.OrderBook/Program.cs @@ -0,0 +1,52 @@ +using BingX.Net.Interfaces; +using CryptoExchange.Net; +using CryptoExchange.Net.SharedApis; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console; + +var collection = new ServiceCollection(); +collection.AddBingX(); +var provider = collection.BuildServiceProvider(); + +var bookFactory = provider.GetRequiredService<IBingXOrderBookFactory>(); + +// Create and start the order book +var book = bookFactory.Create(new SharedSymbol(TradingMode.Spot, "ETH", "USDT")); +var result = await book.StartAsync(); +if (!result.Success) +{ + Console.WriteLine(result); + return; +} + +// Create Spectre table +var table = new Table(); +table.ShowRowSeparators = true; +table.AddColumn("Bid Quantity", x => { x.RightAligned(); }) + .AddColumn("Bid Price", x => { x.RightAligned(); }) + .AddColumn("Ask Price", x => { x.LeftAligned(); }) + .AddColumn("Ask Quantity", x => { x.LeftAligned(); }); + +for(var i = 0; i < 10; i++) + table.AddEmptyRow(); + +await AnsiConsole.Live(table) + .StartAsync(async ctx => + { + while (true) + { + var snapshot = book.Book; + for (var i = 0; i < 10; i++) + { + var bid = snapshot.bids.ElementAt(i); + var ask = snapshot.asks.ElementAt(i); + table.UpdateCell(i, 0, ExchangeHelpers.Normalize(bid.Quantity).ToString()); + table.UpdateCell(i, 1, ExchangeHelpers.Normalize(bid.Price).ToString()); + table.UpdateCell(i, 2, ExchangeHelpers.Normalize(ask.Price).ToString()); + table.UpdateCell(i, 3, ExchangeHelpers.Normalize(ask.Quantity).ToString()); + } + + ctx.Refresh(); + await Task.Delay(500); + } + }); diff --git a/Examples/BingX.Examples.Tracker/BingX.Examples.Tracker.csproj b/Examples/BingX.Examples.Tracker/BingX.Examples.Tracker.csproj new file mode 100644 index 0000000..5a74ae1 --- /dev/null +++ b/Examples/BingX.Examples.Tracker/BingX.Examples.Tracker.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Spectre.Console.Cli" Version="0.49.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\BingX.Net\BingX.Net.csproj" /> + </ItemGroup> + +</Project> diff --git a/Examples/BingX.Examples.Tracker/Program.cs b/Examples/BingX.Examples.Tracker/Program.cs new file mode 100644 index 0000000..5e7495f --- /dev/null +++ b/Examples/BingX.Examples.Tracker/Program.cs @@ -0,0 +1,104 @@ +using BingX.Net.Interfaces; +using CryptoExchange.Net.SharedApis; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console; +using System.Globalization; + +var collection = new ServiceCollection(); +collection.AddBingX(); +var provider = collection.BuildServiceProvider(); + +var trackerFactory = provider.GetRequiredService<IBingXTrackerFactory>(); + +// Create and start the tracker, keep track of the last 10 minutes +var tracker = trackerFactory.CreateTradeTracker(new SharedSymbol(TradingMode.Spot, "ETH", "USDT"), period: TimeSpan.FromMinutes(10)); +var result = await tracker.StartAsync(); +if (!result.Success) +{ + Console.WriteLine(result); + return; +} + +// Create Spectre table +var table = new Table(); +table.ShowRowSeparators = true; +table.AddColumn("5 Min Data").AddColumn("-5 Min", x => { x.RightAligned(); }) + .AddColumn("Now", x => { x.RightAligned(); }) + .AddColumn("Dif", x => { x.RightAligned(); }); + +table.AddRow("Count", "", "", ""); +table.AddRow("Average price", "", "", ""); +table.AddRow("Average weighted price", "", "", ""); +table.AddRow("Buy/Sell Ratio", "", "", ""); +table.AddRow("Volume", "", "", ""); +table.AddRow("Value", "", "", ""); +table.AddRow("Complete", "", "", ""); +table.AddRow("", "", "", ""); +table.AddRow("Status", "", "", ""); +table.AddRow("Synced From", "", "", ""); + +// Set default culture for currency display +CultureInfo ci = new CultureInfo("en-US"); +Thread.CurrentThread.CurrentCulture = ci; +Thread.CurrentThread.CurrentUICulture = ci; + +await AnsiConsole.Live(table) + .StartAsync(async ctx => + { + while (true) + { + // Get the stats from 10 minutes until 5 minutes ago + var secondLastMinute = tracker.GetStats(DateTime.UtcNow.AddMinutes(-10), DateTime.UtcNow.AddMinutes(-5)); + + // Get the stats from 5 minutes ago until now + var lastMinute = tracker.GetStats(DateTime.UtcNow.AddMinutes(-5)); + + // Get the differences between them + var compare = secondLastMinute.CompareTo(lastMinute); + + // Update the columns + UpdateDec(0, 1, secondLastMinute.TradeCount); + UpdateDec(0, 2, lastMinute.TradeCount); + UpdateStr(0, 3, $"[{(compare.TradeCountDif.Difference < 0 ? "red" : "green")}]{compare.TradeCountDif.Difference} / {compare.TradeCountDif.PercentageDifference}%[/]"); + + UpdateStr(1, 1, secondLastMinute.AveragePrice?.ToString("C")); + UpdateStr(1, 2, lastMinute.AveragePrice?.ToString("C")); + UpdateStr(1, 3, $"[{(compare.AveragePriceDif?.Difference < 0 ? "red" : "green")}]{compare.AveragePriceDif?.Difference?.ToString("C")} / {compare.AveragePriceDif?.PercentageDifference}%[/]"); + + UpdateStr(2, 1, secondLastMinute.VolumeWeightedAveragePrice?.ToString("C")); + UpdateStr(2, 2, lastMinute.VolumeWeightedAveragePrice?.ToString("C")); + UpdateStr(2, 3, $"[{(compare.VolumeWeightedAveragePriceDif?.Difference < 0 ? "red" : "green")}]{compare.VolumeWeightedAveragePriceDif?.Difference?.ToString("C")} / {compare.VolumeWeightedAveragePriceDif?.PercentageDifference}%[/]"); + + UpdateDec(3, 1, secondLastMinute.BuySellRatio); + UpdateDec(3, 2, lastMinute.BuySellRatio); + UpdateStr(3, 3, $"[{(compare.BuySellRatioDif?.Difference < 0 ? "red" : "green")}]{compare.BuySellRatioDif?.Difference} / {compare.BuySellRatioDif?.PercentageDifference}%[/]"); + + UpdateDec(4, 1, secondLastMinute.Volume); + UpdateDec(4, 2, lastMinute.Volume); + UpdateStr(4, 3, $"[{(compare.VolumeDif.Difference < 0 ? "red" : "green")}]{compare.VolumeDif.Difference} / {compare.VolumeDif.PercentageDifference}%[/]"); + + UpdateStr(5, 1, secondLastMinute.QuoteVolume.ToString("C")); + UpdateStr(5, 2, lastMinute.QuoteVolume.ToString("C")); + UpdateStr(5, 3, $"[{(compare.QuoteVolumeDif.Difference < 0 ? "red" : "green")}]{compare.QuoteVolumeDif.Difference?.ToString("C")} / {compare.QuoteVolumeDif.PercentageDifference}%[/]"); + + UpdateStr(6, 1, secondLastMinute.Complete.ToString()); + UpdateStr(6, 2, lastMinute.Complete.ToString()); + + UpdateStr(8, 1, tracker.Status.ToString()); + UpdateStr(9, 1, tracker.SyncedFrom?.ToString()); + + ctx.Refresh(); + await Task.Delay(500); + } + }); + + +void UpdateDec(int row, int col, decimal? val) +{ + table.UpdateCell(row, col, val?.ToString() ?? string.Empty); +} + +void UpdateStr(int row, int col, string? val) +{ + table.UpdateCell(row, col, val ?? string.Empty); +} diff --git a/Examples/Examples.sln b/Examples/Examples.sln index 590ca1c..084dfbc 100644 --- a/Examples/Examples.sln +++ b/Examples/Examples.sln @@ -7,6 +7,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BingX.Examples.Api", "BingX EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BingX.Examples.Console", "BingX.Examples.Console\BingX.Examples.Console.csproj", "{FD4F95C8-D9B7-4F81-9245-4CE667DFD421}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BingX.Net", "..\BingX.Net\BingX.Net.csproj", "{90651429-48DD-454F-BEAA-14058E43877A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BingX.Examples.OrderBook", "BingX.Examples.OrderBook\BingX.Examples.OrderBook.csproj", "{33D58C29-15EB-48E2-A6E9-FC9FD84B43E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BingX.Examples.Tracker", "BingX.Examples.Tracker\BingX.Examples.Tracker.csproj", "{0051E490-FA97-4B88-BB14-30AAF17DAAA1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +27,18 @@ Global {FD4F95C8-D9B7-4F81-9245-4CE667DFD421}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD4F95C8-D9B7-4F81-9245-4CE667DFD421}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD4F95C8-D9B7-4F81-9245-4CE667DFD421}.Release|Any CPU.Build.0 = Release|Any CPU + {90651429-48DD-454F-BEAA-14058E43877A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90651429-48DD-454F-BEAA-14058E43877A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90651429-48DD-454F-BEAA-14058E43877A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90651429-48DD-454F-BEAA-14058E43877A}.Release|Any CPU.Build.0 = Release|Any CPU + {33D58C29-15EB-48E2-A6E9-FC9FD84B43E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33D58C29-15EB-48E2-A6E9-FC9FD84B43E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33D58C29-15EB-48E2-A6E9-FC9FD84B43E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33D58C29-15EB-48E2-A6E9-FC9FD84B43E8}.Release|Any CPU.Build.0 = Release|Any CPU + {0051E490-FA97-4B88-BB14-30AAF17DAAA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0051E490-FA97-4B88-BB14-30AAF17DAAA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0051E490-FA97-4B88-BB14-30AAF17DAAA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0051E490-FA97-4B88-BB14-30AAF17DAAA1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Examples/README.md b/Examples/README.md index ba25ea5..bd922af 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -4,4 +4,10 @@ A minimal API showing how to integrate BingX.Net in a web API project ### BingX.Examples.Console -A simple console client demonstrating basic usage \ No newline at end of file +A simple console client demonstrating basic usage + +### BingX.Examples.OrderBook +Example of using the client side order book implementation + +### BingX.Examples.Tracker +Example of using the trade tracker \ No newline at end of file