mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc2d3fc2d2 | |||
| 187ca6a4ef | |||
| 3b2a85d210 | |||
| c512bee825 | |||
| 0943b052b9 | |||
| a896fffdb3 | |||
| 177daf903b | |||
| aa1ebdc4ed | |||
| 38058c4a70 | |||
| a7eb483479 | |||
| c76931a3b4 | |||
| b90b7e9e0c | |||
| beda53d36d | |||
| 0668f669c1 | |||
| 64250e13db | |||
| 451d38d5e7 | |||
| e11e437bbb |
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||||
<PackageVersion>10.0.0</PackageVersion>
|
<PackageVersion>10.0.1</PackageVersion>
|
||||||
<AssemblyVersion>10.0.0</AssemblyVersion>
|
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||||
<FileVersion>10.0.0</FileVersion>
|
<FileVersion>10.0.1</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CryptoExchange.Net" Version="9.13.0" />
|
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
|
||||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
Protobuf support for CryptoExchange.Net.
|
Protobuf support for CryptoExchange.Net.
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 10.0.1 - 16 Dec 2025
|
||||||
|
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
* Version 10.0.0 - 16 Dec 2025
|
* Version 10.0.0 - 16 Dec 2025
|
||||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -63,8 +63,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
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 IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(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 AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
|
|||||||
@@ -160,11 +160,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
ParameterPositions[method] = position;
|
ParameterPositions[method] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
@@ -172,11 +167,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi2Client : RestApiClient
|
public class TestRestApi2Client : RestApiClient
|
||||||
@@ -198,12 +188,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
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)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
@@ -212,10 +197,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestError
|
public class TestError
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ using System.Linq;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication
|
||||||
{
|
{
|
||||||
@@ -76,12 +78,20 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authenticate a request
|
/// Authenticate a REST request
|
||||||
/// </summary>
|
/// </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>
|
/// <param name="requestConfig">The request configuration</param>
|
||||||
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
|
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>
|
/// <summary>
|
||||||
/// SHA256 sign the data and return the bytes
|
/// SHA256 sign the data and return the bytes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -442,6 +452,14 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="buff"></param>
|
/// <param name="buff"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string BytesToHexString(byte[] buff)
|
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
|
#if NET9_0_OR_GREATER
|
||||||
return Convert.ToHexString(buff);
|
return Convert.ToHexString(buff);
|
||||||
@@ -453,6 +471,26 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
#endif
|
#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>
|
/// <summary>
|
||||||
/// Convert byte array to base64 string
|
/// Convert byte array to base64 string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -466,32 +504,50 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get current timestamp including the time sync offset from the api client
|
/// Get current timestamp including the time sync offset from the api client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
protected DateTime GetTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
/// <returns></returns>
|
|
||||||
protected DateTime GetTimestamp(RestApiClient apiClient)
|
|
||||||
{
|
{
|
||||||
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 result = TimeProvider.GetTime().Add(TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||||
|
if (includeOneSecondOffset)
|
||||||
|
result = result.AddSeconds(-1);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
protected string GetMillisecondTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
/// <returns></returns>
|
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
|
||||||
{
|
/// <summary>
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
/// 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>
|
/// <summary>
|
||||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
protected long GetMillisecondTimestampLong(RestApiClient apiClient, bool includeOneSecondOffset = true)
|
||||||
/// <returns></returns>
|
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
|
||||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
|
||||||
{
|
/// <summary>
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
/// 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>
|
/// <summary>
|
||||||
/// Return the serialized request body
|
/// Return the serialized request body
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||||
{
|
{
|
||||||
|
private string? _clientName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logger
|
/// Logger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -23,6 +25,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
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>
|
/// <summary>
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract TimeSpan? GetTimeOffset();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int TotalRequestsMade { get; set; }
|
public int TotalRequestsMade { get; set; }
|
||||||
|
|
||||||
@@ -115,6 +109,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
|
TimeOffsetManager.RegisterRestApi(ClientName);
|
||||||
|
|
||||||
RequestFactory.Configure(options, httpClient);
|
RequestFactory.Configure(options, httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,11 +237,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
currentTry++;
|
currentTry++;
|
||||||
|
|
||||||
var error = await CheckTimeSync(requestId, definition).ConfigureAwait(false);
|
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
|
||||||
if (error != null)
|
|
||||||
return new WebCallResult<T>(error);
|
|
||||||
|
|
||||||
error = await RateLimitAsync(
|
var error = await RateLimitAsync(
|
||||||
baseAddress,
|
baseAddress,
|
||||||
requestId,
|
requestId,
|
||||||
definition,
|
definition,
|
||||||
@@ -300,28 +294,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>
|
/// <summary>
|
||||||
/// Check rate limits for the request
|
/// Check rate limits for the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -516,10 +488,20 @@ namespace CryptoExchange.Net.Clients
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
||||||
error = await MessageHandler.ParseErrorResponse(
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
error = await MessageHandler.ParseErrorResponse(
|
||||||
(int)response.StatusCode,
|
(int)response.StatusCode,
|
||||||
response.ResponseHeaders,
|
response.ResponseHeaders,
|
||||||
responseStream).ConfigureAwait(false);
|
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);
|
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 +540,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (deserializeError != null)
|
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); ;
|
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
|
try
|
||||||
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
|
{
|
||||||
if (responseError != null)
|
// Check the deserialized response to see if it's an error or not
|
||||||
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);
|
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);
|
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 +697,44 @@ namespace CryptoExchange.Net.Clients
|
|||||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
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 (!definition.Authenticated)
|
||||||
if (timeSyncParams == null)
|
return;
|
||||||
return null;
|
|
||||||
|
|
||||||
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)
|
// 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
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
await syncTask.ConfigureAwait(false);
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
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 localTime = DateTime.UtcNow;
|
||||||
var result = await GetServerTimestampAsync().ConfigureAwait(false);
|
var result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
|
||||||
return result.Error;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TotalRequestsMade == 1)
|
if (TotalRequestsMade == 1)
|
||||||
@@ -735,18 +744,29 @@ namespace CryptoExchange.Net.Clients
|
|||||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
|
||||||
return result.Error;
|
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);
|
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
|
||||||
timeSyncParams.UpdateTimeOffset(offset);
|
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
{
|
||||||
}
|
_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 bool ShouldCache(RequestDefinition definition)
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
|
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
|
||||||
|
|
||||||
@@ -140,6 +142,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MaxIndividualSubscriptionsPerConnection { get; set; }
|
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
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -181,6 +187,24 @@ namespace CryptoExchange.Net.Clients
|
|||||||
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
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>
|
/// <summary>
|
||||||
/// Add a query to periodically send on each connection
|
/// Add a query to periodically send on each connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -296,7 +320,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!success)
|
if (!success)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
subscription.HandleSubQueryResponse(response);
|
subscription.HandleSubQueryResponse(socketConnection, response);
|
||||||
subscription.Status = SubscriptionStatus.Subscribed;
|
subscription.Status = SubscriptionStatus.Subscribed;
|
||||||
if (ct != default)
|
if (ct != default)
|
||||||
{
|
{
|
||||||
@@ -575,7 +599,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Should return the request which can be used to authenticate a socket connection
|
/// Should return the request which can be used to authenticate a socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Adds a system subscription. Used for example to reply to ping requests
|
/// Adds a system subscription. Used for example to reply to ping requests
|
||||||
@@ -685,6 +710,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||||
connection.DedicatedRequestConnection.Authenticated = 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);
|
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<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>
|
<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>
|
<PackageVersion>10.2.0</PackageVersion>
|
||||||
<AssemblyVersion>10.0.0</AssemblyVersion>
|
<AssemblyVersion>10.2.0</AssemblyVersion>
|
||||||
<FileVersion>10.0.0</FileVersion>
|
<FileVersion>10.2.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<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>
|
<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>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -242,8 +242,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a long value
|
/// Generate a long value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="maxLength">Max character length</param>
|
/// <param name="maxLength">Max number of digits</param>
|
||||||
/// <returns></returns>
|
|
||||||
public static long RandomLong(int maxLength)
|
public static long RandomLong(int maxLength)
|
||||||
{
|
{
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||||
@@ -259,6 +258,25 @@ namespace CryptoExchange.Net
|
|||||||
return value;
|
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>
|
/// <summary>
|
||||||
/// Generate a random string of specified length
|
/// Generate a random string of specified length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
first = false;
|
first = false;
|
||||||
|
|
||||||
if (parameter.GetType().IsArray)
|
if (parameter.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
if (serializationType == ArrayParametersSerialization.Array)
|
if (serializationType == ArrayParametersSerialization.Array)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,9 +47,21 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)> OnBestOffersChanged;
|
event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)> OnBestOffersChanged;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp of the last update
|
/// Timestamp of when the last update was applied to the book, local time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
DateTime UpdateTime { get; }
|
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>
|
/// <summary>
|
||||||
/// The number of asks in the book
|
/// The number of asks in the book
|
||||||
@@ -126,5 +138,13 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
string ToString(int rows);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?> _orderBookResyncing;
|
||||||
private static readonly Action<ILogger, string, string, Exception?> _orderBookResynced;
|
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, 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, long, long, long, long, Exception?> _orderBookUpdateBuffered;
|
||||||
private static readonly Action<ILogger, string, string, decimal, decimal, Exception?> _orderBookOutOfSyncDetected;
|
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, Exception?> _orderBookReconnectingSocket;
|
||||||
private static readonly Action<ILogger, string, string, long, long, Exception?> _orderBookSkippedMessage;
|
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, 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, Exception?> _orderBookOutOfSync;
|
||||||
|
private static readonly Action<ILogger, string, string, long, long, long, Exception?> _orderBookUpdateSkippedStartEnd;
|
||||||
|
|
||||||
static SymbolOrderBookLoggingExtensions()
|
static SymbolOrderBookLoggingExtensions()
|
||||||
{
|
{
|
||||||
@@ -73,7 +75,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
"{Api} order book {Symbol} Processing {NumberBufferedUpdated} buffered updates");
|
"{Api} order book {Symbol} Processing {NumberBufferedUpdated} buffered updates");
|
||||||
|
|
||||||
_orderBookUpdateSkipped = LoggerMessage.Define<string, string, long, long>(
|
_orderBookUpdateSkipped = LoggerMessage.Define<string, string, long, long>(
|
||||||
LogLevel.Debug,
|
LogLevel.Trace,
|
||||||
new EventId(5008, "OrderBookUpdateSkipped"),
|
new EventId(5008, "OrderBookUpdateSkipped"),
|
||||||
"{Api} order book {Symbol} update skipped #{SequenceNumber}, currently at #{LastSequenceNumber}");
|
"{Api} order book {Symbol} update skipped #{SequenceNumber}, currently at #{LastSequenceNumber}");
|
||||||
|
|
||||||
@@ -92,10 +94,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
new EventId(5011, "OrderBookMessageSkippedResubscribing"),
|
new EventId(5011, "OrderBookMessageSkippedResubscribing"),
|
||||||
"{Api} order book {Symbol} Skipping message because of resubscribing");
|
"{Api} order book {Symbol} Skipping message because of resubscribing");
|
||||||
|
|
||||||
_orderBookDataSet = LoggerMessage.Define<string, string, long, long, long>(
|
_orderBookDataSet = LoggerMessage.Define<string, string, long, long, long?>(
|
||||||
LogLevel.Debug,
|
LogLevel.Trace,
|
||||||
new EventId(5012, "OrderBookDataSet"),
|
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>(
|
_orderBookUpdateBuffered = LoggerMessage.Define<string, string, long, long, long, long>(
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
@@ -136,6 +138,17 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(5020, "OrderBookOutOfSyncChecksum"),
|
new EventId(5020, "OrderBookOutOfSyncChecksum"),
|
||||||
"{Api} order book {Symbol} out of sync. Checksum mismatch, resyncing");
|
"{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)
|
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);
|
_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);
|
_orderBookDataSet(logger, api, symbol, bidCount, askCount, endUpdateId, null);
|
||||||
}
|
}
|
||||||
@@ -229,9 +242,18 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_orderBookProcessedMessage(logger, api, symbol, firstUpdateId, lastUpdateId, null);
|
_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)
|
public static void OrderBookOutOfSyncChecksum(this ILogger logger, string api, string symbol)
|
||||||
{
|
{
|
||||||
_orderBookOutOfSyncChecksum(logger, api, symbol, null);
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? DataTime { get; set; }
|
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>
|
/// <summary>
|
||||||
/// The stream producing the update
|
/// The stream producing the update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -43,6 +53,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SocketUpdateType? UpdateType { get; set; }
|
public SocketUpdateType? UpdateType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sequence number of the update
|
||||||
|
/// </summary>
|
||||||
|
public long? SequenceNumber { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -116,12 +131,28 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the sequence number of the update
|
||||||
|
/// </summary>
|
||||||
|
public DataEvent<T> WithSequenceNumber(long? sequenceNumber)
|
||||||
|
{
|
||||||
|
SequenceNumber = sequenceNumber;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specify the data timestamp
|
/// Specify the data timestamp
|
||||||
/// </summary>
|
/// </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;
|
DataTime = timestamp;
|
||||||
|
if (offset == null)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
DataTimeLocal = DataTime + offset;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+11
-7
@@ -3,24 +3,28 @@ using System;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.OrderBook
|
namespace CryptoExchange.Net.OrderBook
|
||||||
{
|
{
|
||||||
internal class ProcessQueueItem
|
internal class OrderBookUpdate
|
||||||
{
|
{
|
||||||
public long StartUpdateId { get; set; }
|
public DateTime? LocalDataTime { get; set; }
|
||||||
public long EndUpdateId { 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[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
public ISymbolOrderBookEntry[] Asks { 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 DateTime? LocalDataTime { get; set; }
|
||||||
public long EndUpdateId { get; set; }
|
public DateTime? ServerDataTime { get; set; }
|
||||||
|
public long? SequenceNumber { get; set; }
|
||||||
public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
public ISymbolOrderBookEntry[] Asks { 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; }
|
public int Checksum { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -38,6 +39,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
private readonly AsyncResetEvent _queueEvent;
|
private readonly AsyncResetEvent _queueEvent;
|
||||||
private readonly ConcurrentQueue<object> _processQueue;
|
private readonly ConcurrentQueue<object> _processQueue;
|
||||||
private bool _validateChecksum;
|
private bool _validateChecksum;
|
||||||
|
private bool _firstUpdateAfterSnapshotDone;
|
||||||
|
|
||||||
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
|
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
|
||||||
{
|
{
|
||||||
@@ -49,6 +51,13 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
|
private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
|
||||||
|
|
||||||
|
private enum SequenceNumberResult
|
||||||
|
{
|
||||||
|
Skip,
|
||||||
|
Ok,
|
||||||
|
OutOfSync
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A buffer to store messages received before the initial book snapshot is processed. These messages
|
/// 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
|
/// 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
|
/// the book will resynchronize as it is deemed out of sync
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _sequencesAreConsecutive;
|
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>
|
/// <summary>
|
||||||
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
|
/// 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
|
/// 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/>
|
/// <inheritdoc/>
|
||||||
public DateTime UpdateTime { get; private set; }
|
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/>
|
/// <inheritdoc/>
|
||||||
public int AskCount { get; private set; }
|
public int AskCount { get; private set; }
|
||||||
|
|
||||||
@@ -257,6 +280,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
_processBuffer.Clear();
|
_processBuffer.Clear();
|
||||||
_bookSet = false;
|
_bookSet = false;
|
||||||
|
_firstUpdateAfterSnapshotDone = false;
|
||||||
|
|
||||||
Status = OrderBookStatus.Connecting;
|
Status = OrderBookStatus.Connecting;
|
||||||
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
|
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
|
||||||
@@ -406,45 +430,91 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
|
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
|
||||||
/// the order book will be resynchronized
|
/// the order book will be resynchronized
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="checksum"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual bool DoChecksum(int checksum) => true;
|
protected virtual bool DoChecksum(int checksum) => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot
|
/// 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
|
/// received from a socket subscription. Will clear any previous data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
|
/// <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="askList">List of asks</param>
|
||||||
/// <param name="bidList">List of bids</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 });
|
_processQueue.Enqueue(
|
||||||
|
new OrderBookSnapshot
|
||||||
|
{
|
||||||
|
LocalDataTime = localDataTime,
|
||||||
|
ServerDataTime = serverDataTime,
|
||||||
|
SequenceNumber = orderBookSequenceNumber,
|
||||||
|
Asks = askList,
|
||||||
|
Bids = bidList
|
||||||
|
});
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
/// 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>
|
/// </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="bids">List of updated/new bids</param>
|
||||||
/// <param name="asks">List of updated/new asks</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 });
|
_processQueue.Enqueue(
|
||||||
|
new OrderBookUpdate
|
||||||
|
{
|
||||||
|
LocalDataTime = localDataTime,
|
||||||
|
ServerDataTime = serverDataTime,
|
||||||
|
StartSequenceNumber = sequenceNumber,
|
||||||
|
EndSequenceNumber = sequenceNumber,
|
||||||
|
Asks = asks,
|
||||||
|
Bids = bids
|
||||||
|
});
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="firstUpdateId">The sequence number of the first update</param>
|
/// <param name="firstSequenceNumber">The sequence number of the first update</param>
|
||||||
/// <param name="lastUpdateId">The sequence number of the last update</param>
|
/// <param name="lastSequenceNumber">The sequence number of the last update</param>
|
||||||
/// <param name="bids">List of updated/new bids</param>
|
/// <param name="bids">List of updated/new bids</param>
|
||||||
/// <param name="asks">List of updated/new asks</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 });
|
_processQueue.Enqueue(
|
||||||
|
new OrderBookUpdate
|
||||||
|
{
|
||||||
|
LocalDataTime = localDataTime,
|
||||||
|
ServerDataTime = serverDataTime,
|
||||||
|
StartSequenceNumber = firstSequenceNumber,
|
||||||
|
EndSequenceNumber = lastSequenceNumber,
|
||||||
|
Asks = asks,
|
||||||
|
Bids = bids
|
||||||
|
});
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,12 +523,27 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="bids">List of updated/new bids</param>
|
/// <param name="bids">List of updated/new bids</param>
|
||||||
/// <param name="asks">List of updated/new asks</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)
|
||||||
{
|
{
|
||||||
var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0);
|
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);
|
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();
|
_queueEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,9 +551,10 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// Add a checksum value to the process queue
|
/// Add a checksum value to the process queue
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="checksum">The checksum value</param>
|
/// <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 });
|
_processQueue.Enqueue(new OrderBookChecksum() { Checksum = checksum, SequenceNumber = sequenceNumber });
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +567,12 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
_logger.OrderBookProcessingBufferedUpdates(Api, Symbol, _processBuffer.Count);
|
_logger.OrderBookProcessingBufferedUpdates(Api, Symbol, _processBuffer.Count);
|
||||||
|
|
||||||
foreach (var bufferEntry in _processBuffer)
|
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();
|
_processBuffer.Clear();
|
||||||
}
|
}
|
||||||
@@ -489,26 +580,10 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update order book with an entry
|
/// Update order book with an entry
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sequence">Sequence number of the update</param>
|
|
||||||
/// <param name="type">Type of entry</param>
|
/// <param name="type">Type of entry</param>
|
||||||
/// <param name="entry">The 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;
|
UpdateTime = DateTime.UtcNow;
|
||||||
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
|
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||||
if (entry.Quantity == 0)
|
if (entry.Quantity == 0)
|
||||||
@@ -614,6 +689,12 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
var stringBuilder = new StringBuilder();
|
var stringBuilder = new StringBuilder();
|
||||||
var book = Book;
|
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");
|
stringBuilder.AppendLine($" Ask quantity Ask price | Bid price Bid quantity");
|
||||||
for(var i = 0; i < numberOfEntries; i++)
|
for(var i = 0; i < numberOfEntries; i++)
|
||||||
{
|
{
|
||||||
@@ -625,6 +706,22 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
return stringBuilder.ToString();
|
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)
|
private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk)
|
||||||
{
|
{
|
||||||
var (bestBid, bestAsk) = BestOffers;
|
var (bestBid, bestAsk) = BestOffers;
|
||||||
@@ -641,8 +738,11 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
// Clear queue
|
// Clear queue
|
||||||
while (_processQueue.TryDequeue(out _)) { }
|
while (_processQueue.TryDequeue(out _)) { }
|
||||||
|
|
||||||
|
LastSequenceNumber = 0;
|
||||||
_processBuffer.Clear();
|
_processBuffer.Clear();
|
||||||
_bookSet = false;
|
_bookSet = false;
|
||||||
|
_firstUpdateAfterSnapshotDone = false;
|
||||||
|
|
||||||
DoReset();
|
DoReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,17 +780,17 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is InitialOrderBookItem iobi)
|
if (item is OrderBookSnapshot snapshot)
|
||||||
ProcessInitialOrderBookItem(iobi);
|
ProcessOrderBookSnapshot(snapshot);
|
||||||
if (item is ProcessQueueItem pqi)
|
if (item is OrderBookUpdate update)
|
||||||
ProcessQueueItem(pqi);
|
ProcessQueueItem(update);
|
||||||
else if (item is ChecksumItem ci)
|
else if (item is OrderBookChecksum checksum)
|
||||||
ProcessChecksum(ci);
|
ProcessChecksum(checksum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessInitialOrderBookItem(InitialOrderBookItem item)
|
private void ProcessOrderBookSnapshot(OrderBookSnapshot item)
|
||||||
{
|
{
|
||||||
lock (_bookLock)
|
lock (_bookLock)
|
||||||
{
|
{
|
||||||
@@ -702,20 +802,25 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
foreach (var bid in item.Bids)
|
foreach (var bid in item.Bids)
|
||||||
_bids.Add(bid.Price, bid);
|
_bids.Add(bid.Price, bid);
|
||||||
|
|
||||||
LastSequenceNumber = item.EndUpdateId;
|
if (item.SequenceNumber != null)
|
||||||
|
LastSequenceNumber = item.SequenceNumber.Value;
|
||||||
|
|
||||||
AskCount = _asks.Count;
|
AskCount = _asks.Count;
|
||||||
BidCount = _bids.Count;
|
BidCount = _bids.Count;
|
||||||
|
|
||||||
UpdateTime = DateTime.UtcNow;
|
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();
|
CheckProcessBuffer();
|
||||||
|
|
||||||
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
|
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
|
||||||
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessQueueItem(ProcessQueueItem item)
|
private void ProcessQueueItem(OrderBookUpdate item)
|
||||||
{
|
{
|
||||||
lock (_bookLock)
|
lock (_bookLock)
|
||||||
{
|
{
|
||||||
@@ -725,19 +830,19 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
Asks = item.Asks,
|
Asks = item.Asks,
|
||||||
Bids = item.Bids,
|
Bids = item.Bids,
|
||||||
FirstUpdateId = item.StartUpdateId,
|
FirstUpdateId = item.StartSequenceNumber,
|
||||||
LastUpdateId = item.EndUpdateId,
|
LastUpdateId = item.EndSequenceNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Trace))
|
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
|
else
|
||||||
{
|
{
|
||||||
CheckProcessBuffer();
|
CheckProcessBuffer();
|
||||||
var (prevBestBid, prevBestAsk) = BestOffers;
|
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)
|
if (_asks.Count == 0 || _bids.Count == 0)
|
||||||
return;
|
return;
|
||||||
@@ -750,13 +855,16 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateServerTime = item.ServerDataTime;
|
||||||
|
UpdateLocalTime = item.LocalDataTime;
|
||||||
|
|
||||||
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
|
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
|
||||||
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessChecksum(ChecksumItem ci)
|
private void ProcessChecksum(OrderBookChecksum ci)
|
||||||
{
|
{
|
||||||
lock (_bookLock)
|
lock (_bookLock)
|
||||||
{
|
{
|
||||||
@@ -776,6 +884,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ci.SequenceNumber != null)
|
||||||
|
LastSequenceNumber = ci.SequenceNumber.Value;
|
||||||
|
|
||||||
if (!checksumResult)
|
if (!checksumResult)
|
||||||
{
|
{
|
||||||
_logger.OrderBookOutOfSyncChecksum(Api, Symbol);
|
_logger.OrderBookOutOfSyncChecksum(Api, Symbol);
|
||||||
@@ -813,40 +924,98 @@ 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entry in bids)
|
foreach (var entry in bids)
|
||||||
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Bid, entry);
|
UpdateValue(OrderBookEntryType.Bid, entry);
|
||||||
|
|
||||||
foreach (var entry in asks)
|
foreach (var entry in asks)
|
||||||
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry);
|
UpdateValue(OrderBookEntryType.Ask, entry);
|
||||||
|
|
||||||
if (Levels.HasValue && _strictLevels)
|
if (Levels.HasValue && _strictLevels)
|
||||||
{
|
{
|
||||||
while (this._bids.Count > Levels.Value)
|
while (_bids.Count > Levels.Value)
|
||||||
{
|
{
|
||||||
BidCount--;
|
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--;
|
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))
|
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)
|
||||||
|
// 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>
|
internal class DescComparer<T> : IComparer<T>
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
#if NET9_0_OR_GREATER
|
#if NET9_0_OR_GREATER
|
||||||
private readonly Lock _listenersLock = new Lock();
|
private readonly Lock _listenersLock = new Lock();
|
||||||
@@ -274,6 +275,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
private ISocketMessageHandler? _byteMessageConverter;
|
private ISocketMessageHandler? _byteMessageConverter;
|
||||||
private ISocketMessageHandler? _textMessageConverter;
|
private ISocketMessageHandler? _textMessageConverter;
|
||||||
|
|
||||||
|
private long _lastSequenceNumber;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary.
|
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -293,7 +296,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// Cache for deserialization, only caches for a single message
|
/// Cache for deserialization, only caches for a single message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Dictionary<Type, object> _deserializationCache = new Dictionary<Type, object>();
|
private readonly Dictionary<Type, object> _deserializationCache = new Dictionary<Type, object>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// New socket connection
|
/// New socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -340,6 +343,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
Status = SocketStatus.Closed;
|
Status = SocketStatus.Closed;
|
||||||
Authenticated = false;
|
Authenticated = false;
|
||||||
|
_lastSequenceNumber = 0;
|
||||||
|
|
||||||
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
||||||
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
||||||
@@ -371,6 +375,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
Status = SocketStatus.Reconnecting;
|
Status = SocketStatus.Reconnecting;
|
||||||
DisconnectTime = DateTime.UtcNow;
|
DisconnectTime = DateTime.UtcNow;
|
||||||
Authenticated = false;
|
Authenticated = false;
|
||||||
|
_lastSequenceNumber = 0;
|
||||||
|
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
{
|
{
|
||||||
@@ -877,8 +882,16 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
|
|
||||||
bool anyDuplicateSubscription;
|
bool anyDuplicateSubscription;
|
||||||
lock (_listenersLock)
|
if (ApiClient.ClientOptions.UseUpdatedDeserialization)
|
||||||
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageMatcher.HandlerLinks.All(l => subscription.MessageMatcher.ContainsCheck(l)));
|
{
|
||||||
|
lock (_listenersLock)
|
||||||
|
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lock (_listenersLock)
|
||||||
|
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageMatcher.HandlerLinks.All(l => subscription.MessageMatcher.ContainsCheck(l)));
|
||||||
|
}
|
||||||
|
|
||||||
bool shouldCloseConnection;
|
bool shouldCloseConnection;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
@@ -1228,7 +1241,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
subQuery.OnComplete = () =>
|
subQuery.OnComplete = () =>
|
||||||
{
|
{
|
||||||
subscription.Status = subQuery.Result!.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
|
subscription.Status = subQuery.Result!.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response);
|
subscription.HandleSubQueryResponse(this, subQuery.Response);
|
||||||
};
|
};
|
||||||
|
|
||||||
taskList.Add(SendAndWaitQueryAsync(subQuery));
|
taskList.Add(SendAndWaitQueryAsync(subQuery));
|
||||||
@@ -1268,10 +1281,27 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
var result = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false);
|
var result = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false);
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(this, subQuery.Response!);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the sequence number for this connection
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateSequenceNumber(long sequenceNumber)
|
||||||
|
{
|
||||||
|
if (ApiClient.EnforceSequenceNumbers
|
||||||
|
&& _lastSequenceNumber != 0
|
||||||
|
&& _lastSequenceNumber + 1 != sequenceNumber)
|
||||||
|
{
|
||||||
|
// Not sequential
|
||||||
|
_logger.LogWarning("[Sckt {SocketId}] update not in sequence. Last recorded sequence number: {LastSequence}, update sequence number: {UpdateSequence}. Reconnecting", SocketId, _lastSequenceNumber, sequenceNumber);
|
||||||
|
_ = TriggerReconnectAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastSequenceNumber = sequenceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Periodically sends data over a socket connection
|
/// Periodically sends data over a socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -149,14 +149,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a subscription query response
|
/// Handle a subscription query response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public virtual void HandleSubQueryResponse(SocketConnection connection, object? message) { }
|
||||||
public virtual void HandleSubQueryResponse(object? message) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle an unsubscription query response
|
/// Handle an unsubscription query response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public virtual void HandleUnsubQueryResponse(SocketConnection connection, object message) { }
|
||||||
public virtual void HandleUnsubQueryResponse(object message) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new unsubscription query
|
/// Create a new unsubscription query
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manager for timing offsets in APIs
|
||||||
|
/// </summary>
|
||||||
|
public static class TimeOffsetManager
|
||||||
|
{
|
||||||
|
class SocketTimeOffset
|
||||||
|
{
|
||||||
|
private DateTime _lastRollOver = DateTime.UtcNow;
|
||||||
|
private double? _fallbackLowest;
|
||||||
|
private double? _currentLowestOffset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the estimated offset, resolves to the lowest offset in time measured in the last two minutes
|
||||||
|
/// </summary>
|
||||||
|
public double? Offset
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_currentLowestOffset == null)
|
||||||
|
// If there is no current lowest offset return the fallback (which might or might not be null)
|
||||||
|
return _fallbackLowest;
|
||||||
|
|
||||||
|
if (_fallbackLowest == null)
|
||||||
|
// If there is no fallback return the current lowest offset
|
||||||
|
return _currentLowestOffset;
|
||||||
|
|
||||||
|
// If there is both a fallback and a current offset return the min offset of those
|
||||||
|
return Math.Min(_currentLowestOffset.Value, _fallbackLowest.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(double offsetMs)
|
||||||
|
{
|
||||||
|
if (_currentLowestOffset == null || _currentLowestOffset > offsetMs)
|
||||||
|
{
|
||||||
|
_currentLowestOffset = offsetMs;
|
||||||
|
_fallbackLowest = offsetMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - _lastRollOver > TimeSpan.FromMinutes(1))
|
||||||
|
{
|
||||||
|
_fallbackLowest = _currentLowestOffset;
|
||||||
|
_currentLowestOffset = null;
|
||||||
|
_lastRollOver = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestTimeOffset
|
||||||
|
{
|
||||||
|
public SemaphoreSlim SemaphoreSlim { get; } = new SemaphoreSlim(1, 1);
|
||||||
|
public DateTime? LastUpdate { get; set; }
|
||||||
|
public double? Offset { get; set; }
|
||||||
|
|
||||||
|
public void Update(double offsetMs)
|
||||||
|
{
|
||||||
|
LastUpdate = DateTime.UtcNow;
|
||||||
|
Offset = offsetMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConcurrentDictionary<string, SocketTimeOffset> _lastSocketDelays = new ConcurrentDictionary<string, SocketTimeOffset>();
|
||||||
|
private static ConcurrentDictionary<string, RestTimeOffset> _lastRestDelays = new ConcurrentDictionary<string, RestTimeOffset>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update WebSocket API offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api">API name</param>
|
||||||
|
/// <param name="offsetMs">Offset in milliseconds</param>
|
||||||
|
public static void UpdateSocketOffset(string api, double offsetMs)
|
||||||
|
{
|
||||||
|
if (!_lastSocketDelays.TryGetValue(api, out var offsetValues))
|
||||||
|
{
|
||||||
|
offsetValues = new SocketTimeOffset();
|
||||||
|
_lastSocketDelays.TryAdd(api, offsetValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastSocketDelays[api].Update(offsetMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update REST API offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api">API name</param>
|
||||||
|
/// <param name="offsetMs">Offset in milliseconds</param>
|
||||||
|
public static void UpdateRestOffset(string api, double offsetMs)
|
||||||
|
{
|
||||||
|
_lastRestDelays[api].Update(offsetMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get REST API offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api">API name</param>
|
||||||
|
public static TimeSpan? GetRestOffset(string api) => _lastRestDelays.TryGetValue(api, out var val) && val.Offset != null ? TimeSpan.FromMilliseconds(val.Offset.Value) : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get REST API last update time
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api">API name</param>
|
||||||
|
public static DateTime? GetRestLastUpdateTime(string api) => _lastRestDelays.TryGetValue(api, out var val) && val.LastUpdate != null ? val.LastUpdate.Value : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a REST API client to be tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api"></param>
|
||||||
|
internal static void RegisterRestApi(string api)
|
||||||
|
{
|
||||||
|
_lastRestDelays[api] = new RestTimeOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enter exclusive access for the API to update the time offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async ValueTask EnterAsync(string api)
|
||||||
|
{
|
||||||
|
await _lastRestDelays[api].SemaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Release exclusive access for the API
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api"></param>
|
||||||
|
public static void Release(string api) => _lastRestDelays[api].SemaphoreSlim.Release();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get WebSocket API offset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api">API name</param>
|
||||||
|
public static TimeSpan? GetSocketOffset(string api) => _lastSocketDelays.TryGetValue(api, out var val) && val.Offset != null ? TimeSpan.FromMilliseconds(val.Offset.Value) : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset the WebSocket API update timestamp to trigger a new time offset calculation
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="api">API name</param>
|
||||||
|
public static void ResetRestUpdateTime(string api)
|
||||||
|
{
|
||||||
|
_lastRestDelays[api].LastUpdate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,32 +5,32 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="11.10.0" />
|
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="9.10.0" />
|
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="2.11.0" />
|
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="BloFin.Net" Version="1.3.0" />
|
<PackageReference Include="BloFin.Net" Version="2.1.1" />
|
||||||
<PackageReference Include="Bybit.Net" Version="5.12.0" />
|
<PackageReference Include="Bybit.Net" Version="6.1.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="9.10.0" />
|
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
|
||||||
<PackageReference Include="CoinW.Net" Version="1.7.0" />
|
<PackageReference Include="CoinW.Net" Version="2.1.1" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="2.11.0" />
|
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="DeepCoin.Net" Version="2.10.0" />
|
<PackageReference Include="DeepCoin.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="2.12.0" />
|
<PackageReference Include="GateIo.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="HyperLiquid.Net" Version="2.16.0" />
|
<PackageReference Include="HyperLiquid.Net" Version="3.2.0" />
|
||||||
<PackageReference Include="JK.BingX.Net" Version="2.10.0" />
|
<PackageReference Include="JK.BingX.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="2.11.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="3.11.0" />
|
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="3.10.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
||||||
<PackageReference Include="Jkorf.Aster.Net" Version="1.2.0" />
|
<PackageReference Include="Jkorf.Aster.Net" Version="2.1.0" />
|
||||||
<PackageReference Include="JKorf.BitMEX.Net" Version="2.10.0" />
|
<PackageReference Include="JKorf.BitMEX.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.10.0" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="7.10.0" />
|
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
|
||||||
<PackageReference Include="JKorf.Upbit.Net" Version="1.1.0" />
|
<PackageReference Include="JKorf.Upbit.Net" Version="2.1.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="6.10.0" />
|
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="7.10.0" />
|
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Toobit.Net" Version="1.9.0" />
|
<PackageReference Include="Toobit.Net" Version="2.1.0" />
|
||||||
<PackageReference Include="WhiteBit.Net" Version="2.11.0" />
|
<PackageReference Include="WhiteBit.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="XT.Net" Version="2.10.0" />
|
<PackageReference Include="XT.Net" Version="3.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||||
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
||||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||||
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.First().LastPrice)),
|
||||||
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
||||||
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
|
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
|
||||||
// BloFin doesn't support the ETH/BTC pair
|
// BloFin doesn't support the ETH/BTC pair
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
{
|
{
|
||||||
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
|
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
|
||||||
<h4>@book.Key - @book.Value.Symbol</h4>
|
<h4>@book.Key - @book.Value.Symbol</h4>
|
||||||
|
<div>@(book.Value.DataAge?.TotalMilliseconds)ms since last update</div>
|
||||||
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
|
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < 3; i++)
|
||||||
|
|||||||
@@ -6,20 +6,20 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="11.10.0" />
|
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="9.10.0" />
|
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="2.11.0" />
|
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="5.12.0" />
|
<PackageReference Include="Bybit.Net" Version="6.1.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="9.10.0" />
|
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="2.11.0" />
|
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="2.12.0" />
|
<PackageReference Include="GateIo.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="2.11.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="3.11.0" />
|
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="3.10.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.10.0" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="7.10.0" />
|
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="6.10.0" />
|
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="7.10.0" />
|
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="11.10.0" />
|
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="2.11.0" />
|
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="3.10.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -66,6 +66,39 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
|||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 10.2.0 - 12 Jan 2026
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* Version 10.1.0 - 07 Jan 2026
|
||||||
|
* 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
|
||||||
|
* Added some utility methods
|
||||||
|
|
||||||
|
* Version 10.0.2 - 19 Dec 2025
|
||||||
|
* Fixed duplicate subscription check with updated deserialization
|
||||||
|
* Added exception handlers for REST response processing
|
||||||
|
|
||||||
|
* Version 10.0.1 - 18 Dec 2025
|
||||||
|
* Fixed query array parameter serialization
|
||||||
|
|
||||||
* Version 10.0.0 - 16 Dec 2025
|
* Version 10.0.0 - 16 Dec 2025
|
||||||
* -
|
* -
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user