1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-11 16:32:57 +00:00

Compare commits

..

92 Commits

Author SHA1 Message Date
Jkorf 204bda8622 Updated to version 10.7.1 2026-02-25 11:00:43 +01:00
Jkorf 78e3523a4f Updated SocketConnection internal locking to fix potential deadlock 2026-02-25 09:26:12 +01:00
Jkorf 89a73747b0 Fix for test socket not working correctly with read/write socket connection lock 2026-02-24 13:15:08 +01:00
Jkorf 02b70398b3 Updated lock call SocketConnection to outside the try 2026-02-24 13:14:48 +01:00
Jkorf 73fcb47b17 Updated to version 10.7.0 2026-02-24 11:51:37 +01:00
Jkorf d41ca3459e Updated internal lock for subscription to ReaderWriterLockSlim on SocketConnection 2026-02-24 11:32:35 +01:00
Jkorf bea2b2bd7b Added Http options to Rest client options 2026-02-24 09:58:48 +01:00
Jan Korf b29cdc41f3 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<GetClosedOrdersRequest> to GetClosedOrdersOptions 
Updated I(Futures/Spot)OrderRestClient.Get(Futures/Spot)UserTradesOptions from PaginatedEndpointOptions<GetUserTradesRequest> 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
2026-02-23 14:53:38 +01:00
JKorf 0ce2e778f4 Added check for invalid json in JsonSocketMessageHandler and virtual GetTypeIdentifierNonJson for handling non-json messages 2026-02-22 16:29:05 +01:00
JKorf 36c2411d46 Added parsing of REST response data up to 128 characters for error responses 2026-02-22 16:05:49 +01:00
Jkorf 6d3e72745a Removed check for OnlyTrackProvidedSymbols and empty initial tracking list 2026-02-17 14:54:57 +01:00
Jkorf 51c74baa26 Updated to version 10.6.2 2026-02-17 14:39:06 +01:00
Jkorf 419e01d009 Fix for websocket queries which don't expects response getting stuck in subscribing state 2026-02-17 14:35:02 +01:00
Jkorf 297eee0e1f Updated to version 10.6.1 2026-02-16 15:42:33 +01:00
Jkorf 6a9231b1e3 Fixed exception when stopping SymbolOrderBook instance when update is received while closing 2026-02-16 15:39:36 +01:00
Jkorf e40f2a15b6 Updated to version 10.6.0 2026-02-16 09:38:55 +01:00
Jkorf b94085a27a Added some code comments 2026-02-16 09:36:44 +01:00
Jkorf 5e083811df Added check SymbolOrderBook is still alive when trying to add updates to prevent unnoticed growing in the background when subscription isn't closed while book is 2026-02-16 09:33:39 +01:00
Jkorf 7dcf5cd6ea Added AddTrackedSymbolsAsync and RemoveTrackedSymbolAsync methods to UserDataTracker, updated minimal fromTime filter in UserDataTracker to 5 seconds 2026-02-16 09:11:01 +01:00
Jkorf 1471a4733f Updated symbol tracking logic on UserDataTracker, added check for startTime filter for polling being to close to current time 2026-02-13 10:12:43 +01:00
Jkorf f39d9f7cfb Updated to version 10.5.4 2026-02-12 11:43:20 +01:00
Jkorf 9fab8faa45 Fixed bug in polling time filter for UserDataTracker items 2026-02-12 11:35:04 +01:00
Jkorf 226f175343 Fixed type check ExchangeParameters GetValue 2026-02-11 14:50:39 +01:00
Jkorf 813bd9f5a1 Updated to version 10.5.3 2026-02-11 13:10:08 +01:00
Jkorf c8d2b4f09d Added check EnumConverter to detect undefined int value parsing 2026-02-11 12:57:03 +01:00
Jkorf 6560b82a3e Fixed orders getting incorrectly set to canceled state for UserDataTracker spot and futures orders 2026-02-11 11:44:32 +01:00
JKorf e151af8f37 Updated client versions examples 2026-02-10 18:38:45 +01:00
Jkorf bdf7a07c6f Updated to version 10.5.2 2026-02-10 16:04:17 +01:00
Jkorf a8ffe90bf2 Added call to ApiClient.HandleUnhandledMessage when no websocket message processor is found based on topic to allow additional processing 2026-02-10 15:59:35 +01:00
Jkorf 3372b9eb44 Set query completed after setting Result 2026-02-10 15:40:43 +01:00
Jkorf df25221960 Combined subscribe and re-subscribe logic 2026-02-10 15:40:13 +01:00
Jkorf 7c67a014f5 Added check for subscribe queries with TimeoutBehavior.Success to complete when subscription has received update 2026-02-10 14:31:42 +01:00
Jkorf 65291b3195 Updated to version 10.5.1 2026-02-10 11:53:23 +01:00
Jkorf ece6a9d27d Fixed trading mode selection for futures listen key method in FuturesUserDataTracker 2026-02-10 11:51:19 +01:00
Jkorf 1ed29b0474 Updated to version 10.5.0 2026-02-10 10:22:05 +01:00
Jkorf be2eb01353 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-10 10:15:46 +01:00
JKorf 39cd66596d Added some tests 2026-02-09 21:49:07 +01:00
Jkorf 63f51811a9 Updated logging unmatched websocket message 2026-02-09 16:29:24 +01:00
Jkorf abda065237 Updated websocket message forwarding logic 2026-02-09 12:57:52 +01:00
Jkorf 759c8b9a58 Fixed bug in UserDataTracker orders logic incorrectly setting order to canceled status 2026-02-09 09:07:17 +01:00
JKorf 8e18482781 Added keep alive for listenkeys to UserDataTracker 2026-02-08 17:02:09 +01:00
Jkorf 87dd2d9d40 Updated to version 10.4.1 2026-02-06 17:19:08 +01:00
Jkorf f2b0cb2f0d Updated UserDataTracker to only track symbol when position size > 0, update log verbosity 2026-02-06 17:16:31 +01:00
Jkorf de0a954a91 Updated to version 10.4.0 2026-02-06 12:00:50 +01:00
Jkorf 4a79ce22ec Added timeout/ct task cleanup AsyncResetEvent 2026-02-06 11:57:56 +01:00
JKorf 40d480e1fc Added AutoTimestamp option for socket client, fixed socket client timestamp offset bug 2026-02-05 21:54:16 +01:00
Jan Korf 74e5cf6fc9 Feature/userdata tracker (#271)
Added user data tracking logic
Added LastReceiveTime, SocketStatus and SubscriptionStatus properties to UpdateSubscription
Added SharedTransferStatus Enum and property to SharedDeposit
Added PositionMode property to SharedPosition model
Added IsZero property to SharedQuantity
Renamed IWebSocket LastActionTime to LastReceiveTime
Updated CryptoExchangeWebsocketClient LastReceiveTime logic
Updated Subscription status change event handler to run sync instead of separate task
2026-02-05 16:05:13 +01:00
Jkorf 2fd3912795 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-03 08:47:26 +01:00
JKorf ec44307a0c Updated AsyncResetEvent implementation 2026-02-02 21:01:35 +01:00
Jkorf ba55705385 Added Disposed property on BaseClient and IRestClient/ISocketClient interfaces 2026-02-02 09:33:47 +01:00
Jkorf 2c63a83117 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-02-02 08:42:53 +01:00
JKorf 71b1e5e906 Added additional methods for requesting supported symbols to Shared ISpotSymbolRestClient/IFuturesSymbolRestClient interfaces 2026-02-01 15:02:48 +01:00
Jkorf eaeba6f27e Made Interval property of Kline tracker public 2026-01-28 10:31:44 +01:00
Jkorf 913bdaa855 Updated to version 10.3.1 2026-01-27 14:01:26 +01:00
Jkorf 5aa5790d0a Docs 2026-01-23 15:19:33 +01:00
Jkorf a8321e083e Fixed potential collection modified exception upon logging message not handled in websocket message handling 2026-01-22 09:57:02 +01:00
Jkorf ce3fa5f186 Updated to version 10.3.0 2026-01-22 09:33:47 +01:00
Jkorf fff70a9c65 Updated some testing 2026-01-21 15:38:22 +01:00
Jkorf cff33bb5ac Added handling for setting AutoTimestamp in client options when not implemented 2026-01-21 15:37:59 +01:00
Jkorf 21c8133292 Fixed HandleSubQueryResponse not getting called 2026-01-21 11:16:57 +01:00
Jkorf 76772e91ba Remove legacy websocket message handling 2026-01-21 11:14:06 +01:00
Jkorf 218e0260ce Added PlatformInfo class for specifying platform information 2026-01-21 10:14:00 +01:00
Jkorf 96b3904266 Fixed state handling for subscriptions where queries do not get a response 2026-01-20 15:19:42 +01:00
Jkorf bc8faf9822 Updated to version 10.2.5 2026-01-19 10:26:04 +01:00
JKorf 90c1b89ceb Added GetRestOffsets and GetWebsocketOffsets to TimeOffsetManager 2026-01-18 17:46:42 +01:00
JKorf 21206ffb25 Updated SymbolOrderBook.WaitUntilFirstUpdateBufferedAsync 2026-01-18 17:36:18 +01:00
JKorf 5942423bfb Updated to version 10.2.4 2026-01-17 16:29:56 +01:00
JKorf dc4abc42a7 Added WaitUntilFirstUpdateBufferedAsync method on SymbolOrderBook, fixed sequencen validation bug SymbolOrderBook 2026-01-17 16:26:25 +01:00
Jkorf c71a81e686 Added some util methods, Added CommaSplitStringConverter 2026-01-16 16:39:21 +01:00
Jkorf 550c0eabf1 Updated to version 10.2.3 2026-01-14 08:46:07 +01:00
JKorf 28a2a0c7fd Fixed semaphore exception when creating a new REST client while time sync is in progress on another client 2026-01-13 22:11:18 +01:00
JKorf 7dd1cd5bbd Added HandleUnhandledMessage virtual method to SocketApiClient to allow some processing for messages which couldn't be mapped via the normal way 2026-01-13 21:21:30 +01:00
Jkorf a7ff4416bd Updated to version 10.2.2 2026-01-13 11:18:16 +01:00
Jkorf 669d1f7c9e Allow the same websocket connection sequence number to be recorded multiple times 2026-01-13 11:16:37 +01:00
Jkorf 34ee2d3690 Updated to version 10.2.1 2026-01-13 09:29:04 +01:00
Jkorf 005fb7875d Fixed parameter URL creation for array values with ArrayParametersSerialization.MultipleValues 2026-01-13 09:25:35 +01:00
Jkorf fa9300ce97 Removed duplicate logging for rest responses in Trace verbosity 2026-01-13 09:25:07 +01:00
Jkorf fc2d3fc2d2 Updated to version 10.2.0 2026-01-12 14:30:38 +01:00
Jkorf 187ca6a4ef Fixed warning 2026-01-12 14:30:09 +01:00
Jan Korf 3b2a85d210 Feature/websocket sequencing (#267)
Added EnforceSequenceNumbers property on SocketApiClient to configure whether websocket message contain sequence numbers and if these should be checked to be sequential
Added fallback to existing websocket connection if no dedicated request connection was found
Added IntBoolConverter base class for arbitrary int value to bool mapping
Added SequenceNumber property to DataEvent object
Added _skipSequenceCheckFirstUpdateAfterSnapshotSet property for SymbolOrderBook implementations
Updated SymbolOrderBook sequenceNumber validation
Updated SymbolOrderBook log verbosities
Renamed SetInitialOrderBook to SetSnapshot in SymbolOrderBook
Renamed updateId references to sequenceNumber in SymbolOrderBook
2026-01-12 14:26:50 +01:00
Jkorf c512bee825 Updated examples 2026-01-07 14:56:18 +01:00
Jkorf 0943b052b9 Updated to version 10.1.0 2026-01-07 10:03:45 +01:00
Jan Korf a896fffdb3 Time offset management (#266)
Updated time sync / time offset management for REST API's
Added time offset tracking for WebSocket API's
Added GetAuthenticationQuery virtual method on AuthenticationProvider
Updated AuthenticationProvider GetTimestamp methods to include a one second offset by default
Added AuthenticationProvider GetTimestamp methods for SocketApiClient instances
Added ClientName property on BaseApiClient, resolving to the type name
Added ObjectOrArrayConverter JsonConverterFactory implementation for resolving json data which might be returned as object or array
Added UpdateServerTime, UpdateLocalTime and DataAge properties to (I)SymbolOrderBook
Added OutputToConsoleAsync method to (I)SymbolOrderBook
Updated SymbolOrderBook string representation
Added DataTimeLocal and DataAge properties to DataEvent object
Added SocketConnection parameter to subscription HandleSubQueryResponse and HandleUnsubQueryResponse methods
2026-01-07 10:00:14 +01:00
JKorf 177daf903b Added some utils methods 2025-12-30 09:53:30 +01:00
Jkorf aa1ebdc4ed Updated CryptoExchange.Net to version 10.0.2 2025-12-19 11:34:26 +01:00
Jkorf 38058c4a70 Updated to version 10.0.2 2025-12-19 10:14:59 +01:00
Jkorf a7eb483479 Added exception handlers for REST response processing 2025-12-19 10:09:14 +01:00
Jkorf c76931a3b4 Fixed duplicate subscription check with updated deserialization 2025-12-19 09:58:22 +01:00
Jkorf b90b7e9e0c Updated CryptoExchange.Net to 10.0.1 2025-12-18 11:13:56 +01:00
Jkorf beda53d36d Updated to version 10.0.1 2025-12-18 10:56:10 +01:00
Jkorf 0668f669c1 Fixed query parameter array serialization 2025-12-18 10:36:22 +01:00
Jkorf 64250e13db Updated examples 2025-12-17 10:55:48 +01:00
141 changed files with 7456 additions and 2359 deletions
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="10.0.0" />
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
@@ -4,6 +4,7 @@ using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
@@ -139,5 +140,17 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.False(result1);
}
[Test]
public async Task CancellingWait_Should_ReturnFalse()
{
var evnt = new AsyncResetEvent(false, true);
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
var result1 = await waiter1;
ClassicAssert.False(result1);
}
}
}
@@ -19,20 +19,6 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(result.Success);
}
[TestCase]
public void DeserializingInvalidJson_Should_GiveErrorResult()
{
// arrange
var client = new TestBaseClient();
// act
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
@@ -9,7 +9,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference>
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,465 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class ExchangeSymbolCacheTests
{
private SharedSpotSymbol[] CreateTestSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("BTC", "EUR", "BTCEUR", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("XRP", "USDT", "XRPUSDT", false, TradingMode.Spot)
};
}
private SharedSpotSymbol[] CreateFuturesSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT-PERP", true, TradingMode.PerpetualLinear),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT-PERP", true, TradingMode.PerpetualLinear)
};
}
[Test]
public void UpdateSymbolInfo_NewTopic_Should_AddToCache()
{
// arrange
var topicId = "NewExchange";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.True);
}
[Test]
public void UpdateSymbolInfo_Should_StoreAllSymbols()
{
// arrange
var topicId = "ExchangeWithSymbols";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
}
[Test]
public void UpdateSymbolInfo_CalledTwiceWithinAnHour_Should_NotUpdate()
{
// arrange
var topicId = "ExchangeNoUpdate";
var initialSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var updatedSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
// assert - should still have only the initial symbol since less than 60 minutes passed
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
// The second update should not have been applied
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithEmptyArray_Should_CreateEmptyCache()
{
// arrange
var topicId = "EmptyExchange";
var symbols = Array.Empty<SharedSpotSymbol>();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.False);
}
[Test]
public void HasCached_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
// assert
Assert.That(result, Is.False);
}
[Test]
public void HasCached_ExistingTopicWithSymbols_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeWithData";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.True);
}
[Test]
public void HasCached_ExistingTopicWithNoSymbols_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoData";
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSupports";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_ByName_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSupport";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_DifferentTradingMode_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeDifferentMode";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void GetSymbolsForBaseAsset_ExistingBaseAsset_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(result.Any(x => x.QuoteAsset == "USDT"), Is.True);
Assert.That(result.Any(x => x.QuoteAsset == "EUR"), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_CaseInsensitive_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeCaseInsensitive";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistingBaseAsset_Should_ReturnEmptyArray()
{
// arrange
var topicId = "ExchangeNoBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistentTopic_Should_ReturnEmptyArray()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void ParseSymbol_ExistingSymbol_Should_ReturnSharedSymbol()
{
// arrange
var topicId = "ExchangeParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.BaseAsset, Is.EqualTo("BTC"));
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
}
[Test]
public void ParseSymbol_NonExistingSymbol_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNoParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NullSymbolName_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNullSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NonExistentTopic_Should_ReturnNull()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void MultipleTopics_Should_MaintainSeparateData()
{
// arrange
var topic1 = "Exchange1";
var topic2 = "Exchange2";
var symbols1 = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var symbols2 = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithDifferentTradingModes_Should_StoreCorrectly()
{
// arrange
var topicId = "ExchangeMixedModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
// assert
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_Should_ReturnAllTradingModes()
{
// arrange
var topicId = "ExchangeAllModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
Assert.That(result.Any(x => x.TradingMode == TradingMode.Spot), Is.True);
Assert.That(result.Any(x => x.TradingMode == TradingMode.PerpetualLinear), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_WithMultipleMatchingSymbols_Should_ReturnAll()
{
// arrange
var topicId = "ExchangeMultiple";
var symbols = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
};
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
// assert
Assert.That(result.Length, Is.EqualTo(3));
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
}
}
}
@@ -81,6 +81,25 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
{
// arrange
var client = new TestRestClient();
var response = "<html>...</html>";
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is DeserializeError);
Assert.That(result.Error.Message.Contains(response));
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
@@ -0,0 +1,633 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedQuantityTests
{
[Test]
public void SharedQuantityReference_IsZero_AllNull_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_AllZero_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(0, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_BaseAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(1.5m, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_QuoteAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, 100m, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_ContractsSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, 10m);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_NegativeValue_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(-1m, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_Should_SetBaseAssetQuantity()
{
// arrange
var expectedQuantity = 1.5m;
// act
var quantity = SharedQuantity.Base(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Base_WithZero_Should_SetZeroQuantity()
{
// arrange & act
var quantity = SharedQuantity.Base(0m);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0m));
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_WithLargeValue_Should_SetCorrectly()
{
// arrange
var largeValue = 999999.123456789m;
// act
var quantity = SharedQuantity.Base(largeValue);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(largeValue));
}
[Test]
public void SharedQuantity_Quote_Should_SetQuoteAssetQuantity()
{
// arrange
var expectedQuantity = 100m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Quote_WithDecimal_Should_PreserveDecimals()
{
// arrange
var expectedQuantity = 50.123456m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_Should_SetContractQuantity()
{
// arrange
var expectedQuantity = 10m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_WithFractionalValue_Should_SetCorrectly()
{
// arrange
var expectedQuantity = 2.5m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_BaseFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 50m;
var expectedBase = 2m; // 100 / 50 = 2
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedBase));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_BaseFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 100m;
var price = 7m;
var lotSize = 0.1m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.1 = 14.3
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.3m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithHighPrecision_Should_HandleCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var price = 0.00001m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 2m;
var price = 50m;
var expectedQuote = 100m; // 2 * 50 = 100
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuote));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_QuoteFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
// 1.234567 * 10 = 12.34567 should round to 12.35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(12.35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 3.456m;
var price = 10m;
var lotSize = 1m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, 8, lotSize);
// assert
// 3.456 * 10 = 34.56 should adjust to nearest 1 = 35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithSmallValues_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 0.001m;
var price = 0.1m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0.0001m));
}
[Test]
public void SharedQuantity_ContractsFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 10m;
var expectedContracts = 10m; // 100 / 10 = 10
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 100m;
var contractSize = 7m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.5 = 14.5
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.5m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithFractionalContract_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 1m;
var contractSize = 0.1m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(10m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
var expectedContracts = 2m; // 1000 / 50 / 10 = 2
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var contractSize = 3m;
var price = 7m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 100 / 7 / 3 = 4.761904... should round to 4.76
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(4.76m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 7m;
var price = 13m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, 8, lotSize);
// assert
// 1000 / 13 / 7 = 10.989... should adjust to nearest 0.5 = 11.0
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(11.0m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithComplexValues_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 5000m;
var contractSize = 0.01m;
var price = 25000m;
var decimalPlaces = 4;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 5000 / 25000 / 0.01 = 20
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(20m));
}
[Test]
public void SharedOrderQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedOrderQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetBaseAsset()
{
// arrange
var baseAsset = 5m;
// act
var quantity = new SharedOrderQuantity(baseAssetQuantity: baseAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetQuoteAsset()
{
// arrange
var quoteAsset = 100m;
// act
var quantity = new SharedOrderQuantity(quoteAssetQuantity: quoteAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetContracts()
{
// arrange
var contracts = 10m;
// act
var quantity = new SharedOrderQuantity(contractQuantity: contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetAllValues()
{
// arrange
var baseAsset = 1m;
var quoteAsset = 50m;
var contracts = 5m;
// act
var quantity = new SharedOrderQuantity(baseAsset, quoteAsset, contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_WithNullValues_Should_HandleCorrectly()
{
// arrange & act
var quantity = new SharedOrderQuantity(null, null, null);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(10m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentValues_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(20m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentTypes_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Quote(10m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedOrderQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = new SharedOrderQuantity(5m, 100m, 2m);
var quantity2 = new SharedOrderQuantity(5m, 100m, 2m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
// Default decimalPlaces = 8, default lotSize = 0.00000001
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
}
}
@@ -0,0 +1,400 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedSymbolTests
{
[Test]
public void SharedSymbol_Constructor_Should_SetAllProperties()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.Null);
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithDeliveryTime_Should_SetDeliveryTime()
{
// arrange
var tradingMode = TradingMode.DeliveryLinear;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var deliveryTime = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc);
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliveryTime);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.EqualTo(deliveryTime));
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithNullDeliveryTime_Should_SetToNull()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "ETH";
var quoteAsset = "BTC";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime: null);
// assert
Assert.That(symbol.DeliverTime, Is.Null);
}
[TestCase(TradingMode.Spot)]
[TestCase(TradingMode.PerpetualLinear)]
[TestCase(TradingMode.PerpetualInverse)]
[TestCase(TradingMode.DeliveryLinear)]
[TestCase(TradingMode.DeliveryInverse)]
public void SharedSymbol_Constructor_WithDifferentTradingModes_Should_SetCorrectly(TradingMode tradingMode)
{
// arrange
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_Should_SetSymbolName()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "BTC-USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
Assert.That(symbol.DeliverTime, Is.Null);
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithCustomFormat_Should_SetCorrectly()
{
// arrange
var tradingMode = TradingMode.PerpetualLinear;
var baseAsset = "ETH";
var quoteAsset = "USDT";
var symbolName = "ETHUSDT-PERP";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithEmptyString_Should_SetEmptyString()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(""));
}
[Test]
public void GetSymbol_WithSymbolNameSet_Should_ReturnSymbolName()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "CUSTOM-BTC-USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("CUSTOM-BTC-USDT"));
}
[Test]
public void GetSymbol_WithSymbolNameNull_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}/{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC/USDT"));
}
[Test]
public void GetSymbol_WithComplexFormatFunction_Should_ApplyCorrectly()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => t == TradingMode.PerpetualLinear ? $"{b}{q}-PERP" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("ETHUSDT-PERP"));
}
[Test]
public void GetSymbol_WithDeliveryTime_Should_PassDeliveryTimeToFormatter()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime);
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => d.HasValue ? $"{b}{q}_{d.Value:yyyyMMdd}" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDT_20260625"));
}
[Test]
public void GetSymbol_WithTradingMode_Should_PassTradingModeToFormatter()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualInverse, "BTC", "USD");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) =>
{
return t switch
{
TradingMode.Spot => $"{b}{q}",
TradingMode.PerpetualLinear => $"{b}{q}-PERP",
TradingMode.PerpetualInverse => $"{b}{q}I-PERP",
_ => $"{b}{q}"
};
});
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDI-PERP"));
}
[Test]
public void GetSymbol_WithEmptySymbolName_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC-USDT"));
}
[Test]
public void GetSymbol_WithWhitespaceSymbolName_Should_ReturnWhitespace()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", " ");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo(" "));
}
[Test]
public void SharedSymbol_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentBaseAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentQuoteAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "EUR");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentTradingMode_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentDeliveryTime_Should_NotBeEqual()
{
// arrange
var deliveryTime1 = new DateTime(2026, 6, 25);
var deliveryTime2 = new DateTime(2026, 9, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime1);
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime2);
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentSymbolName_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTC-USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_OneWithSymbolNameOneWithout_Should_NotBeEqual()
{
// NOTE; although this should probably be equal it's considered not because the SymbolName property isn't equal
// Overridding equality to ignore SymbolName would be possible but would break the default record equality behavior and cause confusion
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_WithAllPropertiesSet_Should_BeEqual()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_Properties_Should_BeSettable()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
symbol.BaseAsset = "ETH";
symbol.QuoteAsset = "EUR";
symbol.TradingMode = TradingMode.PerpetualLinear;
symbol.SymbolName = "CUSTOM";
symbol.DeliverTime = DateTime.UtcNow;
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo("ETH"));
Assert.That(symbol.QuoteAsset, Is.EqualTo("EUR"));
Assert.That(symbol.TradingMode, Is.EqualTo(TradingMode.PerpetualLinear));
Assert.That(symbol.SymbolName, Is.EqualTo("CUSTOM"));
Assert.That(symbol.DeliverTime, Is.Not.Null);
}
[Test]
public void SharedSymbol_WithSpecialCharactersInAssets_Should_HandleCorrectly()
{
// arrange
var baseAsset = "BTC-123";
var quoteAsset = "USDT_2.0";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
[Test]
public void SharedSymbol_WithLongAssetNames_Should_HandleCorrectly()
{
// arrange
var baseAsset = "VERYLONGASSETNAMEFORTESTING";
var quoteAsset = "ANOTHERVERYLONGASSETNAME";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
@@ -51,21 +52,11 @@ namespace CryptoExchange.Net.UnitTests
public CallResult<T> Deserialize<T>(string data)
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result;
if (!valid)
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>();
return deserializeResult;
return new CallResult<T>(JsonSerializer.Deserialize<T>(data));
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
@@ -142,7 +142,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
@@ -160,11 +159,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
ParameterPositions[method] = position;
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
@@ -172,11 +166,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestRestApi2Client : RestApiClient
@@ -188,7 +177,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
RequestFactory = new Mock<IRequestFactory>().Object;
}
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
@@ -198,12 +186,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
@@ -212,10 +195,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestError
@@ -19,11 +19,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
private ErrorMapping _errorMapping = new ErrorMapping([]);
public override JsonSerializerOptions Options => new JsonSerializerOptions();
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (result.Item1 != null)
return result.Item1;
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
var errorData = result.Item2.Deserialize<TestError>();
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
}
}
}
@@ -11,6 +11,8 @@ using System.Linq;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
namespace CryptoExchange.Net.Authentication
{
@@ -76,12 +78,20 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Authenticate a request
/// Authenticate a REST request
/// </summary>
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="apiClient">The API client sending the request</param>
/// <param name="requestConfig">The request configuration</param>
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <summary>
/// Get an authentication query for a websocket
/// </summary>
/// <param name="apiClient">The API client sending the request</param>
/// <param name="connection">The connection to authenticate</param>
/// <param name="context">Optional context required for creating the authentication query</param>
public virtual Query? GetAuthenticationQuery(SocketApiClient apiClient, SocketConnection connection, Dictionary<string, object?>? context = null) => null;
/// <summary>
/// SHA256 sign the data and return the bytes
/// </summary>
@@ -442,6 +452,14 @@ namespace CryptoExchange.Net.Authentication
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(byte[] buff)
=> BytesToHexString(new ArraySegment<byte>(buff));
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(ArraySegment<byte> buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
@@ -453,6 +471,26 @@ namespace CryptoExchange.Net.Authentication
#endif
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
protected static byte[] HexToBytesString(string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
/// <summary>
/// Convert byte array to base64 string
/// </summary>
@@ -466,32 +504,53 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected DateTime GetTimestamp(RestApiClient apiClient)
protected DateTime GetTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
{
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetRestOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
result = result.AddSeconds(-1);
return result;
}
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
protected DateTime GetTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
{
var timestamp = TimeProvider.GetTime();
if(apiClient.ApiOptions.AutoTimestamp ?? apiClient.ClientOptions.AutoTimestamp)
timestamp = timestamp.Add(-TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
timestamp = timestamp.AddSeconds(-1);
return timestamp;
}
/// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected string GetMillisecondTimestamp(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
}
protected string GetMillisecondTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
/// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary>
protected string GetMillisecondTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
}
protected long GetMillisecondTimestampLong(RestApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
protected long GetMillisecondTimestampLong(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
/// <summary>
/// Return the serialized request body
@@ -13,6 +13,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
/// <summary>
/// Client name
/// </summary>
protected string? _clientName;
/// <summary>
/// Logger
/// </summary>
@@ -23,6 +28,21 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected bool _disposing;
/// <summary>
/// Name of the client
/// </summary>
protected internal string ClientName
{
get
{
if (_clientName != null)
return _clientName;
_clientName = GetType().Name;
return _clientName;
}
}
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
+7
View File
@@ -39,6 +39,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public string Exchange { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
public bool Disposed { get; private set; }
/// <summary>
/// Api clients in this client
/// </summary>
@@ -125,6 +130,8 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public virtual void Dispose()
{
Disposed = true;
foreach (var client in ApiClients)
client.Dispose();
}
+106 -82
View File
@@ -32,12 +32,6 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
/// <inheritdoc />
public abstract TimeSpan? GetTimeOffset();
/// <inheritdoc />
public int TotalRequestsMade { get; set; }
@@ -115,15 +109,11 @@ namespace CryptoExchange.Net.Clients
options,
apiOptions)
{
TimeOffsetManager.RegisterRestApi(ClientName);
RequestFactory.Configure(options, httpClient);
}
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected abstract IStreamMessageAccessor CreateAccessor();
/// <summary>
/// Create a serializer instance
/// </summary>
@@ -241,11 +231,9 @@ namespace CryptoExchange.Net.Clients
{
currentTry++;
var error = await CheckTimeSync(requestId, definition).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
error = await RateLimitAsync(
var error = await RateLimitAsync(
baseAddress,
requestId,
definition,
@@ -300,28 +288,6 @@ namespace CryptoExchange.Net.Clients
}
}
private async ValueTask<Error?> CheckTimeSync(int requestId, RequestDefinition definition)
{
if (!definition.Authenticated)
return null;
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeError = await syncTask.ConfigureAwait(false);
if (syncTimeError != null)
{
_logger.RestApiFailedToSyncTime(requestId, syncTimeError!.ToString());
return syncTimeError;
}
}
return null;
}
/// <summary>
/// Check rate limits for the request
/// </summary>
@@ -471,26 +437,19 @@ namespace CryptoExchange.Net.Clients
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
string? originalData = null;
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
if (outputOriginalData || MessageHandler.RequiresSeekableStream)
if (outputOriginalData || MessageHandler.RequiresSeekableStream || !response.IsSuccessStatusCode)
{
// If we want to return the original string data from the stream, but still want to process it
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
if (outputOriginalData)
// Create a seekable stream from the response stream if:
// 1. We need to output the original data
// 2. The message handler requires a seekable stream
// 3. The response indicates error and we want to output (part of) the returned data
responseStream = await CopyStreamAsync(responseStream).ConfigureAwait(false);
using var reader = new StreamReader(responseStream, Encoding.UTF8, false, 4096, true);
if (outputOriginalData)
{
memoryStream.Position = 0;
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
if (_logger.IsEnabled(LogLevel.Trace))
_logger.RestApiReceivedResponse(request.RequestId, originalData);
responseStream.Position = 0;
}
// Continue processing from the memory stream since the response stream is already read and we can't seek it
responseStream.Close();
memoryStream.Position = 0;
responseStream = memoryStream;
}
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
@@ -516,10 +475,19 @@ namespace CryptoExchange.Net.Clients
else
{
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
error = await MessageHandler.ParseErrorResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
try
{
error = await MessageHandler.ParseErrorResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
}
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
@@ -558,10 +526,19 @@ namespace CryptoExchange.Net.Clients
if (deserializeError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
// Check the deserialized response to see if it's an error or not
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
if (responseError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
try
{
// Check the deserialized response to see if it's an error or not
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
if (responseError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
}
@@ -706,26 +683,53 @@ namespace CryptoExchange.Net.Clients
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
internal async ValueTask<Error?> SyncTimeAsync()
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return null;
if (!definition.Authenticated)
return;
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName);
var syncTask = CheckTimeOffsetAsync();
if (lastUpdateTime == null)
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return null;
}
// Initially with first request we'll need to wait for the time syncing before making the actual request.
// If it's not the first request we can just continue and let it complete in the background
await syncTask.ConfigureAwait(false);
}
return;
}
internal async ValueTask CheckTimeOffsetAsync()
{
if (!(ApiOptions.AutoTimestamp ?? ClientOptions.AutoTimestamp))
// Time syncing not enabled
return;
await TimeOffsetManager.EnterAsync(ClientName).ConfigureAwait(false);
try
{
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName);
if (DateTime.UtcNow - lastUpdateTime < (ApiOptions.TimestampRecalculationInterval ?? ClientOptions.TimestampRecalculationInterval))
// Time syncing was recently done
return;
var localTime = DateTime.UtcNow;
var result = await GetServerTimestampAsync().ConfigureAwait(false);
WebCallResult<DateTime> result;
try
{
result = await GetServerTimestampAsync().ConfigureAwait(false);
}
catch (NotImplementedException)
{
throw new ArgumentException("AutoTimestamp is not available for this API");
}
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.Error;
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
}
if (TotalRequestsMade == 1)
@@ -735,18 +739,38 @@ namespace CryptoExchange.Net.Clients
result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return result.Error;
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
}
}
// Calculate time offset between local and server
// Estimate the offset as the round trip time / 2
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
timeSyncParams.UpdateTimeOffset(offset);
timeSyncParams.TimeSyncState.Semaphore.Release();
}
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{
_logger.LogInformation("{ClientName} Time offset within limits ({Offset}ms), set offset to 0ms", ClientName, Math.Round(offset.TotalMilliseconds));
offset = TimeSpan.Zero;
}
else
{
_logger.LogInformation("{ClientName} Time offset set to {Offset}ms", ClientName, Math.Round(offset.TotalMilliseconds));
}
return null;
TimeOffsetManager.UpdateRestOffset(ClientName, offset.TotalMilliseconds);
}
finally
{
TimeOffsetManager.Release(ClientName);
}
}
private async Task<Stream> CopyStreamAsync(Stream responseStream)
{
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
responseStream.Close();
memoryStream.Position = 0;
return memoryStream;
}
private bool ShouldCache(RequestDefinition definition)
+37 -77
View File
@@ -32,8 +32,10 @@ namespace CryptoExchange.Net.Clients
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{
#region Fields
/// <inheritdoc/>
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
/// <inheritdoc/>
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
@@ -97,11 +99,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <summary>
/// Whether to continue processing and forward unparsable messages to handlers
/// </summary>
protected internal bool ProcessUnparsableMessages { get; set; } = false;
/// <inheritdoc />
public double IncomingKbps
{
@@ -140,6 +137,10 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public int? MaxIndividualSubscriptionsPerConnection { get; set; }
/// <summary>
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
/// </summary>
public bool EnforceSequenceNumbers { get; set; }
#endregion
/// <summary>
@@ -159,12 +160,6 @@ namespace CryptoExchange.Net.Clients
{
}
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
/// <summary>
/// Create a serializer instance
/// </summary>
@@ -181,6 +176,24 @@ namespace CryptoExchange.Net.Clients
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
}
/// <summary>
/// Update the timestamp offset between client and server based on the timestamp
/// </summary>
/// <param name="timestamp">Timestamp received from the server</param>
public virtual void UpdateTimeOffset(DateTime timestamp)
{
if (timestamp == default)
return;
TimeOffsetManager.UpdateSocketOffset(ClientName, (DateTime.UtcNow - timestamp).TotalMilliseconds);
}
/// <summary>
/// Get the time offset between client and server
/// </summary>
/// <returns></returns>
public virtual TimeSpan? GetTimeOffset() => TimeOffsetManager.GetSocketOffset(ClientName);
/// <summary>
/// Add a query to periodically send on each connection
/// </summary>
@@ -291,52 +304,9 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
void HandleSubscriptionComplete(bool success, object? response)
{
if (!success)
return;
subscription.HandleSubQueryResponse(response);
subscription.Status = SubscriptionStatus.Subscribed;
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false);
}
}
subscription.Status = SubscriptionStatus.Subscribing;
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
if (subQuery != null)
{
subQuery.OnComplete = () => HandleSubscriptionComplete(subQuery.Result?.Success ?? false, subQuery.Response);
// Send the request and wait for answer
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, ct).ConfigureAwait(false);
if (!subResult)
{
var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
else
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a server process error we still might need to send an unsubscribe to prevent messages coming in later
subscription.Status = SubscriptionStatus.Pending;
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
}
}
else
{
HandleSubscriptionComplete(true, null);
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult)
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
@@ -575,7 +545,8 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection));
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests
@@ -685,6 +656,10 @@ namespace CryptoExchange.Net.Clients
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
if (connection == null)
// Fall back to an existing connection if there is no dedicated request connection available
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
@@ -722,7 +697,6 @@ namespace CryptoExchange.Net.Clients
// Create new socket connection
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection)
{
@@ -773,14 +747,13 @@ namespace CryptoExchange.Net.Clients
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
}
/// <summary>
/// Process an unhandled message
/// </summary>
/// <param name="message">The message that wasn't processed</param>
protected virtual void HandleUnhandledMessage(IMessageAccessor message)
{
}
/// <param name="connection">The socket connection</param>
/// <param name="typeIdentifier">The type as identified</param>
/// <param name="data">The data</param>
protected internal virtual bool HandleUnhandledMessage(SocketConnection connection, string typeIdentifier, ReadOnlySpan<byte> data) => false;
/// <summary>
/// Process connect rate limited
@@ -834,7 +807,6 @@ namespace CryptoExchange.Net.Clients
Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
UseUpdatedDeserialization = ClientOptions.UseUpdatedDeserialization
};
/// <summary>
@@ -1027,7 +999,6 @@ namespace CryptoExchange.Net.Clients
sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
});
}
});
@@ -1058,21 +1029,10 @@ namespace CryptoExchange.Net.Clients
base.Dispose();
}
/// <summary>
/// Get the listener identifier for the message
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
/// <summary>
/// Preprocess a stream message
/// </summary>
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data;
/// <summary>
/// Preprocess a stream message
/// </summary>
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
/// <summary>
/// Create a new message converter instance
@@ -1,49 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Node accessor
/// </summary>
public readonly struct NodeAccessor
{
/// <summary>
/// Index
/// </summary>
public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
{
Index = index;
Property = property;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
}
}
@@ -1,50 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message access definition
/// </summary>
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{
private readonly List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{
_path.Add(node);
}
/// <summary>
/// ctor
/// </summary>
public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary>
/// Create a new message path
/// </summary>
/// <returns></returns>
public static MessagePath Get()
{
return new MessagePath();
}
/// <summary>
/// IEnumerable implementation
/// </summary>
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -1,43 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{
/// <summary>
/// Add a string node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary>
/// Add a property name node accessor
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static MessagePath PropertyName(this MessagePath path)
{
path.Add(NodeAccessor.PropertyName());
return path;
}
/// <summary>
/// Add a int node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="index"></param>
/// <returns></returns>
public static MessagePath Index(this MessagePath path, int index)
{
path.Add(NodeAccessor.Int(index));
return path;
}
}
}
@@ -1,21 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing
{
/// <summary>
/// Message node type
/// </summary>
public enum NodeType
{
/// <summary>
/// Array node
/// </summary>
Array,
/// <summary>
/// Object node
/// </summary>
Object,
/// <summary>
/// Value node
/// </summary>
Value
}
}
@@ -0,0 +1,30 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated string values
/// </summary>
public class CommaSplitStringConverter : JsonConverter<string[]>
{
/// <inheritdoc />
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value));
}
}
}
@@ -168,7 +168,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (!_unknownValuesWarned.Contains(stringValue))
{
_unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: {string.Join(", ", _mappingToEnum!.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should added please open an issue on the Github repo");
}
}
@@ -246,6 +246,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
// If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true);
if (!Enum.IsDefined(objectType, result))
{
result = default;
return false;
}
return true;
}
catch (Exception)
@@ -0,0 +1,38 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Bool converter
/// </summary>
public class IntBoolConverter : JsonConverter<bool>
{
private readonly int _trueValue;
/// <summary>
/// ctor
/// </summary>
/// <param name="trueValue">The int value representing the true value</param>
public IntBoolConverter(int trueValue)
{
_trueValue = trueValue;
}
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
return false;
return reader.GetDecimal() == _trueValue;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteNumberValue(_trueValue);
}
}
}
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
public abstract class JsonRestMessageHandler : IRestMessageHandler
{
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
private const int _errorResponseSnippetLimit = 128;
/// <summary>
/// Empty rate limit error
@@ -80,7 +81,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
}
catch (Exception ex)
{
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
var errorMsg = "Deserialization failed, invalid JSON";
if (stream.CanSeek)
{
var dataSnippet = new char[_errorResponseSnippetLimit];
stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written);
errorMsg += $": {data}";
if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)";
}
var error = new DeserializeError(errorMsg, ex);
return (error, null);
}
}
@@ -165,6 +165,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
return null;
}
/// <summary>
/// Return type identifier for non-json messages
/// </summary>
protected virtual string? GetTypeIdentifierNonJson(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
return null;
}
/// <inheritdoc />
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
{
@@ -173,6 +181,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
int? arrayIndex = null;
_searchResult.Clear();
if (data[0] != 0x5B && data[0] != 0x7B)
{
// Message doesn't start with `{` or `[`, not valid for processing as json
return GetTypeIdentifierNonJson(data, webSocketMessageType);
}
var reader = new Utf8JsonReader(data);
while (reader.Read())
{
@@ -0,0 +1,58 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for parsing object or array responses
/// </summary>
public class ObjectOrArrayConverter : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert) => true;
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var type = typeof(InternalObjectOrArrayConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(type)!;
}
private class InternalObjectOrArrayConverter<T> : JsonConverter<T>
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartObject && !typeToConvert.IsArray)
{
// Object to object
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray && typeToConvert.IsArray)
{
// Array to array
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
// Array to object
JsonDocument.ParseValue(ref reader).Deserialize<T[]>(options);
return default;
}
else
{
// Object to array
JsonDocument.ParseValue(ref reader);
return default;
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, options);
}
}
}
}
@@ -1,373 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// System.Text.Json message accessor
/// </summary>
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
{
/// <summary>
/// The JsonDocument loaded
/// </summary>
protected JsonDocument? _document;
private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc />
public bool IsValid { get; set; }
/// <inheritdoc />
public abstract bool OriginalDataAvailable { get; }
/// <inheritdoc />
public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
if (!IsValid)
return new CallResult<object>(GetOriginalString());
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize(type, _customSerializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public CallResult<T> Deserialize<T>(MessagePath? path = null)
{
if (_document == null)
throw new InvalidOperationException("No json document loaded");
try
{
var result = _document.Deserialize<T>(_customSerializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
{
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
return _document.RootElement.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var node = GetPathNode(path);
if (!node.HasValue)
return null;
return node.Value.ValueKind switch
{
JsonValueKind.Object => NodeType.Object,
JsonValueKind.Array => NodeType.Array,
_ => NodeType.Value
};
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T? GetValue<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_customSerializerOptions);
}
catch { }
return default;
}
if (typeof(T) == typeof(string))
{
if (value.Value.ValueKind == JsonValueKind.Number)
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>(_customSerializerOptions);
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public T?[]? GetValues<T>(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
}
private JsonElement? GetPathNode(MessagePath path)
{
if (!IsValid)
throw new InvalidOperationException("Can't access json data on non-json message");
if (_document == null)
throw new InvalidOperationException("No json document loaded");
JsonElement? currentToken = _document.RootElement;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = node.Index!.Value;
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
return null;
currentToken = currentToken.Value[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
return null;
currentToken = token;
}
else
{
// Property name
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
return null;
throw new NotImplementedException();
}
if (currentToken == null)
return null;
}
return currentToken;
}
/// <inheritdoc />
public abstract string GetOriginalString();
/// <inheritdoc />
public abstract void Clear();
}
/// <summary>
/// System.Text.Json stream message accessor
/// </summary>
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
{
private Stream? _stream;
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream)
{
if (bufferStream && stream is not MemoryStream)
{
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
_stream = new MemoryStream();
stream.CopyTo(_stream);
_stream.Position = 0;
}
else if (bufferStream)
{
// We need to buffer the stream, and the current stream is seekable, store as is
_stream = stream;
}
else
{
// We don't need to buffer the stream, so don't bother keeping the reference
}
try
{
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString()
{
if (_stream is null)
throw new NullReferenceException("Stream not initialized");
_stream.Position = 0;
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
return textReader.ReadToEnd();
}
/// <inheritdoc />
public override void Clear()
{
_stream?.Dispose();
_stream = null;
_document?.Dispose();
_document = null;
}
}
/// <summary>
/// System.Text.Json byte message accessor
/// </summary>
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
_bytes = data;
try
{
var firstByte = data.Span[0];
if (firstByte != 0x7b && firstByte != 0x5b)
{
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsValid = false;
return new CallResult(new DeserializeError("Not a json value"));
}
_document = JsonDocument.Parse(data);
IsValid = true;
return CallResult.SuccessResult;
}
catch (Exception ex)
{
// Not a json message
IsValid = false;
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
}
}
/// <inheritdoc />
public override string GetOriginalString() =>
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray());
#else
Encoding.UTF8.GetString(_bytes.Span);
#endif
/// <inheritdoc />
public override bool OriginalDataAvailable => true;
/// <inheritdoc />
public override void Clear()
{
_bytes = null;
_document?.Dispose();
_document = null;
}
}
}
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>10.0.0</PackageVersion>
<AssemblyVersion>10.0.0</AssemblyVersion>
<FileVersion>10.0.0</FileVersion>
<PackageVersion>10.7.1</PackageVersion>
<AssemblyVersion>10.7.1</AssemblyVersion>
<FileVersion>10.7.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
+54 -5
View File
@@ -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;
@@ -242,8 +243,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Generate a long value
/// </summary>
/// <param name="maxLength">Max character length</param>
/// <returns></returns>
/// <param name="maxLength">Max number of digits</param>
public static long RandomLong(int maxLength)
{
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
@@ -259,6 +259,25 @@ namespace CryptoExchange.Net
return value;
}
/// <summary>
/// Generate a long value between two values
/// </summary>
/// <param name="minValue">Min value</param>
/// <param name="maxValue">Max value</param>
/// <returns></returns>
public static long RandomLong(long minValue, long maxValue)
{
#if NET8_0_OR_GREATER
var buf = RandomNumberGenerator.GetBytes(8);
#else
byte[] buf = new byte[8];
var random = new Random();
random.NextBytes(buf);
#endif
long longRand = BitConverter.ToInt64(buf, 0);
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
}
/// <summary>
/// Generate a random string of specified length
/// </summary>
@@ -292,11 +311,11 @@ namespace CryptoExchange.Net
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
{
var result = new List<T>();
ExchangeWebResult<T[]> batch;
INextPageToken? nextPageToken = null;
PageRequest? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
@@ -305,12 +324,42 @@ namespace CryptoExchange.Net
break;
result.AddRange(batch.Data);
nextPageToken = batch.NextPageToken;
nextPageToken = batch.NextPageRequest;
if (nextPageToken == null)
break;
}
}
/// <summary>
/// Apply filters to the data set
/// </summary>
/// <typeparam name="T">Type</typeparam>
/// <param name="data">Data set</param>
/// <param name="timeSelector">Time selector for the data</param>
/// <param name="startTime">Start time filter</param>
/// <param name="endTime">End time filter</param>
/// <param name="direction">Data direction</param>
public static IEnumerable<T> ApplyFilter<T>(
IEnumerable<T> data,
Func<T, DateTime> 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;
}
/// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary>
+60
View File
@@ -32,6 +32,66 @@ namespace CryptoExchange.Net
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
}
/// <summary>
/// Whether the specific topic has been cached
/// </summary>
/// <param name="topicId">Id</param>
public static bool HasCached(string topicId)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Count > 0;
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbolName">The symbol name</param>
public static bool SupportsSymbol(string topicId, string symbolName)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return false;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return false;
return true;
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbol">The symbol info</param>
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset);
}
/// <summary>
/// Get all symbols for a specific base asset
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="baseAsset">Base asset name</param>
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return [];
return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.ToArray();
}
/// <summary>
/// Parse a symbol name to a SharedSymbol
/// </summary>
+31 -148
View File
@@ -70,12 +70,17 @@ namespace CryptoExchange.Net
first = false;
if (parameter.GetType().IsArray)
if (parameter.Value.GetType().IsArray)
{
if (serializationType == ArrayParametersSerialization.Array)
{
foreach(var entry in (object[])parameter.Value)
bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value)
{
if (!firstArrayValue)
uriString.Append('&');
firstArrayValue = false;
uriString.Append(parameter.Key);
uriString.Append("[]=");
if (urlEncodeValues)
@@ -86,8 +91,12 @@ namespace CryptoExchange.Net
}
else if (serializationType == ArrayParametersSerialization.MultipleValues)
{
bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value)
{
if (!firstArrayValue)
uriString.Append('&');
firstArrayValue = false;
uriString.Append(parameter.Key);
uriString.Append("=");
if (urlEncodeValues)
@@ -283,122 +292,6 @@ namespace CryptoExchange.Net
return sb.ToString();
}
/// <summary>
/// Create a new uri with the provided parameters as query
/// </summary>
/// <param name="parameters"></param>
/// <param name="baseUri"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
{
var uriBuilder = new UriBuilder();
uriBuilder.Scheme = baseUri.Scheme;
uriBuilder.Host = baseUri.Host;
uriBuilder.Port = baseUri.Port;
uriBuilder.Path = baseUri.AbsolutePath;
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
foreach (var parameter in parameters)
{
if (parameter.Value.GetType().IsArray)
{
if (arraySerialization == ArrayParametersSerialization.JsonArray)
{
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Create a new uri with the provided parameters as query
/// </summary>
/// <param name="parameters"></param>
/// <param name="baseUri"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
{
var uriBuilder = new UriBuilder();
uriBuilder.Scheme = baseUri.Scheme;
uriBuilder.Host = baseUri.Host;
uriBuilder.Port = baseUri.Port;
uriBuilder.Path = baseUri.AbsolutePath;
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
foreach (var parameter in parameters)
{
if (parameter.Value.GetType().IsArray)
{
if (arraySerialization == ArrayParametersSerialization.JsonArray)
{
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Add parameter to URI
/// </summary>
/// <param name="uri"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public static Uri AddQueryParameter(this Uri uri, string name, string value)
{
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
httpValueCollection.Remove(name);
httpValueCollection.Add(name, value);
var ub = new UriBuilder(uri);
ub.Query = httpValueCollection.ToString();
return ub.Uri;
}
/// <summary>
/// Decompress using GzipStream
/// </summary>
@@ -410,20 +303,6 @@ namespace CryptoExchange.Net
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
}
/// <summary>
/// Decompress using GzipStream
/// </summary>
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
{
using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(dataStream, CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream);
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
}
/// <summary>
/// Decompress using GzipStream
/// </summary>
@@ -436,22 +315,6 @@ namespace CryptoExchange.Net
return new ReadOnlySpan<byte>(output.GetBuffer(), 0, (int)output.Length);
}
/// <summary>
/// Decompress using DeflateStream
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
{
var output = new MemoryStream();
using var compressStream = new MemoryStream(input.ToArray());
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
decompressor.CopyTo(output);
output.Position = 0;
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
}
/// <summary>
/// Whether the trading mode is linear
/// </summary>
@@ -607,6 +470,26 @@ namespace CryptoExchange.Net
return services;
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
public static byte[] HexStringToBytes(this string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
}
}
@@ -22,5 +22,10 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// The exchange name
/// </summary>
string Exchange { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
bool Disposed { get; }
}
}
@@ -35,6 +35,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary>
public int CurrentSubscriptions { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
bool Disposed { get; }
/// <summary>
/// Unsubscribe from a stream using the subscription id received when starting the subscription
/// </summary>
@@ -1,109 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Objects;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Message accessor
/// </summary>
public interface IMessageAccessor
{
/// <summary>
/// Is this a valid message
/// </summary>
bool IsValid { get; }
/// <summary>
/// Is the original data available for retrieval
/// </summary>
bool OriginalDataAvailable { get; }
/// <summary>
/// The underlying data object
/// </summary>
object? Underlying { get; }
/// <summary>
/// Clear internal data structure
/// </summary>
void Clear();
/// <summary>
/// Get the type of node
/// </summary>
/// <returns></returns>
NodeType? GetNodeType();
/// <summary>
/// Get the type of node
/// </summary>
/// <param name="path">Access path</param>
/// <returns></returns>
NodeType? GetNodeType(MessagePath path);
/// <summary>
/// Get the value of a path
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
T? GetValue<T>(MessagePath path);
/// <summary>
/// Get the values of an array
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
T?[]? GetValues<T>(MessagePath path);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<object> Deserialize(Type type, MessagePath? path = null);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
CallResult<T> Deserialize<T>(MessagePath? path = null);
/// <summary>
/// Get the original string value
/// </summary>
/// <returns></returns>
string GetOriginalString();
}
/// <summary>
/// Stream message accessor
/// </summary>
public interface IStreamMessageAccessor : IMessageAccessor
{
/// <summary>
/// Load a stream message
/// </summary>
/// <param name="stream"></param>
/// <param name="bufferStream"></param>
Task<CallResult> Read(Stream stream, bool bufferStream);
}
/// <summary>
/// Byte message accessor
/// </summary>
public interface IByteMessageAccessor : IMessageAccessor
{
/// <summary>
/// Load a data message
/// </summary>
/// <param name="data"></param>
CallResult Read(ReadOnlyMemory<byte> data);
}
}
@@ -47,9 +47,21 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)> OnBestOffersChanged;
/// <summary>
/// Timestamp of the last update
/// Timestamp of when the last update was applied to the book, local time
/// </summary>
DateTime UpdateTime { get; }
/// <summary>
/// Timestamp of the last event that was applied, server time
/// </summary>
DateTime? UpdateServerTime { get; }
/// <summary>
/// Timestamp of the last event that was applied, in local time, estimated based on timestamp difference between client and server
/// </summary>
DateTime? UpdateLocalTime { get; }
/// <summary>
/// Age of the data, in local time, estimated based on timestamp difference between client and server + the period since last update
/// </summary>
TimeSpan? DataAge { get; }
/// <summary>
/// The number of asks in the book
@@ -126,5 +138,13 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <returns></returns>
string ToString(int rows);
/// <summary>
/// Output the orderbook to the console
/// </summary>
/// <param name="numberOfEntries">Number of rows to display</param>
/// <param name="refreshInterval">Refresh interval</param>
/// <param name="ct">Cancellation token</param>
Task OutputToConsoleAsync(int numberOfEntries, TimeSpan refreshInterval, CancellationToken ct = default);
}
}
+15 -9
View File
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -105,31 +106,36 @@ namespace CryptoExchange.Net
/// <summary>
/// Create a new HttpMessageHandler instance
/// </summary>
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
public static HttpMessageHandler CreateHttpClientMessageHandler(RestExchangeOptions options)
{
#if NET5_0_OR_GREATER
var socketHandler = new SocketsHttpHandler();
try
{
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
if (options.HttpKeepAliveInterval != null && options.HttpKeepAliveInterval != TimeSpan.Zero)
{
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
socketHandler.KeepAlivePingDelay = options.HttpKeepAliveInterval.Value;
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
}
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
socketHandler.EnableMultipleHttp2Connections = options.HttpEnableMultipleHttp2Connections;
socketHandler.PooledConnectionLifetime = options.HttpPooledConnectionLifetime;
socketHandler.PooledConnectionIdleTimeout = options.HttpPooledConnectionIdleTimeout;
socketHandler.MaxConnectionsPerServer = options.HttpMaxConnectionsPerServer;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
if (options.Proxy != null)
{
socketHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
};
}
return socketHandler;
@@ -143,12 +149,12 @@ namespace CryptoExchange.Net
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
if (options.Proxy != null)
{
httpHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
};
}
return httpHandler;
@@ -22,7 +22,6 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
private static readonly Action<ILogger, int?, string?, Exception?> _restApiReceivedResponse;
static RestApiClientLoggingExtensions()
{
@@ -90,11 +89,6 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug,
new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] Request cancelled by user");
_restApiReceivedResponse = LoggerMessage.Define<int?, string?>(
LogLevel.Trace,
new EventId(4013, "RestApiReceivedResponse"),
"[Req {RequestId}] Received response: {Data}");
}
@@ -161,10 +155,5 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_restApiCancellationRequested(logger, requestId, null);
}
public static void RestApiReceivedResponse(this ILogger logger, int requestId, string? originalData)
{
_restApiReceivedResponse(logger, requestId, originalData, null);
}
}
}
@@ -37,7 +37,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendingPeriodic;
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
private static readonly Action<ILogger, int, string, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
static SocketConnectionLoggingExtension()
@@ -177,10 +177,10 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]");
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
_failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning,
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData(logger, socketId, requestId, data, null);
}
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string listenId, string listenIds)
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
@@ -22,13 +22,15 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, string, Exception?> _orderBookResyncing;
private static readonly Action<ILogger, string, string, Exception?> _orderBookResynced;
private static readonly Action<ILogger, string, string, Exception?> _orderBookMessageSkippedBecauseOfResubscribing;
private static readonly Action<ILogger, string, string, long, long, long, Exception?> _orderBookDataSet;
private static readonly Action<ILogger, string, string, long, long, long?, Exception?> _orderBookDataSet;
private static readonly Action<ILogger, string, string, long, long, long, long, Exception?> _orderBookUpdateBuffered;
private static readonly Action<ILogger, string, string, decimal, decimal, Exception?> _orderBookOutOfSyncDetected;
private static readonly Action<ILogger, string, string, Exception?> _orderBookReconnectingSocket;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookSkippedMessage;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookProcessedMessage;
private static readonly Action<ILogger, string, string, long, Exception?> _orderBookProcessedMessageSingle;
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookOutOfSync;
private static readonly Action<ILogger, string, string, long, long, long, Exception?> _orderBookUpdateSkippedStartEnd;
static SymbolOrderBookLoggingExtensions()
{
@@ -73,7 +75,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"{Api} order book {Symbol} Processing {NumberBufferedUpdated} buffered updates");
_orderBookUpdateSkipped = LoggerMessage.Define<string, string, long, long>(
LogLevel.Debug,
LogLevel.Trace,
new EventId(5008, "OrderBookUpdateSkipped"),
"{Api} order book {Symbol} update skipped #{SequenceNumber}, currently at #{LastSequenceNumber}");
@@ -92,10 +94,10 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(5011, "OrderBookMessageSkippedResubscribing"),
"{Api} order book {Symbol} Skipping message because of resubscribing");
_orderBookDataSet = LoggerMessage.Define<string, string, long, long, long>(
LogLevel.Debug,
_orderBookDataSet = LoggerMessage.Define<string, string, long, long, long?>(
LogLevel.Trace,
new EventId(5012, "OrderBookDataSet"),
"{Api} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{EndUpdateId}");
"{Api} order book {Symbol} snapshot set: {BidCount} bids, {AskCount} asks. #{EndUpdateId}");
_orderBookUpdateBuffered = LoggerMessage.Define<string, string, long, long, long, long>(
LogLevel.Trace,
@@ -136,6 +138,17 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Warning,
new EventId(5020, "OrderBookOutOfSyncChecksum"),
"{Api} order book {Symbol} out of sync. Checksum mismatch, resyncing");
_orderBookProcessedMessageSingle = LoggerMessage.Define<string, string, long>(
LogLevel.Trace,
new EventId(5021, "OrderBookProcessedMessage"),
"{Api} order book {Symbol} update processed #{UpdateId}");
_orderBookUpdateSkippedStartEnd = LoggerMessage.Define<string, string, long, long, long>(
LogLevel.Trace,
new EventId(5022, "OrderBookUpdateSkippedStartEnd"),
"{Api} order book {Symbol} update skipped #{SequenceStart}-#{SequenceEnd}, currently at #{LastSequenceNumber}");
}
public static void OrderBookStatusChanged(this ILogger logger, string api, string symbol, OrderBookStatus previousStatus, OrderBookStatus newStatus)
@@ -194,7 +207,7 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_orderBookMessageSkippedBecauseOfResubscribing(logger, api, symbol, null);
}
public static void OrderBookDataSet(this ILogger logger, string api, string symbol, long bidCount, long askCount, long endUpdateId)
public static void OrderBookDataSet(this ILogger logger, string api, string symbol, long bidCount, long askCount, long? endUpdateId)
{
_orderBookDataSet(logger, api, symbol, bidCount, askCount, endUpdateId, null);
}
@@ -229,9 +242,18 @@ namespace CryptoExchange.Net.Logging.Extensions
_orderBookProcessedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null);
}
public static void OrderBookProcessedMessage(this ILogger logger, string api, string symbol, long updateId)
{
_orderBookProcessedMessageSingle(logger, api, symbol, updateId, null);
}
public static void OrderBookOutOfSyncChecksum(this ILogger logger, string api, string symbol)
{
_orderBookOutOfSyncChecksum(logger, api, symbol, null);
}
public static void OrderBookUpdateSkipped(this ILogger logger, string api, string symbol, long sequenceStart, long sequenceEnd, long lastSequenceNumber)
{
_orderBookUpdateSkippedStartEnd(logger, api, symbol, sequenceStart, sequenceEnd, lastSequenceNumber, null);
}
}
}
@@ -1,129 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Async auto reset based on Stephen Toub`s implementation
/// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-2-asyncautoresetevent/
/// </summary>
public class AsyncResetEvent : IDisposable
{
private static readonly Task<bool> _completed = Task.FromResult(true);
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
#if NET9_0_OR_GREATER
private readonly Lock _waitsLock = new Lock();
#else
private readonly object _waitsLock = new object();
#endif
private bool _signaled;
private readonly bool _reset;
/// <summary>
/// New AsyncResetEvent
/// </summary>
/// <param name="initialState"></param>
/// <param name="reset"></param>
public AsyncResetEvent(bool initialState = false, bool reset = true)
{
_signaled = initialState;
_reset = reset;
}
/// <summary>
/// Wait for the AutoResetEvent to be set
/// </summary>
/// <returns></returns>
public async Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
{
CancellationTokenRegistration registration = default;
try
{
Task<bool> waiter = _completed;
lock (_waitsLock)
{
if (_signaled)
{
if (_reset)
_signaled = false;
}
else if (!ct.IsCancellationRequested)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (timeout.HasValue)
{
var timeoutSource = new CancellationTokenSource(timeout.Value);
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
ct = cancellationSource.Token;
}
registration = ct.Register(() =>
{
lock (_waitsLock)
{
tcs.TrySetResult(false);
// Not the cleanest but it works
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
}
}, useSynchronizationContext: false);
_waits.Enqueue(tcs);
waiter = tcs.Task;
}
}
return await waiter.ConfigureAwait(false);
}
finally
{
registration.Dispose();
}
}
/// <summary>
/// Signal a waiter
/// </summary>
public void Set()
{
lock (_waitsLock)
{
if (!_reset)
{
// Act as ManualResetEvent. Once set keep it signaled and signal everyone who is waiting
_signaled = true;
while (_waits.Count > 0)
{
var toRelease = _waits.Dequeue();
toRelease.TrySetResult(true);
}
}
else
{
// Act as AutoResetEvent. When set signal 1 waiter
if (_waits.Count > 0)
{
var toRelease = _waits.Dequeue();
toRelease.TrySetResult(true);
}
else if (!_signaled)
{
_signaled = true;
}
}
}
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
_waits.Clear();
}
}
}
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Async auto/manual reset event implementation
/// </summary>
public class AsyncResetEvent
{
private readonly Queue<TaskCompletionSource<bool>> _waiters = new();
private readonly bool _autoReset;
private bool _signaled;
#if NET9_0_OR_GREATER
private readonly Lock _waitersLock = new Lock();
#else
private readonly object _waitersLock = new object();
#endif
/// <summary>
/// ctor
/// </summary>
public AsyncResetEvent(bool initialState = false, bool autoReset = true)
{
_signaled = initialState;
_autoReset = autoReset;
}
/// <summary>
/// Wait for the set event
/// </summary>
/// <returns></returns>
public async Task<bool> WaitAsync(
TimeSpan? timeout = null,
CancellationToken ct = default)
{
TaskCompletionSource<bool> tcs;
lock (_waitersLock)
{
if (_signaled)
{
// Already was signaled, can return immediately
if (_autoReset)
_signaled = false;
return true;
}
tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_waiters.Enqueue(tcs);
}
CancellationTokenSource? delayCts = null;
try
{
if (timeout.HasValue || ct.CanBeCanceled)
{
delayCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
var delayTask = Task.Delay(
timeout ?? Timeout.InfiniteTimeSpan,
delayCts.Token);
var completedTask =
await Task.WhenAny(tcs.Task, delayTask)
.ConfigureAwait(false);
if (completedTask != tcs.Task)
{
// This was a timeout or cancellation, need to remove tcs from waiters
// if the tcs was set instead it will be removed in the Set method
if (tcs.TrySetResult(false))
{
lock (_waitersLock)
{
// Dequeue and put in the back of the queue again except for the one we need to remove
int count = _waiters.Count;
for (int i = 0; i < count; i++)
{
var w = _waiters.Dequeue();
if (w != tcs)
_waiters.Enqueue(w);
}
}
}
return false;
}
}
else
{
await tcs.Task.ConfigureAwait(false);
}
return true;
}
finally
{
// Actively stop the delay if tcs.Task won
delayCts?.Cancel();
delayCts?.Dispose();
}
}
/// <summary>
/// Signal a waiter
/// </summary>
public void Set()
{
if (!_autoReset && _signaled)
// Already signaled and not resetting
return;
lock (_waitersLock)
{
if (_autoReset)
{
while (_waiters.Count > 0)
{
// Try to dequeue and set the result
// If result setting was not successful it means timeout/cancellation happened at the same time
// If this is the case this Set isn't the one setting the result and we need to continue
var w = _waiters.Dequeue();
if (w.TrySetResult(true))
return;
}
// No queued waiters, set signaled for next waiter
_signaled = true;
}
else
{
_signaled = true;
// Signal all current waiters
while (_waiters.Count > 0)
{
var w = _waiters.Dequeue();
w.TrySetResult(true);
}
}
}
}
}
}
+6 -6
View File
@@ -531,11 +531,11 @@ namespace CryptoExchange.Net.Objects
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageToken">Next page token</param>
/// <param name="nextPageRequest">Next page request</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null)
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageToken);
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
}
/// <summary>
@@ -545,11 +545,11 @@ namespace CryptoExchange.Net.Objects
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageToken">Next page token</param>
/// <param name="nextPageRequest">Next page token</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null)
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageToken);
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
}
/// <summary>
+34
View File
@@ -250,6 +250,40 @@
DEX
}
/// <summary>
/// Type of platform
/// </summary>
public enum PlatformType
{
/// <summary>
/// Platform to trade cryptocurrency
/// </summary>
CryptoCurrencyExchange,
/// <summary>
/// Platform for trading on predictions
/// </summary>
PredictionMarket,
/// <summary>
/// Other
/// </summary>
Other
}
/// <summary>
/// Centralization type
/// </summary>
public enum CentralizationType
{
/// <summary>
/// Centralized, a person or company is in full control
/// </summary>
Centralized,
/// <summary>
/// Decentralized, governance is split over different entities with no single entity in full control
/// </summary>
Decentralized
}
/// <summary>
/// Timeout behavior for queries
/// </summary>
+9 -1
View File
@@ -211,7 +211,15 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
public DeserializeError(string? message = null, Exception? exception = null)
: base(null,
_errorInfo with
{
Message = message?.Length > 0
? message
: _errorInfo.Message
},
exception) { }
}
/// <summary>
@@ -7,6 +7,11 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public class ApiOptions
{
/// <summary>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool? AutoTimestamp { get; set; }
/// <summary>
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
/// Note that this comes at a performance cost
@@ -8,6 +8,10 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public class ExchangeOptions
{
/// <summary>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool AutoTimestamp { get; set; }
/// <summary>
/// Proxy settings
/// </summary>
@@ -8,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public class RestApiOptions : ApiOptions
{
/// <summary>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool? AutoTimestamp { get; set; }
/// <summary>
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
/// </summary>
@@ -8,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public class RestExchangeOptions: ExchangeOptions
{
/// <summary>
/// Whether or not to automatically sync the local time with the server time
/// </summary>
public bool AutoTimestamp { get; set; }
/// <summary>
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
/// </summary>
@@ -37,10 +32,29 @@ namespace CryptoExchange.Net.Objects.Options
#else
= new Version(1, 1);
#endif
/// <summary>
/// Http client keep alive interval for keeping connections open
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
/// </summary>
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
#if NET5_0_OR_GREATER
/// <summary>
/// Enable multiple simultaneous HTTP 2 connections. Only applied when using dependency injection
/// </summary>
public bool HttpEnableMultipleHttp2Connections { get; set; } = false;
/// <summary>
/// Lifetime of pooled HTTP connections; the time before a connection is recreated. Only applied when using dependency injection
/// </summary>
public TimeSpan HttpPooledConnectionLifetime { get; set; } = TimeSpan.FromMinutes(15);
/// <summary>
/// Idle timeout of pooled HTTP connections; the time before an open connection is closed when there are no requests. Only applied when using dependency injection
/// </summary>
public TimeSpan HttpPooledConnectionIdleTimeout { get; set; } = TimeSpan.FromMinutes(2);
/// <summary>
/// Max number of connections per server. Only applied when using dependency injection
/// </summary>
public int HttpMaxConnectionsPerServer { get; set; } = int.MaxValue;
#endif
/// <summary>
/// Set the values of this options on the target options
@@ -59,6 +73,12 @@ namespace CryptoExchange.Net.Objects.Options
item.CachingMaxAge = CachingMaxAge;
item.HttpVersion = HttpVersion;
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
#if NET5_0_OR_GREATER
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
item.HttpPooledConnectionLifetime = HttpPooledConnectionLifetime;
item.HttpPooledConnectionIdleTimeout = HttpPooledConnectionIdleTimeout;
item.HttpEnableMultipleHttp2Connections = HttpEnableMultipleHttp2Connections;
#endif
return item;
}
}
@@ -27,6 +27,7 @@ namespace CryptoExchange.Net.Objects.Options
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.SocketNoDataTimeout = SocketNoDataTimeout;
item.AutoTimestamp = AutoTimestamp;
item.MaxSocketConnections = MaxSocketConnections;
return item;
}
@@ -76,9 +76,13 @@ namespace CryptoExchange.Net.Objects.Options
public int? ReceiveBufferSize { get; set; }
/// <summary>
/// Whether or not to use the updated deserialization logic, default is true
/// ctor
/// </summary>
public bool UseUpdatedDeserialization { get; set; } = true;
public SocketExchangeOptions()
{
// Enable auto timestamping by default for sockets
AutoTimestamp = true;
}
/// <summary>
/// Create a copy of this options
@@ -88,6 +92,7 @@ namespace CryptoExchange.Net.Objects.Options
public T Set<T>(T item) where T : SocketExchangeOptions, new()
{
item.ApiCredentials = ApiCredentials?.Copy();
item.AutoTimestamp = AutoTimestamp;
item.OutputOriginalData = OutputOriginalData;
item.ReconnectPolicy = ReconnectPolicy;
item.DelayAfterConnect = DelayAfterConnect;
@@ -101,7 +106,6 @@ namespace CryptoExchange.Net.Objects.Options
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimiterEnabled = RateLimiterEnabled;
item.ReceiveBufferSize = ReceiveBufferSize;
item.UseUpdatedDeserialization = UseUpdatedDeserialization;
return item;
}
}
@@ -96,6 +96,23 @@ namespace CryptoExchange.Net.Objects
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a DateTime value as string
/// </summary>
public void AddString(string key, DateTime value)
{
base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary>
/// Add a DateTime value as string. Not added if value is null
/// </summary>
public void AddOptionalString(string key, DateTime? value)
{
if (value != null)
base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp
/// </summary>
@@ -241,6 +258,45 @@ namespace CryptoExchange.Net.Objects
base.Add(key, int.Parse(stringVal));
}
}
/// <summary>
/// Add key as comma separated values
/// </summary>
public void AddCommaSeparated(string key, IEnumerable<string> values)
{
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
public void AddOptionalCommaSeparated(string key, IEnumerable<string>? values)
{
if (values == null || !values.Any())
return;
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as boolean lower case value
/// </summary>
public void AddBoolString(string key, bool value)
{
base.Add(key, value.ToString().ToLower());
}
/// <summary>
/// Add key as boolean lower case value if it's not null
/// </summary>
public void AddOptionalBoolString(string key, bool? value)
{
if (value == null)
return;
base.Add(key, value.ToString()!.ToLower());
}
/// <summary>
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
@@ -0,0 +1,51 @@
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Information on the platform
/// </summary>
public record PlatformInfo
{
/// <summary>
/// Platform id
/// </summary>
public string Id { get; }
/// <summary>
/// Display name
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Logo
/// </summary>
public string Logo { get; }
/// <summary>
/// Url to main application
/// </summary>
public string Url { get; }
/// <summary>
/// Urls to the API documentation
/// </summary>
public string[] ApiDocsUrl { get; }
/// <summary>
/// Platform type
/// </summary>
public PlatformType PlatformType { get; }
/// <summary>
/// Centralization type
/// </summary>
public CentralizationType CentralizationType { get; }
/// <summary>
/// ctor
/// </summary>
public PlatformInfo(string id, string displayName, string logo, string url, string[] apiDocsUrl, PlatformType platformType, CentralizationType centralizationType)
{
Id = id;
DisplayName = displayName;
Logo = logo;
Url = url;
ApiDocsUrl = apiDocsUrl;
PlatformType = platformType;
CentralizationType = centralizationType;
}
}
}
@@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public DateTime? DataTime { get; set; }
/// <summary>
/// The timestamp of the data in local time. Note that this is an estimation based on average delay from the server.
/// </summary>
public DateTime? DataTimeLocal { get; set; }
/// <summary>
/// The age of the data. Note that this is an estimation based on average delay from the server.
/// </summary>
public TimeSpan? DataAge => DateTime.UtcNow - DataTimeLocal;
/// <summary>
/// The stream producing the update
/// </summary>
@@ -43,6 +53,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public SocketUpdateType? UpdateType { get; set; }
/// <summary>
/// Sequence number of the update
/// </summary>
public long? SequenceNumber { get; set; }
/// <summary>
/// ctor
/// </summary>
@@ -116,12 +131,28 @@ namespace CryptoExchange.Net.Objects.Sockets
return this;
}
/// <summary>
/// Specify the sequence number of the update
/// </summary>
public DataEvent<T> WithSequenceNumber(long? sequenceNumber)
{
SequenceNumber = sequenceNumber;
return this;
}
/// <summary>
/// Specify the data timestamp
/// </summary>
public DataEvent<T> WithDataTimestamp(DateTime? timestamp)
public DataEvent<T> WithDataTimestamp(DateTime? timestamp, TimeSpan? offset)
{
if (timestamp == null || timestamp == default(DateTime))
return this;
DataTime = timestamp;
if (offset == null)
return this;
DataTimeLocal = DataTime + offset;
return this;
}
@@ -139,6 +170,6 @@ namespace CryptoExchange.Net.Objects.Sockets
}
/// <inheritdoc />
public override string ToString() => base.ToString().TrimEnd('-') + Data?.ToString();
public override string ToString() => base.ToString().TrimEnd(' ', '-') + " - " + Data?.ToString();
}
}
@@ -109,6 +109,21 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public int Id => _subscription.Id;
/// <summary>
/// The last timestamp anything was received from the server
/// </summary>
public DateTime? LastReceiveTime => _connection.LastReceiveTime;
/// <summary>
/// The current websocket status
/// </summary>
public SocketStatus SocketStatus => _connection.Status;
/// <summary>
/// The current subscription status
/// </summary>
public SubscriptionStatus SubscriptionStatus => _subscription.Status;
/// <summary>
/// ctor
/// </summary>
@@ -73,11 +73,6 @@ namespace CryptoExchange.Net.Objects.Sockets
/// The buffer size to use for receiving data
/// </summary>
public int? ReceiveBufferSize { get; set; } = null;
/// <summary>
/// Whether or not to use the updated deserialization logic
/// </summary>
public bool UseUpdatedDeserialization { get; set; }
/// <summary>
/// ctor
@@ -1,95 +0,0 @@
using System;
using System.Threading;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// The time synchronization state of an API client
/// </summary>
public class TimeSyncState
{
/// <summary>
/// Name of the API
/// </summary>
public string ApiName { get; set; }
/// <summary>
/// Semaphore to use for checking the time syncing. Should be shared instance among the API client
/// </summary>
public SemaphoreSlim Semaphore { get; }
/// <summary>
/// Last sync time for the API client
/// </summary>
public DateTime LastSyncTime { get; set; }
/// <summary>
/// Time offset for the API client
/// </summary>
public TimeSpan TimeOffset { get; set; }
/// <summary>
/// ctor
/// </summary>
public TimeSyncState(string apiName)
{
ApiName = apiName;
Semaphore = new SemaphoreSlim(1, 1);
}
}
/// <summary>
/// Time synchronization info
/// </summary>
public class TimeSyncInfo
{
/// <summary>
/// Logger
/// </summary>
public ILogger Logger { get; }
/// <summary>
/// Should synchronize time
/// </summary>
public bool SyncTime { get; }
/// <summary>
/// Timestamp recalulcation interval
/// </summary>
public TimeSpan RecalculationInterval { get; }
/// <summary>
/// Time sync state for the API client
/// </summary>
public TimeSyncState TimeSyncState { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="recalculationInterval"></param>
/// <param name="syncTime"></param>
/// <param name="syncState"></param>
public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
{
Logger = logger;
SyncTime = syncTime;
RecalculationInterval = recalculationInterval;
TimeSyncState = syncState;
}
/// <summary>
/// Set the time offset
/// </summary>
/// <param name="offset"></param>
public void UpdateTimeOffset(TimeSpan offset)
{
TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName);
TimeSyncState.TimeOffset = TimeSpan.Zero;
}
else
{
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds));
TimeSyncState.TimeOffset = offset;
}
}
}
}
@@ -3,24 +3,28 @@ using System;
namespace CryptoExchange.Net.OrderBook
{
internal class ProcessQueueItem
internal class OrderBookUpdate
{
public long StartUpdateId { get; set; }
public long EndUpdateId { get; set; }
public DateTime? LocalDataTime { get; set; }
public DateTime? ServerDataTime { get; set; }
public long StartSequenceNumber { get; set; }
public long EndSequenceNumber { get; set; }
public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
}
internal class InitialOrderBookItem
internal class OrderBookSnapshot
{
public long StartUpdateId { get; set; }
public long EndUpdateId { get; set; }
public DateTime? LocalDataTime { get; set; }
public DateTime? ServerDataTime { get; set; }
public long? SequenceNumber { get; set; }
public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
}
internal class ChecksumItem
internal class OrderBookChecksum
{
public long? SequenceNumber { get; set; }
public int Checksum { get; set; }
}
}
+286 -65
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
@@ -38,6 +39,7 @@ namespace CryptoExchange.Net.OrderBook
private readonly AsyncResetEvent _queueEvent;
private readonly ConcurrentQueue<object> _processQueue;
private bool _validateChecksum;
private bool _firstUpdateAfterSnapshotDone;
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
{
@@ -49,6 +51,13 @@ namespace CryptoExchange.Net.OrderBook
private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
private enum SequenceNumberResult
{
Skip,
Ok,
OutOfSync
}
/// <summary>
/// A buffer to store messages received before the initial book snapshot is processed. These messages
/// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower
@@ -76,7 +85,12 @@ namespace CryptoExchange.Net.OrderBook
/// the book will resynchronize as it is deemed out of sync
/// </summary>
protected bool _sequencesAreConsecutive;
/// <summary>
/// Whether the first update message after a snapshot may have overlapping sequence numbers instead of the snapshot sequence number + 1
/// </summary>
protected bool _skipSequenceCheckFirstUpdateAfterSnapshotSet;
/// <summary>
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
/// the current level 25 ask out of the top 25, should the level 26 entry be removed from the book or does the server handle this
@@ -133,6 +147,15 @@ namespace CryptoExchange.Net.OrderBook
/// <inheritdoc/>
public DateTime UpdateTime { get; private set; }
/// <inheritdoc/>
public DateTime? UpdateServerTime { get; private set; }
/// <inheritdoc/>
public DateTime? UpdateLocalTime { get; set; }
/// <inheritdoc/>
public TimeSpan? DataAge => DateTime.UtcNow - UpdateLocalTime;
/// <inheritdoc/>
public int AskCount { get; private set; }
@@ -257,6 +280,7 @@ namespace CryptoExchange.Net.OrderBook
_processBuffer.Clear();
_bookSet = false;
_firstUpdateAfterSnapshotDone = false;
Status = OrderBookStatus.Connecting;
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
@@ -308,7 +332,6 @@ namespace CryptoExchange.Net.OrderBook
public async Task StopAsync()
{
_logger.OrderBookStopping(Api, Symbol);
Status = OrderBookStatus.Disconnected;
_cts?.Cancel();
_queueEvent.Set();
if (_processTask != null)
@@ -321,6 +344,7 @@ namespace CryptoExchange.Net.OrderBook
_subscription.ConnectionRestored -= HandleConnectionRestored;
}
Status = OrderBookStatus.Disconnected;
_logger.OrderBookStopped(Api, Symbol);
}
@@ -406,45 +430,100 @@ namespace CryptoExchange.Net.OrderBook
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
/// the order book will be resynchronized
/// </summary>
/// <param name="checksum"></param>
/// <returns></returns>
protected virtual bool DoChecksum(int checksum) => true;
/// <summary>
/// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot
/// received from a socket subscription
/// Set snapshot data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot
/// received from a socket subscription. Will clear any previous data.
/// </summary>
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
/// <param name="askList">List of asks</param>
/// <param name="bidList">List of bids</param>
protected void SetInitialOrderBook(long orderBookSequenceNumber, ISymbolOrderBookEntry[] bidList, ISymbolOrderBookEntry[] askList)
/// <param name="serverDataTime">Server data timestamp</param>
/// <param name="localDataTime">local data timestamp</param>
protected void SetSnapshot(
long? orderBookSequenceNumber,
ISymbolOrderBookEntry[] bidList,
ISymbolOrderBookEntry[] askList,
DateTime? serverDataTime = null,
DateTime? localDataTime = null)
{
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
throw new InvalidOperationException("Trying to set snapshot while book is not working");
_processQueue.Enqueue(
new OrderBookSnapshot
{
LocalDataTime = localDataTime,
ServerDataTime = serverDataTime,
SequenceNumber = orderBookSequenceNumber,
Asks = askList,
Bids = bidList
});
_queueEvent.Set();
}
/// <summary>
/// Add an update to the process queue. Updates the book by providing changed bids and asks, along with an update number which should be higher than the previous update numbers
/// </summary>
/// <param name="updateId">The sequence number</param>
/// <param name="sequenceNumber">The sequence number</param>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(long updateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks)
/// <param name="serverDataTime">Server data timestamp</param>
/// <param name="localDataTime">local data timestamp</param>
protected void UpdateOrderBook(
long sequenceNumber,
ISymbolOrderBookEntry[] bids,
ISymbolOrderBookEntry[] asks,
DateTime? serverDataTime = null,
DateTime? localDataTime = null)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids });
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
throw new InvalidOperationException("Trying to update order book while book is not working");
_processQueue.Enqueue(
new OrderBookUpdate
{
LocalDataTime = localDataTime,
ServerDataTime = serverDataTime,
StartSequenceNumber = sequenceNumber,
EndSequenceNumber = sequenceNumber,
Asks = asks,
Bids = bids
});
_queueEvent.Set();
}
/// <summary>
/// Add an update to the process queue. Updates the book by providing changed bids and asks, along with the first and last sequence number in the update
/// </summary>
/// <param name="firstUpdateId">The sequence number of the first update</param>
/// <param name="lastUpdateId">The sequence number of the last update</param>
/// <param name="firstSequenceNumber">The sequence number of the first update</param>
/// <param name="lastSequenceNumber">The sequence number of the last update</param>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks)
/// <param name="serverDataTime">Server data timestamp</param>
/// <param name="localDataTime">local data timestamp</param>
protected void UpdateOrderBook(
long firstSequenceNumber,
long lastSequenceNumber,
ISymbolOrderBookEntry[] bids,
ISymbolOrderBookEntry[] asks,
DateTime? serverDataTime = null,
DateTime? localDataTime = null)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
throw new InvalidOperationException("Trying to update order book while book is not working");
_processQueue.Enqueue(
new OrderBookUpdate
{
LocalDataTime = localDataTime,
ServerDataTime = serverDataTime,
StartSequenceNumber = firstSequenceNumber,
EndSequenceNumber = lastSequenceNumber,
Asks = asks,
Bids = bids
});
_queueEvent.Set();
}
@@ -453,12 +532,30 @@ namespace CryptoExchange.Net.OrderBook
/// </summary>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(ISymbolOrderSequencedBookEntry[] bids, ISymbolOrderSequencedBookEntry[] asks)
/// <param name="serverDataTime">Server data timestamp</param>
/// <param name="localDataTime">local data timestamp</param>
protected void UpdateOrderBook(
ISymbolOrderSequencedBookEntry[] bids,
ISymbolOrderSequencedBookEntry[] asks,
DateTime? serverDataTime = null,
DateTime? localDataTime = null)
{
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
throw new InvalidOperationException("Trying to update order book while book is not working");
var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0);
var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue);
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = lowest, EndUpdateId = highest, Asks = asks, Bids = bids });
_processQueue.Enqueue(
new OrderBookUpdate
{
LocalDataTime = localDataTime,
ServerDataTime = serverDataTime,
StartSequenceNumber = lowest,
EndSequenceNumber = highest,
Asks = asks,
Bids = bids
});
_queueEvent.Set();
}
@@ -466,9 +563,13 @@ namespace CryptoExchange.Net.OrderBook
/// Add a checksum value to the process queue
/// </summary>
/// <param name="checksum">The checksum value</param>
protected void AddChecksum(int checksum)
/// <param name="sequenceNumber">The sequence number of the message if it's a separate message with separate number</param>
protected void AddChecksum(int checksum, long? sequenceNumber = null)
{
_processQueue.Enqueue(new ChecksumItem() { Checksum = checksum });
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
throw new InvalidOperationException("Trying to add checksum while book is not working");
_processQueue.Enqueue(new OrderBookChecksum() { Checksum = checksum, SequenceNumber = sequenceNumber });
_queueEvent.Set();
}
@@ -481,7 +582,12 @@ namespace CryptoExchange.Net.OrderBook
_logger.OrderBookProcessingBufferedUpdates(Api, Symbol, _processBuffer.Count);
foreach (var bufferEntry in _processBuffer)
ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks);
{
if (_stopProcessing)
break;
ProcessUpdate(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks, true);
}
_processBuffer.Clear();
}
@@ -489,26 +595,10 @@ namespace CryptoExchange.Net.OrderBook
/// <summary>
/// Update order book with an entry
/// </summary>
/// <param name="sequence">Sequence number of the update</param>
/// <param name="type">Type of entry</param>
/// <param name="entry">The entry</param>
protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry)
protected virtual bool UpdateValue(OrderBookEntryType type, ISymbolOrderBookEntry entry)
{
if (sequence <= LastSequenceNumber)
{
_logger.OrderBookSkippedMessage(Api, Symbol, sequence, LastSequenceNumber);
return false;
}
if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
{
// Out of sync
_logger.OrderBookOutOfSync(Api, Symbol, LastSequenceNumber + 1, sequence);
_stopProcessing = true;
Resubscribe();
return false;
}
UpdateTime = DateTime.UtcNow;
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
if (entry.Quantity == 0)
@@ -565,6 +655,42 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(true);
}
/// <summary>
/// Wait until an update has been buffered
/// </summary>
/// <param name="minWait">Min wait time</param>
/// <param name="maxWait">Max wait time</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected async Task<CallResult> WaitUntilFirstUpdateBufferedAsync(TimeSpan? minWait, TimeSpan maxWait, CancellationToken ct)
{
var startWait = DateTime.UtcNow;
while (_processBuffer.Count == 0)
{
if (ct.IsCancellationRequested)
return new CallResult(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > maxWait)
return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try
{
await Task.Delay(20, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{ }
}
if (minWait != null)
{
var dif = DateTime.UtcNow - startWait;
if (dif < minWait)
await Task.Delay(minWait.Value - dif).ConfigureAwait(false);
}
return CallResult.SuccessResult;
}
/// <summary>
/// IDisposable implementation for the order book
/// </summary>
@@ -614,6 +740,12 @@ namespace CryptoExchange.Net.OrderBook
{
var stringBuilder = new StringBuilder();
var book = Book;
stringBuilder.AppendLine($"{Exchange} - {Symbol}");
stringBuilder.AppendLine($"Update time local: {UpdateTime:HH:mm:ss.fff} ({Math.Round((DateTime.UtcNow - UpdateTime).TotalMilliseconds)}ms ago)");
stringBuilder.AppendLine($"Data timestamp server: {UpdateServerTime:HH:mm:ss.fff}");
stringBuilder.AppendLine($"Data timestamp local: {UpdateLocalTime:HH:mm:ss.fff}");
stringBuilder.AppendLine($"Data age: {DataAge?.TotalMilliseconds}ms");
stringBuilder.AppendLine();
stringBuilder.AppendLine($" Ask quantity Ask price | Bid price Bid quantity");
for(var i = 0; i < numberOfEntries; i++)
{
@@ -625,6 +757,22 @@ namespace CryptoExchange.Net.OrderBook
return stringBuilder.ToString();
}
/// <inheritdoc />
public Task OutputToConsoleAsync(int numberOfEntries, TimeSpan refreshInterval, CancellationToken ct = default)
{
return Task.Run(async () =>
{
var referenceTime = DateTime.UtcNow;
while (!ct.IsCancellationRequested)
{
Console.Clear();
Console.WriteLine(ToString(numberOfEntries));
var delay = Math.Max(1, (DateTime.UtcNow - referenceTime).TotalMilliseconds % refreshInterval.TotalMilliseconds);
try { await Task.Delay(refreshInterval.Add(TimeSpan.FromMilliseconds(-delay)), ct).ConfigureAwait(false); } catch { }
}
});
}
private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk)
{
var (bestBid, bestAsk) = BestOffers;
@@ -641,8 +789,11 @@ namespace CryptoExchange.Net.OrderBook
// Clear queue
while (_processQueue.TryDequeue(out _)) { }
LastSequenceNumber = 0;
_processBuffer.Clear();
_bookSet = false;
_firstUpdateAfterSnapshotDone = false;
DoReset();
}
@@ -680,17 +831,17 @@ namespace CryptoExchange.Net.OrderBook
continue;
}
if (item is InitialOrderBookItem iobi)
ProcessInitialOrderBookItem(iobi);
if (item is ProcessQueueItem pqi)
ProcessQueueItem(pqi);
else if (item is ChecksumItem ci)
ProcessChecksum(ci);
if (item is OrderBookSnapshot snapshot)
ProcessOrderBookSnapshot(snapshot);
if (item is OrderBookUpdate update)
ProcessQueueItem(update);
else if (item is OrderBookChecksum checksum)
ProcessChecksum(checksum);
}
}
}
private void ProcessInitialOrderBookItem(InitialOrderBookItem item)
private void ProcessOrderBookSnapshot(OrderBookSnapshot item)
{
lock (_bookLock)
{
@@ -702,20 +853,25 @@ namespace CryptoExchange.Net.OrderBook
foreach (var bid in item.Bids)
_bids.Add(bid.Price, bid);
LastSequenceNumber = item.EndUpdateId;
if (item.SequenceNumber != null)
LastSequenceNumber = item.SequenceNumber.Value;
AskCount = _asks.Count;
BidCount = _bids.Count;
UpdateTime = DateTime.UtcNow;
_logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId);
UpdateServerTime = item.ServerDataTime;
UpdateLocalTime = item.LocalDataTime;
_logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.SequenceNumber);
CheckProcessBuffer();
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
}
}
private void ProcessQueueItem(ProcessQueueItem item)
private void ProcessQueueItem(OrderBookUpdate item)
{
lock (_bookLock)
{
@@ -725,19 +881,19 @@ namespace CryptoExchange.Net.OrderBook
{
Asks = item.Asks,
Bids = item.Bids,
FirstUpdateId = item.StartUpdateId,
LastUpdateId = item.EndUpdateId,
FirstUpdateId = item.StartSequenceNumber,
LastUpdateId = item.EndSequenceNumber,
});
if (_logger.IsEnabled(LogLevel.Trace))
_logger.OrderBookUpdateBuffered(Api, Symbol, item.StartUpdateId, item.EndUpdateId, item.Asks.Length, item.Bids.Length);
_logger.OrderBookUpdateBuffered(Api, Symbol, item.StartSequenceNumber, item.EndSequenceNumber, item.Asks.Length, item.Bids.Length);
}
else
{
CheckProcessBuffer();
var (prevBestBid, prevBestAsk) = BestOffers;
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
ProcessUpdate(item.StartSequenceNumber, item.EndSequenceNumber, item.Bids, item.Asks, false);
if (_asks.Count == 0 || _bids.Count == 0)
return;
@@ -750,13 +906,16 @@ namespace CryptoExchange.Net.OrderBook
return;
}
UpdateServerTime = item.ServerDataTime;
UpdateLocalTime = item.LocalDataTime;
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
CheckBestOffersChanged(prevBestBid, prevBestAsk);
}
}
}
private void ProcessChecksum(ChecksumItem ci)
private void ProcessChecksum(OrderBookChecksum ci)
{
lock (_bookLock)
{
@@ -776,6 +935,9 @@ namespace CryptoExchange.Net.OrderBook
throw;
}
if (ci.SequenceNumber != null)
LastSequenceNumber = ci.SequenceNumber.Value;
if (!checksumResult)
{
_logger.OrderBookOutOfSyncChecksum(Api, Symbol);
@@ -813,40 +975,99 @@ namespace CryptoExchange.Net.OrderBook
});
}
private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
private void ProcessUpdate(
long updateSequenceNumberStart,
long updateSequenceNumberEnd,
IEnumerable<ISymbolOrderBookEntry> bids,
IEnumerable<ISymbolOrderBookEntry> asks,
bool fromBuffer)
{
if (lastUpdateId <= LastSequenceNumber)
var sequenceResult = fromBuffer ? ValidateBufferSequenceNumber(updateSequenceNumberStart, updateSequenceNumberEnd) : ValidateLiveSequenceNumber(updateSequenceNumberStart);
if (sequenceResult == SequenceNumberResult.Skip)
{
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
if (updateSequenceNumberStart != updateSequenceNumberEnd)
_logger.OrderBookUpdateSkipped(Api, Symbol, updateSequenceNumberStart, updateSequenceNumberEnd, LastSequenceNumber);
else
_logger.OrderBookUpdateSkipped(Api, Symbol, updateSequenceNumberStart, LastSequenceNumber);
return;
}
if (sequenceResult == SequenceNumberResult.OutOfSync)
{
_logger.OrderBookOutOfSync(Api, Symbol, LastSequenceNumber + 1, updateSequenceNumberStart);
_stopProcessing = true;
Resubscribe();
return;
}
foreach (var entry in bids)
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Bid, entry);
UpdateValue(OrderBookEntryType.Bid, entry);
foreach (var entry in asks)
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry);
UpdateValue(OrderBookEntryType.Ask, entry);
if (Levels.HasValue && _strictLevels)
{
while (this._bids.Count > Levels.Value)
while (_bids.Count > Levels.Value)
{
BidCount--;
this._bids.Remove(this._bids.Last().Key);
_bids.Remove(_bids.Last().Key);
}
while (this._asks.Count > Levels.Value)
while (_asks.Count > Levels.Value)
{
AskCount--;
this._asks.Remove(this._asks.Last().Key);
_asks.Remove(this._asks.Last().Key);
}
}
LastSequenceNumber = lastUpdateId;
_firstUpdateAfterSnapshotDone = true;
LastSequenceNumber = updateSequenceNumberEnd;
if (_logger.IsEnabled(LogLevel.Trace))
_logger.OrderBookProcessedMessage(Api, Symbol, firstUpdateId, lastUpdateId);
}
{
if (updateSequenceNumberStart != updateSequenceNumberEnd)
_logger.OrderBookProcessedMessage(Api, Symbol, updateSequenceNumberStart, updateSequenceNumberEnd);
else
_logger.OrderBookProcessedMessage(Api, Symbol, updateSequenceNumberStart);
}
}
private SequenceNumberResult ValidateBufferSequenceNumber(long startSequenceNumber, long endSequenceNumber)
{
if (endSequenceNumber <= LastSequenceNumber)
// Buffered update is from before the snapshot, ignore
return SequenceNumberResult.Skip;
if (_sequencesAreConsecutive && startSequenceNumber != LastSequenceNumber + 1)
{
if (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet)
// Buffered update is not the next sequence number when it was expected to be
return SequenceNumberResult.OutOfSync;
}
// Buffered sequence number is larger than the last sequence number
return SequenceNumberResult.Ok;
}
private SequenceNumberResult ValidateLiveSequenceNumber(long sequenceNumber)
{
if (sequenceNumber < LastSequenceNumber
&& (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet))
// Update is somehow from before the current state
return SequenceNumberResult.OutOfSync;
if (_sequencesAreConsecutive
&& LastSequenceNumber != 0
&& sequenceNumber != LastSequenceNumber + 1)
{
if (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet)
return SequenceNumberResult.OutOfSync;
}
return SequenceNumberResult.Ok;
}
}
internal class DescComparer<T> : IComparer<T>
@@ -17,11 +17,11 @@ namespace CryptoExchange.Net.RateLimiting.Filters
/// <param name="path"></param>
public PathStartFilter(string path)
{
_path = path;
_path = path.TrimStart('/');
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
=> definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase);
}
}
+12 -5
View File
@@ -12,14 +12,16 @@ namespace CryptoExchange.Net.Requests
public class RequestFactory : IRequestFactory
{
private HttpClient? _httpClient;
private RestExchangeOptions? _options;
/// <inheritdoc />
public void Configure(RestExchangeOptions options, HttpClient? client = null)
{
if (client == null)
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
client = CreateClient(options);
_httpClient = client;
_options = options;
}
/// <inheritdoc />
@@ -39,15 +41,20 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
{
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
var newOptions = new RestExchangeOptions();
_options!.Set(newOptions);
newOptions.Proxy = proxy;
newOptions.RequestTimeout = requestTimeout;
newOptions.HttpKeepAliveInterval = httpKeepAliveInterval;
_httpClient = CreateClient(newOptions);
}
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
private static HttpClient CreateClient(RestExchangeOptions options)
{
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
var handler = LibraryHelpers.CreateHttpClientMessageHandler(options);
var client = new HttpClient(handler)
{
Timeout = requestTimeout
Timeout = options.RequestTimeout
};
return client;
}
@@ -0,0 +1,21 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Transfer status
/// </summary>
public enum SharedTransferStatus
{
/// <summary>
/// In progress
/// </summary>
InProgress,
/// <summary>
/// Failed
/// </summary>
Failed,
/// <summary>
/// Completed
/// </summary>
Completed
}
}
@@ -1,111 +0,0 @@
using System;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// A token which a request can use to retrieve the next page if there are more pages in the result set
/// </summary>
public interface INextPageToken
{
}
/// <summary>
/// A datetime offset token
/// </summary>
public record DateTimeToken: INextPageToken
{
/// <summary>
/// Last result time
/// </summary>
public DateTime LastTime { get; set; }
/// <summary>
/// ctor
/// </summary>
public DateTimeToken(DateTime timestamp)
{
LastTime = timestamp;
}
}
/// <summary>
/// A current page index token
/// </summary>
public record PageToken: INextPageToken
{
/// <summary>
/// The next page index
/// </summary>
public int Page { get; set; }
/// <summary>
/// Page size
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// ctor
/// </summary>
public PageToken(int page, int pageSize)
{
Page = page;
PageSize = pageSize;
}
}
/// <summary>
/// A id offset token
/// </summary>
public record FromIdToken : INextPageToken
{
/// <summary>
/// The last id from previous result
/// </summary>
public string FromToken { get; set; }
/// <summary>
/// ctor
/// </summary>
public FromIdToken(string fromToken)
{
FromToken = fromToken;
}
}
/// <summary>
/// A cursor token
/// </summary>
public record CursorToken : INextPageToken
{
/// <summary>
/// The next page cursor
/// </summary>
public string Cursor { get; set; }
/// <summary>
/// ctor
/// </summary>
public CursorToken(string cursor)
{
Cursor = cursor;
}
}
/// <summary>
/// A result offset token
/// </summary>
public record OffsetToken : INextPageToken
{
/// <summary>
/// Offset in the result set
/// </summary>
public int Offset { get; set; }
/// <summary>
/// ctor
/// </summary>
public OffsetToken(int offset)
{
Offset = offset;
}
}
}
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
/// Get funding rate records
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -73,14 +73,14 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Spot get closed orders request options
/// </summary>
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedFuturesOrdersOptions { get; }
GetClosedOrdersOptions GetClosedFuturesOrdersOptions { get; }
/// <summary>
/// Get info on closed futures orders
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
/// <summary>
/// Futures get order trades request options
@@ -96,14 +96,14 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Futures user trades request options
/// </summary>
PaginatedEndpointOptions<GetUserTradesRequest> GetFuturesUserTradesOptions { get; }
GetUserTradesOptions GetFuturesUserTradesOptions { get; }
/// <summary>
/// Get futures user trade records
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
/// <summary>
/// Futures cancel order request options
@@ -12,6 +12,25 @@ namespace CryptoExchange.Net.SharedApis
/// Futures symbol request options
/// </summary>
EndpointOptions<GetSymbolsRequest> GetFuturesSymbolsOptions { get; }
/// <summary>
/// Get all futures symbols for a specific base asset
/// </summary>
/// <param name="baseAsset">Asset, for example `ETH`</param>
Task<ExchangeResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
/// <summary>
/// Gets whether the client supports a futures symbol
/// </summary>
/// <param name="symbol">The symbol</param>
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(SharedSymbol symbol);
/// <summary>
/// Gets whether the client supports a futures symbol
/// </summary>
/// <param name="symbolName">The symbol name</param>
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(string symbolName);
/// <summary>
/// Get info on all futures symbols supported on the exchange
/// </summary>
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
/// Get index price kline/candlestick data
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
/// Get mark price kline/candlestick data
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
/// Get position history
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -30,9 +30,9 @@ namespace CryptoExchange.Net.SharedApis
/// Get deposit records
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
/// Get kline/candlestick data
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
/// Get public trade history
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
/// Get withdrawal records
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -72,14 +72,14 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Spot get closed orders request options
/// </summary>
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedSpotOrdersOptions { get; }
GetClosedOrdersOptions GetClosedSpotOrdersOptions { get; }
/// <summary>
/// Get info on closed spot orders
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
/// <summary>
/// Spot get order trades request options
@@ -95,14 +95,14 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Spot user trades request options
/// </summary>
PaginatedEndpointOptions<GetUserTradesRequest> GetSpotUserTradesOptions { get; }
GetUserTradesOptions GetSpotUserTradesOptions { get; }
/// <summary>
/// Get spot user trade records
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
/// <summary>
/// Spot cancel order request options
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -13,6 +14,24 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
EndpointOptions<GetSymbolsRequest> GetSpotSymbolsOptions { get; }
/// <summary>
/// Get all spot symbols for a specific base asset
/// </summary>
/// <param name="baseAsset">Asset, for example `ETH`</param>
Task<ExchangeResult<SharedSymbol[]>> GetSpotSymbolsForBaseAssetAsync(string baseAsset);
/// <summary>
/// Gets whether the client supports a spot symbol
/// </summary>
/// <param name="symbol">The symbol</param>
Task<ExchangeResult<bool>> SupportsSpotSymbolAsync(SharedSymbol symbol);
/// <summary>
/// Gets whether the client supports a spot symbol
/// </summary>
/// <param name="symbolName">The symbol name</param>
Task<ExchangeResult<bool>> SupportsSpotSymbolAsync(string symbolName);
/// <summary>
/// Get info on all available spot symbols on the exchange
/// </summary>
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
@@ -99,6 +100,9 @@ namespace CryptoExchange.Net.SharedApis
if (val == null)
return default;
if (val.Value is T typeVal)
return typeVal;
try
{
Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
@@ -38,6 +38,17 @@ namespace CryptoExchange.Net.SharedApis
Exchange = exchange;
}
/// <summary>
/// ctor
/// </summary>
public ExchangeResult(
string exchange,
T result) :
base(result, null, null)
{
Exchange = exchange;
}
/// <inheritdoc />
public override string ToString() => $"{Exchange} - " + base.ToString();
}
@@ -24,9 +24,9 @@ namespace CryptoExchange.Net.SharedApis
public TradingMode[]? DataTradeMode { get; }
/// <summary>
/// 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
/// </summary>
public INextPageToken? NextPageToken { get; }
public PageRequest? NextPageRequest { get; }
/// <summary>
/// ctor
@@ -46,7 +46,7 @@ namespace CryptoExchange.Net.SharedApis
string exchange,
TradingMode dataTradeMode,
WebCallResult<T> 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;
}
/// <summary>
@@ -74,7 +74,7 @@ namespace CryptoExchange.Net.SharedApis
string exchange,
TradingMode[]? dataTradeModes,
WebCallResult<T> 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;
}
/// <summary>
@@ -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;
}
/// <summary>
@@ -144,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
/// <returns></returns>
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
{
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageRequest);
}
/// <inheritdoc />
@@ -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
/// </summary>
public class GetClosedOrdersOptions : PaginatedEndpointOptions<GetClosedOrdersRequest>
{
/// <summary>
/// Whether the start/end time filter is supported
/// </summary>
public bool TimeFilterSupported { get; set; }
/// <summary>
/// ctor
/// </summary>
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;
}
/// <inheritdoc />
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();
}
}
@@ -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
/// </summary>
public class GetDepositsOptions : PaginatedEndpointOptions<GetDepositsRequest>
{
/// <summary>
/// Whether the start/end time filter is supported
/// </summary>
public bool TimeFilterSupported { get; set; }
/// <summary>
/// ctor
/// </summary>
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;
}
/// <inheritdoc />
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();
}
}
@@ -1,4 +1,8 @@
namespace CryptoExchange.Net.SharedApis
using CryptoExchange.Net.Objects;
using System;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for requesting funding rate history
@@ -8,8 +12,43 @@
/// <summary>
/// ctor
/// </summary>
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)
{
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override string ToString(string exchange)
{
var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
return sb.ToString();
}
}
}
@@ -18,15 +18,12 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of data points which can be requested
/// </summary>
public int? MaxTotalDataPoints { get; set; }
/// <summary>
/// The max age of the data that can be requested
/// </summary>
public TimeSpan? MaxAge { get; set; }
/// <summary>
/// ctor
/// </summary>
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
/// <summary>
/// ctor
/// </summary>
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}");
@@ -1,4 +1,8 @@
namespace CryptoExchange.Net.SharedApis
using CryptoExchange.Net.Objects;
using System;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for requesting position history
@@ -8,8 +12,43 @@
/// <summary>
/// ctor
/// </summary>
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)
{
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override string ToString(string exchange)
{
var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
return sb.ToString();
}
}
}
@@ -9,34 +9,27 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public class GetTradeHistoryOptions : PaginatedEndpointOptions<GetTradeHistoryRequest>
{
/// <summary>
/// The max age of data that can be requested
/// </summary>
public TimeSpan? MaxAge { get; set; }
/// <summary>
/// ctor
/// </summary>
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)
{
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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();
}
}
}
@@ -0,0 +1,54 @@
using CryptoExchange.Net.Objects;
using System;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for requesting user trades
/// </summary>
public class GetUserTradesOptions : PaginatedEndpointOptions<GetUserTradesRequest>
{
/// <summary>
/// ctor
/// </summary>
public GetUserTradesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
{
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override string ToString(string exchange)
{
var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
return sb.ToString();
}
}
}
@@ -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
/// </summary>
public class GetWithdrawalsOptions : PaginatedEndpointOptions<GetWithdrawalsRequest>
{
/// <summary>
/// Whether the start/end time filter is supported
/// </summary>
public bool TimeFilterSupported { get; set; }
/// <summary>
/// ctor
/// </summary>
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;
}
/// <inheritdoc />
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();
}
}
@@ -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
{
/// <summary>
/// Type of pagination supported
/// Whether ascending data retrieval and pagination is available
/// </summary>
public SharedPaginationSupport PaginationSupport { get; }
public bool SupportsAscending { get; set; }
/// <summary>
/// Whether ascending data retrieval and pagination is available
/// </summary>
public bool SupportsDescending { get; set; }
/// <summary>
/// Whether filtering based on start/end time is supported
@@ -28,12 +33,23 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public int MaxLimit { get; set; }
/// <summary>
/// Max age of data that can be requested
/// </summary>
public TimeSpan? MaxAge { get; set; }
/// <summary>
/// ctor
/// </summary>
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();
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Data direction
/// </summary>
public enum DataDirection
{
/// <summary>
/// Old to new order
/// </summary>
Ascending,
/// <summary>
/// New to old order
/// </summary>
Descending
}
}
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Next page request info
/// </summary>
public class PageRequest
{
/// <summary>
/// Pagination cursor
/// </summary>
public string? Cursor { get; set; }
/// <summary>
/// Page number
/// </summary>
public int? Page { get; set; }
/// <summary>
/// Result offset
/// </summary>
public int? Offset { get; set; }
/// <summary>
/// From id filter
/// </summary>
public string? FromId { get; set; }
/// <summary>
/// Start time filter
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// End time filter
/// </summary>
public DateTime? EndTime { get; set; }
}
}
@@ -0,0 +1,405 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Pagination methods
/// </summary>
public static class Pagination
{
/// <summary>
/// Get pagination parameters
/// </summary>
/// <param name="direction">The data direction</param>
/// <param name="limit">Result limit</param>
/// <param name="requestStartTime">User request start time</param>
/// <param name="requestEndTime">User request end time</param>
/// <param name="paginationRequest">Provided page request</param>
/// <param name="setOtherTimeLimiter">Whether to set start time if direction is descending, or end time if direction is ascending</param>
/// <param name="maxPeriod">Max period the time filters can span</param>
/// <returns></returns>
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
};
}
/// <summary>
/// Get the next page request parameters from result kline data
/// </summary>
/// <param name="nextPageRequest">Callback for returning the next page request</param>
/// <param name="resultCount">Number of results in data</param>
/// <param name="timestamps">Timestamps of the result data</param>
/// <param name="requestStartTime">User request start time</param>
/// <param name="requestEndTime">User request end time</param>
/// <param name="lastPaginationData">The last used pagination data</param>
/// <param name="interval">Kline interval</param>
/// <returns></returns>
public static PageRequest? GetNextPageRequestKlines(
Func<PageRequest?> nextPageRequest,
int resultCount,
IEnumerable<DateTime> 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;
}
/// <summary>
/// Get the next page request parameters from result data
/// </summary>
/// <param name="nextPageRequest">Callback for returning the next page request</param>
/// <param name="resultCount">Number of results in data</param>
/// <param name="timestamps">Timestamps of the result data</param>
/// <param name="requestStartTime">User request start time</param>
/// <param name="requestEndTime">User request end time</param>
/// <param name="lastPaginationData">The last used pagination data</param>
/// <param name="maxPeriod">Max period the time filters can span</param>
/// <param name="maxAge">Max age of the data</param>
/// <returns></returns>
public static PageRequest? GetNextPageRequest(
Func<PageRequest?> nextPageRequest,
int resultCount,
IEnumerable<DateTime> 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;
}
/// <summary>
/// Check whether there is (potentially) another page available
/// </summary>
/// <param name="resultCount">Number of result entries</param>
/// <param name="timestamps">Timestamps</param>
/// <param name="requestStartTime">User request start time</param>
/// <param name="requestEndTime">User request end time</param>
/// <param name="limit">Max number of results requested</param>
/// <param name="direction">Data direction</param>
/// <param name="interval">Kline interval</param>
/// <returns></returns>
public static bool HasNextPageKlines(
int resultCount,
IEnumerable<DateTime> 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;
}
}
/// <summary>
/// Check whether there is (potentially) another page available
/// </summary>
/// <param name="resultCount">Number of result entries</param>
/// <param name="timestamps">Timestamps</param>
/// <param name="requestStartTime">User request start time</param>
/// <param name="requestEndTime">User request end time</param>
/// <param name="limit">Max number of results requested</param>
/// <param name="direction">Data direction</param>
/// <returns></returns>
public static bool HasNextPage(
int resultCount,
IEnumerable<DateTime> 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;
}
}
/// <summary>
/// Get the next page PageRequest
/// </summary>
public static PageRequest NextPageFromPage(PaginationParameters lastPaginationData)
{
return new PageRequest { Page = (lastPaginationData.Page ?? 1) + 1 };
}
/// <summary>
/// Get the next offset PageRequest
/// </summary>
public static PageRequest NextPageFromOffset(PaginationParameters lastPaginationData, int resultCount)
{
return new PageRequest { Offset = (lastPaginationData.Offset ?? 0) + resultCount };
}
/// <summary>
/// Get the next page cursor PageRequest
/// </summary>
public static PageRequest NextPageFromCursor(string nextCursor)
{
return new PageRequest { Cursor = nextCursor };
}
/// <summary>
/// Get the next id PageRequest
/// </summary>
public static PageRequest NextPageFromId(long nextFromId)
{
return new PageRequest { FromId = nextFromId.ToString() };
}
/// <summary>
/// Get the next id PageRequest
/// </summary>
public static PageRequest NextPageFromId(string nextFromId)
{
return new PageRequest { FromId = nextFromId };
}
/// <summary>
/// Get the next start/end time PageRequest
/// </summary>
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 };
}
/// <summary>
/// Get the next start/end time klines PageRequest
/// </summary>
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 };
}
}
/// <summary>
/// Whether another time period is to be requested
/// </summary>
/// <param name="requestStartTime">User request start time</param>
/// <param name="requestEndTime">User request end time</param>
/// <param name="direction">Data direction</param>
/// <param name="lastPaginationParameters">Pagination parameters used</param>
/// <param name="period">Max time period a request can span</param>
/// <param name="maxAge">Max age of data that can be requested</param>
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;
}
}
}
/// <summary>
/// Get the start/end time for the next data period
/// </summary>
/// <param name="requestStartTime">User request start time</param>
/// <param name="requestEndTime">User request end time</param>
/// <param name="direction">Data direction</param>
/// <param name="lastPaginationParameters">Pagination parameters used</param>
/// <param name="period">Max time period a request can span</param>
/// <param name="maxAge">Max age of data that can be requested</param>
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);
}
}
}
@@ -0,0 +1,43 @@
using System;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Pagination parameters
/// </summary>
public record PaginationParameters
{
/// <summary>
/// Data direction
/// </summary>
public DataDirection Direction { get; set; }
/// <summary>
/// Start time filter
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// End time filter
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// Id filter
/// </summary>
public string? FromId { get; set; }
/// <summary>
/// Result offset
/// </summary>
public int? Offset { get; set; }
/// <summary>
/// Page number
/// </summary>
public int? Page { get; set; }
/// <summary>
/// Pagination cursor
/// </summary>
public string? Cursor { get; set; }
/// <summary>
/// Max number of results
/// </summary>
public int Limit { get; set; }
}
}
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of results
/// </summary>
public int? Limit { get; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of results
/// </summary>
public int? Limit { get; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of results
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of results
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -32,13 +36,15 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -27,6 +27,10 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of results
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -35,13 +39,15 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
/// <summary>
@@ -51,13 +57,15 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -10,15 +10,19 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Filter by start time
/// </summary>
public DateTime StartTime { get; }
public DateTime StartTime { get; set; }
/// <summary>
/// Filter by end time
/// </summary>
public DateTime EndTime { get; }
public DateTime? EndTime { get; set; }
/// <summary>
/// Max number of results
/// </summary>
public int? Limit { get; }
public int? Limit { get; set; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of results
/// </summary>
public int? Limit { get; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
/// Max number of results
/// </summary>
public int? Limit { get; }
/// <summary>
/// Data direction
/// </summary>
public DataDirection? Direction { get; set; }
/// <summary>
/// ctor
@@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="startTime">Filter by start time</param>
/// <param name="endTime">Filter by end time</param>
/// <param name="limit">Max number of results</param>
/// <param name="direction">Data direction</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
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;
}
}
}
@@ -44,15 +44,21 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public bool Completed { get; set; }
/// <summary>
/// Status of the deposit
/// </summary>
public SharedTransferStatus Status { get; set; }
/// <summary>
/// ctor
/// </summary>
public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp)
public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp, SharedTransferStatus status)
{
Asset = asset;
Quantity = quantity;
Timestamp = timestamp;
Completed = completed;
Status = status;
}
}
@@ -20,6 +20,10 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public SharedPositionSide PositionSide { get; set; }
/// <summary>
/// Whether the position is one way mode
/// </summary>
public SharedPositionMode PositionMode { get; set; }
/// <summary>
/// Average open price
/// </summary>
public decimal? AverageOpenPrice { get; set; }

Some files were not shown because too many files have changed in this diff Show More