From 53b09b73e60cd2a68b5ff77b44038f9c5c42d204 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Mon, 28 Oct 2024 13:58:18 +0100 Subject: [PATCH] Trackers (#121) Updated examples Updated CryptoExchange.Net to v8.1.0 Moved FormatSymbol to HTXExchange class Added support Side setting on SharedTrade model Added HTXTrackerFactory Added overload to Create method on HTXOrderBookFactory support SharedSymbol parameter Fixed rate limiting incorrectly applied to websocket market data connections --- Examples/Examples.sln | 18 +++ .../HTX.Examples.Api/HTX.Examples.Api.csproj | 7 +- .../HTX.Examples.Console.csproj | 4 +- .../HTX.Examples.OrderBook.csproj | 18 +++ Examples/HTX.Examples.OrderBook/Program.cs | 52 +++++++++ .../HTX.Examples.Tracker.csproj | 18 +++ Examples/HTX.Examples.Tracker/Program.cs | 104 ++++++++++++++++++ Examples/README.md | 8 +- .../Clients/SpotApi/HTXRestClientSpotApi.cs | 5 +- .../SpotApi/HTXRestClientSpotApiShared.cs | 5 +- .../Clients/SpotApi/HTXSocketClientSpotApi.cs | 3 +- .../SpotApi/HTXSocketClientSpotApiShared.cs | 5 +- .../HTXRestClientUsdtFuturesApi.cs | 5 +- .../HTXRestClientUsdtFuturesApiShared.cs | 9 +- .../HTXSocketClientUsdtFuturesApi.cs | 5 +- .../HTXSocketClientUsdtFuturesApiShared.cs | 5 +- .../ServiceCollectionExtensions.cs | 2 + HTX.Net/HTX.Net.csproj | 4 +- HTX.Net/HTX.Net.xml | 65 +++++++++++ HTX.Net/HTXExchange.cs | 24 +++- HTX.Net/HTXTrackerFactory.cs | 91 +++++++++++++++ HTX.Net/Interfaces/IHTXOrderBookFactory.cs | 11 +- HTX.Net/Interfaces/IHTXTrackerFactory.cs | 34 ++++++ .../SymbolOrderBooks/HTXOrderBookFactory.cs | 19 +++- 24 files changed, 490 insertions(+), 31 deletions(-) create mode 100644 Examples/HTX.Examples.OrderBook/HTX.Examples.OrderBook.csproj create mode 100644 Examples/HTX.Examples.OrderBook/Program.cs create mode 100644 Examples/HTX.Examples.Tracker/HTX.Examples.Tracker.csproj create mode 100644 Examples/HTX.Examples.Tracker/Program.cs create mode 100644 HTX.Net/HTXTrackerFactory.cs create mode 100644 HTX.Net/Interfaces/IHTXTrackerFactory.cs diff --git a/Examples/Examples.sln b/Examples/Examples.sln index ee21e598..65fd5340 100644 --- a/Examples/Examples.sln +++ b/Examples/Examples.sln @@ -7,6 +7,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTX.Examples.Api", "HTX.Exa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTX.Examples.Console", "HTX.Examples.Console\HTX.Examples.Console.csproj", "{FD4F95C8-D9B7-4F81-9245-4CE667DFD421}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTX.Examples.OrderBook", "HTX.Examples.OrderBook\HTX.Examples.OrderBook.csproj", "{B73A14C6-BED9-4C51-A2C4-6E5FA43741B2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTX.Examples.Tracker", "HTX.Examples.Tracker\HTX.Examples.Tracker.csproj", "{4A6E7966-5E0C-4FFF-A941-987CF91577CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HTX.Net", "..\HTX.Net\HTX.Net.csproj", "{1761E80A-9D75-4D7F-8A88-31E4499D6DB9}" +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 + {B73A14C6-BED9-4C51-A2C4-6E5FA43741B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B73A14C6-BED9-4C51-A2C4-6E5FA43741B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B73A14C6-BED9-4C51-A2C4-6E5FA43741B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B73A14C6-BED9-4C51-A2C4-6E5FA43741B2}.Release|Any CPU.Build.0 = Release|Any CPU + {4A6E7966-5E0C-4FFF-A941-987CF91577CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A6E7966-5E0C-4FFF-A941-987CF91577CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A6E7966-5E0C-4FFF-A941-987CF91577CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A6E7966-5E0C-4FFF-A941-987CF91577CC}.Release|Any CPU.Build.0 = Release|Any CPU + {1761E80A-9D75-4D7F-8A88-31E4499D6DB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1761E80A-9D75-4D7F-8A88-31E4499D6DB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1761E80A-9D75-4D7F-8A88-31E4499D6DB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1761E80A-9D75-4D7F-8A88-31E4499D6DB9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Examples/HTX.Examples.Api/HTX.Examples.Api.csproj b/Examples/HTX.Examples.Api/HTX.Examples.Api.csproj index 73c1f77a..24ec871f 100644 --- a/Examples/HTX.Examples.Api/HTX.Examples.Api.csproj +++ b/Examples/HTX.Examples.Api/HTX.Examples.Api.csproj @@ -1,16 +1,19 @@ - net7.0 + net8.0 enable enable true - + + + + diff --git a/Examples/HTX.Examples.Console/HTX.Examples.Console.csproj b/Examples/HTX.Examples.Console/HTX.Examples.Console.csproj index 0b0515dd..4e3aff93 100644 --- a/Examples/HTX.Examples.Console/HTX.Examples.Console.csproj +++ b/Examples/HTX.Examples.Console/HTX.Examples.Console.csproj @@ -2,13 +2,13 @@ Exe - net7.0 + net8.0 enable enable - + diff --git a/Examples/HTX.Examples.OrderBook/HTX.Examples.OrderBook.csproj b/Examples/HTX.Examples.OrderBook/HTX.Examples.OrderBook.csproj new file mode 100644 index 00000000..b638fd1b --- /dev/null +++ b/Examples/HTX.Examples.OrderBook/HTX.Examples.OrderBook.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Examples/HTX.Examples.OrderBook/Program.cs b/Examples/HTX.Examples.OrderBook/Program.cs new file mode 100644 index 00000000..39873c7e --- /dev/null +++ b/Examples/HTX.Examples.OrderBook/Program.cs @@ -0,0 +1,52 @@ +using HTX.Net.Interfaces; +using CryptoExchange.Net; +using CryptoExchange.Net.SharedApis; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console; + +var collection = new ServiceCollection(); +collection.AddHTX(); +var provider = collection.BuildServiceProvider(); + +var trackerFactory = provider.GetRequiredService(); + +// Creat and start the order book +var book = trackerFactory.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/HTX.Examples.Tracker/HTX.Examples.Tracker.csproj b/Examples/HTX.Examples.Tracker/HTX.Examples.Tracker.csproj new file mode 100644 index 00000000..b638fd1b --- /dev/null +++ b/Examples/HTX.Examples.Tracker/HTX.Examples.Tracker.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Examples/HTX.Examples.Tracker/Program.cs b/Examples/HTX.Examples.Tracker/Program.cs new file mode 100644 index 00000000..6367efe6 --- /dev/null +++ b/Examples/HTX.Examples.Tracker/Program.cs @@ -0,0 +1,104 @@ +using HTX.Net.Interfaces; +using CryptoExchange.Net.SharedApis; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console; +using System.Globalization; + +var collection = new ServiceCollection(); +collection.AddHTX(); +var provider = collection.BuildServiceProvider(); + +var trackerFactory = provider.GetRequiredService(); + +// Creat 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/README.md b/Examples/README.md index 3d175704..c6ad441c 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -4,4 +4,10 @@ A minimal API showing how to integrate HTX.Net in a web API project ### HTX.Examples.Console -A simple console client demonstrating basic usage \ No newline at end of file +A simple console client demonstrating basic usage + +### HTX.Examples.OrderBook +Example of using the client side order book implementation + +### HTX.Examples.Tracker +Example of using the trade tracker \ No newline at end of file diff --git a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs index 07327b44..dd486d75 100644 --- a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs +++ b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApi.cs @@ -65,12 +65,9 @@ internal HTXRestClientSpotApi(ILogger logger, HttpClient? httpClient, HTXRestOpt protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(); - /// public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) - { - return $"{baseAsset.ToLowerInvariant()}{quoteAsset.ToLowerInvariant()}"; - } + => HTXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) diff --git a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiShared.cs b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiShared.cs index 875f62fb..c39d198e 100644 --- a/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiShared.cs +++ b/HTX.Net/Clients/SpotApi/HTXRestClientSpotApiShared.cs @@ -148,7 +148,10 @@ async Task>> IRecentTradeRestClient.G 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()); + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.SelectMany(x => x.Details.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp) + { + Side = x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell + })).ToArray()); } #endregion diff --git a/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs index cebfa270..28388d4c 100644 --- a/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs +++ b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApi.cs @@ -57,7 +57,8 @@ internal HTXSocketClientSpotApi(ILogger logger, HTXSocketOptions options) public IHTXSocketClientSpotApiShared SharedClient => this; /// - public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) => $"{baseAsset.ToLowerInvariant()}{quoteAsset.ToLowerInvariant()}"; + public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + => HTXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); /// public override string? GetListenerIdentifier(IMessageAccessor message) diff --git a/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApiShared.cs b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApiShared.cs index 6c94c62e..eba79962 100644 --- a/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApiShared.cs +++ b/HTX.Net/Clients/SpotApi/HTXSocketClientSpotApiShared.cs @@ -53,7 +53,10 @@ async Task> ITradeSocketClient.SubscribeToTra 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); + var result = await SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, update.Data.Details.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp) + { + Side = x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell + }))), ct).ConfigureAwait(false); return new ExchangeResult(Exchange, result); } diff --git a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs index 66e84c43..4e9a6c90 100644 --- a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs +++ b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApi.cs @@ -52,11 +52,10 @@ 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, TradingMode tradingMode, DateTime? deliverTime = null) - { - return $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}" + (!deliverTime.HasValue ? string.Empty: ("-" + deliverTime.Value.ToString("yyMMdd"))); - } + => HTXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) diff --git a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiShared.cs b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiShared.cs index cc65e685..c06c49f3 100644 --- a/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiShared.cs +++ b/HTX.Net/Clients/UsdtFutures/HTXRestClientUsdtFuturesApiShared.cs @@ -73,7 +73,7 @@ async Task> IFuturesTickerRestClient.GetF 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)) + return resultTicker.Result.AsExchangeResult(Exchange, request.Symbol.TradingMode, new SharedFuturesTicker(symbol, resultTicker.Result.Data.ClosePrice, resultTicker.Result.Data.HighPrice, resultTicker.Result.Data.LowPrice, 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, @@ -103,7 +103,7 @@ async Task>> IFuturesTickerRe 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)) + return new SharedFuturesTicker(x.ContractCode!, x.ClosePrice, x.HighPrice, x.LowPrice, x.Volume ?? 0, x.OpenPrice == null ? null : Math.Round((x.ClosePrice ?? 0) / x.OpenPrice.Value * 100 - 100, 2)) { FundingRate = funding?.FundingRate, NextFundingTime = funding?.FundingTime @@ -977,7 +977,10 @@ async Task>> IRecentTradeRestClient.G 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()); + return result.AsExchangeResult>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp) + { + Side = x.Direction == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell + }).ToArray()); } #endregion diff --git a/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs b/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs index 25b9957d..abfb06ee 100644 --- a/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs +++ b/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApi.cs @@ -50,10 +50,7 @@ internal HTXSocketClientUsdtFuturesApi(ILogger logger, HTXSocketOptions options) /// 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"))); - } - + => HTXExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime); /// 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 index f2426864..5283db47 100644 --- a/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApiShared.cs +++ b/HTX.Net/Clients/UsdtFutures/HTXSocketClientUsdtFuturesApiShared.cs @@ -38,7 +38,10 @@ async Task> ITradeSocketClient.SubscribeToTra 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); + var result = await SubscribeToTradeUpdatesAsync(symbol, update => handler(update.AsExchangeEvent(Exchange, update.Data.Trades.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp) + { + Side = x.Side == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell + }))), ct).ConfigureAwait(false); return new ExchangeResult(Exchange, result); } diff --git a/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs b/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs index 3b4b1737..2f19121e 100644 --- a/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs +++ b/HTX.Net/ExtensionMethods/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Clients; +using HTX.Net; using HTX.Net.Clients; using HTX.Net.Interfaces; using HTX.Net.Interfaces.Clients; @@ -58,6 +59,7 @@ public static IServiceCollection AddHTX( services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.RegisterSharedRestInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); services.RegisterSharedSocketInterfaces(x => x.GetRequiredService().SpotApi.SharedClient); diff --git a/HTX.Net/HTX.Net.csproj b/HTX.Net/HTX.Net.csproj index 96d0e136..f9179c36 100644 --- a/HTX.Net/HTX.Net.csproj +++ b/HTX.Net/HTX.Net.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;netstandard2.1 enable @@ -48,10 +48,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + 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 178631f2..c55433dd 100644 --- a/HTX.Net/HTX.Net.xml +++ b/HTX.Net/HTX.Net.xml @@ -3418,6 +3418,16 @@ Urls to the API documentation + + + Format a base and quote asset to an HTX recognized symbol + + Base asset + Quote asset + Trading mode + Delivery time for delivery futures + + Rate limiter configuration for the HTX API @@ -3433,6 +3443,26 @@ Event for when a rate limit is triggered + + + + + + ctor + + + + + ctor + + Service provider for resolving logging and clients + + + + + + + Client for accessing the HTX API. @@ -6560,6 +6590,14 @@ Usdt futures order book factory methods + + + Create a SymbolOrderBook for the symbol + + The symbol + Book options + + Create a SymbolOrderBook for the Spot API @@ -6576,6 +6614,30 @@ Order book options + + + Tracker factory + + + + + Create a new kline tracker + + The symbol + Kline interval + The max amount of klines to retain + The max period the data should be retained + + + + + Create a new trade tracker for a symbol + + The symbol + The max amount of klines to retain + The max period the data should be retained + + Api addresses usable for the HTX clients @@ -17421,6 +17483,9 @@ Service provider for resolving logging and clients + + + diff --git a/HTX.Net/HTXExchange.cs b/HTX.Net/HTXExchange.cs index 66eb3b5d..ab2f782e 100644 --- a/HTX.Net/HTXExchange.cs +++ b/HTX.Net/HTXExchange.cs @@ -1,6 +1,8 @@ using CryptoExchange.Net.RateLimiting; +using CryptoExchange.Net.RateLimiting.Filters; using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Interfaces; +using CryptoExchange.Net.SharedApis; namespace HTX.Net { @@ -26,6 +28,22 @@ public static class HTXExchange "https://www.htx.com/en-us/opend/newApiPages/" }; + /// + /// Format a base and quote asset to an HTX recognized symbol + /// + /// Base asset + /// Quote asset + /// Trading mode + /// Delivery time for delivery futures + /// + public static string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null) + { + if (tradingMode == TradingMode.Spot) + return $"{baseAsset.ToLowerInvariant()}{quoteAsset.ToLowerInvariant()}"; + + return $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}" + (!deliverTime.HasValue ? string.Empty : ("-" + deliverTime.Value.ToString("yyMMdd"))); + } + /// /// Rate limiter configuration for the HTX API /// @@ -55,8 +73,8 @@ private void Initialize() SpotMarketLimit = new RateLimitGate("Spot Market Limit") .AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new IGuardFilter[] { }, 10, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding)); SpotConnection = new RateLimitGate("Spot Connection Messages") - .AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new IGuardFilter[] { }, 50, TimeSpan.FromSeconds(1), RateLimitWindowType.Fixed)) // 50 requests per second per connection - .AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new IGuardFilter[] { }, 100, TimeSpan.FromSeconds(1), RateLimitWindowType.Fixed)); // 100 requests per second per IP + .AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new IGuardFilter[] { new ExactPathFilter("/ws/v2"), new LimitItemTypeFilter(RateLimitItemType.Request) }, 50, TimeSpan.FromSeconds(1), RateLimitWindowType.Fixed)) // 50 requests per second per connection + .AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new IGuardFilter[] { new ExactPathFilter("/ws/v2"), new LimitItemTypeFilter(RateLimitItemType.Request) }, 100, TimeSpan.FromSeconds(1), RateLimitWindowType.Fixed)); // 100 requests per second per IP UsdtTrade = new RateLimitGate("USDT-M Trade") .AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKeyPerEndpoint, new IGuardFilter[] { }, 72, TimeSpan.FromSeconds(3), RateLimitWindowType.Fixed)); @@ -67,7 +85,7 @@ private void Initialize() UsdtPublicReference = new RateLimitGate("USDT-M Public Reference") .AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new IGuardFilter[] { }, 240, TimeSpan.FromSeconds(1), RateLimitWindowType.Fixed)); UsdtConnection = new RateLimitGate("USDT-M Connection Messages") - .AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new IGuardFilter[] { }, 40, TimeSpan.FromSeconds(1), RateLimitWindowType.Fixed)); // 40 requests per second per connection + .AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new IGuardFilter[] { new LimitItemTypeFilter(RateLimitItemType.Request) }, 40, TimeSpan.FromSeconds(1), RateLimitWindowType.Fixed)); // 40 requests per second per connection EndpointLimit.RateLimitTriggered += RateLimitTriggered; SpotMarketLimit.RateLimitTriggered += RateLimitTriggered; diff --git a/HTX.Net/HTXTrackerFactory.cs b/HTX.Net/HTXTrackerFactory.cs new file mode 100644 index 00000000..a384ae88 --- /dev/null +++ b/HTX.Net/HTXTrackerFactory.cs @@ -0,0 +1,91 @@ +using CryptoExchange.Net.SharedApis; +using CryptoExchange.Net.Trackers.Klines; +using CryptoExchange.Net.Trackers.Trades; +using HTX.Net.Clients; +using HTX.Net.Interfaces; +using HTX.Net.Interfaces.Clients; +using Microsoft.Extensions.DependencyInjection; + +namespace HTX.Net +{ + /// + public class HTXTrackerFactory : IHTXTrackerFactory + { + private readonly IServiceProvider? _serviceProvider; + + /// + /// ctor + /// + public HTXTrackerFactory() + { + } + + /// + /// ctor + /// + /// Service provider for resolving logging and clients + public HTXTrackerFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + public IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null) + { + var restClient = _serviceProvider?.GetRequiredService() ?? new HTXRestClient(); + var socketClient = _serviceProvider?.GetRequiredService() ?? new HTXSocketClient(); + + IKlineRestClient sharedRestClient; + IKlineSocketClient sharedSocketClient; + if (symbol.TradingMode == TradingMode.Spot) + { + sharedRestClient = restClient.SpotApi.SharedClient; + sharedSocketClient = socketClient.SpotApi.SharedClient; + } + else + { + sharedRestClient = restClient.UsdtFuturesApi.SharedClient; + sharedSocketClient = socketClient.UsdtFuturesApi.SharedClient; + } + + return new KlineTracker( + _serviceProvider?.GetRequiredService().CreateLogger(restClient.Exchange), + sharedRestClient, + sharedSocketClient, + symbol, + interval, + limit, + period + ); + } + /// + public ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null) + { + var restClient = _serviceProvider?.GetRequiredService() ?? new HTXRestClient(); + var socketClient = _serviceProvider?.GetRequiredService() ?? new HTXSocketClient(); + + IRecentTradeRestClient? sharedRestClient; + ITradeSocketClient sharedSocketClient; + if (symbol.TradingMode == TradingMode.Spot) + { + sharedRestClient = restClient.SpotApi.SharedClient; + sharedSocketClient = socketClient.SpotApi.SharedClient; + } + else + { + sharedRestClient = restClient.UsdtFuturesApi.SharedClient; + sharedSocketClient = socketClient.UsdtFuturesApi.SharedClient; + } + + return new TradeTracker( + _serviceProvider?.GetRequiredService().CreateLogger(restClient.Exchange), + sharedRestClient, + null, + sharedSocketClient, + symbol, + limit, + period + ); + } + } +} diff --git a/HTX.Net/Interfaces/IHTXOrderBookFactory.cs b/HTX.Net/Interfaces/IHTXOrderBookFactory.cs index 10ca9870..528a0d8b 100644 --- a/HTX.Net/Interfaces/IHTXOrderBookFactory.cs +++ b/HTX.Net/Interfaces/IHTXOrderBookFactory.cs @@ -1,4 +1,5 @@ -using HTX.Net.Objects.Options; +using CryptoExchange.Net.SharedApis; +using HTX.Net.Objects.Options; namespace HTX.Net.Interfaces { @@ -16,6 +17,14 @@ public interface IHTXOrderBookFactory /// public IOrderBookFactory UsdtFutures { get; } + /// + /// Create a SymbolOrderBook for the symbol + /// + /// The symbol + /// Book options + /// + ISymbolOrderBook Create(SharedSymbol symbol, Action? options = null); + /// /// Create a SymbolOrderBook for the Spot API /// diff --git a/HTX.Net/Interfaces/IHTXTrackerFactory.cs b/HTX.Net/Interfaces/IHTXTrackerFactory.cs new file mode 100644 index 00000000..b3554c0e --- /dev/null +++ b/HTX.Net/Interfaces/IHTXTrackerFactory.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 HTX.Net.Interfaces +{ + /// + /// Tracker factory + /// + public interface IHTXTrackerFactory + { + /// + /// Create a new kline tracker + /// + /// The symbol + /// Kline interval + /// The max amount of klines to retain + /// The max period the data should be retained + /// + IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null); + + /// + /// Create a new trade tracker for a symbol + /// + /// The symbol + /// The max amount of klines to retain + /// The max period the data should be retained + /// + ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null); + } +} diff --git a/HTX.Net/SymbolOrderBooks/HTXOrderBookFactory.cs b/HTX.Net/SymbolOrderBooks/HTXOrderBookFactory.cs index 7f1e15ff..88528de1 100644 --- a/HTX.Net/SymbolOrderBooks/HTXOrderBookFactory.cs +++ b/HTX.Net/SymbolOrderBooks/HTXOrderBookFactory.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.OrderBook; +using CryptoExchange.Net.SharedApis; using HTX.Net.Interfaces; using HTX.Net.Interfaces.Clients; using HTX.Net.Objects.Options; @@ -24,8 +25,22 @@ public HTXOrderBookFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - Spot = new OrderBookFactory((symbol, options) => CreateSpot(symbol, options), (baseAsset, quoteAsset, options) => CreateSpot(baseAsset.ToLowerInvariant() + quoteAsset.ToLowerInvariant(), options)); - UsdtFutures = new OrderBookFactory((symbol, options) => CreateUsdtFutures(symbol, options), (baseAsset, quoteAsset, options) => CreateUsdtFutures(baseAsset.ToLowerInvariant() + "-" + quoteAsset.ToLowerInvariant(), options)); + Spot = new OrderBookFactory( + CreateSpot, + (sharedSymbol, options) => CreateSpot(HTXExchange.FormatSymbol(sharedSymbol.BaseAsset, sharedSymbol.QuoteAsset, sharedSymbol.TradingMode, sharedSymbol.DeliverTime), options)); + UsdtFutures = new OrderBookFactory( + CreateUsdtFutures, + (sharedSymbol, options) => CreateUsdtFutures(HTXExchange.FormatSymbol(sharedSymbol.BaseAsset, sharedSymbol.QuoteAsset, sharedSymbol.TradingMode, sharedSymbol.DeliverTime), options)); + } + + /// + public ISymbolOrderBook Create(SharedSymbol symbol, Action? options = null) + { + var symbolName = HTXExchange.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime); + if (symbol.TradingMode == TradingMode.Spot) + return CreateSpot(symbolName, options); + + return CreateUsdtFutures(symbolName, options); } ///