From b29cdc41f377218f616c6d8df77c6597efcb49a9 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Mon, 23 Feb 2026 14:53:38 +0100 Subject: [PATCH] Shared interfaces pagination update (#274) Updated INextPageToken parameter on Shared interfaces to PageRequest type, functionality unchanged Added SupportsAscending and SupportsDescending properties to PaginatedEndpointOptions to expose supported data directions Added MaxAge property to PaginatedEndpointOptions to expose the max age of data that can be requested Added Direction property to Shared interfaces paginated requests to configure pagination data direction Removed PaginationSupport property from PaginatedEndpointOptions, replaced by above new properties Updated Shared GetTradeHistoryRequest EndTime property to be optional Updated I(Futures/Spot)OrderRestClient.GetClosed(Futures/Spot)OrdersOptions from PaginatedEndpointOptions to GetClosedOrdersOptions Updated I(Futures/Spot)OrderRestClient.Get(Futures/Spot)UserTradesOptions from PaginatedEndpointOptions to GetUserTradesOptions Updated rate limiting PathStartFilter to ignore added or missing slash before the path Fixed KlineTracker throwing exception if there is no data in the initial snapshot --- .../RestClientTests.cs | 2 +- CryptoExchange.Net/ExchangeHelpers.cs | 37 +- CryptoExchange.Net/Objects/CallResult.cs | 12 +- .../RateLimiting/Filters/PathStartFilter.cs | 4 +- .../SharedApis/Interfaces/INextPageToken.cs | 111 ----- .../Rest/Futures/IFundingRateRestClient.cs | 4 +- .../Rest/Futures/IFuturesOrderRestClient.cs | 12 +- .../Futures/IIndexPriceKlineRestClient.cs | 4 +- .../Rest/Futures/IMarkPriceKlineRestClient.cs | 4 +- .../Futures/IPositionHistoryRestClient.cs | 4 +- .../Interfaces/Rest/IDepositRestClient.cs | 4 +- .../Interfaces/Rest/IKlineRestClient.cs | 4 +- .../Rest/ITradeHistoryRestClient.cs | 4 +- .../Interfaces/Rest/IWithdrawalRestClient .cs | 4 +- .../Rest/Spot/ISpotOrderRestClient.cs | 12 +- .../SharedApis/Models/ExchangeWebResult.cs | 18 +- .../Endpoints/GetClosedOrdersOptions.cs | 33 +- .../Options/Endpoints/GetDepositsOptions.cs | 33 +- .../Endpoints/GetFundingRateHistoryOptions.cs | 43 +- .../Options/Endpoints/GetKlinesOptions.cs | 28 +- .../Endpoints/GetPositionHistoryOptions.cs | 43 +- .../Endpoints/GetTradeHistoryOptions.cs | 25 +- .../Options/Endpoints/GetUserTradesOptions.cs | 54 +++ .../Endpoints/GetWithdrawalsOptions.cs | 33 +- .../Endpoints/PaginatedEndpointOptions.cs | 30 +- .../Models/Pagination/DataDirection.cs | 17 + .../Models/Pagination/PageRequest.cs | 37 ++ .../Models/Pagination/Pagination.cs | 405 ++++++++++++++++++ .../Models/Pagination/PaginationParameters.cs | 43 ++ .../Models/Rest/GetClosedOrdersRequest.cs | 8 +- .../Models/Rest/GetDepositsRequest.cs | 8 +- .../Rest/GetFundingRateHistoryRequest.cs | 8 +- .../Models/Rest/GetKlinesRequest.cs | 8 +- .../Models/Rest/GetPositionHistoryRequest.cs | 12 +- .../Models/Rest/GetTradeHistoryRequest.cs | 14 +- .../Models/Rest/GetUserTradesRequest.cs | 8 +- .../Models/Rest/GetWithdrawalsRequest.cs | 8 +- .../Trackers/Klines/KlineTracker.cs | 5 +- .../Trackers/Trades/TradeTracker.cs | 4 +- 39 files changed, 909 insertions(+), 238 deletions(-) delete mode 100644 CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Pagination/DataDirection.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Pagination/PageRequest.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Pagination/Pagination.cs create mode 100644 CryptoExchange.Net/SharedApis/Models/Pagination/PaginationParameters.cs diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 555cf70d..d17a4560 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests // assert ClassicAssert.IsFalse(result.Success); Assert.That(result.Error != null); - Assert.That(result.Error is ServerError); + Assert.That(result.Error is DeserializeError); Assert.That(result.Error.Message.Contains(response)); } diff --git a/CryptoExchange.Net/ExchangeHelpers.cs b/CryptoExchange.Net/ExchangeHelpers.cs index 9cd859b4..1e200a49 100644 --- a/CryptoExchange.Net/ExchangeHelpers.cs +++ b/CryptoExchange.Net/ExchangeHelpers.cs @@ -4,6 +4,7 @@ using CryptoExchange.Net.SharedApis; using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Threading; @@ -310,11 +311,11 @@ namespace CryptoExchange.Net /// The request parameters /// Cancellation token /// - public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) + public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) { var result = new List(); ExchangeWebResult batch; - INextPageToken? nextPageToken = null; + PageRequest? nextPageToken = null; while (true) { batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false); @@ -323,12 +324,42 @@ namespace CryptoExchange.Net break; result.AddRange(batch.Data); - nextPageToken = batch.NextPageToken; + nextPageToken = batch.NextPageRequest; if (nextPageToken == null) break; } } + /// + /// Apply filters to the data set + /// + /// Type + /// Data set + /// Time selector for the data + /// Start time filter + /// End time filter + /// Data direction + public static IEnumerable ApplyFilter( + IEnumerable data, + Func timeSelector, + DateTime? startTime, + DateTime? endTime, + DataDirection direction) + { + if (direction == DataDirection.Ascending) + data = data.OrderBy(timeSelector); + else + data = data.OrderByDescending(timeSelector); + + if (startTime != null) + data = data.Where(x => timeSelector(x) >= startTime.Value); + + if (endTime != null) + data = data.Where(x => timeSelector(x) < endTime.Value); + + return data; + } + /// /// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price /// diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index 74f79ac6..a4a02e13 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -531,11 +531,11 @@ namespace CryptoExchange.Net.Objects /// The exchange /// Trade mode the result applies to /// Data - /// Next page token + /// Next page request /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null) + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null) { - return new ExchangeWebResult(exchange, tradeMode, As(data), nextPageToken); + return new ExchangeWebResult(exchange, tradeMode, As(data), nextPageRequest); } /// @@ -545,11 +545,11 @@ namespace CryptoExchange.Net.Objects /// The exchange /// Trade modes the result applies to /// Data - /// Next page token + /// Next page token /// - public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null) + public ExchangeWebResult AsExchangeResult(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null) { - return new ExchangeWebResult(exchange, tradeModes, As(data), nextPageToken); + return new ExchangeWebResult(exchange, tradeModes, As(data), nextPageRequest); } /// diff --git a/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs b/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs index ba68b8b6..1001b663 100644 --- a/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs +++ b/CryptoExchange.Net/RateLimiting/Filters/PathStartFilter.cs @@ -17,11 +17,11 @@ namespace CryptoExchange.Net.RateLimiting.Filters /// public PathStartFilter(string path) { - _path = path; + _path = path.TrimStart('/'); } /// public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) - => definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase); + => definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs b/CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs deleted file mode 100644 index ceb1e73d..00000000 --- a/CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; - -namespace CryptoExchange.Net.SharedApis -{ - /// - /// A token which a request can use to retrieve the next page if there are more pages in the result set - /// - public interface INextPageToken - { - } - - /// - /// A datetime offset token - /// - public record DateTimeToken: INextPageToken - { - /// - /// Last result time - /// - public DateTime LastTime { get; set; } - - /// - /// ctor - /// - public DateTimeToken(DateTime timestamp) - { - LastTime = timestamp; - } - } - - /// - /// A current page index token - /// - public record PageToken: INextPageToken - { - /// - /// The next page index - /// - public int Page { get; set; } - /// - /// Page size - /// - public int PageSize { get; set; } - - /// - /// ctor - /// - public PageToken(int page, int pageSize) - { - Page = page; - PageSize = pageSize; - } - } - - /// - /// A id offset token - /// - public record FromIdToken : INextPageToken - { - /// - /// The last id from previous result - /// - public string FromToken { get; set; } - - /// - /// ctor - /// - public FromIdToken(string fromToken) - { - FromToken = fromToken; - } - } - - /// - /// A cursor token - /// - public record CursorToken : INextPageToken - { - /// - /// The next page cursor - /// - public string Cursor { get; set; } - - /// - /// ctor - /// - public CursorToken(string cursor) - { - Cursor = cursor; - } - } - - /// - /// A result offset token - /// - public record OffsetToken : INextPageToken - { - /// - /// Offset in the result set - /// - public int Offset { get; set; } - - /// - /// ctor - /// - public OffsetToken(int offset) - { - Offset = offset; - } - } -} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs index 057cb367..0798554b 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs @@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis /// Get funding rate records /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs index 5c3af62b..39632ddd 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs @@ -73,14 +73,14 @@ namespace CryptoExchange.Net.SharedApis /// /// Spot get closed orders request options /// - PaginatedEndpointOptions GetClosedFuturesOrdersOptions { get; } + GetClosedOrdersOptions GetClosedFuturesOrdersOptions { get; } /// /// Get info on closed futures orders /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// /// Futures get order trades request options @@ -96,14 +96,14 @@ namespace CryptoExchange.Net.SharedApis /// /// Futures user trades request options /// - PaginatedEndpointOptions GetFuturesUserTradesOptions { get; } + GetUserTradesOptions GetFuturesUserTradesOptions { get; } /// /// Get futures user trade records /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// /// Futures cancel order request options diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs index 4c2299f2..85b78a27 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs @@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis /// Get index price kline/candlestick data /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs index 0b025280..a749c166 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs @@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis /// Get mark price kline/candlestick data /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs index 59059275..d51b8c2a 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs @@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis /// Get position history /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs index c50758e1..9e1af83f 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs @@ -30,9 +30,9 @@ namespace CryptoExchange.Net.SharedApis /// Get deposit records /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token /// - Task> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs index a71ac1ea..81e1c515 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs @@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis /// Get kline/candlestick data /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token /// - Task> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs index c20d9ce4..327db6e3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs @@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis /// Get public trade history /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token /// - Task> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetTradeHistoryAsync(GetTradeHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs index c2f36000..fd21b0b2 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs @@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis /// Get withdrawal records /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token /// - Task> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetWithdrawalsAsync(GetWithdrawalsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs index c4d1a321..6a1bd1ba 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs @@ -72,14 +72,14 @@ namespace CryptoExchange.Net.SharedApis /// /// Spot get closed orders request options /// - PaginatedEndpointOptions GetClosedSpotOrdersOptions { get; } + GetClosedOrdersOptions GetClosedSpotOrdersOptions { get; } /// /// Get info on closed spot orders /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// /// Spot get order trades request options @@ -95,14 +95,14 @@ namespace CryptoExchange.Net.SharedApis /// /// Spot user trades request options /// - PaginatedEndpointOptions GetSpotUserTradesOptions { get; } + GetUserTradesOptions GetSpotUserTradesOptions { get; } /// /// Get spot user trade records /// /// Request info - /// The pagination token from the previous request to continue pagination + /// The pagination request from the previous request result `NextPageRequest` property to continue pagination /// Cancellation token - Task> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetSpotUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default); /// /// Spot cancel order request options diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs index 7861d212..6819f79a 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs @@ -24,9 +24,9 @@ namespace CryptoExchange.Net.SharedApis public TradingMode[]? DataTradeMode { get; } /// - /// Token to retrieve the next page with + /// Next page request, can be passed to the next request on the same endpoint to get the next page /// - public INextPageToken? NextPageToken { get; } + public PageRequest? NextPageRequest { get; } /// /// ctor @@ -46,7 +46,7 @@ namespace CryptoExchange.Net.SharedApis string exchange, TradingMode dataTradeMode, WebCallResult result, - INextPageToken? nextPageToken = null) : + PageRequest? nextPageToken = null) : base(result.ResponseStatusCode, result.HttpVersion, result.ResponseHeaders, @@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis { DataTradeMode = new[] { dataTradeMode }; Exchange = exchange; - NextPageToken = nextPageToken; + NextPageRequest = nextPageToken; } /// @@ -74,7 +74,7 @@ namespace CryptoExchange.Net.SharedApis string exchange, TradingMode[]? dataTradeModes, WebCallResult result, - INextPageToken? nextPageToken = null) : + PageRequest? nextPageRequest = null) : base(result.ResponseStatusCode, result.HttpVersion, result.ResponseHeaders, @@ -92,7 +92,7 @@ namespace CryptoExchange.Net.SharedApis { DataTradeMode = dataTradeModes; Exchange = exchange; - NextPageToken = nextPageToken; + NextPageRequest = nextPageRequest; } /// @@ -115,7 +115,7 @@ namespace CryptoExchange.Net.SharedApis ResultDataSource dataSource, [AllowNull] T data, Error? error, - INextPageToken? nextPageToken = null) : base( + PageRequest? nextPageToken = null) : base( code, httpVersion, responseHeaders, @@ -133,7 +133,7 @@ namespace CryptoExchange.Net.SharedApis { DataTradeMode = dataTradeModes; Exchange = exchange; - NextPageToken = nextPageToken; + NextPageRequest = nextPageToken; } /// @@ -144,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis /// public new ExchangeWebResult As([AllowNull] K data) { - return new ExchangeWebResult(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken); + return new ExchangeWebResult(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageRequest); } /// diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs index 1727806e..12f2f942 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Objects; +using System; using System.Text; namespace CryptoExchange.Net.SharedApis @@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis /// public class GetClosedOrdersOptions : PaginatedEndpointOptions { - /// - /// Whether the start/end time filter is supported - /// - public bool TimeFilterSupported { get; set; } - /// /// ctor /// - public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) + public GetClosedOrdersOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) { - TimeFilterSupported = timeFilterSupported; } /// public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { - if (!TimeFilterSupported && request.StartTime != null) - return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported"); + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } @@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis public override string ToString(string exchange) { var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); + sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs index fee678b2..88e39052 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Objects; +using System; using System.Text; namespace CryptoExchange.Net.SharedApis @@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis /// public class GetDepositsOptions : PaginatedEndpointOptions { - /// - /// Whether the start/end time filter is supported - /// - public bool TimeFilterSupported { get; set; } - /// /// ctor /// - public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) + public GetDepositsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) { - TimeFilterSupported = timeFilterSupported; } /// public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { - if (!TimeFilterSupported && request.StartTime != null) - return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } @@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis public override string ToString(string exchange) { var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); + sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs index be857c25..114b6456 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetFundingRateHistoryOptions.cs @@ -1,4 +1,8 @@ -namespace CryptoExchange.Net.SharedApis +using CryptoExchange.Net.Objects; +using System; +using System.Text; + +namespace CryptoExchange.Net.SharedApis { /// /// Options for requesting funding rate history @@ -8,8 +12,43 @@ /// /// ctor /// - public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) + public GetFundingRateHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) { } + + /// + public override Error? ValidateRequest(string exchange, GetFundingRateHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + } + + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); + return sb.ToString(); + } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs index c1712591..3cf3e5d9 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs @@ -18,15 +18,12 @@ namespace CryptoExchange.Net.SharedApis /// Max number of data points which can be requested /// public int? MaxTotalDataPoints { get; set; } - /// - /// The max age of the data that can be requested - /// - public TimeSpan? MaxAge { get; set; } /// /// ctor /// - public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) + public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) { SupportIntervals = new[] { @@ -50,7 +47,8 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) + public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) { SupportIntervals = intervals; } @@ -68,12 +66,29 @@ namespace CryptoExchange.Net.SharedApis if (!IsSupported(request.Interval)) return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported"); + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available"); if (request.Limit > MaxLimit) return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request"); + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction == DataDirection.Ascending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + if (MaxTotalDataPoints.HasValue) { if (request.Limit > MaxTotalDataPoints.Value) @@ -93,6 +108,7 @@ namespace CryptoExchange.Net.SharedApis public override string ToString(string exchange) { var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}"); if (MaxAge != null) sb.AppendLine($"Max age of data: {MaxAge}"); diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs index ffb01585..59f82f00 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetPositionHistoryOptions.cs @@ -1,4 +1,8 @@ -namespace CryptoExchange.Net.SharedApis +using CryptoExchange.Net.Objects; +using System; +using System.Text; + +namespace CryptoExchange.Net.SharedApis { /// /// Options for requesting position history @@ -8,8 +12,43 @@ /// /// ctor /// - public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) + public GetPositionHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) { } + + /// + public override Error? ValidateRequest(string exchange, GetPositionHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + } + + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); + return sb.ToString(); + } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs index d07972fd..6c174dc7 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs @@ -9,34 +9,27 @@ namespace CryptoExchange.Net.SharedApis /// public class GetTradeHistoryOptions : PaginatedEndpointOptions { - /// - /// The max age of data that can be requested - /// - public TimeSpan? MaxAge { get; set; } - /// /// ctor /// - public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication) + public GetTradeHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication) { } /// public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available"); + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } - - /// - public override string ToString(string exchange) - { - var sb = new StringBuilder(base.ToString(exchange)); - if (MaxAge != null) - sb.AppendLine($"Max age of data: {MaxAge}"); - return sb.ToString(); - } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs new file mode 100644 index 00000000..f59d4480 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetUserTradesOptions.cs @@ -0,0 +1,54 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for requesting user trades + /// + public class GetUserTradesOptions : PaginatedEndpointOptions + { + /// + /// ctor + /// + public GetUserTradesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) + { + } + + /// + public override Error? ValidateRequest(string exchange, GetUserTradesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) + { + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } + + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + } + + /// + public override string ToString(string exchange) + { + var sb = new StringBuilder(base.ToString(exchange)); + sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); + return sb.ToString(); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs index 01572378..fa8a2bcd 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Objects; +using System; using System.Text; namespace CryptoExchange.Net.SharedApis @@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis /// public class GetWithdrawalsOptions : PaginatedEndpointOptions { - /// - /// Whether the start/end time filter is supported - /// - public bool TimeFilterSupported { get; set; } - /// /// ctor /// - public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true) + public GetWithdrawalsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit) + : base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true) { - TimeFilterSupported = timeFilterSupported; } /// public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { - if (!TimeFilterSupported && request.StartTime != null) - return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported"); + if (!SupportsAscending && request.Direction == DataDirection.Ascending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported"); + + if (!SupportsDescending && request.Direction == DataDirection.Descending) + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported"); + + if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available"); + + if (!TimePeriodFilterSupport) + { + // When going descending we can still allow startTime filter to limit the results + var now = DateTime.UtcNow; + if ((request.Direction != DataDirection.Descending && request.StartTime != null) + || (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5))) + { + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); + } + } return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } @@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis public override string ToString(string exchange) { var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Time filter supported: {TimeFilterSupported}"); + sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs index ed0e4960..e61cf11a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace CryptoExchange.Net.SharedApis @@ -14,9 +15,13 @@ namespace CryptoExchange.Net.SharedApis #endif { /// - /// Type of pagination supported + /// Whether ascending data retrieval and pagination is available /// - public SharedPaginationSupport PaginationSupport { get; } + public bool SupportsAscending { get; set; } + /// + /// Whether ascending data retrieval and pagination is available + /// + public bool SupportsDescending { get; set; } /// /// Whether filtering based on start/end time is supported @@ -28,12 +33,23 @@ namespace CryptoExchange.Net.SharedApis /// public int MaxLimit { get; set; } + /// + /// Max age of data that can be requested + /// + public TimeSpan? MaxAge { get; set; } + /// /// ctor /// - public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication) + public PaginatedEndpointOptions( + bool supportsAscending, + bool supportsDescending, + bool timePeriodSupport, + int maxLimit, + bool needsAuthentication) : base(needsAuthentication) { - PaginationSupport = paginationType; + SupportsAscending = supportsAscending; + SupportsDescending = supportsDescending; TimePeriodFilterSupport = timePeriodSupport; MaxLimit = maxLimit; } @@ -42,9 +58,11 @@ namespace CryptoExchange.Net.SharedApis public override string ToString(string exchange) { var sb = new StringBuilder(base.ToString(exchange)); - sb.AppendLine($"Pagination type: {PaginationSupport}"); + sb.AppendLine($"Ascending retrieval supported: {SupportsAscending}"); + sb.AppendLine($"Descending retrieval supported: {SupportsDescending}"); sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}"); sb.AppendLine($"Max limit: {MaxLimit}"); + sb.AppendLine($"Max age: {MaxAge}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Pagination/DataDirection.cs b/CryptoExchange.Net/SharedApis/Models/Pagination/DataDirection.cs new file mode 100644 index 00000000..30f541b7 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Pagination/DataDirection.cs @@ -0,0 +1,17 @@ +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Data direction + /// + public enum DataDirection + { + /// + /// Old to new order + /// + Ascending, + /// + /// New to old order + /// + Descending + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Pagination/PageRequest.cs b/CryptoExchange.Net/SharedApis/Models/Pagination/PageRequest.cs new file mode 100644 index 00000000..f77acea2 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Pagination/PageRequest.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Next page request info + /// + public class PageRequest + { + /// + /// Pagination cursor + /// + public string? Cursor { get; set; } + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Result offset + /// + public int? Offset { get; set; } + /// + /// From id filter + /// + public string? FromId { get; set; } + /// + /// Start time filter + /// + public DateTime? StartTime { get; set; } + /// + /// End time filter + /// + public DateTime? EndTime { get; set; } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Pagination/Pagination.cs b/CryptoExchange.Net/SharedApis/Models/Pagination/Pagination.cs new file mode 100644 index 00000000..81042ef9 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Pagination/Pagination.cs @@ -0,0 +1,405 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Pagination methods + /// + public static class Pagination + { + /// + /// Get pagination parameters + /// + /// The data direction + /// Result limit + /// User request start time + /// User request end time + /// Provided page request + /// Whether to set start time if direction is descending, or end time if direction is ascending + /// Max period the time filters can span + /// + public static PaginationParameters GetPaginationParameters( + DataDirection direction, + int limit, + DateTime? requestStartTime, + DateTime requestEndTime, + PageRequest? paginationRequest, + bool setOtherTimeLimiter = true, + TimeSpan? maxPeriod = null + ) + { + var startTime = paginationRequest?.StartTime ?? requestStartTime; + var endTime = paginationRequest?.EndTime ?? requestEndTime; + if (maxPeriod != null) + { + if (direction == DataDirection.Ascending) + { + if (startTime == null) + { + startTime = endTime.Add(-maxPeriod.Value); + } + else + { + endTime = startTime.Value.Add(maxPeriod.Value); + if (endTime > DateTime.UtcNow) + endTime = DateTime.UtcNow; + } + } + else + { + startTime = endTime.Add(-maxPeriod.Value); + } + } + + return new PaginationParameters + { + Limit = limit, + StartTime = direction == DataDirection.Ascending || setOtherTimeLimiter ? startTime : null, + EndTime = direction == DataDirection.Descending || setOtherTimeLimiter ? endTime : null, + Direction = direction, + FromId = paginationRequest?.FromId, + Offset = paginationRequest?.Offset, + Page = paginationRequest?.Page, + Cursor = paginationRequest?.Cursor + }; + } + + /// + /// Get the next page request parameters from result kline data + /// + /// Callback for returning the next page request + /// Number of results in data + /// Timestamps of the result data + /// User request start time + /// User request end time + /// The last used pagination data + /// Kline interval + /// + public static PageRequest? GetNextPageRequestKlines( + Func nextPageRequest, + int resultCount, + IEnumerable timestamps, + DateTime? requestStartTime, + DateTime requestEndTime, + PaginationParameters lastPaginationData, + SharedKlineInterval interval + ) + { + if (HasNextPageKlines(resultCount, timestamps, requestStartTime, requestEndTime, lastPaginationData.Limit, lastPaginationData.Direction, interval)) + { + var result = nextPageRequest(); + if (result != null) + { + result.StartTime ??= lastPaginationData.StartTime; + result.EndTime ??= lastPaginationData.EndTime; + return result; + } + } + + return null; + } + + /// + /// Get the next page request parameters from result data + /// + /// Callback for returning the next page request + /// Number of results in data + /// Timestamps of the result data + /// User request start time + /// User request end time + /// The last used pagination data + /// Max period the time filters can span + /// Max age of the data + /// + public static PageRequest? GetNextPageRequest( + Func nextPageRequest, + int resultCount, + IEnumerable timestamps, + DateTime? requestStartTime, + DateTime requestEndTime, + PaginationParameters lastPaginationData, + TimeSpan? maxPeriod = null, + TimeSpan? maxAge = null + ) + { + if (HasNextPage(resultCount, timestamps, requestStartTime, requestEndTime, lastPaginationData.Limit, lastPaginationData.Direction)) + { + var result = nextPageRequest(); + if (result != null) + { + result.StartTime ??= lastPaginationData.StartTime; + result.EndTime ??= lastPaginationData.EndTime; + return result; + } + } + + if (maxPeriod != null) + { + if (HasNextPeriod(requestStartTime, requestEndTime, lastPaginationData.Direction, lastPaginationData, maxPeriod.Value, maxAge)) + { + var (startTime, endTime) = GetNextPeriod(requestStartTime, requestEndTime, lastPaginationData.Direction, lastPaginationData, maxPeriod.Value, maxAge); + return new PageRequest + { + StartTime = startTime, + EndTime = endTime + }; + } + } + + return null; + } + + /// + /// Check whether there is (potentially) another page available + /// + /// Number of result entries + /// Timestamps + /// User request start time + /// User request end time + /// Max number of results requested + /// Data direction + /// Kline interval + /// + public static bool HasNextPageKlines( + int resultCount, + IEnumerable timestamps, + DateTime? requestStartTime, + DateTime requestEndTime, + int limit, + DataDirection direction, + SharedKlineInterval interval + ) + { + if (resultCount < limit) + return false; + + if (direction == DataDirection.Ascending) + { + if (timestamps.Max().AddSeconds((int)interval) >= requestEndTime) + return false; + + return true; + } + else + { + if (timestamps.Min().AddSeconds((int)interval) < requestStartTime) + return false; + + return true; + } + } + + /// + /// Check whether there is (potentially) another page available + /// + /// Number of result entries + /// Timestamps + /// User request start time + /// User request end time + /// Max number of results requested + /// Data direction + /// + public static bool HasNextPage( + int resultCount, + IEnumerable timestamps, + DateTime? requestStartTime, + DateTime requestEndTime, + int limit, + DataDirection direction) + { + if (resultCount < limit) + return false; + + if (!timestamps.Any()) + return false; + + if (direction == DataDirection.Ascending) + { + if (timestamps.Max() >= requestEndTime) + return false; + + return true; + } + else + { + if (timestamps.Min() < requestStartTime) + return false; + + return true; + } + } + + /// + /// Get the next page PageRequest + /// + public static PageRequest NextPageFromPage(PaginationParameters lastPaginationData) + { + return new PageRequest { Page = (lastPaginationData.Page ?? 1) + 1 }; + } + /// + /// Get the next offset PageRequest + /// + public static PageRequest NextPageFromOffset(PaginationParameters lastPaginationData, int resultCount) + { + return new PageRequest { Offset = (lastPaginationData.Offset ?? 0) + resultCount }; + } + /// + /// Get the next page cursor PageRequest + /// + public static PageRequest NextPageFromCursor(string nextCursor) + { + return new PageRequest { Cursor = nextCursor }; + } + /// + /// Get the next id PageRequest + /// + public static PageRequest NextPageFromId(long nextFromId) + { + return new PageRequest { FromId = nextFromId.ToString() }; + } + /// + /// Get the next id PageRequest + /// + public static PageRequest NextPageFromId(string nextFromId) + { + return new PageRequest { FromId = nextFromId }; + } + /// + /// Get the next start/end time PageRequest + /// + public static PageRequest NextPageFromTime(PaginationParameters lastPaginationData, DateTime lastTimestamp, bool setOtherTimeLimiter = true) + { + if (lastPaginationData.Direction == DataDirection.Ascending) + return new PageRequest { StartTime = lastTimestamp.AddMilliseconds(1), EndTime = setOtherTimeLimiter ? lastPaginationData.EndTime : null }; + else + return new PageRequest { EndTime = lastTimestamp.AddMilliseconds(-1), StartTime = setOtherTimeLimiter ? lastPaginationData.StartTime : null }; + } + + /// + /// Get the next start/end time klines PageRequest + /// + public static PageRequest NextPageFromTimeKlines(DataDirection direction, GetKlinesRequest request, DateTime lastTimestamp, int limit) + { + if (direction == DataDirection.Ascending) + { + var nextStartTime = lastTimestamp.AddSeconds((int)request.Interval); + var endTime = nextStartTime.AddSeconds(limit * (int)request.Interval); + var requestEndTime = request.EndTime ?? DateTime.UtcNow; + if (endTime > requestEndTime) + endTime = requestEndTime; + + return new PageRequest { StartTime = nextStartTime, EndTime = endTime }; + } + else + { + var nextEndTime = lastTimestamp.AddSeconds(-(int)request.Interval); + var startTime = nextEndTime.AddSeconds(-(limit * (int)request.Interval)); + var requestStartTime = request.StartTime ?? DateTime.UtcNow; + if (startTime < requestStartTime) + startTime = requestStartTime; + return new PageRequest { StartTime = startTime, EndTime = nextEndTime }; + } + } + + /// + /// Whether another time period is to be requested + /// + /// User request start time + /// User request end time + /// Data direction + /// Pagination parameters used + /// Max time period a request can span + /// Max age of data that can be requested + public static bool HasNextPeriod( + DateTime? requestStartTime, + DateTime requestEndTime, + DataDirection direction, + PaginationParameters lastPaginationParameters, + TimeSpan period, + TimeSpan? maxAge) + { + if (direction == DataDirection.Ascending && lastPaginationParameters.StartTime == null) + throw new InvalidOperationException("Invalid pagination data; no start time for ascending pagination"); + + if (direction == DataDirection.Ascending) + { + return (requestEndTime - lastPaginationParameters.EndTime!.Value).TotalSeconds > 1; + } + else + { + var lastPageStartTime = lastPaginationParameters.StartTime ?? lastPaginationParameters.EndTime!.Value.Add(-period); + if (requestStartTime != null) + { + var nextPeriodDuration = lastPageStartTime - requestStartTime.Value; + return nextPeriodDuration.TotalSeconds > 1; + } + else + { + var nextStartTime = lastPageStartTime - period; + if (maxAge != null) + { + var minStartTime = DateTime.UtcNow - maxAge.Value; + if ((nextStartTime.Add(period) - minStartTime).TotalSeconds < 1) + return false; + } + + var nextPeriodDuration = lastPageStartTime - nextStartTime; + return (nextPeriodDuration).TotalSeconds > 1; + } + } + } + + /// + /// Get the start/end time for the next data period + /// + /// User request start time + /// User request end time + /// Data direction + /// Pagination parameters used + /// Max time period a request can span + /// Max age of data that can be requested + public static (DateTime? startTime, DateTime? endTime) GetNextPeriod( + DateTime? requestStartTime, + DateTime requestEndTime, + DataDirection direction, + PaginationParameters lastPaginationParameters, + TimeSpan period, + TimeSpan? maxAge + ) + { + DateTime? nextStartTime = null; + DateTime? nextEndTime = null; + if (direction == DataDirection.Ascending) + { + if (lastPaginationParameters.StartTime != null) + nextStartTime = lastPaginationParameters.StartTime.Value.Add(period); + if (lastPaginationParameters.EndTime != null) + nextEndTime = lastPaginationParameters.EndTime.Value.Add(period); + } + else + { + if (lastPaginationParameters.StartTime != null) + nextStartTime = lastPaginationParameters.StartTime.Value.Add(-period); + if (lastPaginationParameters.EndTime != null) + nextEndTime = lastPaginationParameters.EndTime.Value.Add(-period); + } + + if (nextStartTime != null && nextStartTime < requestStartTime) + nextStartTime = requestStartTime; + + if (nextStartTime != null && maxAge != null && nextStartTime < DateTime.UtcNow - maxAge) + { + nextStartTime = DateTime.UtcNow.Add(-maxAge.Value); + // Add 30 seconds to max sure the client/server time offset and latency doesn't push the timestamp over the limit + nextStartTime = nextStartTime.Value.Add(TimeSpan.FromSeconds(30)); + } + + if (nextEndTime != null && nextEndTime > requestEndTime) + nextEndTime = requestEndTime; + + return (nextStartTime, nextEndTime); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Pagination/PaginationParameters.cs b/CryptoExchange.Net/SharedApis/Models/Pagination/PaginationParameters.cs new file mode 100644 index 00000000..cf8e35d4 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Pagination/PaginationParameters.cs @@ -0,0 +1,43 @@ +using System; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Pagination parameters + /// + public record PaginationParameters + { + /// + /// Data direction + /// + public DataDirection Direction { get; set; } + /// + /// Start time filter + /// + public DateTime? StartTime { get; set; } + /// + /// End time filter + /// + public DateTime? EndTime { get; set; } + /// + /// Id filter + /// + public string? FromId { get; set; } + /// + /// Result offset + /// + public int? Offset { get; set; } + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Pagination cursor + /// + public string? Cursor { get; set; } + /// + /// Max number of results + /// + public int Limit { get; set; } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs index 4920d8e3..5e1808ea 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetClosedOrdersRequest.cs @@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// public int? Limit { get; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs index 5f53be22..78fc887f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetDepositsRequest.cs @@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// public int? Limit { get; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { Asset = asset; StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs index 213657fe..6f9ad50f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetFundingRateHistoryRequest.cs @@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// public int? Limit { get; set; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs index 6eef5a22..54b1013a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetKlinesRequest.cs @@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// public int? Limit { get; set; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -32,13 +36,15 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { Interval = interval; StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs index 7bbb209a..db62b2fa 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetPositionHistoryRequest.cs @@ -27,6 +27,10 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// public int? Limit { get; set; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -35,13 +39,15 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { Symbol = symbol; StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } /// @@ -51,13 +57,15 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { TradingMode = tradeMode; StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs index 8664f747..2eaea31c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetTradeHistoryRequest.cs @@ -10,15 +10,19 @@ namespace CryptoExchange.Net.SharedApis /// /// Filter by start time /// - public DateTime StartTime { get; } + public DateTime StartTime { get; set; } /// /// Filter by end time /// - public DateTime EndTime { get; } + public DateTime? EndTime { get; set; } /// /// Max number of results /// - public int? Limit { get; } + public int? Limit { get; set; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime endTime, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs index 8fd84078..0cf88b0f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetUserTradesRequest.cs @@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// public int? Limit { get; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) { StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs index 5130079d..0438b701 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetWithdrawalsRequest.cs @@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis /// Max number of results /// public int? Limit { get; } + /// + /// Data direction + /// + public DataDirection? Direction { get; set; } /// /// ctor @@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis /// Filter by start time /// Filter by end time /// Max number of results + /// Data direction /// Exchange specific parameters - public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) + public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) { Asset = asset; StartTime = startTime; EndTime = endTime; Limit = limit; + Direction = direction; } } } diff --git a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs index 50834749..f6be4867 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs @@ -332,7 +332,8 @@ namespace CryptoExchange.Net.Trackers.Klines _data.Add(item.OpenTime, item); } - _firstTimestamp = _data.Min(v => v.Key); + _firstTimestamp = _data.Count == 0 ? null : _data.Min(v => v.Key); + ApplyWindow(false); _logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key); } @@ -375,7 +376,7 @@ namespace CryptoExchange.Net.Trackers.Klines } } - _firstTimestamp = _data.Min(x => x.Key); + _firstTimestamp = _data.Count == 0 ? null : _data.Min(x => x.Key); _changed = true; SetSyncStatus(); diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index 76c17324..6a7e0095 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -259,11 +259,11 @@ namespace CryptoExchange.Net.Trackers.Trades var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value); var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow); var data = new List(); - await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false)) + await foreach (var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false)) { if (!result) return result; - + if (Limit != null && data.Count > Limit) break;