mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 00:43:03 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de0a954a91 | |||
| 4a79ce22ec | |||
| 40d480e1fc | |||
| 74e5cf6fc9 | |||
| 2fd3912795 | |||
| ec44307a0c | |||
| ba55705385 | |||
| 2c63a83117 | |||
| 71b1e5e906 | |||
| eaeba6f27e | |||
| 913bdaa855 | |||
| 5aa5790d0a | |||
| a8321e083e | |||
| ce3fa5f186 | |||
| fff70a9c65 | |||
| cff33bb5ac | |||
| 21c8133292 | |||
| 76772e91ba | |||
| 218e0260ce | |||
| 96b3904266 | |||
| bc8faf9822 | |||
| 90c1b89ceb | |||
| 21206ffb25 | |||
| 5942423bfb | |||
| dc4abc42a7 | |||
| c71a81e686 | |||
| 550c0eabf1 | |||
| 28a2a0c7fd | |||
| 7dd1cd5bbd | |||
| a7ff4416bd | |||
| 669d1f7c9e | |||
| 34ee2d3690 | |||
| 005fb7875d | |||
| fa9300ce97 | |||
| fc2d3fc2d2 | |||
| 187ca6a4ef | |||
| 3b2a85d210 | |||
| c512bee825 | |||
| 0943b052b9 | |||
| a896fffdb3 | |||
| 177daf903b | |||
| aa1ebdc4ed | |||
| 38058c4a70 | |||
| a7eb483479 | |||
| c76931a3b4 | |||
| b90b7e9e0c | |||
| beda53d36d | |||
| 0668f669c1 | |||
| 64250e13db | |||
| 451d38d5e7 | |||
| e11e437bbb | |||
| b8a1ad798d |
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -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>9.13.0</PackageVersion>
|
<PackageVersion>10.0.1</PackageVersion>
|
||||||
<AssemblyVersion>9.13.0</AssemblyVersion>
|
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||||
<FileVersion>9.13.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,12 @@
|
|||||||
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
|
||||||
|
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
* Version 9.13.0 - 10 Nov 2025
|
* Version 9.13.0 - 10 Nov 2025
|
||||||
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using NUnit.Framework.Legacy;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
@@ -139,5 +140,17 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
ClassicAssert.False(result1);
|
ClassicAssert.False(result1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task CancellingWait_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
var evnt = new AsyncResetEvent(false, true);
|
||||||
|
|
||||||
|
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
|
||||||
|
|
||||||
|
var result1 = await waiter1;
|
||||||
|
|
||||||
|
ClassicAssert.False(result1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(result.Success);
|
Assert.That(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public void DeserializingInvalidJson_Should_GiveErrorResult()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new TestBaseClient();
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
|
||||||
|
|
||||||
// assert
|
|
||||||
ClassicAssert.IsFalse(result.Success);
|
|
||||||
Assert.That(result.Error != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
|
<PackageReference Include="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>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
@@ -51,21 +52,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
public CallResult<T> Deserialize<T>(string data)
|
public CallResult<T> Deserialize<T>(string data)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
return new CallResult<T>(JsonSerializer.Deserialize<T>(data));
|
||||||
var accessor = CreateAccessor();
|
|
||||||
var valid = accessor.Read(stream, true).Result;
|
|
||||||
if (!valid)
|
|
||||||
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
|
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
|
||||||
return deserializeResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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 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();
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
/// <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()}";
|
||||||
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
@@ -160,11 +159,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
ParameterPositions[method] = position;
|
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 +166,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
|
||||||
@@ -188,7 +177,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -198,12 +186,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
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 +195,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,53 @@ 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 timestamp = TimeProvider.GetTime();
|
||||||
|
if(apiClient.ApiOptions.AutoTimestamp ?? apiClient.ClientOptions.AutoTimestamp)
|
||||||
|
timestamp = timestamp.Add(-TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||||
|
|
||||||
|
if (includeOneSecondOffset)
|
||||||
|
timestamp = timestamp.AddSeconds(-1);
|
||||||
|
|
||||||
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client name
|
||||||
|
/// </summary>
|
||||||
|
protected string? _clientName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logger
|
/// Logger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -23,6 +28,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>
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether client is disposed
|
||||||
|
/// </summary>
|
||||||
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api clients in this client
|
/// Api clients in this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -125,6 +130,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
|
Disposed = true;
|
||||||
|
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 +109,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
|
TimeOffsetManager.RegisterRestApi(ClientName);
|
||||||
|
|
||||||
RequestFactory.Configure(options, httpClient);
|
RequestFactory.Configure(options, httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a message accessor instance
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected abstract IStreamMessageAccessor CreateAccessor();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -241,11 +231,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 +288,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<Error?> CheckTimeSync(int requestId, RequestDefinition definition)
|
|
||||||
{
|
|
||||||
if (!definition.Authenticated)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var syncTask = SyncTimeAsync();
|
|
||||||
var timeSyncInfo = GetTimeSyncInfo();
|
|
||||||
|
|
||||||
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
|
||||||
{
|
|
||||||
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
|
||||||
var syncTimeError = await syncTask.ConfigureAwait(false);
|
|
||||||
if (syncTimeError != null)
|
|
||||||
{
|
|
||||||
_logger.RestApiFailedToSyncTime(requestId, syncTimeError!.ToString());
|
|
||||||
return syncTimeError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check rate limits for the request
|
/// Check rate limits for the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -482,9 +448,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
memoryStream.Position = 0;
|
memoryStream.Position = 0;
|
||||||
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
|
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Trace))
|
|
||||||
_logger.RestApiReceivedResponse(request.RequestId, originalData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue processing from the memory stream since the response stream is already read and we can't seek it
|
// Continue processing from the memory stream since the response stream is already read and we can't seek it
|
||||||
@@ -516,10 +479,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 +531,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 +688,53 @@ 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);
|
WebCallResult<DateTime> result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (NotImplementedException)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("AutoTimestamp is not available for this API");
|
||||||
|
}
|
||||||
|
|
||||||
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; }
|
||||||
|
|
||||||
@@ -97,11 +99,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to continue processing and forward unparsable messages to handlers
|
|
||||||
/// </summary>
|
|
||||||
protected internal bool ProcessUnparsableMessages { get; set; } = false;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@@ -140,6 +137,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>
|
||||||
@@ -159,12 +160,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a message accessor instance
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -181,6 +176,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 +309,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)
|
||||||
{
|
{
|
||||||
@@ -332,6 +345,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!subQuery.ExpectsResponse)
|
||||||
|
HandleSubscriptionComplete(true, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -575,7 +591,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 +702,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);
|
||||||
@@ -722,7 +743,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
// Create new socket connection
|
// Create new socket connection
|
||||||
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
|
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
|
||||||
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
||||||
if (dedicatedRequestConnection)
|
if (dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
@@ -773,14 +793,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
|
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process an unhandled message
|
/// Process an unhandled message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The message that wasn't processed</param>
|
/// <param name="connection">The socket connection</param>
|
||||||
protected virtual void HandleUnhandledMessage(IMessageAccessor message)
|
/// <param name="typeIdentifier">The type as identified</param>
|
||||||
{
|
/// <param name="data">The data</param>
|
||||||
}
|
protected internal virtual bool HandleUnhandledMessage(SocketConnection connection, string typeIdentifier, ReadOnlySpan<byte> data) => false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process connect rate limited
|
/// Process connect rate limited
|
||||||
@@ -834,7 +853,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
Proxy = ClientOptions.Proxy,
|
Proxy = ClientOptions.Proxy,
|
||||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
||||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
||||||
UseUpdatedDeserialization = ClientOptions.UseUpdatedDeserialization
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1027,7 +1045,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
||||||
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
|
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
|
||||||
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
||||||
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1058,21 +1075,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the listener identifier for the message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="messageAccessor"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Preprocess a stream message
|
/// Preprocess a stream message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data;
|
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data;
|
||||||
/// <summary>
|
|
||||||
/// Preprocess a stream message
|
|
||||||
/// </summary>
|
|
||||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new message converter instance
|
/// Create a new message converter instance
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Node accessor
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct NodeAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Index
|
|
||||||
/// </summary>
|
|
||||||
public int? Index { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Property name
|
|
||||||
/// </summary>
|
|
||||||
public string? Property { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Type (0 = int, 1 = string, 2 = prop name)
|
|
||||||
/// </summary>
|
|
||||||
public int Type { get; }
|
|
||||||
|
|
||||||
private NodeAccessor(int? index, string? property, int type)
|
|
||||||
{
|
|
||||||
Index = index;
|
|
||||||
Property = property;
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an int node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a string node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a property name node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message access definition
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct MessagePath : IEnumerable<NodeAccessor>
|
|
||||||
{
|
|
||||||
private readonly List<NodeAccessor> _path;
|
|
||||||
|
|
||||||
internal void Add(NodeAccessor node)
|
|
||||||
{
|
|
||||||
_path.Add(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public MessagePath()
|
|
||||||
{
|
|
||||||
_path = new List<NodeAccessor>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new message path
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath Get()
|
|
||||||
{
|
|
||||||
return new MessagePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IEnumerable implementation
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerator<NodeAccessor> GetEnumerator()
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _path.Count; i++)
|
|
||||||
yield return _path[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message path extension methods
|
|
||||||
/// </summary>
|
|
||||||
public static class MessagePathExtension
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Add a string node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <param name="propName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath Property(this MessagePath path, string propName)
|
|
||||||
{
|
|
||||||
path.Add(NodeAccessor.String(propName));
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a property name node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath PropertyName(this MessagePath path)
|
|
||||||
{
|
|
||||||
path.Add(NodeAccessor.PropertyName());
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a int node accessor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <param name="index"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static MessagePath Index(this MessagePath path, int index)
|
|
||||||
{
|
|
||||||
path.Add(NodeAccessor.Int(index));
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message node type
|
|
||||||
/// </summary>
|
|
||||||
public enum NodeType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Array node
|
|
||||||
/// </summary>
|
|
||||||
Array,
|
|
||||||
/// <summary>
|
|
||||||
/// Object node
|
|
||||||
/// </summary>
|
|
||||||
Object,
|
|
||||||
/// <summary>
|
|
||||||
/// Value node
|
|
||||||
/// </summary>
|
|
||||||
Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for comma separated string values
|
||||||
|
/// </summary>
|
||||||
|
public class CommaSplitStringConverter : JsonConverter<string[]>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return str!.Split(',').ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(string.Join(",", value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json message accessor
|
|
||||||
/// </summary>
|
|
||||||
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The JsonDocument loaded
|
|
||||||
/// </summary>
|
|
||||||
protected JsonDocument? _document;
|
|
||||||
|
|
||||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsValid { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public object? Underlying => throw new NotImplementedException();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
_customSerializerOptions = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
return new CallResult<object>(GetOriginalString());
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize(type, _customSerializerOptions);
|
|
||||||
return new CallResult<object>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize<T>(_customSerializerOptions);
|
|
||||||
return new CallResult<T>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType()
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
return _document.RootElement.ValueKind switch
|
|
||||||
{
|
|
||||||
JsonValueKind.Object => NodeType.Object,
|
|
||||||
JsonValueKind.Array => NodeType.Array,
|
|
||||||
_ => NodeType.Value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var node = GetPathNode(path);
|
|
||||||
if (!node.HasValue)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return node.Value.ValueKind switch
|
|
||||||
{
|
|
||||||
JsonValueKind.Object => NodeType.Object,
|
|
||||||
JsonValueKind.Array => NodeType.Array,
|
|
||||||
_ => NodeType.Value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public T? GetValue<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(T) == typeof(string))
|
|
||||||
{
|
|
||||||
if (value.Value.ValueKind == JsonValueKind.Number)
|
|
||||||
return (T)(object)value.Value.GetInt64().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public T?[]? GetValues<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
var value = GetPathNode(path);
|
|
||||||
if (value == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonElement? GetPathNode(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
JsonElement? currentToken = _document.RootElement;
|
|
||||||
foreach (var node in path)
|
|
||||||
{
|
|
||||||
if (node.Type == 0)
|
|
||||||
{
|
|
||||||
// Int value
|
|
||||||
var val = node.Index!.Value;
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
currentToken = currentToken.Value[val];
|
|
||||||
}
|
|
||||||
else if (node.Type == 1)
|
|
||||||
{
|
|
||||||
// String value
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
|
|
||||||
return null;
|
|
||||||
currentToken = token;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Property name
|
|
||||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken == null)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string GetOriginalString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract void Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
|
|
||||||
{
|
|
||||||
private Stream? _stream;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
|
||||||
{
|
|
||||||
if (bufferStream && stream is not MemoryStream)
|
|
||||||
{
|
|
||||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
|
||||||
_stream = new MemoryStream();
|
|
||||||
stream.CopyTo(_stream);
|
|
||||||
_stream.Position = 0;
|
|
||||||
}
|
|
||||||
else if (bufferStream)
|
|
||||||
{
|
|
||||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
|
||||||
_stream = stream;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
|
||||||
IsValid = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsValid = false;
|
|
||||||
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString()
|
|
||||||
{
|
|
||||||
if (_stream is null)
|
|
||||||
throw new NullReferenceException("Stream not initialized");
|
|
||||||
|
|
||||||
_stream.Position = 0;
|
|
||||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
|
||||||
return textReader.ReadToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_stream?.Dispose();
|
|
||||||
_stream = null;
|
|
||||||
_document?.Dispose();
|
|
||||||
_document = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// System.Text.Json byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
|
|
||||||
{
|
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
_bytes = data;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var firstByte = data.Span[0];
|
|
||||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
|
||||||
{
|
|
||||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
|
||||||
IsValid = false;
|
|
||||||
return new CallResult(new DeserializeError("Not a json value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
|
||||||
IsValid = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsValid = false;
|
|
||||||
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString() =>
|
|
||||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
|
||||||
#if NETSTANDARD2_0
|
|
||||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
|
||||||
#else
|
|
||||||
Encoding.UTF8.GetString(_bytes.Span);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_bytes = null;
|
|
||||||
_document?.Dispose();
|
|
||||||
_document = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.4.0</PackageVersion>
|
||||||
<AssemblyVersion>10.0.0</AssemblyVersion>
|
<AssemblyVersion>10.4.0</AssemblyVersion>
|
||||||
<FileVersion>10.0.0</FileVersion>
|
<FileVersion>10.4.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>
|
||||||
|
|||||||
@@ -32,6 +32,66 @@ namespace CryptoExchange.Net
|
|||||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the specific topic has been cached
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id</param>
|
||||||
|
public static bool HasCached(string topicId)
|
||||||
|
{
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return exchangeInfo.Symbols.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a specific exchange(topic) support the provided symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="symbolName">The symbol name</param>
|
||||||
|
public static bool SupportsSymbol(string topicId, string symbolName)
|
||||||
|
{
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a specific exchange(topic) support the provided symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="symbol">The symbol info</param>
|
||||||
|
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return exchangeInfo.Symbols.Any(x =>
|
||||||
|
x.Value.TradingMode == symbol.TradingMode
|
||||||
|
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||||
|
&& x.Value.QuoteAsset == symbol.QuoteAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all symbols for a specific base asset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="baseAsset">Base asset name</param>
|
||||||
|
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
|
||||||
|
{
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return exchangeInfo.Symbols
|
||||||
|
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a symbol name to a SharedSymbol
|
/// Parse a symbol name to a SharedSymbol
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -70,12 +70,17 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
first = false;
|
first = false;
|
||||||
|
|
||||||
if (parameter.GetType().IsArray)
|
if (parameter.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
if (serializationType == ArrayParametersSerialization.Array)
|
if (serializationType == ArrayParametersSerialization.Array)
|
||||||
{
|
{
|
||||||
foreach(var entry in (object[])parameter.Value)
|
bool firstArrayValue = true;
|
||||||
|
foreach (var entry in (object[])parameter.Value)
|
||||||
{
|
{
|
||||||
|
if (!firstArrayValue)
|
||||||
|
uriString.Append('&');
|
||||||
|
firstArrayValue = false;
|
||||||
|
|
||||||
uriString.Append(parameter.Key);
|
uriString.Append(parameter.Key);
|
||||||
uriString.Append("[]=");
|
uriString.Append("[]=");
|
||||||
if (urlEncodeValues)
|
if (urlEncodeValues)
|
||||||
@@ -86,8 +91,12 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||||
{
|
{
|
||||||
|
bool firstArrayValue = true;
|
||||||
foreach (var entry in (object[])parameter.Value)
|
foreach (var entry in (object[])parameter.Value)
|
||||||
{
|
{
|
||||||
|
if (!firstArrayValue)
|
||||||
|
uriString.Append('&');
|
||||||
|
firstArrayValue = false;
|
||||||
uriString.Append(parameter.Key);
|
uriString.Append(parameter.Key);
|
||||||
uriString.Append("=");
|
uriString.Append("=");
|
||||||
if (urlEncodeValues)
|
if (urlEncodeValues)
|
||||||
@@ -283,122 +292,6 @@ namespace CryptoExchange.Net
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new uri with the provided parameters as query
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <param name="baseUri"></param>
|
|
||||||
/// <param name="arraySerialization"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
|
||||||
{
|
|
||||||
var uriBuilder = new UriBuilder();
|
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
|
||||||
uriBuilder.Host = baseUri.Host;
|
|
||||||
uriBuilder.Port = baseUri.Port;
|
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
{
|
|
||||||
if (parameter.Value.GetType().IsArray)
|
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var item in (object[])parameter.Value)
|
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uriBuilder.Query = httpValueCollection.ToString();
|
|
||||||
return uriBuilder.Uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new uri with the provided parameters as query
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <param name="baseUri"></param>
|
|
||||||
/// <param name="arraySerialization"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
|
|
||||||
{
|
|
||||||
var uriBuilder = new UriBuilder();
|
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
|
||||||
uriBuilder.Host = baseUri.Host;
|
|
||||||
uriBuilder.Port = baseUri.Port;
|
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
{
|
|
||||||
if (parameter.Value.GetType().IsArray)
|
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var item in (object[])parameter.Value)
|
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uriBuilder.Query = httpValueCollection.ToString();
|
|
||||||
return uriBuilder.Uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add parameter to URI
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri"></param>
|
|
||||||
/// <param name="name"></param>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
|
||||||
{
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
|
||||||
|
|
||||||
httpValueCollection.Remove(name);
|
|
||||||
httpValueCollection.Add(name, value);
|
|
||||||
|
|
||||||
var ub = new UriBuilder(uri);
|
|
||||||
ub.Query = httpValueCollection.ToString();
|
|
||||||
|
|
||||||
return ub.Uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using GzipStream
|
/// Decompress using GzipStream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -410,20 +303,6 @@ namespace CryptoExchange.Net
|
|||||||
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decompress using GzipStream
|
|
||||||
/// </summary>
|
|
||||||
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
using var decompressedStream = new MemoryStream();
|
|
||||||
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
|
||||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
|
||||||
: new MemoryStream(data.ToArray());
|
|
||||||
using var deflateStream = new GZipStream(dataStream, CompressionMode.Decompress);
|
|
||||||
deflateStream.CopyTo(decompressedStream);
|
|
||||||
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using GzipStream
|
/// Decompress using GzipStream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -436,22 +315,6 @@ namespace CryptoExchange.Net
|
|||||||
return new ReadOnlySpan<byte>(output.GetBuffer(), 0, (int)output.Length);
|
return new ReadOnlySpan<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decompress using DeflateStream
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
|
||||||
{
|
|
||||||
var output = new MemoryStream();
|
|
||||||
|
|
||||||
using var compressStream = new MemoryStream(input.ToArray());
|
|
||||||
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
|
|
||||||
decompressor.CopyTo(output);
|
|
||||||
output.Position = 0;
|
|
||||||
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is linear
|
/// Whether the trading mode is linear
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -607,6 +470,26 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a hex encoded string to byte array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hexString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static byte[] HexStringToBytes(this string hexString)
|
||||||
|
{
|
||||||
|
if (hexString.StartsWith("0x"))
|
||||||
|
hexString = hexString.Substring(2);
|
||||||
|
|
||||||
|
byte[] bytes = new byte[hexString.Length / 2];
|
||||||
|
for (int i = 0; i < hexString.Length; i += 2)
|
||||||
|
{
|
||||||
|
string hexSubstring = hexString.Substring(i, 2);
|
||||||
|
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,10 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// The exchange name
|
/// The exchange name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string Exchange { get; }
|
string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether client is disposed
|
||||||
|
/// </summary>
|
||||||
|
bool Disposed { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int CurrentSubscriptions { get; }
|
public int CurrentSubscriptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether client is disposed
|
||||||
|
/// </summary>
|
||||||
|
bool Disposed { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message accessor
|
|
||||||
/// </summary>
|
|
||||||
public interface IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Is this a valid message
|
|
||||||
/// </summary>
|
|
||||||
bool IsValid { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Is the original data available for retrieval
|
|
||||||
/// </summary>
|
|
||||||
bool OriginalDataAvailable { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The underlying data object
|
|
||||||
/// </summary>
|
|
||||||
object? Underlying { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Clear internal data structure
|
|
||||||
/// </summary>
|
|
||||||
void Clear();
|
|
||||||
/// <summary>
|
|
||||||
/// Get the type of node
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
NodeType? GetNodeType();
|
|
||||||
/// <summary>
|
|
||||||
/// Get the type of node
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Access path</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
NodeType? GetNodeType(MessagePath path);
|
|
||||||
/// <summary>
|
|
||||||
/// Get the value of a path
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
T? GetValue<T>(MessagePath path);
|
|
||||||
/// <summary>
|
|
||||||
/// Get the values of an array
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
T?[]? GetValues<T>(MessagePath path);
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize the message into this type
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
CallResult<object> Deserialize(Type type, MessagePath? path = null);
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize the message into this type
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
CallResult<T> Deserialize<T>(MessagePath? path = null);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the original string value
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
string GetOriginalString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public interface IStreamMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Load a stream message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream"></param>
|
|
||||||
/// <param name="bufferStream"></param>
|
|
||||||
Task<CallResult> Read(Stream stream, bool bufferStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public interface IByteMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Load a data message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
CallResult Read(ReadOnlyMemory<byte> data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,9 +47,21 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </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,7 +22,6 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
||||||
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
||||||
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
|
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
|
||||||
private static readonly Action<ILogger, int?, string?, Exception?> _restApiReceivedResponse;
|
|
||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
{
|
{
|
||||||
@@ -90,11 +89,6 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4012, "RestApiCancellationRequested"),
|
new EventId(4012, "RestApiCancellationRequested"),
|
||||||
"[Req {RequestId}] Request cancelled by user");
|
"[Req {RequestId}] Request cancelled by user");
|
||||||
|
|
||||||
_restApiReceivedResponse = LoggerMessage.Define<int?, string?>(
|
|
||||||
LogLevel.Trace,
|
|
||||||
new EventId(4013, "RestApiReceivedResponse"),
|
|
||||||
"[Req {RequestId}] Received response: {Data}");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,10 +155,5 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_restApiCancellationRequested(logger, requestId, null);
|
_restApiCancellationRequested(logger, requestId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiReceivedResponse(this ILogger logger, int requestId, string? originalData)
|
|
||||||
{
|
|
||||||
_restApiReceivedResponse(logger, requestId, originalData, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Async auto reset based on Stephen Toub`s implementation
|
|
||||||
/// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-2-asyncautoresetevent/
|
|
||||||
/// </summary>
|
|
||||||
public class AsyncResetEvent : IDisposable
|
|
||||||
{
|
|
||||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
|
||||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
private readonly Lock _waitsLock = new Lock();
|
|
||||||
#else
|
|
||||||
private readonly object _waitsLock = new object();
|
|
||||||
#endif
|
|
||||||
private bool _signaled;
|
|
||||||
private readonly bool _reset;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// New AsyncResetEvent
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="initialState"></param>
|
|
||||||
/// <param name="reset"></param>
|
|
||||||
public AsyncResetEvent(bool initialState = false, bool reset = true)
|
|
||||||
{
|
|
||||||
_signaled = initialState;
|
|
||||||
_reset = reset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Wait for the AutoResetEvent to be set
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
CancellationTokenRegistration registration = default;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Task<bool> waiter = _completed;
|
|
||||||
lock (_waitsLock)
|
|
||||||
{
|
|
||||||
if (_signaled)
|
|
||||||
{
|
|
||||||
if (_reset)
|
|
||||||
_signaled = false;
|
|
||||||
}
|
|
||||||
else if (!ct.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
if (timeout.HasValue)
|
|
||||||
{
|
|
||||||
var timeoutSource = new CancellationTokenSource(timeout.Value);
|
|
||||||
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
|
|
||||||
ct = cancellationSource.Token;
|
|
||||||
}
|
|
||||||
|
|
||||||
registration = ct.Register(() =>
|
|
||||||
{
|
|
||||||
lock (_waitsLock)
|
|
||||||
{
|
|
||||||
tcs.TrySetResult(false);
|
|
||||||
|
|
||||||
// Not the cleanest but it works
|
|
||||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
|
||||||
}
|
|
||||||
}, useSynchronizationContext: false);
|
|
||||||
|
|
||||||
|
|
||||||
_waits.Enqueue(tcs);
|
|
||||||
waiter = tcs.Task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await waiter.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
registration.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Signal a waiter
|
|
||||||
/// </summary>
|
|
||||||
public void Set()
|
|
||||||
{
|
|
||||||
lock (_waitsLock)
|
|
||||||
{
|
|
||||||
if (!_reset)
|
|
||||||
{
|
|
||||||
// Act as ManualResetEvent. Once set keep it signaled and signal everyone who is waiting
|
|
||||||
_signaled = true;
|
|
||||||
while (_waits.Count > 0)
|
|
||||||
{
|
|
||||||
var toRelease = _waits.Dequeue();
|
|
||||||
toRelease.TrySetResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Act as AutoResetEvent. When set signal 1 waiter
|
|
||||||
if (_waits.Count > 0)
|
|
||||||
{
|
|
||||||
var toRelease = _waits.Dequeue();
|
|
||||||
toRelease.TrySetResult(true);
|
|
||||||
}
|
|
||||||
else if (!_signaled)
|
|
||||||
{
|
|
||||||
_signaled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_waits.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Async auto/manual reset event implementation
|
||||||
|
/// </summary>
|
||||||
|
public class AsyncResetEvent
|
||||||
|
{
|
||||||
|
private readonly Queue<TaskCompletionSource<bool>> _waiters = new();
|
||||||
|
private readonly bool _autoReset;
|
||||||
|
private bool _signaled;
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _waitersLock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _waitersLock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public AsyncResetEvent(bool initialState = false, bool autoReset = true)
|
||||||
|
{
|
||||||
|
_signaled = initialState;
|
||||||
|
_autoReset = autoReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait for the set event
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> WaitAsync(
|
||||||
|
TimeSpan? timeout = null,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> tcs;
|
||||||
|
|
||||||
|
lock (_waitersLock)
|
||||||
|
{
|
||||||
|
if (_signaled)
|
||||||
|
{
|
||||||
|
// Already was signaled, can return immediately
|
||||||
|
if (_autoReset)
|
||||||
|
_signaled = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
_waiters.Enqueue(tcs);
|
||||||
|
}
|
||||||
|
|
||||||
|
CancellationTokenSource? delayCts = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (timeout.HasValue || ct.CanBeCanceled)
|
||||||
|
{
|
||||||
|
delayCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
|
||||||
|
var delayTask = Task.Delay(
|
||||||
|
timeout ?? Timeout.InfiniteTimeSpan,
|
||||||
|
delayCts.Token);
|
||||||
|
|
||||||
|
var completedTask =
|
||||||
|
await Task.WhenAny(tcs.Task, delayTask)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (completedTask != tcs.Task)
|
||||||
|
{
|
||||||
|
// This was a timeout or cancellation, need to remove tcs from waiters
|
||||||
|
// if the tcs was set instead it will be removed in the Set method
|
||||||
|
if (tcs.TrySetResult(false))
|
||||||
|
{
|
||||||
|
lock (_waitersLock)
|
||||||
|
{
|
||||||
|
// Dequeue and put in the back of the queue again except for the one we need to remove
|
||||||
|
int count = _waiters.Count;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var w = _waiters.Dequeue();
|
||||||
|
if (w != tcs)
|
||||||
|
_waiters.Enqueue(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await tcs.Task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Actively stop the delay if tcs.Task won
|
||||||
|
delayCts?.Cancel();
|
||||||
|
delayCts?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signal a waiter
|
||||||
|
/// </summary>
|
||||||
|
public void Set()
|
||||||
|
{
|
||||||
|
if (!_autoReset && _signaled)
|
||||||
|
// Already signaled and not resetting
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_waitersLock)
|
||||||
|
{
|
||||||
|
if (_autoReset)
|
||||||
|
{
|
||||||
|
while (_waiters.Count > 0)
|
||||||
|
{
|
||||||
|
// Try to dequeue and set the result
|
||||||
|
// If result setting was not successful it means timeout/cancellation happened at the same time
|
||||||
|
// If this is the case this Set isn't the one setting the result and we need to continue
|
||||||
|
var w = _waiters.Dequeue();
|
||||||
|
if (w.TrySetResult(true))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No queued waiters, set signaled for next waiter
|
||||||
|
_signaled = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_signaled = true;
|
||||||
|
|
||||||
|
// Signal all current waiters
|
||||||
|
while (_waiters.Count > 0)
|
||||||
|
{
|
||||||
|
var w = _waiters.Dequeue();
|
||||||
|
w.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -250,6 +250,40 @@
|
|||||||
DEX
|
DEX
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of platform
|
||||||
|
/// </summary>
|
||||||
|
public enum PlatformType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Platform to trade cryptocurrency
|
||||||
|
/// </summary>
|
||||||
|
CryptoCurrencyExchange,
|
||||||
|
/// <summary>
|
||||||
|
/// Platform for trading on predictions
|
||||||
|
/// </summary>
|
||||||
|
PredictionMarket,
|
||||||
|
/// <summary>
|
||||||
|
/// Other
|
||||||
|
/// </summary>
|
||||||
|
Other
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Centralization type
|
||||||
|
/// </summary>
|
||||||
|
public enum CentralizationType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Centralized, a person or company is in full control
|
||||||
|
/// </summary>
|
||||||
|
Centralized,
|
||||||
|
/// <summary>
|
||||||
|
/// Decentralized, governance is split over different entities with no single entity in full control
|
||||||
|
/// </summary>
|
||||||
|
Decentralized
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timeout behavior for queries
|
/// Timeout behavior for queries
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApiOptions
|
public class ApiOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not to automatically sync the local time with the server time
|
||||||
|
/// </summary>
|
||||||
|
public bool? AutoTimestamp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
|
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
|
||||||
/// Note that this comes at a performance cost
|
/// Note that this comes at a performance cost
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExchangeOptions
|
public class ExchangeOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not to automatically sync the local time with the server time
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoTimestamp { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy settings
|
/// Proxy settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RestApiOptions : ApiOptions
|
public class RestApiOptions : ApiOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Whether or not to automatically sync the local time with the server time
|
|
||||||
/// </summary>
|
|
||||||
public bool? AutoTimestamp { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RestExchangeOptions: ExchangeOptions
|
public class RestExchangeOptions: ExchangeOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Whether or not to automatically sync the local time with the server time
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoTimestamp { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.ApiCredentials = ApiCredentials?.Copy();
|
item.ApiCredentials = ApiCredentials?.Copy();
|
||||||
item.OutputOriginalData = OutputOriginalData;
|
item.OutputOriginalData = OutputOriginalData;
|
||||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||||
|
item.AutoTimestamp = AutoTimestamp;
|
||||||
item.MaxSocketConnections = MaxSocketConnections;
|
item.MaxSocketConnections = MaxSocketConnections;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,9 +76,13 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
public int? ReceiveBufferSize { get; set; }
|
public int? ReceiveBufferSize { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not to use the updated deserialization logic, default is true
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UseUpdatedDeserialization { get; set; } = true;
|
public SocketExchangeOptions()
|
||||||
|
{
|
||||||
|
// Enable auto timestamping by default for sockets
|
||||||
|
AutoTimestamp = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a copy of this options
|
/// Create a copy of this options
|
||||||
@@ -88,6 +92,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
||||||
{
|
{
|
||||||
item.ApiCredentials = ApiCredentials?.Copy();
|
item.ApiCredentials = ApiCredentials?.Copy();
|
||||||
|
item.AutoTimestamp = AutoTimestamp;
|
||||||
item.OutputOriginalData = OutputOriginalData;
|
item.OutputOriginalData = OutputOriginalData;
|
||||||
item.ReconnectPolicy = ReconnectPolicy;
|
item.ReconnectPolicy = ReconnectPolicy;
|
||||||
item.DelayAfterConnect = DelayAfterConnect;
|
item.DelayAfterConnect = DelayAfterConnect;
|
||||||
@@ -101,7 +106,6 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||||
item.ReceiveBufferSize = ReceiveBufferSize;
|
item.ReceiveBufferSize = ReceiveBufferSize;
|
||||||
item.UseUpdatedDeserialization = UseUpdatedDeserialization;
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,23 @@ namespace CryptoExchange.Net.Objects
|
|||||||
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a DateTime value as string
|
||||||
|
/// </summary>
|
||||||
|
public void AddString(string key, DateTime value)
|
||||||
|
{
|
||||||
|
base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a DateTime value as string. Not added if value is null
|
||||||
|
/// </summary>
|
||||||
|
public void AddOptionalString(string key, DateTime? value)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a datetime value as milliseconds timestamp
|
/// Add a datetime value as milliseconds timestamp
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -241,6 +258,45 @@ namespace CryptoExchange.Net.Objects
|
|||||||
base.Add(key, int.Parse(stringVal));
|
base.Add(key, int.Parse(stringVal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add key as comma separated values
|
||||||
|
/// </summary>
|
||||||
|
public void AddCommaSeparated(string key, IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
base.Add(key, string.Join(",", values));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add key as comma separated values if there are values provided
|
||||||
|
/// </summary>
|
||||||
|
public void AddOptionalCommaSeparated(string key, IEnumerable<string>? values)
|
||||||
|
{
|
||||||
|
if (values == null || !values.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
base.Add(key, string.Join(",", values));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add key as boolean lower case value
|
||||||
|
/// </summary>
|
||||||
|
public void AddBoolString(string key, bool value)
|
||||||
|
{
|
||||||
|
base.Add(key, value.ToString().ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add key as boolean lower case value if it's not null
|
||||||
|
/// </summary>
|
||||||
|
public void AddOptionalBoolString(string key, bool? value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
base.Add(key, value.ToString()!.ToLower());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
|
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Information on the platform
|
||||||
|
/// </summary>
|
||||||
|
public record PlatformInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Platform id
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Display name
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Logo
|
||||||
|
/// </summary>
|
||||||
|
public string Logo { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Url to main application
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Urls to the API documentation
|
||||||
|
/// </summary>
|
||||||
|
public string[] ApiDocsUrl { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Platform type
|
||||||
|
/// </summary>
|
||||||
|
public PlatformType PlatformType { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Centralization type
|
||||||
|
/// </summary>
|
||||||
|
public CentralizationType CentralizationType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public PlatformInfo(string id, string displayName, string logo, string url, string[] apiDocsUrl, PlatformType platformType, CentralizationType centralizationType)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
DisplayName = displayName;
|
||||||
|
Logo = logo;
|
||||||
|
Url = url;
|
||||||
|
ApiDocsUrl = apiDocsUrl;
|
||||||
|
PlatformType = platformType;
|
||||||
|
CentralizationType = centralizationType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,21 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id => _subscription.Id;
|
public int Id => _subscription.Id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last timestamp anything was received from the server
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastReceiveTime => _connection.LastReceiveTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current websocket status
|
||||||
|
/// </summary>
|
||||||
|
public SocketStatus SocketStatus => _connection.Status;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current subscription status
|
||||||
|
/// </summary>
|
||||||
|
public SubscriptionStatus SubscriptionStatus => _subscription.Status;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -73,11 +73,6 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// The buffer size to use for receiving data
|
/// The buffer size to use for receiving data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ReceiveBufferSize { get; set; } = null;
|
public int? ReceiveBufferSize { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether or not to use the updated deserialization logic
|
|
||||||
/// </summary>
|
|
||||||
public bool UseUpdatedDeserialization { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The time synchronization state of an API client
|
|
||||||
/// </summary>
|
|
||||||
public class TimeSyncState
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the API
|
|
||||||
/// </summary>
|
|
||||||
public string ApiName { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Semaphore to use for checking the time syncing. Should be shared instance among the API client
|
|
||||||
/// </summary>
|
|
||||||
public SemaphoreSlim Semaphore { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Last sync time for the API client
|
|
||||||
/// </summary>
|
|
||||||
public DateTime LastSyncTime { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Time offset for the API client
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan TimeOffset { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public TimeSyncState(string apiName)
|
|
||||||
{
|
|
||||||
ApiName = apiName;
|
|
||||||
Semaphore = new SemaphoreSlim(1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time synchronization info
|
|
||||||
/// </summary>
|
|
||||||
public class TimeSyncInfo
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Logger
|
|
||||||
/// </summary>
|
|
||||||
public ILogger Logger { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Should synchronize time
|
|
||||||
/// </summary>
|
|
||||||
public bool SyncTime { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp recalulcation interval
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan RecalculationInterval { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Time sync state for the API client
|
|
||||||
/// </summary>
|
|
||||||
public TimeSyncState TimeSyncState { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger"></param>
|
|
||||||
/// <param name="recalculationInterval"></param>
|
|
||||||
/// <param name="syncTime"></param>
|
|
||||||
/// <param name="syncState"></param>
|
|
||||||
public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
SyncTime = syncTime;
|
|
||||||
RecalculationInterval = recalculationInterval;
|
|
||||||
TimeSyncState = syncState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the time offset
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="offset"></param>
|
|
||||||
public void UpdateTimeOffset(TimeSpan offset)
|
|
||||||
{
|
|
||||||
TimeSyncState.LastSyncTime = DateTime.UtcNow;
|
|
||||||
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
|
|
||||||
{
|
|
||||||
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName);
|
|
||||||
TimeSyncState.TimeOffset = TimeSpan.Zero;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds));
|
|
||||||
TimeSyncState.TimeOffset = offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+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)
|
||||||
@@ -565,6 +640,42 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
return new CallResult<bool>(true);
|
return new CallResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wait until an update has been buffered
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minWait">Min wait time</param>
|
||||||
|
/// <param name="maxWait">Max wait time</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected async Task<CallResult> WaitUntilFirstUpdateBufferedAsync(TimeSpan? minWait, TimeSpan maxWait, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var startWait = DateTime.UtcNow;
|
||||||
|
while (_processBuffer.Count == 0)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult(new CancellationRequestedError());
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - startWait > maxWait)
|
||||||
|
return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(20, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minWait != null)
|
||||||
|
{
|
||||||
|
var dif = DateTime.UtcNow - startWait;
|
||||||
|
if (dif < minWait)
|
||||||
|
await Task.Delay(minWait.Value - dif).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// IDisposable implementation for the order book
|
/// IDisposable implementation for the order book
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -614,6 +725,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 +742,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 +774,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 +816,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 +838,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 +866,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 +891,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 +920,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 +960,99 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
private void ProcessUpdate(
|
||||||
|
long updateSequenceNumberStart,
|
||||||
|
long updateSequenceNumberEnd,
|
||||||
|
IEnumerable<ISymbolOrderBookEntry> bids,
|
||||||
|
IEnumerable<ISymbolOrderBookEntry> asks,
|
||||||
|
bool fromBuffer)
|
||||||
{
|
{
|
||||||
if (lastUpdateId <= LastSequenceNumber)
|
var sequenceResult = fromBuffer ? ValidateBufferSequenceNumber(updateSequenceNumberStart, updateSequenceNumberEnd) : ValidateLiveSequenceNumber(updateSequenceNumberStart);
|
||||||
|
if (sequenceResult == SequenceNumberResult.Skip)
|
||||||
{
|
{
|
||||||
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
|
if (updateSequenceNumberStart != updateSequenceNumberEnd)
|
||||||
|
_logger.OrderBookUpdateSkipped(Api, Symbol, updateSequenceNumberStart, updateSequenceNumberEnd, LastSequenceNumber);
|
||||||
|
else
|
||||||
|
_logger.OrderBookUpdateSkipped(Api, Symbol, updateSequenceNumberStart, LastSequenceNumber);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sequenceResult == SequenceNumberResult.OutOfSync)
|
||||||
|
{
|
||||||
|
_logger.OrderBookOutOfSync(Api, Symbol, LastSequenceNumber + 1, updateSequenceNumberStart);
|
||||||
|
_stopProcessing = true;
|
||||||
|
Resubscribe();
|
||||||
return;
|
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
|
||||||
|
&& (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet))
|
||||||
|
// Update is somehow from before the current state
|
||||||
|
return SequenceNumberResult.OutOfSync;
|
||||||
|
|
||||||
|
if (_sequencesAreConsecutive
|
||||||
|
&& LastSequenceNumber != 0
|
||||||
|
&& sequenceNumber != LastSequenceNumber + 1)
|
||||||
|
{
|
||||||
|
if (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet)
|
||||||
|
return SequenceNumberResult.OutOfSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SequenceNumberResult.Ok;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DescComparer<T> : IComparer<T>
|
internal class DescComparer<T> : IComparer<T>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Transfer status
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTransferStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// In progress
|
||||||
|
/// </summary>
|
||||||
|
InProgress,
|
||||||
|
/// <summary>
|
||||||
|
/// Failed
|
||||||
|
/// </summary>
|
||||||
|
Failed,
|
||||||
|
/// <summary>
|
||||||
|
/// Completed
|
||||||
|
/// </summary>
|
||||||
|
Completed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,25 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Futures symbol request options
|
/// Futures symbol request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
EndpointOptions<GetSymbolsRequest> GetFuturesSymbolsOptions { get; }
|
EndpointOptions<GetSymbolsRequest> GetFuturesSymbolsOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all futures symbols for a specific base asset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAsset">Asset, for example `ETH`</param>
|
||||||
|
Task<ExchangeResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the client supports a futures symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(SharedSymbol symbol);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the client supports a futures symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbolName">The symbol name</param>
|
||||||
|
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(string symbolName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get info on all futures symbols supported on the exchange
|
/// Get info on all futures symbols supported on the exchange
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@@ -13,6 +14,24 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
EndpointOptions<GetSymbolsRequest> GetSpotSymbolsOptions { get; }
|
EndpointOptions<GetSymbolsRequest> GetSpotSymbolsOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all spot symbols for a specific base asset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAsset">Asset, for example `ETH`</param>
|
||||||
|
Task<ExchangeResult<SharedSymbol[]>> GetSpotSymbolsForBaseAssetAsync(string baseAsset);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the client supports a spot symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
Task<ExchangeResult<bool>> SupportsSpotSymbolAsync(SharedSymbol symbol);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the client supports a spot symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbolName">The symbol name</param>
|
||||||
|
Task<ExchangeResult<bool>> SupportsSpotSymbolAsync(string symbolName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get info on all available spot symbols on the exchange
|
/// Get info on all available spot symbols on the exchange
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ExchangeResult(
|
||||||
|
string exchange,
|
||||||
|
T result) :
|
||||||
|
base(result, null, null)
|
||||||
|
{
|
||||||
|
Exchange = exchange;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => $"{Exchange} - " + base.ToString();
|
public override string ToString() => $"{Exchange} - " + base.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,15 +44,21 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Completed { get; set; }
|
public bool Completed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status of the deposit
|
||||||
|
/// </summary>
|
||||||
|
public SharedTransferStatus Status { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp)
|
public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp, SharedTransferStatus status)
|
||||||
{
|
{
|
||||||
Asset = asset;
|
Asset = asset;
|
||||||
Quantity = quantity;
|
Quantity = quantity;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
Completed = completed;
|
Completed = completed;
|
||||||
|
Status = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SharedPositionSide PositionSide { get; set; }
|
public SharedPositionSide PositionSide { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Whether the position is one way mode
|
||||||
|
/// </summary>
|
||||||
|
public SharedPositionMode PositionMode { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// Average open price
|
/// Average open price
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? AverageOpenPrice { get; set; }
|
public decimal? AverageOpenPrice { get; set; }
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? QuantityInContracts { get; set; }
|
public decimal? QuantityInContracts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether all values are null or zero
|
||||||
|
/// </summary>
|
||||||
|
public bool IsZero => !(QuantityInBaseAsset > 0) && !(QuantityInQuoteAsset > 0) && !(QuantityInContracts > 0);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
private int _reconnectAttempt;
|
private int _reconnectAttempt;
|
||||||
private readonly int _receiveBufferSize;
|
private readonly int _receiveBufferSize;
|
||||||
|
|
||||||
private const int _defaultReceiveBufferSize = 1048576;
|
|
||||||
private const int _sendBufferSize = 4096;
|
private const int _sendBufferSize = 4096;
|
||||||
|
|
||||||
private int _bytesReceived = 0;
|
private int _bytesReceived = 0;
|
||||||
@@ -71,7 +70,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The timestamp this socket has been active for the last time
|
/// The timestamp this socket has been active for the last time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime LastActionTime { get; private set; }
|
public DateTime? LastReceiveTime { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Uri Uri => Parameters.Uri;
|
public Uri Uri => Parameters.Uri;
|
||||||
@@ -95,9 +94,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<Task>? OnClose;
|
public event Func<Task>? OnClose;
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task>? OnStreamMessage;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<int, Task>? OnRequestSent;
|
public event Func<int, Task>? OnRequestSent;
|
||||||
|
|
||||||
@@ -139,10 +135,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_sendEvent = new AsyncResetEvent();
|
_sendEvent = new AsyncResetEvent();
|
||||||
_sendBuffer = new ConcurrentQueue<SendItem>();
|
_sendBuffer = new ConcurrentQueue<SendItem>();
|
||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
if (websocketParameters.UseUpdatedDeserialization)
|
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? 65536;
|
||||||
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? 65536;
|
|
||||||
else
|
|
||||||
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize;
|
|
||||||
|
|
||||||
_closeSem = new SemaphoreSlim(1, 1);
|
_closeSem = new SemaphoreSlim(1, 1);
|
||||||
_socket = CreateSocket();
|
_socket = CreateSocket();
|
||||||
@@ -225,7 +218,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
_logger.SocketConnectingCanceled(Id);
|
_logger.SocketConnectingCanceled(Id);
|
||||||
|
}
|
||||||
else if (!_ctsSource.IsCancellationRequested)
|
else if (!_ctsSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// if _ctsSource was canceled this was already logged
|
// if _ctsSource was canceled this was already logged
|
||||||
@@ -271,11 +266,10 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
var sendTask = SendLoopAsync();
|
var sendTask = SendLoopAsync();
|
||||||
Task receiveTask;
|
Task receiveTask;
|
||||||
#if !NETSTANDARD2_0
|
#if !NETSTANDARD2_0
|
||||||
if (Parameters.UseUpdatedDeserialization)
|
receiveTask = ReceiveLoopNewAsync();
|
||||||
receiveTask = ReceiveLoopNewAsync();
|
#else
|
||||||
else
|
receiveTask = ReceiveLoopAsync();
|
||||||
#endif
|
#endif
|
||||||
receiveTask = ReceiveLoopAsync();
|
|
||||||
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||||
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||||
_logger.SocketFinishedProcessing(Id);
|
_logger.SocketFinishedProcessing(Id);
|
||||||
@@ -492,7 +486,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
_ctsSource?.Dispose();
|
_ctsSource?.Dispose();
|
||||||
_sendEvent.Dispose();
|
|
||||||
_logger.SocketDisposed(Id);
|
_logger.SocketDisposed(Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,6 +571,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if NETSTANDARD2_0
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loop for receiving and reassembling data
|
/// Loop for receiving and reassembling data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -627,6 +621,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LastReceiveTime = DateTime.UtcNow;
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
// Connection closed
|
// Connection closed
|
||||||
@@ -666,10 +661,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (_logger.IsEnabled(LogLevel.Trace))
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
_logger.SocketReceivedSingleMessage(Id, receiveResult.Count);
|
_logger.SocketReceivedSingleMessage(Id, receiveResult.Count);
|
||||||
|
|
||||||
if (!Parameters.UseUpdatedDeserialization)
|
ProcessDataNew(receiveResult.MessageType, new ReadOnlySpan<byte>(buffer.Array!, buffer.Offset, receiveResult.Count));
|
||||||
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array!, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
ProcessDataNew(receiveResult.MessageType, new ReadOnlySpan<byte>(buffer.Array!, buffer.Offset, receiveResult.Count));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -703,11 +695,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_logger.SocketReassembledMessage(Id, multipartStream!.Length);
|
_logger.SocketReassembledMessage(Id, multipartStream!.Length);
|
||||||
|
|
||||||
// Get the underlying buffer of the memory stream holding the written data and delimit it (GetBuffer return the full array, not only the written part)
|
// Get the underlying buffer of the memory stream holding the written data and delimit it (GetBuffer return the full array, not only the written part)
|
||||||
|
ProcessDataNew(receiveResult.MessageType, new ReadOnlySpan<byte>(multipartStream!.GetBuffer(), 0, (int)multipartStream.Length));
|
||||||
if (!Parameters.UseUpdatedDeserialization)
|
|
||||||
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream!.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
ProcessDataNew(receiveResult.MessageType, new ReadOnlySpan<byte>(multipartStream!.GetBuffer(), 0, (int)multipartStream.Length));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -732,6 +720,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_logger.SocketReceiveLoopFinished(Id);
|
_logger.SocketReceiveLoopFinished(Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if !NETSTANDARD2_0
|
#if !NETSTANDARD2_0
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -783,6 +772,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LastReceiveTime = DateTime.UtcNow;
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
// Connection closed
|
// Connection closed
|
||||||
@@ -891,22 +881,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected void ProcessDataNew(WebSocketMessageType type, ReadOnlySpan<byte> data)
|
protected void ProcessDataNew(WebSocketMessageType type, ReadOnlySpan<byte> data)
|
||||||
{
|
{
|
||||||
LastActionTime = DateTime.UtcNow;
|
|
||||||
_connection.HandleStreamMessage2(type, data);
|
_connection.HandleStreamMessage2(type, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process a stream message
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected async Task ProcessData(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
LastActionTime = DateTime.UtcNow;
|
|
||||||
await (OnStreamMessage?.Invoke(type, data) ?? Task.CompletedTask).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if there is no data received for a period longer than the specified timeout
|
/// Checks if there is no data received for a period longer than the specified timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -914,7 +891,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
protected async Task CheckTimeoutAsync()
|
protected async Task CheckTimeoutAsync()
|
||||||
{
|
{
|
||||||
_logger.SocketStartingTaskForNoDataReceivedCheck(Id, Parameters.Timeout);
|
_logger.SocketStartingTaskForNoDataReceivedCheck(Id, Parameters.Timeout);
|
||||||
LastActionTime = DateTime.UtcNow;
|
LastReceiveTime = DateTime.UtcNow;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
@@ -922,7 +899,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
|
if (DateTime.UtcNow - LastReceiveTime > Parameters.Timeout)
|
||||||
{
|
{
|
||||||
_logger.SocketNoDataReceiveTimoutReconnect(Id, Parameters.Timeout);
|
_logger.SocketNoDataReceiveTimoutReconnect(Id, Parameters.Timeout);
|
||||||
_ = ReconnectAsync().ConfigureAwait(false);
|
_ = ReconnectAsync().ConfigureAwait(false);
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ namespace CryptoExchange.Net.Sockets.Default.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
event Func<Task> OnClose;
|
event Func<Task> OnClose;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Websocket message received event
|
|
||||||
/// </summary>
|
|
||||||
event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
|
||||||
/// <summary>
|
|
||||||
/// Websocket sent event, RequestId as parameter
|
/// Websocket sent event, RequestId as parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Func<int, Task> OnRequestSent;
|
event Func<int, Task> OnRequestSent;
|
||||||
@@ -73,6 +69,10 @@ namespace CryptoExchange.Net.Sockets.Default.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsOpen { get; }
|
bool IsOpen { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Last timestamp something was received from the server
|
||||||
|
/// </summary>
|
||||||
|
DateTime? LastReceiveTime { get; }
|
||||||
|
/// <summary>
|
||||||
/// Connect the socket
|
/// Connect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action? ActivityUnpaused;
|
public event Action? ActivityUnpaused;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unhandled message event
|
|
||||||
/// </summary>
|
|
||||||
public event Action<IMessageAccessor>? UnhandledMessage;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connection was rate limited and couldn't be established
|
/// Connection was rate limited and couldn't be established
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -183,6 +178,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? DisconnectTime { get; set; }
|
public DateTime? DisconnectTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Last timestamp something was received from the server
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastReceiveTime => _socket.LastReceiveTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tag for identification
|
/// Tag for identification
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -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();
|
||||||
@@ -268,12 +269,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
private SocketStatus _status;
|
private SocketStatus _status;
|
||||||
|
|
||||||
private readonly IMessageSerializer _serializer;
|
private readonly IMessageSerializer _serializer;
|
||||||
private IByteMessageAccessor? _stringMessageAccessor;
|
|
||||||
private IByteMessageAccessor? _byteMessageAccessor;
|
|
||||||
|
|
||||||
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>
|
||||||
@@ -288,12 +289,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// The underlying websocket
|
/// The underlying websocket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IWebsocket _socket;
|
private readonly IWebsocket _socket;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache for deserialization, only caches for a single message
|
|
||||||
/// </summary>
|
|
||||||
private readonly Dictionary<Type, object> _deserializationCache = new Dictionary<Type, object>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// New socket connection
|
/// New socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -307,7 +303,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_socket = socketFactory.CreateWebsocket(logger, this, parameters);
|
_socket = socketFactory.CreateWebsocket(logger, this, parameters);
|
||||||
_logger.SocketCreatedForAddress(_socket.Id, parameters.Uri.ToString());
|
_logger.SocketCreatedForAddress(_socket.Id, parameters.Uri.ToString());
|
||||||
|
|
||||||
_socket.OnStreamMessage += HandleStreamMessage;
|
|
||||||
_socket.OnRequestSent += HandleRequestSentAsync;
|
_socket.OnRequestSent += HandleRequestSentAsync;
|
||||||
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
|
_socket.OnRequestRateLimited += HandleRequestRateLimitedAsync;
|
||||||
_socket.OnConnectRateLimited += HandleConnectRateLimitedAsync;
|
_socket.OnConnectRateLimited += HandleConnectRateLimitedAsync;
|
||||||
@@ -340,6 +335,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 +367,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)
|
||||||
{
|
{
|
||||||
@@ -566,8 +563,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
if (deserializationType == null)
|
if (deserializationType == null)
|
||||||
{
|
{
|
||||||
// No handler found for identifier either, can't process
|
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
||||||
_logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray()));
|
{
|
||||||
|
// No handler found for identifier either, can't process
|
||||||
|
_logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray()));
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,12 +637,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|| route.TopicFilter == null
|
|| route.TopicFilter == null
|
||||||
|| route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
|
|| route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
processed = true;
|
||||||
|
|
||||||
if (isQuery && query!.Completed)
|
if (isQuery && query!.Completed)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
processed = true;
|
|
||||||
processor.Handle(this, receiveTime, originalData, result, route);
|
processor.Handle(this, receiveTime, originalData, result, route);
|
||||||
|
|
||||||
if (isQuery && !route.MultipleReaders)
|
if (isQuery && !route.MultipleReaders)
|
||||||
{
|
{
|
||||||
complete = true;
|
complete = true;
|
||||||
@@ -657,146 +658,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
if (!processed)
|
if (!processed)
|
||||||
{
|
{
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, topicFilter!,
|
|
||||||
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle a message
|
|
||||||
/// </summary>
|
|
||||||
protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
var receiveTime = DateTime.UtcNow;
|
|
||||||
string? originalData = null;
|
|
||||||
|
|
||||||
// 1. Decrypt/Preprocess if necessary
|
|
||||||
data = ApiClient.PreprocessStreamMessage(this, type, data);
|
|
||||||
|
|
||||||
// 2. Read data into accessor
|
|
||||||
IByteMessageAccessor accessor;
|
|
||||||
if (type == WebSocketMessageType.Binary)
|
|
||||||
accessor = _stringMessageAccessor ??= ApiClient.CreateAccessor(type);
|
|
||||||
else
|
|
||||||
accessor = _byteMessageAccessor ??= ApiClient.CreateAccessor(type);
|
|
||||||
|
|
||||||
var result = accessor.Read(data);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bool outputOriginalData = ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData;
|
|
||||||
if (outputOriginalData)
|
|
||||||
{
|
|
||||||
originalData = accessor.GetOriginalString();
|
|
||||||
_logger.ReceivedData(SocketId, originalData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages)
|
|
||||||
{
|
|
||||||
_logger.FailedToParse(SocketId, result.Error!.Message ?? result.Error!.ErrorDescription!);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Determine the identifying properties of this message
|
|
||||||
var listenId = ApiClient.GetListenerIdentifier(accessor);
|
|
||||||
if (listenId == null)
|
|
||||||
{
|
|
||||||
originalData ??= "[OutputOriginalData is false]";
|
|
||||||
if (!ApiClient.UnhandledMessageExpected)
|
|
||||||
_logger.FailedToEvaluateMessage(SocketId, originalData);
|
|
||||||
|
|
||||||
UnhandledMessage?.Invoke(accessor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool processed = false;
|
|
||||||
var totalUserTime = 0;
|
|
||||||
|
|
||||||
List<IMessageProcessor> localListeners;
|
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
localListeners = _listeners.ToList();
|
|
||||||
|
|
||||||
foreach (var processor in localListeners)
|
|
||||||
{
|
{
|
||||||
foreach (var listener in processor.MessageMatcher.GetHandlerLinks(listenId))
|
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, topicFilter!,
|
||||||
{
|
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
|
||||||
processed = true;
|
|
||||||
_logger.ProcessorMatched(SocketId, listener.ToString(), listenId);
|
|
||||||
|
|
||||||
// 4. Determine the type to deserialize to for this processor
|
|
||||||
var messageType = listener.DeserializationType;
|
|
||||||
if (messageType == null)
|
|
||||||
{
|
|
||||||
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processor is Subscription subscriptionProcessor && subscriptionProcessor.Status == SubscriptionStatus.Subscribing)
|
|
||||||
{
|
|
||||||
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
|
||||||
subscriptionProcessor.Status = SubscriptionStatus.Subscribed;
|
|
||||||
if (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed)
|
|
||||||
// If this subscription has a query waiting for a timeout (success if there is no error response)
|
|
||||||
// then time it out now as the data is being received, so we assume it's successful
|
|
||||||
subscriptionProcessor.SubscriptionQuery.Timeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Deserialize the message
|
|
||||||
_deserializationCache.TryGetValue(messageType, out var deserialized);
|
|
||||||
|
|
||||||
if (deserialized == null)
|
|
||||||
{
|
|
||||||
var desResult = processor.Deserialize(accessor, messageType);
|
|
||||||
if (!desResult)
|
|
||||||
{
|
|
||||||
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialized = desResult.Data;
|
|
||||||
_deserializationCache.Add(messageType, deserialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Pass the message to the handler
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var innerSw = Stopwatch.StartNew();
|
|
||||||
processor.Handle(this, receiveTime, originalData, deserialized, listener);
|
|
||||||
if (processor is Query query && query.RequiredResponses != 1)
|
|
||||||
_logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}");
|
|
||||||
totalUserTime += (int)innerSw.ElapsedMilliseconds;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.UserMessageProcessingFailed(SocketId, ex.Message, ex);
|
|
||||||
if (processor is Subscription subscription)
|
|
||||||
subscription.InvokeExceptionHandler(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!processed)
|
|
||||||
{
|
|
||||||
if (!ApiClient.UnhandledMessageExpected)
|
|
||||||
{
|
|
||||||
List<string> listenerIds;
|
|
||||||
lock (_listenersLock)
|
|
||||||
listenerIds = _listeners.Select(l => l.MessageMatcher.ToString()).ToList();
|
|
||||||
|
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
|
||||||
UnhandledMessage?.Invoke(accessor);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_deserializationCache.Clear();
|
|
||||||
accessor.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +744,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
bool anyDuplicateSubscription;
|
bool anyDuplicateSubscription;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageMatcher.HandlerLinks.All(l => subscription.MessageMatcher.ContainsCheck(l)));
|
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
||||||
|
|
||||||
bool shouldCloseConnection;
|
bool shouldCloseConnection;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
@@ -926,16 +792,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
Status = SocketStatus.Disposed;
|
Status = SocketStatus.Disposed;
|
||||||
periodicEvent?.Set();
|
periodicEvent?.Set();
|
||||||
periodicEvent?.Dispose();
|
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether or not a new subscription can be added to this connection
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public bool CanAddSubscription() => Status == SocketStatus.None || Status == SocketStatus.Connected;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a subscription to this connection
|
/// Add a subscription to this connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1228,10 +1087,17 @@ 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));
|
||||||
|
|
||||||
|
if (!subQuery.ExpectsResponse)
|
||||||
|
{
|
||||||
|
// If there won't be an answer we can immediately set this
|
||||||
|
subscription.Status = SubscriptionStatus.Subscribed;
|
||||||
|
subscription.HandleSubQueryResponse(this, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(taskList).ConfigureAwait(false);
|
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||||
@@ -1255,6 +1121,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
await SendAndWaitQueryAsync(unsubscribeRequest).ConfigureAwait(false);
|
await SendAndWaitQueryAsync(unsubscribeRequest).ConfigureAwait(false);
|
||||||
|
subscription.HandleUnsubQueryResponse(this, unsubscribeRequest.Response);
|
||||||
_logger.SubscriptionUnsubscribed(SocketId, subscription.Id);
|
_logger.SubscriptionUnsubscribed(SocketId, subscription.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1268,10 +1135,28 @@ 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 // Initial value is 0
|
||||||
|
&& _lastSequenceNumber != sequenceNumber // When there are multiple listeners for the same message it's possible this gets recorded multiple times, shouldn't be an issue
|
||||||
|
&& _lastSequenceNumber + 1 != sequenceNumber) // Expected value
|
||||||
|
{
|
||||||
|
// 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>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_status = value;
|
_status = value;
|
||||||
Task.Run(() => StatusChanged?.Invoke(value));
|
StatusChanged?.Invoke(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +70,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Authenticated { get; }
|
public bool Authenticated { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Matcher for this subscription
|
|
||||||
/// </summary>
|
|
||||||
public MessageMatcher MessageMatcher { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Router for this subscription
|
/// Router for this subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -149,14 +144,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
|
||||||
@@ -174,19 +167,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected abstract Query? GetUnsubQuery(SocketConnection connection);
|
protected abstract Query? GetUnsubQuery(SocketConnection connection);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle an update message
|
|
||||||
/// </summary>
|
|
||||||
public CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data, MessageHandlerLink matcher)
|
|
||||||
{
|
|
||||||
ConnectionInvocations++;
|
|
||||||
TotalInvocations++;
|
|
||||||
return matcher.Handle(connection, receiveTime, originalData, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle an update message
|
/// Handle an update message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -226,12 +206,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <param name="Id">The id of the subscription</param>
|
/// <param name="Id">The id of the subscription</param>
|
||||||
/// <param name="Status">Subscription status</param>
|
/// <param name="Status">Subscription status</param>
|
||||||
/// <param name="Invocations">Number of times this subscription got a message</param>
|
/// <param name="Invocations">Number of times this subscription got a message</param>
|
||||||
/// <param name="ListenMatcher">Matcher for this subscription</param>
|
/// <param name="MessageRouter">Router for this subscription</param>
|
||||||
public record SubscriptionState(
|
public record SubscriptionState(
|
||||||
int Id,
|
int Id,
|
||||||
SubscriptionStatus Status,
|
SubscriptionStatus Status,
|
||||||
int Invocations,
|
int Invocations,
|
||||||
MessageMatcher ListenMatcher
|
MessageRouter MessageRouter
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -240,7 +220,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public SubscriptionState GetState()
|
public SubscriptionState GetState()
|
||||||
{
|
{
|
||||||
return new SubscriptionState(Id, Status, TotalInvocations, MessageMatcher);
|
return new SubscriptionState(Id, Status, TotalInvocations, MessageRouter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,6 @@ namespace CryptoExchange.Net.Sockets.HighPerf
|
|||||||
{
|
{
|
||||||
Status = SocketStatus.Disposed;
|
Status = SocketStatus.Disposed;
|
||||||
periodicEvent?.Set();
|
periodicEvent?.Set();
|
||||||
periodicEvent?.Dispose();
|
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +244,9 @@ namespace CryptoExchange.Net.Sockets.HighPerf
|
|||||||
public virtual ValueTask<CallResult> SendAsync<T>(T obj)
|
public virtual ValueTask<CallResult> SendAsync<T>(T obj)
|
||||||
{
|
{
|
||||||
if (_serializer is IByteMessageSerializer byteSerializer)
|
if (_serializer is IByteMessageSerializer byteSerializer)
|
||||||
|
{
|
||||||
return SendBytesAsync(byteSerializer.Serialize(obj));
|
return SendBytesAsync(byteSerializer.Serialize(obj));
|
||||||
|
}
|
||||||
else if (_serializer is IStringMessageSerializer stringSerializer)
|
else if (_serializer is IStringMessageSerializer stringSerializer)
|
||||||
{
|
{
|
||||||
if (obj is string str)
|
if (obj is string str)
|
||||||
|
|||||||
@@ -15,27 +15,12 @@ namespace CryptoExchange.Net.Sockets.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The matcher for this listener
|
|
||||||
/// </summary>
|
|
||||||
public MessageMatcher MessageMatcher { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The message router for this processor
|
/// The message router for this processor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MessageRouter MessageRouter { get; }
|
public MessageRouter MessageRouter { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a message
|
/// Handle a message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageHandlerLink matchedHandler);
|
|
||||||
/// <summary>
|
|
||||||
/// Handle a message
|
|
||||||
/// </summary>
|
|
||||||
CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageRoute route);
|
CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageRoute route);
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a message into object of type
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessor"></param>
|
|
||||||
/// <param name="type"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
CallResult<object> Deserialize(IMessageAccessor accessor, Type type);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Sockets.Default;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Message link type
|
|
||||||
/// </summary>
|
|
||||||
public enum MessageLinkType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Match when the listen id matches fully to the value
|
|
||||||
/// </summary>
|
|
||||||
Full,
|
|
||||||
/// <summary>
|
|
||||||
/// Match when the listen id starts with the value
|
|
||||||
/// </summary>
|
|
||||||
StartsWith
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Matches a message listen id to a specific listener
|
|
||||||
/// </summary>
|
|
||||||
public class MessageMatcher
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Linkers in this matcher
|
|
||||||
/// </summary>
|
|
||||||
public MessageHandlerLink[] HandlerLinks { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
private MessageMatcher(params MessageHandlerLink[] links)
|
|
||||||
{
|
|
||||||
HandlerLinks = links;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create message matcher
|
|
||||||
/// </summary>
|
|
||||||
public static MessageMatcher Create(string value)
|
|
||||||
{
|
|
||||||
return new MessageMatcher(new MessageHandlerLink<string>(MessageLinkType.Full, value, (con, receiveTime, originalData, msg) => new CallResult<string>(default, null, null)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create message matcher
|
|
||||||
/// </summary>
|
|
||||||
public static MessageMatcher Create<T>(string value)
|
|
||||||
{
|
|
||||||
return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, (con, receiveTime, originalData, msg) => new CallResult<T>(default, null, null)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create message matcher
|
|
||||||
/// </summary>
|
|
||||||
public static MessageMatcher Create<T>(string value, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
|
|
||||||
{
|
|
||||||
return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, handler));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create message matcher
|
|
||||||
/// </summary>
|
|
||||||
public static MessageMatcher Create<T>(IEnumerable<string> values, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
|
|
||||||
{
|
|
||||||
return new MessageMatcher(values.Select(x => new MessageHandlerLink<T>(MessageLinkType.Full, x, handler)).ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create message matcher
|
|
||||||
/// </summary>
|
|
||||||
public static MessageMatcher Create<T>(MessageLinkType type, string value, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
|
|
||||||
{
|
|
||||||
return new MessageMatcher(new MessageHandlerLink<T>(type, value, handler));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create message matcher
|
|
||||||
/// </summary>
|
|
||||||
public static MessageMatcher Create(params MessageHandlerLink[] linkers)
|
|
||||||
{
|
|
||||||
return new MessageMatcher(linkers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this matcher contains a specific link
|
|
||||||
/// </summary>
|
|
||||||
public bool ContainsCheck(MessageHandlerLink link) => HandlerLinks.Any(x => x.Type == link.Type && x.Value == link.Value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get any handler links matching with the listen id
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<MessageHandlerLink> GetHandlerLinks(string listenId) => HandlerLinks.Where(x => x.Check(listenId));
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString() => string.Join(",", HandlerLinks.Select(x => x.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Message handler link
|
|
||||||
/// </summary>
|
|
||||||
public abstract class MessageHandlerLink
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Type of check
|
|
||||||
/// </summary>
|
|
||||||
public MessageLinkType Type { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// String value of the check
|
|
||||||
/// </summary>
|
|
||||||
public string Value { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialization type
|
|
||||||
/// </summary>
|
|
||||||
public abstract Type DeserializationType { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public MessageHandlerLink(MessageLinkType type, string value)
|
|
||||||
{
|
|
||||||
Type = type;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this listen id matches this link
|
|
||||||
/// </summary>
|
|
||||||
public bool Check(string listenId)
|
|
||||||
{
|
|
||||||
if (Type == MessageLinkType.Full)
|
|
||||||
return Value.Equals(listenId, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
return listenId.StartsWith(Value, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Message handler
|
|
||||||
/// </summary>
|
|
||||||
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString() => $"{Type} match for \"{Value}\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Message handler link
|
|
||||||
/// </summary>
|
|
||||||
public class MessageHandlerLink<TServer>: MessageHandlerLink
|
|
||||||
{
|
|
||||||
private Func<SocketConnection, DateTime, string?, TServer, CallResult> _handler;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Type DeserializationType => typeof(TServer);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public MessageHandlerLink(string value, Func<SocketConnection, DateTime, string?, TServer, CallResult> handler)
|
|
||||||
: this(MessageLinkType.Full, value, handler)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public MessageHandlerLink(MessageLinkType type, string value, Func<SocketConnection, DateTime, string?, TServer, CallResult> handler)
|
|
||||||
: base(type, value)
|
|
||||||
{
|
|
||||||
_handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data)
|
|
||||||
{
|
|
||||||
return _handler(connection, receiveTime, originalData, (TServer)data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -59,11 +59,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public object? Response { get; set; }
|
public object? Response { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Matcher for this query
|
|
||||||
/// </summary>
|
|
||||||
public MessageMatcher MessageMatcher { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Router for this query
|
/// Router for this query
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -146,9 +141,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false);
|
public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mark request as timeout
|
/// Mark request as timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -160,11 +152,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="error"></param>
|
/// <param name="error"></param>
|
||||||
public abstract void Fail(Error error);
|
public abstract void Fail(Error error);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle a response message
|
|
||||||
/// </summary>
|
|
||||||
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageHandlerLink check);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a response message
|
/// Handle a response message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -223,35 +210,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return Result ?? CallResult.SuccessResult;
|
return Result ?? CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageHandlerLink check)
|
|
||||||
{
|
|
||||||
if (!PreCheckMessage(connection, message))
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
|
|
||||||
CurrentResponses++;
|
|
||||||
if (CurrentResponses == RequiredResponses)
|
|
||||||
Response = message;
|
|
||||||
|
|
||||||
if (Result?.Success != false)
|
|
||||||
// If an error result is already set don't override that
|
|
||||||
Result = check.Handle(connection, receiveTime, originalData, message);
|
|
||||||
|
|
||||||
if (CurrentResponses == RequiredResponses)
|
|
||||||
{
|
|
||||||
Completed = true;
|
|
||||||
_event.Set();
|
|
||||||
OnComplete?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate if a message is actually processable by this query
|
|
||||||
/// </summary>
|
|
||||||
public virtual bool PreCheckMessage(SocketConnection connection, object message) => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Timeout()
|
public override void Timeout()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Testing.Implementations
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
{
|
{
|
||||||
internal class TestSocket : IWebsocket
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
public class TestSocket : IWebsocket
|
||||||
{
|
{
|
||||||
public event Action<string>? OnMessageSend;
|
public event Action<string>? OnMessageSend;
|
||||||
|
|
||||||
@@ -28,7 +29,6 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
public event Func<Exception, Task>? OnError;
|
public event Func<Exception, Task>? OnError;
|
||||||
#pragma warning restore 0067
|
#pragma warning restore 0067
|
||||||
public event Func<int, Task>? OnRequestSent;
|
public event Func<int, Task>? OnRequestSent;
|
||||||
public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task>? OnStreamMessage;
|
|
||||||
public event Func<Task>? OnOpen;
|
public event Func<Task>? OnOpen;
|
||||||
|
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
@@ -39,20 +39,17 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
public static int lastId = 0;
|
public static int lastId = 0;
|
||||||
|
public DateTime? LastReceiveTime { get; }
|
||||||
#if NET9_0_OR_GREATER
|
#if NET9_0_OR_GREATER
|
||||||
public static readonly Lock lastIdLock = new Lock();
|
public static readonly Lock lastIdLock = new Lock();
|
||||||
#else
|
#else
|
||||||
public static readonly object lastIdLock = new object();
|
public static readonly object lastIdLock = new object();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private bool _newDeserialization;
|
|
||||||
|
|
||||||
public SocketConnection? Connection { get; set; }
|
public SocketConnection? Connection { get; set; }
|
||||||
|
|
||||||
public TestSocket(bool newDeserialization, string address)
|
public TestSocket(string address)
|
||||||
{
|
{
|
||||||
_newDeserialization = newDeserialization;
|
|
||||||
|
|
||||||
Uri = new Uri(address);
|
Uri = new Uri(address);
|
||||||
lock (lastIdLock)
|
lock (lastIdLock)
|
||||||
{
|
{
|
||||||
@@ -107,20 +104,23 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
|
|
||||||
public void InvokeMessage(string data)
|
public void InvokeMessage(string data)
|
||||||
{
|
{
|
||||||
if (!_newDeserialization)
|
if (Connection == null)
|
||||||
{
|
throw new ArgumentNullException(nameof(Connection));
|
||||||
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (Connection == null)
|
|
||||||
throw new ArgumentNullException(nameof(Connection));
|
|
||||||
|
|
||||||
Connection.HandleStreamMessage2(WebSocketMessageType.Text, Encoding.UTF8.GetBytes(data));
|
Connection.HandleStreamMessage2(WebSocketMessageType.Text, Encoding.UTF8.GetBytes(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ReconnectAsync()
|
||||||
|
{
|
||||||
|
if (OnReconnecting != null)
|
||||||
|
await OnReconnecting().ConfigureAwait(false);
|
||||||
|
|
||||||
|
await Task.Delay(10).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (OnReconnected != null)
|
||||||
|
await OnReconnected().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ReconnectAsync() => Task.CompletedTask;
|
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
|
|
||||||
public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
|
public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a client instance
|
/// Get a client instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract TClient GetClient(ILoggerFactory loggerFactory, bool newDeserialization);
|
public abstract TClient GetClient(ILoggerFactory loggerFactory);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
|
/// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
|
||||||
@@ -34,11 +34,11 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// Create a client
|
/// Create a client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected TClient CreateClient(bool useNewDeserialization)
|
protected TClient CreateClient()
|
||||||
{
|
{
|
||||||
var fact = new LoggerFactory();
|
var fact = new LoggerFactory();
|
||||||
fact.AddProvider(new TraceLoggerProvider());
|
fact.AddProvider(new TraceLoggerProvider());
|
||||||
return GetClient(fact, useNewDeserialization);
|
return GetClient(fact);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -58,16 +58,15 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// Execute a REST endpoint call and check for any errors or warnings.
|
/// Execute a REST endpoint call and check for any errors or warnings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Type of the update</typeparam>
|
/// <typeparam name="T">Type of the update</typeparam>
|
||||||
/// <param name="useNewDeserialization">Whether to use the new deserialization method</param>
|
|
||||||
/// <param name="expression">The call expression</param>
|
/// <param name="expression">The call expression</param>
|
||||||
/// <param name="expectUpdate">Whether an update is expected</param>
|
/// <param name="expectUpdate">Whether an update is expected</param>
|
||||||
/// <param name="authRequest">Whether this is an authenticated request</param>
|
/// <param name="authRequest">Whether this is an authenticated request</param>
|
||||||
public async Task RunAndCheckUpdate<T>(bool useNewDeserialization, Expression<Func<TClient, Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>>> expression, bool expectUpdate, bool authRequest)
|
public async Task RunAndCheckUpdate<T>(Expression<Func<TClient, Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>>> expression, bool expectUpdate, bool authRequest)
|
||||||
{
|
{
|
||||||
if (!ShouldRun())
|
if (!ShouldRun())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var client = CreateClient(useNewDeserialization);
|
var client = CreateClient();
|
||||||
|
|
||||||
var expressionBody = (MethodCallExpression)expression.Body;
|
var expressionBody = (MethodCallExpression)expression.Body;
|
||||||
if (authRequest && !Authenticated)
|
if (authRequest && !Authenticated)
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TestHelpers
|
public class TestHelpers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Deep compare the values of two objects
|
||||||
|
/// </summary>
|
||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
internal static bool AreEqual<T>(T? self, T? to, params string[] ignore) where T : class
|
public static bool AreEqual<T>(T? self, T? to, params string[] ignore) where T : class
|
||||||
{
|
{
|
||||||
if (self != null && to != null)
|
if (self != null && to != null)
|
||||||
{
|
{
|
||||||
@@ -61,9 +64,12 @@ namespace CryptoExchange.Net.Testing
|
|||||||
return self == to;
|
return self == to;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TestSocket ConfigureSocketClient<T>(T client, string address) where T : BaseSocketClient
|
/// <summary>
|
||||||
|
/// Configure a socket client
|
||||||
|
/// </summary>
|
||||||
|
public static TestSocket ConfigureSocketClient<T>(T client, string address) where T : BaseSocketClient
|
||||||
{
|
{
|
||||||
var socket = new TestSocket(client.ClientOptions.UseUpdatedDeserialization, address);
|
var socket = new TestSocket(address);
|
||||||
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
||||||
{
|
{
|
||||||
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (!_lastRestDelays.ContainsKey(api))
|
||||||
|
_lastRestDelays.TryAdd(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>
|
||||||
|
/// Get a dictionary of API/Client name -> time offset for Rest api's
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, TimeSpan?> GetRestOffsets()
|
||||||
|
{
|
||||||
|
return _lastRestDelays.ToDictionary(
|
||||||
|
x => x.Key,
|
||||||
|
x => (x.Value.Offset == null ? (TimeSpan?)null : TimeSpan.FromMilliseconds(x.Value.Offset.Value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a dictionary of API/Client name -> time offset for Websocket api's
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, TimeSpan?> GetWebsocketOffsets()
|
||||||
|
{
|
||||||
|
return _lastSocketDelays.ToDictionary(
|
||||||
|
x => x.Key,
|
||||||
|
x => (x.Value.Offset == null ? (TimeSpan?)null : TimeSpan.FromMilliseconds(x.Value.Offset.Value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,11 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
SharedKline? Last { get; }
|
SharedKline? Last { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The kline interval
|
||||||
|
/// </summary>
|
||||||
|
public SharedKlineInterval Interval { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event for when a new kline is added
|
/// Event for when a new kline is added
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _changed = false;
|
protected bool _changed = false;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The kline interval
|
|
||||||
/// </summary>
|
|
||||||
protected readonly SharedKlineInterval _interval;
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the snapshot has been set
|
/// Whether the snapshot has been set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _snapshotSet;
|
protected bool _snapshotSet;
|
||||||
@@ -66,6 +62,10 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected DateTime? _firstTimestamp;
|
protected DateTime? _firstTimestamp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The kline interval
|
||||||
|
/// </summary>
|
||||||
|
public SharedKlineInterval Interval { get; }
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public SyncStatus Status
|
public SyncStatus Status
|
||||||
{
|
{
|
||||||
@@ -165,7 +165,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
Exchange = restClient.Exchange;
|
Exchange = restClient.Exchange;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Period = period;
|
Period = period;
|
||||||
_interval = interval;
|
Interval = interval;
|
||||||
_socketClient = socketClient;
|
_socketClient = socketClient;
|
||||||
_restClient = restClient;
|
_restClient = restClient;
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
Status = SyncStatus.Syncing;
|
Status = SyncStatus.Syncing;
|
||||||
_logger.KlineTrackerStarting(SymbolName);
|
_logger.KlineTrackerStarting(SymbolName);
|
||||||
|
|
||||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
|
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, Interval),
|
||||||
update =>
|
update =>
|
||||||
{
|
{
|
||||||
AddOrUpdate(update.Data);
|
AddOrUpdate(update.Data);
|
||||||
@@ -237,7 +237,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
|
|
||||||
var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100);
|
var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100);
|
||||||
|
|
||||||
var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit);
|
var request = new GetKlinesRequest(Symbol, Interval, startTime, DateTime.UtcNow, limit: limit);
|
||||||
var data = new List<SharedKline>();
|
var data = new List<SharedKline>();
|
||||||
await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false))
|
await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data tracker interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserDataTracker<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the tracker is currently fully connected
|
||||||
|
/// </summary>
|
||||||
|
bool Connected { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
||||||
|
/// Websocket updates will be available for all symbols regardless.
|
||||||
|
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
|
||||||
|
/// </summary>
|
||||||
|
event Action<bool>? OnConnectedChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracker values
|
||||||
|
/// </summary>
|
||||||
|
T[] Values { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On data update
|
||||||
|
/// </summary>
|
||||||
|
event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Futures user data tracker
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserFuturesDataTracker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User identifier
|
||||||
|
/// </summary>
|
||||||
|
string? UserIdentifier { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the tracker is currently fully connected
|
||||||
|
/// </summary>
|
||||||
|
bool Connected { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange name
|
||||||
|
/// </summary>
|
||||||
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Balances tracker
|
||||||
|
/// </summary>
|
||||||
|
IUserDataTracker<SharedBalance> Balances { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Orders tracker
|
||||||
|
/// </summary>
|
||||||
|
IUserDataTracker<SharedFuturesOrder> Orders { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Positions tracker
|
||||||
|
/// </summary>
|
||||||
|
IUserDataTracker<SharedPosition> Positions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Trades tracker
|
||||||
|
/// </summary>
|
||||||
|
IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On connection status change
|
||||||
|
/// </summary>
|
||||||
|
event Action<UserDataType, bool>? OnConnectedChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start tracking user data
|
||||||
|
/// </summary>
|
||||||
|
Task<CallResult> StartAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Stop tracking data
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User data tracker
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserSpotDataTracker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User identifier
|
||||||
|
/// </summary>
|
||||||
|
string? UserIdentifier { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the tracker is currently fully connected
|
||||||
|
/// </summary>
|
||||||
|
bool Connected { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange name
|
||||||
|
/// </summary>
|
||||||
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Balances tracker
|
||||||
|
/// </summary>
|
||||||
|
IUserDataTracker<SharedBalance> Balances { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Orders tracker
|
||||||
|
/// </summary>
|
||||||
|
IUserDataTracker<SharedSpotOrder> Orders { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Trades tracker
|
||||||
|
/// </summary>
|
||||||
|
IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On connection status change
|
||||||
|
/// </summary>
|
||||||
|
event Action<UserDataType, bool>? OnConnectedChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start tracking user data
|
||||||
|
/// </summary>
|
||||||
|
Task<CallResult> StartAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Stop tracking data
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task StopAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Balance tracker implementation
|
||||||
|
/// </summary>
|
||||||
|
public class BalanceTracker : UserDataItemTracker<SharedBalance>
|
||||||
|
{
|
||||||
|
private readonly IBalanceRestClient _restClient;
|
||||||
|
private readonly IBalanceSocketClient? _socketClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
private readonly SharedAccountType _accountType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public BalanceTracker(
|
||||||
|
ILogger logger,
|
||||||
|
IBalanceRestClient restClient,
|
||||||
|
IBalanceSocketClient? socketClient,
|
||||||
|
SharedAccountType accountType,
|
||||||
|
TrackerItemConfig config,
|
||||||
|
ExchangeParameters? exchangeParameters = null
|
||||||
|
) : base(logger, UserDataType.Balances, restClient.Exchange, config, false, null)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|
||||||
|
_restClient = restClient;
|
||||||
|
_socketClient = socketClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
_accountType = accountType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool Update(SharedBalance existingItem, SharedBalance updateItem)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
if (existingItem.Total != updateItem.Total)
|
||||||
|
{
|
||||||
|
existingItem.Total = updateItem.Total;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.Available != updateItem.Available)
|
||||||
|
{
|
||||||
|
existingItem.Available = updateItem.Available;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string GetKey(SharedBalance item) => item.Asset + item.IsolatedMarginSymbol;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool? CheckIfUpdateShouldBeApplied(SharedBalance existingItem, SharedBalance updateItem) => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||||
|
|
||||||
|
var accountType = _accountType == SharedAccountType.Spot ? TradingMode.Spot :
|
||||||
|
_accountType == SharedAccountType.PerpetualInverseFutures ? TradingMode.PerpetualInverse :
|
||||||
|
_accountType == SharedAccountType.DeliveryLinearFutures ? TradingMode.DeliveryLinear :
|
||||||
|
_accountType == SharedAccountType.DeliveryInverseFutures ? TradingMode.DeliveryInverse :
|
||||||
|
TradingMode.PerpetualLinear;
|
||||||
|
return ExchangeHelpers.ProcessQueuedAsync<SharedBalance[]>(
|
||||||
|
async handler => await _socketClient.SubscribeToBalanceUpdatesAsync(new SubscribeBalancesRequest(listenKey, accountType, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||||
|
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<bool> DoPollAsync()
|
||||||
|
{
|
||||||
|
var balances = await _restClient.GetBalancesAsync(new GetBalancesRequest(accountType: _accountType, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (balances.Success)
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, balances.Data).ConfigureAwait(false);
|
||||||
|
else
|
||||||
|
_initialPollingError ??= balances.Error;
|
||||||
|
|
||||||
|
return !balances.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Futures order tracker
|
||||||
|
/// </summary>
|
||||||
|
public class FuturesOrderTracker : UserDataItemTracker<SharedFuturesOrder>
|
||||||
|
{
|
||||||
|
private readonly IFuturesOrderRestClient _restClient;
|
||||||
|
private readonly IFuturesOrderSocketClient? _socketClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||||
|
|
||||||
|
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public FuturesOrderTracker(
|
||||||
|
ILogger logger,
|
||||||
|
IFuturesOrderRestClient restClient,
|
||||||
|
IFuturesOrderSocketClient? socketClient,
|
||||||
|
TrackerItemConfig config,
|
||||||
|
IEnumerable<SharedSymbol> symbols,
|
||||||
|
bool onlyTrackProvidedSymbols,
|
||||||
|
ExchangeParameters? exchangeParameters = null
|
||||||
|
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|
||||||
|
_restClient = restClient;
|
||||||
|
_socketClient = socketClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
|
||||||
|
_requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool Update(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
if (updateItem.AveragePrice != null && updateItem.AveragePrice != existingItem.AveragePrice)
|
||||||
|
{
|
||||||
|
existingItem.AveragePrice = updateItem.AveragePrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.OrderPrice != null && updateItem.OrderPrice != existingItem.OrderPrice)
|
||||||
|
{
|
||||||
|
existingItem.OrderPrice = updateItem.OrderPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.Fee != null && updateItem.Fee != existingItem.Fee)
|
||||||
|
{
|
||||||
|
existingItem.Fee = updateItem.Fee;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.FeeAsset != null && updateItem.FeeAsset != existingItem.FeeAsset)
|
||||||
|
{
|
||||||
|
existingItem.FeeAsset = updateItem.FeeAsset;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.OrderQuantity != null && updateItem.OrderQuantity != existingItem.OrderQuantity)
|
||||||
|
{
|
||||||
|
existingItem.OrderQuantity = updateItem.OrderQuantity;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.QuantityFilled != null && updateItem.QuantityFilled != existingItem.QuantityFilled)
|
||||||
|
{
|
||||||
|
existingItem.QuantityFilled = updateItem.QuantityFilled;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.Status != existingItem.Status)
|
||||||
|
{
|
||||||
|
existingItem.Status = updateItem.Status;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.StopLossPrice != existingItem.StopLossPrice)
|
||||||
|
{
|
||||||
|
existingItem.StopLossPrice = updateItem.StopLossPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.TakeProfitPrice != existingItem.TakeProfitPrice)
|
||||||
|
{
|
||||||
|
existingItem.TakeProfitPrice = updateItem.TakeProfitPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.TriggerPrice != existingItem.TriggerPrice)
|
||||||
|
{
|
||||||
|
existingItem.TriggerPrice = updateItem.TriggerPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.UpdateTime != null && updateItem.UpdateTime != existingItem.UpdateTime)
|
||||||
|
{
|
||||||
|
existingItem.UpdateTime = updateItem.UpdateTime;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string GetKey(SharedFuturesOrder item) => item.OrderId;
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override TimeSpan GetAge(DateTime time, SharedFuturesOrder item) => item.Status == SharedOrderStatus.Open ? TimeSpan.Zero : time - (item.UpdateTime ?? item.CreateTime ?? time);
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool? CheckIfUpdateShouldBeApplied(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
||||||
|
{
|
||||||
|
if (existingItem.Status == SharedOrderStatus.Open && updateItem.Status != SharedOrderStatus.Open)
|
||||||
|
// status changed from open to not open
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
|
||||||
|
// status changed from not open to open; stale
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (existingItem.UpdateTime != null && updateItem.UpdateTime != null)
|
||||||
|
{
|
||||||
|
// If both have an update time base of that
|
||||||
|
if (existingItem.UpdateTime < updateItem.UpdateTime)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (existingItem.UpdateTime > updateItem.UpdateTime)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.QuantityFilled != null && updateItem.QuantityFilled != null)
|
||||||
|
{
|
||||||
|
if (existingItem.QuantityFilled.QuantityInBaseAsset != null && updateItem.QuantityFilled.QuantityInBaseAsset != null)
|
||||||
|
{
|
||||||
|
// If base quantity is not null we can base it on that
|
||||||
|
if (existingItem.QuantityFilled.QuantityInBaseAsset < updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
else if (existingItem.QuantityFilled.QuantityInBaseAsset > updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.QuantityFilled.QuantityInQuoteAsset != null && updateItem.QuantityFilled.QuantityInQuoteAsset != null)
|
||||||
|
{
|
||||||
|
// If quote quantity is not null we can base it on that
|
||||||
|
if (existingItem.QuantityFilled.QuantityInQuoteAsset < updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
else if (existingItem.QuantityFilled.QuantityInQuoteAsset > updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.Fee != null && updateItem.Fee != null)
|
||||||
|
{
|
||||||
|
// Higher fee means later processing
|
||||||
|
if (existingItem.Fee < updateItem.Fee)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (existingItem.Fee > updateItem.Fee)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected internal override async Task HandleUpdateAsync(UpdateSource source, SharedFuturesOrder[] @event)
|
||||||
|
{
|
||||||
|
await base.HandleUpdateAsync(source, @event).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var trades = @event.Where(x => x.LastTrade != null).Select(x => x.LastTrade!).ToArray();
|
||||||
|
if (trades.Length != 0 && OnTradeUpdate != null)
|
||||||
|
await OnTradeUpdate.Invoke(source, trades).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||||
|
|
||||||
|
return ExchangeHelpers.ProcessQueuedAsync<SharedFuturesOrder[]>(
|
||||||
|
async handler => await _socketClient.SubscribeToFuturesOrderUpdatesAsync(new SubscribeFuturesOrderRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||||
|
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<bool> DoPollAsync()
|
||||||
|
{
|
||||||
|
var anyError = false;
|
||||||
|
List<SharedFuturesOrder> openOrders = new List<SharedFuturesOrder>();
|
||||||
|
|
||||||
|
if (!_requiresSymbolParameterOpenOrders)
|
||||||
|
{
|
||||||
|
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!openOrdersResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= openOrdersResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
openOrders.AddRange(openOrdersResult.Data);
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var symbol in _symbols.ToList())
|
||||||
|
{
|
||||||
|
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!openOrdersResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= openOrdersResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
openOrders.AddRange(openOrdersResult.Data);
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var symbol in _symbols.ToList())
|
||||||
|
{
|
||||||
|
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!closedOrdersResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= closedOrdersResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
|
||||||
|
// Filter orders to only include where close time is after the start time
|
||||||
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
|
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
||||||
|
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
||||||
|
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
||||||
|
).ToArray();
|
||||||
|
|
||||||
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
|
var openOrdersNotReturned = Values.Where(x =>
|
||||||
|
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
||||||
|
&& x.QuantityFilled?.IsZero == true // With no filled value
|
||||||
|
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
||||||
|
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
var additionalUpdates = new List<SharedFuturesOrder>();
|
||||||
|
foreach (var order in openOrdersNotReturned)
|
||||||
|
{
|
||||||
|
additionalUpdates.Add(order with
|
||||||
|
{
|
||||||
|
Status = SharedOrderStatus.Canceled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
relevantOrders = relevantOrders.Concat(additionalUpdates).ToArray();
|
||||||
|
if (relevantOrders.Length > 0)
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, relevantOrders).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Futures user trade tracker
|
||||||
|
/// </summary>
|
||||||
|
public class FuturesUserTradeTracker : UserDataItemTracker<SharedUserTrade>
|
||||||
|
{
|
||||||
|
private readonly IFuturesOrderRestClient _restClient;
|
||||||
|
private readonly IUserTradeSocketClient? _socketClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
|
||||||
|
internal Func<string[]>? GetTrackedOrderIds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public FuturesUserTradeTracker(
|
||||||
|
ILogger logger,
|
||||||
|
IFuturesOrderRestClient restClient,
|
||||||
|
IUserTradeSocketClient? socketClient,
|
||||||
|
TrackerItemConfig config,
|
||||||
|
IEnumerable<SharedSymbol> symbols,
|
||||||
|
bool onlyTrackProvidedSymbols,
|
||||||
|
ExchangeParameters? exchangeParameters = null
|
||||||
|
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|
||||||
|
_restClient = restClient;
|
||||||
|
_socketClient = socketClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool? CheckIfUpdateShouldBeApplied(SharedUserTrade existingItem, SharedUserTrade updateItem) => false;
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool Update(SharedUserTrade existingItem, SharedUserTrade updateItem) => false; // trades are never updated
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override TimeSpan GetAge(DateTime time, SharedUserTrade item) => time - item.Timestamp;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<bool> DoPollAsync()
|
||||||
|
{
|
||||||
|
var anyError = false;
|
||||||
|
foreach (var symbol in _symbols)
|
||||||
|
{
|
||||||
|
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!tradesResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= tradesResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
|
||||||
|
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
||||||
|
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
||||||
|
if (relevantTrades.Length > 0)
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, tradesResult.Data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||||
|
|
||||||
|
return ExchangeHelpers.ProcessQueuedAsync<SharedUserTrade[]>(
|
||||||
|
async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||||
|
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Position tracker
|
||||||
|
/// </summary>
|
||||||
|
public class PositionTracker : UserDataItemTracker<SharedPosition>
|
||||||
|
{
|
||||||
|
private readonly IFuturesOrderRestClient _restClient;
|
||||||
|
private readonly IPositionSocketClient? _socketClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether websocket position updates are full snapshots and missing positions should be considered 0
|
||||||
|
/// </summary>
|
||||||
|
protected bool WebsocketPositionUpdatesAreFullSnapshots { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public PositionTracker(
|
||||||
|
ILogger logger,
|
||||||
|
IFuturesOrderRestClient restClient,
|
||||||
|
IPositionSocketClient? socketClient,
|
||||||
|
TrackerItemConfig config,
|
||||||
|
IEnumerable<SharedSymbol> symbols,
|
||||||
|
bool onlyTrackProvidedSymbols,
|
||||||
|
bool websocketPositionUpdatesAreFullSnapshots,
|
||||||
|
ExchangeParameters? exchangeParameters = null
|
||||||
|
) : base(logger, UserDataType.Positions, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|
||||||
|
_restClient = restClient;
|
||||||
|
_socketClient = socketClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
|
||||||
|
WebsocketPositionUpdatesAreFullSnapshots = websocketPositionUpdatesAreFullSnapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool Update(SharedPosition existingItem, SharedPosition updateItem)
|
||||||
|
{
|
||||||
|
// Some other way to way to determine sequence? Maybe timestamp?
|
||||||
|
var changed = false;
|
||||||
|
if (existingItem.AverageOpenPrice != updateItem.AverageOpenPrice)
|
||||||
|
{
|
||||||
|
existingItem.AverageOpenPrice = updateItem.AverageOpenPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.Leverage != updateItem.Leverage)
|
||||||
|
{
|
||||||
|
existingItem.Leverage = updateItem.Leverage;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.LiquidationPrice != updateItem.LiquidationPrice)
|
||||||
|
{
|
||||||
|
existingItem.LiquidationPrice = updateItem.LiquidationPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.PositionSize != updateItem.PositionSize)
|
||||||
|
{
|
||||||
|
existingItem.PositionSize = updateItem.PositionSize;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.StopLossPrice != updateItem.StopLossPrice)
|
||||||
|
{
|
||||||
|
existingItem.StopLossPrice = updateItem.StopLossPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.TakeProfitPrice != updateItem.TakeProfitPrice)
|
||||||
|
{
|
||||||
|
existingItem.TakeProfitPrice = updateItem.TakeProfitPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.UnrealizedPnl != null && existingItem.UnrealizedPnl != updateItem.UnrealizedPnl)
|
||||||
|
{
|
||||||
|
existingItem.UnrealizedPnl = updateItem.UnrealizedPnl;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.UpdateTime != null && existingItem.UpdateTime != updateItem.UpdateTime)
|
||||||
|
{
|
||||||
|
existingItem.UpdateTime = updateItem.UpdateTime;
|
||||||
|
// If update time is the only changed prop don't mark it as changed
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected internal override async Task HandleUpdateAsync(UpdateSource source, SharedPosition[] @event)
|
||||||
|
{
|
||||||
|
LastUpdateTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
List<SharedPosition>? toRemove = null;
|
||||||
|
foreach (var item in @event)
|
||||||
|
{
|
||||||
|
if (item is SharedSymbolModel symbolModel)
|
||||||
|
{
|
||||||
|
if (symbolModel.SharedSymbol == null)
|
||||||
|
{
|
||||||
|
toRemove ??= new List<SharedPosition>();
|
||||||
|
toRemove.Add(item);
|
||||||
|
}
|
||||||
|
else if (_onlyTrackProvidedSymbols
|
||||||
|
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
||||||
|
{
|
||||||
|
toRemove ??= new List<SharedPosition>();
|
||||||
|
toRemove.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toRemove != null)
|
||||||
|
@event = @event.Except(toRemove).ToArray();
|
||||||
|
|
||||||
|
if (!_onlyTrackProvidedSymbols)
|
||||||
|
UpdateSymbolsList(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||||
|
|
||||||
|
|
||||||
|
// Update local store
|
||||||
|
var updatedItems = @event.Select(GetKey).ToList();
|
||||||
|
|
||||||
|
if (WebsocketPositionUpdatesAreFullSnapshots)
|
||||||
|
{
|
||||||
|
// Reset any tracking position to zero/null values when it's no longer in the snapshot as it means there is no open position any more
|
||||||
|
var notInSnapshot = _store.Where(x => !updatedItems.Contains(x.Key) && x.Value.PositionSize != 0).ToList();
|
||||||
|
foreach (var position in notInSnapshot)
|
||||||
|
{
|
||||||
|
position.Value.UpdateTime = DateTime.UtcNow;
|
||||||
|
position.Value.AverageOpenPrice = null;
|
||||||
|
position.Value.LiquidationPrice = null;
|
||||||
|
position.Value.PositionSize = 0;
|
||||||
|
position.Value.StopLossPrice = null;
|
||||||
|
position.Value.TakeProfitPrice = null;
|
||||||
|
position.Value.UnrealizedPnl = null;
|
||||||
|
updatedItems.Add(position.Key);
|
||||||
|
|
||||||
|
LastChangeTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in @event)
|
||||||
|
{
|
||||||
|
bool existed = false;
|
||||||
|
_store.AddOrUpdate(GetKey(item), item, (key, existing) =>
|
||||||
|
{
|
||||||
|
existed = true;
|
||||||
|
if (CheckIfUpdateShouldBeApplied(existing, item) == false)
|
||||||
|
{
|
||||||
|
updatedItems.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var updated = Update(existing, item);
|
||||||
|
if (!updated)
|
||||||
|
{
|
||||||
|
updatedItems.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Updated {DataType} {Item}", DataType, key);
|
||||||
|
LastChangeTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existed)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Added {DataType} {Item}", DataType, GetKey(item));
|
||||||
|
LastChangeTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedItems.Count > 0)
|
||||||
|
{
|
||||||
|
await InvokeUpdate(
|
||||||
|
new UserDataUpdate<SharedPosition[]>(source, _exchange, _store.Where(x => updatedItems.Contains(x.Key)).Select(x => x.Value).ToArray())).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string GetKey(SharedPosition item) =>
|
||||||
|
item.SharedSymbol!.TradingMode + item.SharedSymbol.BaseAsset + item.SharedSymbol.QuoteAsset + item.PositionMode + (item.PositionMode != SharedPositionMode.OneWay ? item.PositionSide.ToString() : "");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool? CheckIfUpdateShouldBeApplied(SharedPosition existingItem, SharedPosition updateItem) => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||||
|
|
||||||
|
return ExchangeHelpers.ProcessQueuedAsync<SharedPosition[]>(
|
||||||
|
async handler => await _socketClient.SubscribeToPositionUpdatesAsync(new SubscribePositionRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||||
|
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<bool> DoPollAsync()
|
||||||
|
{
|
||||||
|
var anyError = false;
|
||||||
|
var positionsResult = await _restClient.GetPositionsAsync(new GetPositionsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!positionsResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= positionsResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, positionsResult.Data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spot order tracker
|
||||||
|
/// </summary>
|
||||||
|
public class SpotOrderTracker : UserDataItemTracker<SharedSpotOrder>
|
||||||
|
{
|
||||||
|
private readonly ISpotOrderRestClient _restClient;
|
||||||
|
private readonly ISpotOrderSocketClient? _socketClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||||
|
|
||||||
|
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SpotOrderTracker(
|
||||||
|
ILogger logger,
|
||||||
|
ISpotOrderRestClient restClient,
|
||||||
|
ISpotOrderSocketClient? socketClient,
|
||||||
|
TrackerItemConfig config,
|
||||||
|
IEnumerable<SharedSymbol> symbols,
|
||||||
|
bool onlyTrackProvidedSymbols,
|
||||||
|
ExchangeParameters? exchangeParameters = null
|
||||||
|
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|
||||||
|
_restClient = restClient;
|
||||||
|
_socketClient = socketClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
|
||||||
|
_requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool Update(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
if (updateItem.AveragePrice != null && updateItem.AveragePrice != existingItem.AveragePrice)
|
||||||
|
{
|
||||||
|
existingItem.AveragePrice = updateItem.AveragePrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.OrderPrice != null && updateItem.OrderPrice != existingItem.OrderPrice)
|
||||||
|
{
|
||||||
|
existingItem.OrderPrice = updateItem.OrderPrice;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.Fee != null && updateItem.Fee != existingItem.Fee)
|
||||||
|
{
|
||||||
|
existingItem.Fee = updateItem.Fee;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.FeeAsset != null && updateItem.FeeAsset != existingItem.FeeAsset)
|
||||||
|
{
|
||||||
|
existingItem.FeeAsset = updateItem.FeeAsset;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.OrderQuantity != null && updateItem.OrderQuantity != existingItem.OrderQuantity)
|
||||||
|
{
|
||||||
|
existingItem.OrderQuantity ??= new SharedOrderQuantity();
|
||||||
|
if (updateItem.OrderQuantity.QuantityInBaseAsset != null)
|
||||||
|
{
|
||||||
|
existingItem.OrderQuantity.QuantityInBaseAsset = updateItem.OrderQuantity.QuantityInBaseAsset;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (updateItem.OrderQuantity.QuantityInQuoteAsset != null)
|
||||||
|
{
|
||||||
|
existingItem.OrderQuantity.QuantityInQuoteAsset = updateItem.OrderQuantity.QuantityInQuoteAsset;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (updateItem.OrderQuantity.QuantityInContracts != null)
|
||||||
|
{
|
||||||
|
existingItem.OrderQuantity.QuantityInContracts = updateItem.OrderQuantity.QuantityInContracts;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.QuantityFilled != null && updateItem.QuantityFilled != existingItem.QuantityFilled)
|
||||||
|
{
|
||||||
|
existingItem.QuantityFilled ??= new SharedOrderQuantity();
|
||||||
|
if (updateItem.QuantityFilled.QuantityInBaseAsset != null)
|
||||||
|
{
|
||||||
|
existingItem.QuantityFilled.QuantityInBaseAsset = updateItem.QuantityFilled.QuantityInBaseAsset;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (updateItem.QuantityFilled.QuantityInQuoteAsset != null)
|
||||||
|
{
|
||||||
|
existingItem.QuantityFilled.QuantityInQuoteAsset = updateItem.QuantityFilled.QuantityInQuoteAsset;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (updateItem.QuantityFilled.QuantityInContracts != null)
|
||||||
|
{
|
||||||
|
existingItem.QuantityFilled.QuantityInContracts = updateItem.QuantityFilled.QuantityInContracts;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.Status != existingItem.Status)
|
||||||
|
{
|
||||||
|
existingItem.Status = updateItem.Status;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateItem.UpdateTime != null && updateItem.UpdateTime != existingItem.UpdateTime)
|
||||||
|
{
|
||||||
|
existingItem.UpdateTime = updateItem.UpdateTime;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string GetKey(SharedSpotOrder item) => item.OrderId;
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override TimeSpan GetAge(DateTime time, SharedSpotOrder item) => item.Status == SharedOrderStatus.Open ? TimeSpan.Zero : time - (item.UpdateTime ?? item.CreateTime ?? time);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool? CheckIfUpdateShouldBeApplied(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
||||||
|
{
|
||||||
|
if (existingItem.Status == SharedOrderStatus.Open && updateItem.Status != SharedOrderStatus.Open)
|
||||||
|
// status changed from open to not open
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
|
||||||
|
// status changed from not open to open; stale
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (existingItem.UpdateTime != null && updateItem.UpdateTime != null)
|
||||||
|
{
|
||||||
|
// If both have an update time base of that
|
||||||
|
if (existingItem.UpdateTime < updateItem.UpdateTime)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (existingItem.UpdateTime > updateItem.UpdateTime)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.QuantityFilled != null && updateItem.QuantityFilled != null)
|
||||||
|
{
|
||||||
|
if (existingItem.QuantityFilled.QuantityInBaseAsset != null && updateItem.QuantityFilled.QuantityInBaseAsset != null)
|
||||||
|
{
|
||||||
|
// If base quantity is not null we can base it on that
|
||||||
|
if (existingItem.QuantityFilled.QuantityInBaseAsset < updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
else if (existingItem.QuantityFilled.QuantityInBaseAsset > updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.QuantityFilled.QuantityInQuoteAsset != null && updateItem.QuantityFilled.QuantityInQuoteAsset != null)
|
||||||
|
{
|
||||||
|
// If quote quantity is not null we can base it on that
|
||||||
|
if (existingItem.QuantityFilled.QuantityInQuoteAsset < updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
else if (existingItem.QuantityFilled.QuantityInQuoteAsset > updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem.Fee != null && updateItem.Fee != null)
|
||||||
|
{
|
||||||
|
// Higher fee means later processing
|
||||||
|
if (existingItem.Fee < updateItem.Fee)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (existingItem.Fee > updateItem.Fee)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected internal override async Task HandleUpdateAsync(UpdateSource source, SharedSpotOrder[] @event)
|
||||||
|
{
|
||||||
|
await base.HandleUpdateAsync(source, @event).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var trades = @event.Where(x => x.LastTrade != null).Select(x => x.LastTrade!).ToArray();
|
||||||
|
if (trades.Length != 0 && OnTradeUpdate != null)
|
||||||
|
await OnTradeUpdate(source, trades).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||||
|
|
||||||
|
return ExchangeHelpers.ProcessQueuedAsync<SharedSpotOrder[]>(
|
||||||
|
async handler => await _socketClient.SubscribeToSpotOrderUpdatesAsync(new SubscribeSpotOrderRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||||
|
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<bool> DoPollAsync()
|
||||||
|
{
|
||||||
|
var anyError = false;
|
||||||
|
List<SharedSpotOrder> openOrders = new List<SharedSpotOrder>();
|
||||||
|
|
||||||
|
if (!_requiresSymbolParameterOpenOrders)
|
||||||
|
{
|
||||||
|
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!openOrdersResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= openOrdersResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
openOrders.AddRange(openOrdersResult.Data);
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var symbol in _symbols.ToList())
|
||||||
|
{
|
||||||
|
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!openOrdersResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= openOrdersResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
openOrders.AddRange(openOrdersResult.Data);
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_firstPollDone && anyError)
|
||||||
|
return anyError;
|
||||||
|
|
||||||
|
foreach (var symbol in _symbols.ToList())
|
||||||
|
{
|
||||||
|
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!closedOrdersResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= closedOrdersResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
|
||||||
|
// Filter orders to only include where close time is after the start time
|
||||||
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
|
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
||||||
|
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
||||||
|
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
||||||
|
).ToArray();
|
||||||
|
|
||||||
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
|
var openOrdersNotReturned = Values.Where(x =>
|
||||||
|
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
||||||
|
&& x.QuantityFilled?.IsZero == true // With no filled value
|
||||||
|
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
||||||
|
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
var additionalUpdates = new List<SharedSpotOrder>();
|
||||||
|
foreach (var order in openOrdersNotReturned)
|
||||||
|
{
|
||||||
|
additionalUpdates.Add(order with
|
||||||
|
{
|
||||||
|
Status = SharedOrderStatus.Canceled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
relevantOrders = relevantOrders.Concat(additionalUpdates).ToArray();
|
||||||
|
if (relevantOrders.Length > 0)
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, relevantOrders).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spot user trade tracker
|
||||||
|
/// </summary>
|
||||||
|
public class SpotUserTradeTracker : UserDataItemTracker<SharedUserTrade>
|
||||||
|
{
|
||||||
|
private readonly ISpotOrderRestClient _restClient;
|
||||||
|
private readonly IUserTradeSocketClient? _socketClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
|
||||||
|
internal Func<string[]>? GetTrackedOrderIds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SpotUserTradeTracker(
|
||||||
|
ILogger logger,
|
||||||
|
ISpotOrderRestClient restClient,
|
||||||
|
IUserTradeSocketClient? socketClient,
|
||||||
|
TrackerItemConfig config,
|
||||||
|
IEnumerable<SharedSymbol> symbols,
|
||||||
|
bool onlyTrackProvidedSymbols,
|
||||||
|
ExchangeParameters? exchangeParameters = null
|
||||||
|
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|
||||||
|
_restClient = restClient;
|
||||||
|
_socketClient = socketClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool? CheckIfUpdateShouldBeApplied(SharedUserTrade existingItem, SharedUserTrade updateItem) => false;
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool Update(SharedUserTrade existingItem, SharedUserTrade updateItem) => false; // Trades are never updated
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override TimeSpan GetAge(DateTime time, SharedUserTrade item) => time - item.Timestamp;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<bool> DoPollAsync()
|
||||||
|
{
|
||||||
|
var anyError = false;
|
||||||
|
foreach (var symbol in _symbols)
|
||||||
|
{
|
||||||
|
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!tradesResult.Success)
|
||||||
|
{
|
||||||
|
anyError = true;
|
||||||
|
|
||||||
|
_initialPollingError ??= tradesResult.Error;
|
||||||
|
if (!_firstPollDone)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
|
||||||
|
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
||||||
|
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
||||||
|
if (relevantTrades.Length > 0)
|
||||||
|
await HandleUpdateAsync(UpdateSource.Poll, tradesResult.Data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
if (_socketClient == null)
|
||||||
|
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||||
|
|
||||||
|
return ExchangeHelpers.ProcessQueuedAsync<SharedUserTrade[]>(
|
||||||
|
async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||||
|
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User data tracker
|
||||||
|
/// </summary>
|
||||||
|
public abstract class UserDataItemTracker
|
||||||
|
{
|
||||||
|
private bool _connected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logger
|
||||||
|
/// </summary>
|
||||||
|
protected ILogger _logger;
|
||||||
|
/// <summary>
|
||||||
|
/// Polling wait event
|
||||||
|
/// </summary>
|
||||||
|
protected AsyncResetEvent _pollWaitEvent = new AsyncResetEvent(false, true);
|
||||||
|
/// <summary>
|
||||||
|
/// Initial polling done event
|
||||||
|
/// </summary>
|
||||||
|
protected AsyncResetEvent _initialPollDoneEvent = new AsyncResetEvent(false, false);
|
||||||
|
/// <summary>
|
||||||
|
/// The error from the initial polling;
|
||||||
|
/// </summary>
|
||||||
|
protected Error? _initialPollingError;
|
||||||
|
/// <summary>
|
||||||
|
/// Polling task
|
||||||
|
/// </summary>
|
||||||
|
protected Task? _pollTask;
|
||||||
|
/// <summary>
|
||||||
|
/// Cancellation token
|
||||||
|
/// </summary>
|
||||||
|
protected CancellationTokenSource? _cts;
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket subscription
|
||||||
|
/// </summary>
|
||||||
|
protected UpdateSubscription? _subscription;
|
||||||
|
/// <summary>
|
||||||
|
/// Start time
|
||||||
|
/// </summary>
|
||||||
|
protected DateTime? _startTime = null;
|
||||||
|
/// <summary>
|
||||||
|
/// Last polling attempt
|
||||||
|
/// </summary>
|
||||||
|
protected DateTime? _lastPollAttempt;
|
||||||
|
/// <summary>
|
||||||
|
/// Last polling timestamp
|
||||||
|
/// </summary>
|
||||||
|
protected DateTime? _lastPollTime;
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp of last message received before websocket disconnecting
|
||||||
|
/// </summary>
|
||||||
|
protected DateTime? _lastDataTimeBeforeDisconnect;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether last polling was successful
|
||||||
|
/// </summary>
|
||||||
|
protected bool _lastPollSuccess;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether first polling was done
|
||||||
|
/// </summary>
|
||||||
|
protected bool _firstPollDone;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether websocket was disconnected before a polling
|
||||||
|
/// </summary>
|
||||||
|
protected bool _wasDisconnected;
|
||||||
|
/// <summary>
|
||||||
|
/// Poll at the start
|
||||||
|
/// </summary>
|
||||||
|
protected bool _pollAtStart;
|
||||||
|
/// <summary>
|
||||||
|
/// Poll interval when connected
|
||||||
|
/// </summary>
|
||||||
|
protected TimeSpan _pollIntervalConnected;
|
||||||
|
/// <summary>
|
||||||
|
/// Poll interval when disconnected
|
||||||
|
/// </summary>
|
||||||
|
protected TimeSpan _pollIntervalDisconnected;
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange name
|
||||||
|
/// </summary>
|
||||||
|
protected string _exchange;
|
||||||
|
/// <summary>
|
||||||
|
/// Time completed data is retained
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan _retentionTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data type
|
||||||
|
/// </summary>
|
||||||
|
public UserDataType DataType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp an update was handled. Does not necessarily mean the data was changed
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastUpdateTime { get; protected set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp any change was applied to the data
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastChangeTime { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connection status changed
|
||||||
|
/// </summary>
|
||||||
|
public event Action<bool>? OnConnectedChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_exchange = exchange;
|
||||||
|
|
||||||
|
DataType = dataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start the tracker
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="listenKey">Optional listen key</param>
|
||||||
|
public abstract Task<CallResult> StartAsync(string? listenKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop the tracker
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
|
||||||
|
if (_pollTask != null)
|
||||||
|
await _pollTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the delay until next poll
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected TimeSpan? GetNextPollDelay()
|
||||||
|
{
|
||||||
|
if (!_firstPollDone && _pollAtStart)
|
||||||
|
// First polling should be done immediately
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
|
||||||
|
if (!Connected)
|
||||||
|
{
|
||||||
|
if (_pollIntervalDisconnected == TimeSpan.Zero)
|
||||||
|
// No polling interval
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return _pollIntervalDisconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pollIntervalConnected == TimeSpan.Zero)
|
||||||
|
// No polling interval
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Wait for next poll
|
||||||
|
return _pollIntervalConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Connected
|
||||||
|
{
|
||||||
|
get => _connected;
|
||||||
|
protected set
|
||||||
|
{
|
||||||
|
if (_connected == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_connected = value;
|
||||||
|
if (!_connected)
|
||||||
|
_wasDisconnected = true;
|
||||||
|
else
|
||||||
|
_pollWaitEvent.Set();
|
||||||
|
|
||||||
|
OnConnectedChange?.Invoke(_connected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User data tracker
|
||||||
|
/// </summary>
|
||||||
|
public abstract class UserDataItemTracker<T> : UserDataItemTracker, IUserDataTracker<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data store
|
||||||
|
/// </summary>
|
||||||
|
protected ConcurrentDictionary<string, T> _store = new ConcurrentDictionary<string, T>(StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
/// <summary>
|
||||||
|
/// Tracked symbols list
|
||||||
|
/// </summary>
|
||||||
|
protected readonly List<SharedSymbol> _symbols;
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol lock
|
||||||
|
/// </summary>
|
||||||
|
protected object _symbolLock = new object();
|
||||||
|
/// <summary>
|
||||||
|
/// Only track provided symbols setting
|
||||||
|
/// </summary>
|
||||||
|
protected bool _onlyTrackProvidedSymbols;
|
||||||
|
/// <summary>
|
||||||
|
/// Is SharedSymbol model
|
||||||
|
/// </summary>
|
||||||
|
protected bool _isSymbolModel;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public T[] Values
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_retentionTime != TimeSpan.MaxValue)
|
||||||
|
{
|
||||||
|
var timestamp = DateTime.UtcNow;
|
||||||
|
foreach (var value in _store.Values)
|
||||||
|
{
|
||||||
|
if (GetAge(timestamp, value) > _retentionTime)
|
||||||
|
_store.TryRemove(GetKey(value), out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _store.Values.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<SharedSymbol> TrackedSymbols => _symbols;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange, TrackerItemConfig config, bool onlyTrackProvidedSymbols, IEnumerable<SharedSymbol>? symbols) : base(logger, dataType, exchange)
|
||||||
|
{
|
||||||
|
_onlyTrackProvidedSymbols = onlyTrackProvidedSymbols;
|
||||||
|
_symbols = symbols?.ToList() ?? [];
|
||||||
|
|
||||||
|
_pollIntervalDisconnected = config.PollIntervalDisconnected;
|
||||||
|
_pollIntervalConnected = config.PollIntervalConnected;
|
||||||
|
_pollAtStart = config.PollAtStart;
|
||||||
|
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
|
||||||
|
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoke OnUpdate event
|
||||||
|
/// </summary>
|
||||||
|
protected async Task InvokeUpdate(UserDataUpdate<T[]> data)
|
||||||
|
{
|
||||||
|
if (OnUpdate == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await OnUpdate(data).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async override Task<CallResult> StartAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
_startTime = DateTime.UtcNow;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var start = await SubscribeAsync(listenKey).ConfigureAwait(false);
|
||||||
|
if (!start)
|
||||||
|
return start;
|
||||||
|
|
||||||
|
Connected = true;
|
||||||
|
|
||||||
|
_pollTask = PollAsync();
|
||||||
|
|
||||||
|
await _initialPollDoneEvent.WaitAsync().ConfigureAwait(false);
|
||||||
|
if (_initialPollingError != null)
|
||||||
|
{
|
||||||
|
await StopAsync().ConfigureAwait(false);
|
||||||
|
return new CallResult(_initialPollingError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe the websocket
|
||||||
|
/// </summary>
|
||||||
|
public async Task<CallResult> SubscribeAsync(string? listenKey)
|
||||||
|
{
|
||||||
|
var subscriptionResult = await DoSubscribeAsync(listenKey).ConfigureAwait(false);
|
||||||
|
if (!subscriptionResult)
|
||||||
|
{
|
||||||
|
// Failed
|
||||||
|
// ..
|
||||||
|
return subscriptionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionResult.Data == null)
|
||||||
|
{
|
||||||
|
// No subscription available
|
||||||
|
// ..
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
_subscription = subscriptionResult.Data;
|
||||||
|
_subscription.SubscriptionStatusChanged += SubscriptionStatusChanged;
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the unique identifier for the item
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string GetKey(T item);
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether an update should be applied
|
||||||
|
/// </summary>
|
||||||
|
protected abstract bool? CheckIfUpdateShouldBeApplied(T existingItem, T updateItem);
|
||||||
|
/// <summary>
|
||||||
|
/// Update an existing item with an update
|
||||||
|
/// </summary>
|
||||||
|
protected abstract bool Update(T existingItem, T updateItem);
|
||||||
|
/// <summary>
|
||||||
|
/// Get the age of an item
|
||||||
|
/// </summary>
|
||||||
|
protected virtual TimeSpan GetAge(DateTime time, T item) => TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the tracked symbol list with potential new symbols
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols"></param>
|
||||||
|
protected void UpdateSymbolsList(IEnumerable<SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
lock (_symbolLock)
|
||||||
|
{
|
||||||
|
foreach (var symbol in symbols.Distinct())
|
||||||
|
{
|
||||||
|
if (!_symbols.Any(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset))
|
||||||
|
{
|
||||||
|
_symbols.Add(symbol);
|
||||||
|
_logger.LogDebug("Adding {BaseAsset}/{QuoteAsset} to symbol tracking list", symbol.BaseAsset, symbol.QuoteAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle an update
|
||||||
|
/// </summary>
|
||||||
|
protected internal virtual async Task HandleUpdateAsync(UpdateSource source, T[] @event)
|
||||||
|
{
|
||||||
|
LastUpdateTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (_isSymbolModel)
|
||||||
|
{
|
||||||
|
List<T>? toRemove = null;
|
||||||
|
foreach (var item in @event)
|
||||||
|
{
|
||||||
|
if (item is SharedSymbolModel symbolModel)
|
||||||
|
{
|
||||||
|
if (symbolModel.SharedSymbol == null)
|
||||||
|
{
|
||||||
|
toRemove ??= new List<T>();
|
||||||
|
toRemove.Add(item);
|
||||||
|
}
|
||||||
|
else if (_onlyTrackProvidedSymbols
|
||||||
|
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
||||||
|
{
|
||||||
|
toRemove ??= new List<T>();
|
||||||
|
toRemove.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toRemove != null)
|
||||||
|
@event = @event.Except(toRemove).ToArray();
|
||||||
|
|
||||||
|
if (!_onlyTrackProvidedSymbols)
|
||||||
|
UpdateSymbolsList(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local store
|
||||||
|
var updatedItems = @event.Select(GetKey).ToList();
|
||||||
|
|
||||||
|
foreach (var item in @event)
|
||||||
|
{
|
||||||
|
bool existed = false;
|
||||||
|
_store.AddOrUpdate(GetKey(item), item, (key, existing) =>
|
||||||
|
{
|
||||||
|
existed = true;
|
||||||
|
if (CheckIfUpdateShouldBeApplied(existing, item) == false)
|
||||||
|
{
|
||||||
|
updatedItems.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var updated = Update(existing, item);
|
||||||
|
if (!updated)
|
||||||
|
{
|
||||||
|
updatedItems.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Updated {DataType} {Item}", DataType, key);
|
||||||
|
LastChangeTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existed)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Added {DataType} {Item}", DataType, GetKey(item));
|
||||||
|
LastChangeTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedItems.Count > 0 && OnUpdate != null)
|
||||||
|
{
|
||||||
|
await OnUpdate.Invoke(
|
||||||
|
new UserDataUpdate<T[]>(source, _exchange, _store.Where(x => updatedItems.Contains(x.Key)).Select(x => x.Value).ToArray())).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket subscription implementation
|
||||||
|
/// </summary>
|
||||||
|
protected abstract Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Polling task
|
||||||
|
/// </summary>
|
||||||
|
protected async Task PollAsync()
|
||||||
|
{
|
||||||
|
while (!_cts!.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var delayForNextPoll = GetNextPollDelay();
|
||||||
|
if (delayForNextPoll != TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (delayForNextPoll != null)
|
||||||
|
_logger.LogTrace("{DataType} delay for next polling: {Delay}", DataType, delayForNextPoll);
|
||||||
|
|
||||||
|
await _pollWaitEvent.WaitAsync(delayForNextPoll, _cts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentlyFirstPoll = !_firstPollDone;
|
||||||
|
_firstPollDone = true;
|
||||||
|
if (_cts.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (_lastPollAttempt != null
|
||||||
|
&& (DateTime.UtcNow - _lastPollAttempt.Value) < TimeSpan.FromSeconds(2)
|
||||||
|
&& !(Connected && _wasDisconnected))
|
||||||
|
{
|
||||||
|
if (_lastPollSuccess)
|
||||||
|
// If last poll was less than 2 seconds ago and it was successful don't bother immediately polling again
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Connected)
|
||||||
|
_wasDisconnected = false;
|
||||||
|
|
||||||
|
_lastPollSuccess = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var anyError = await DoPollAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
_initialPollDoneEvent.Set();
|
||||||
|
_lastPollAttempt = DateTime.UtcNow;
|
||||||
|
_lastPollSuccess = !anyError;
|
||||||
|
|
||||||
|
if (anyError && currentlyFirstPoll && _pollAtStart)
|
||||||
|
{
|
||||||
|
if (_initialPollingError == null)
|
||||||
|
throw new Exception("Error in initial polling but error not set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{DataType} UserDataTracker polling exception", DataType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Polling implementation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract Task<bool> DoPollAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle subscription status change
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newState"></param>
|
||||||
|
private void SubscriptionStatusChanged(SubscriptionStatus newState)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("{DataType} stream status changed: {NewState}", DataType, newState);
|
||||||
|
|
||||||
|
if (newState == SubscriptionStatus.Pending)
|
||||||
|
{
|
||||||
|
// Record last data receive time since we need to request data from that timestamp on when polling
|
||||||
|
// Only set to new value if it isn't already set since if we disconnect/reconnect a couple of times without
|
||||||
|
// managing to do a poll we don't want to override the time since we still need to request that earlier data
|
||||||
|
|
||||||
|
if (_lastDataTimeBeforeDisconnect == null)
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = _subscription!.LastReceiveTime;
|
||||||
|
|
||||||
|
// When changing to pending (disconnected) trigger polling to start checking
|
||||||
|
_pollWaitEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connected = newState == SubscriptionStatus.Subscribed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tracker configuration
|
||||||
|
/// </summary>
|
||||||
|
public record TrackerItemConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interval to poll data at as backup, even when the websocket stream is still connected.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan PollIntervalConnected { get; set; } = TimeSpan.Zero;
|
||||||
|
/// <summary>
|
||||||
|
/// Interval to poll data at while the websocket is disconnected.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan PollIntervalDisconnected { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to poll for data initially when starting the tracker.
|
||||||
|
/// </summary>
|
||||||
|
public bool PollAtStart { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pollAtStart">Whether to poll for data initially when starting the tracker</param>
|
||||||
|
/// <param name="pollIntervalConnected">Interval to poll data at as backup, even when the websocket stream is still connected</param>
|
||||||
|
/// <param name="pollIntervalDisconnected">Interval to poll data at while the websocket is disconnected</param>
|
||||||
|
public TrackerItemConfig(bool pollAtStart, TimeSpan pollIntervalConnected, TimeSpan pollIntervalDisconnected)
|
||||||
|
{
|
||||||
|
PollAtStart = pollAtStart;
|
||||||
|
PollIntervalConnected = pollIntervalConnected;
|
||||||
|
PollIntervalDisconnected = pollIntervalDisconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public record TrackerTimedItemConfig: TrackerItemConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The timespan data is retained after being completed
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan RetentionTime { get; set; } = TimeSpan.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pollAtStart">Whether to poll for data initially when starting the tracker</param>
|
||||||
|
/// <param name="pollIntervalConnected">Interval to poll data at as backup, even when the websocket stream is still connected</param>
|
||||||
|
/// <param name="pollIntervalDisconnected">Interval to poll data at while the websocket is disconnected</param>
|
||||||
|
/// <param name="retentionTime">The timespan data is retained after being completed</param>
|
||||||
|
public TrackerTimedItemConfig(bool pollAtStart, TimeSpan pollIntervalConnected, TimeSpan pollIntervalDisconnected, TimeSpan retentionTime) : base(pollAtStart, pollIntervalConnected, pollIntervalDisconnected)
|
||||||
|
{
|
||||||
|
RetentionTime = retentionTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update source
|
||||||
|
/// </summary>
|
||||||
|
public enum UpdateSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Polling result
|
||||||
|
/// </summary>
|
||||||
|
Poll,
|
||||||
|
/// <summary>
|
||||||
|
/// Websocket push
|
||||||
|
/// </summary>
|
||||||
|
Push
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User data tracker configuration
|
||||||
|
/// </summary>
|
||||||
|
public abstract record UserDataTrackerConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Symbols to initially track, used when polling data. Other symbols will get tracked when updates are received for orders or trades on a new symbol and when there are open orders or positions on a new symbol. To only track the symbols specified here set `OnlyTrackProvidedSymbols` to true.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<SharedSymbol> TrackedSymbols { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// If true only orders and trades in the `Symbols` options will get tracked, data on other symbols will be ignored.
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyTrackProvidedSymbols { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to track order trades, can lead to increased requests when polling since they're requested per symbol.
|
||||||
|
/// </summary>
|
||||||
|
public bool TrackTrades { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spot user data tracker config
|
||||||
|
/// </summary>
|
||||||
|
public record SpotUserDataTrackerConfig : UserDataTrackerConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Balance tracking config
|
||||||
|
/// </summary>
|
||||||
|
public TrackerItemConfig BalancesConfig { get; set; } = new TrackerItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(10));
|
||||||
|
/// <summary>
|
||||||
|
/// Order tracking config
|
||||||
|
/// </summary>
|
||||||
|
public TrackerTimedItemConfig OrdersConfig { get; set; } = new TrackerTimedItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||||
|
/// <summary>
|
||||||
|
/// Trade tracking config
|
||||||
|
/// </summary>
|
||||||
|
public TrackerTimedItemConfig UserTradesConfig { get; set; } = new TrackerTimedItemConfig(false, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Futures user data tracker config
|
||||||
|
/// </summary>
|
||||||
|
public record FuturesUserDataTrackerConfig : UserDataTrackerConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Balance tracking config
|
||||||
|
/// </summary>
|
||||||
|
public TrackerItemConfig BalancesConfig { get; set; } = new TrackerItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(10));
|
||||||
|
/// <summary>
|
||||||
|
/// Order tracking config
|
||||||
|
/// </summary>
|
||||||
|
public TrackerTimedItemConfig OrdersConfig { get; set; } = new TrackerTimedItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||||
|
/// <summary>
|
||||||
|
/// Trade tracking config
|
||||||
|
/// </summary>
|
||||||
|
public TrackerTimedItemConfig UserTradesConfig { get; set; } = new TrackerTimedItemConfig(false, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||||
|
/// <summary>
|
||||||
|
/// Position tracking config
|
||||||
|
/// </summary>
|
||||||
|
public TrackerItemConfig PositionConfig { get; set; } = new TrackerItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data type
|
||||||
|
/// </summary>
|
||||||
|
public enum UserDataType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Balances
|
||||||
|
/// </summary>
|
||||||
|
Balances,
|
||||||
|
/// <summary>
|
||||||
|
/// Orders
|
||||||
|
/// </summary>
|
||||||
|
Orders,
|
||||||
|
/// <summary>
|
||||||
|
/// Trades
|
||||||
|
/// </summary>
|
||||||
|
Trades,
|
||||||
|
/// <summary>
|
||||||
|
/// Positions
|
||||||
|
/// </summary>
|
||||||
|
Positions
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User data update
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Data type</typeparam>
|
||||||
|
public class UserDataUpdate<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Source
|
||||||
|
/// </summary>
|
||||||
|
public UpdateSource Source { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange name
|
||||||
|
/// </summary>
|
||||||
|
public string Exchange { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data
|
||||||
|
/// </summary>
|
||||||
|
public T Data { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserDataUpdate(UpdateSource source, string exchange, T data)
|
||||||
|
{
|
||||||
|
Source = source;
|
||||||
|
Exchange = exchange;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User data tracker
|
||||||
|
/// </summary>
|
||||||
|
public abstract class UserDataTracker
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Logger
|
||||||
|
/// </summary>
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
/// <summary>
|
||||||
|
/// Listen key to use for subscriptions
|
||||||
|
/// </summary>
|
||||||
|
protected string? _listenKey;
|
||||||
|
/// <summary>
|
||||||
|
/// List of data trackers
|
||||||
|
/// </summary>
|
||||||
|
protected abstract UserDataItemTracker[] DataTrackers { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? UserIdentifier { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connected status changed
|
||||||
|
/// </summary>
|
||||||
|
public event Action<UserDataType, bool>? OnConnectedChange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange name
|
||||||
|
/// </summary>
|
||||||
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether all trackers are full connected
|
||||||
|
/// </summary>
|
||||||
|
public bool Connected => DataTrackers.All(x => x.Connected);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserDataTracker(
|
||||||
|
ILogger logger,
|
||||||
|
string exchange,
|
||||||
|
UserDataTrackerConfig config,
|
||||||
|
string? userIdentifier)
|
||||||
|
{
|
||||||
|
if (config.OnlyTrackProvidedSymbols && !config.TrackedSymbols.Any())
|
||||||
|
throw new ArgumentException(nameof(config.TrackedSymbols), "Conflicting options; `OnlyTrackProvidedSymbols` but no symbols specific in `TrackedSymbols`");
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
Exchange = exchange;
|
||||||
|
UserIdentifier = userIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start the data tracker
|
||||||
|
/// </summary>
|
||||||
|
public async Task<CallResult> StartAsync()
|
||||||
|
{
|
||||||
|
foreach(var tracker in DataTrackers)
|
||||||
|
tracker.OnConnectedChange += (x) => OnConnectedChange?.Invoke(tracker.DataType, x);
|
||||||
|
|
||||||
|
var result = await DoStartAsync().ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var tasks = new List<Task<CallResult>>();
|
||||||
|
foreach (var dataTracker in DataTrackers)
|
||||||
|
tasks.Add(dataTracker.StartAsync(_listenKey));
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
if (!tasks.All(x => x.Result.Success))
|
||||||
|
{
|
||||||
|
await Task.WhenAll(DataTrackers.Select(x => x.StopAsync())).ConfigureAwait(false);
|
||||||
|
return tasks.First(x => !x.Result.Success).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation specific start logic
|
||||||
|
/// </summary>
|
||||||
|
protected abstract Task<CallResult> DoStartAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop the data tracker
|
||||||
|
/// </summary>
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Stopping UserDataTracker");
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
foreach (var dataTracker in DataTrackers)
|
||||||
|
tasks.Add(dataTracker.StopAsync());
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Stopped UserDataTracker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Linq;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User futures data tracker
|
||||||
|
/// </summary>
|
||||||
|
public abstract class UserFuturesDataTracker : UserDataTracker, IUserFuturesDataTracker
|
||||||
|
{
|
||||||
|
private readonly IFuturesSymbolRestClient _symbolClient;
|
||||||
|
private readonly IListenKeyRestClient? _listenKeyClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override UserDataItemTracker[] DataTrackers { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Balances tracker
|
||||||
|
/// </summary>
|
||||||
|
public IUserDataTracker<SharedBalance> Balances { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Orders tracker
|
||||||
|
/// </summary>
|
||||||
|
public IUserDataTracker<SharedFuturesOrder> Orders { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Positions tracker
|
||||||
|
/// </summary>
|
||||||
|
public IUserDataTracker<SharedPosition> Positions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Trades tracker
|
||||||
|
/// </summary>
|
||||||
|
public IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether websocket position updates are full snapshots and missing positions should be considered 0
|
||||||
|
/// </summary>
|
||||||
|
protected abstract bool WebsocketPositionUpdatesAreFullSnapshots { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserFuturesDataTracker(
|
||||||
|
ILogger logger,
|
||||||
|
IFuturesSymbolRestClient symbolRestClient,
|
||||||
|
IListenKeyRestClient? listenKeyRestClient,
|
||||||
|
IBalanceRestClient balanceRestClient,
|
||||||
|
IBalanceSocketClient? balanceSocketClient,
|
||||||
|
IFuturesOrderRestClient futuresOrderRestClient,
|
||||||
|
IFuturesOrderSocketClient? futuresOrderSocketClient,
|
||||||
|
IUserTradeSocketClient? userTradeSocketClient,
|
||||||
|
IPositionSocketClient? positionSocketClient,
|
||||||
|
string? userIdentifier,
|
||||||
|
FuturesUserDataTrackerConfig config,
|
||||||
|
SharedAccountType? accountType = null,
|
||||||
|
ExchangeParameters? exchangeParameters = null) : base(logger, symbolRestClient.Exchange, config, userIdentifier)
|
||||||
|
{
|
||||||
|
// create trackers
|
||||||
|
_symbolClient = symbolRestClient;
|
||||||
|
_listenKeyClient = listenKeyRestClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
|
||||||
|
var trackers = new List<UserDataItemTracker>();
|
||||||
|
|
||||||
|
var balanceAccountType = accountType ?? SharedAccountType.PerpetualLinearFutures;
|
||||||
|
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, balanceAccountType, config.BalancesConfig, exchangeParameters);
|
||||||
|
Balances = balanceTracker;
|
||||||
|
trackers.Add(balanceTracker);
|
||||||
|
|
||||||
|
var orderTracker = new FuturesOrderTracker(logger, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
|
Orders = orderTracker;
|
||||||
|
trackers.Add(orderTracker);
|
||||||
|
|
||||||
|
var positionTracker = new PositionTracker(logger, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
|
||||||
|
Positions = positionTracker;
|
||||||
|
trackers.Add(positionTracker);
|
||||||
|
|
||||||
|
if (config.TrackTrades)
|
||||||
|
{
|
||||||
|
var tradeTracker = new FuturesUserTradeTracker(logger, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
|
Trades = tradeTracker;
|
||||||
|
trackers.Add(tradeTracker);
|
||||||
|
|
||||||
|
orderTracker.OnTradeUpdate += tradeTracker.HandleUpdateAsync;
|
||||||
|
tradeTracker.GetTrackedOrderIds = () => orderTracker.Values.Select(x => x.OrderId).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
DataTrackers = trackers.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<CallResult> DoStartAsync()
|
||||||
|
{
|
||||||
|
var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!symbolResult)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to start UserFuturesDataTracker; symbols request failed: {Error}", symbolResult.Error);
|
||||||
|
return symbolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_listenKeyClient != null)
|
||||||
|
{
|
||||||
|
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!lkResult)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to start UserFuturesDataTracker; listen key request failed: {Error}", lkResult.Error);
|
||||||
|
return lkResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
_listenKey = lkResult.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Linq;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
|
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spot user data tracker
|
||||||
|
/// </summary>
|
||||||
|
public class UserSpotDataTracker : UserDataTracker, IUserSpotDataTracker
|
||||||
|
{
|
||||||
|
private readonly ISpotSymbolRestClient _symbolClient;
|
||||||
|
private readonly IListenKeyRestClient? _listenKeyClient;
|
||||||
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override UserDataItemTracker[] DataTrackers { get; }
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IUserDataTracker<SharedBalance> Balances { get; }
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IUserDataTracker<SharedSpotOrder> Orders { get; }
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserSpotDataTracker(
|
||||||
|
ILogger logger,
|
||||||
|
ISpotSymbolRestClient symbolRestClient,
|
||||||
|
IListenKeyRestClient? listenKeyRestClient,
|
||||||
|
IBalanceRestClient balanceRestClient,
|
||||||
|
IBalanceSocketClient? balanceSocketClient,
|
||||||
|
ISpotOrderRestClient spotOrderRestClient,
|
||||||
|
ISpotOrderSocketClient? spotOrderSocketClient,
|
||||||
|
IUserTradeSocketClient? userTradeSocketClient,
|
||||||
|
string? userIdentifier,
|
||||||
|
SpotUserDataTrackerConfig config,
|
||||||
|
ExchangeParameters? exchangeParameters = null) : base(logger, symbolRestClient.Exchange, config, userIdentifier)
|
||||||
|
{
|
||||||
|
// create trackers
|
||||||
|
_symbolClient = symbolRestClient;
|
||||||
|
_listenKeyClient = listenKeyRestClient;
|
||||||
|
_exchangeParameters = exchangeParameters;
|
||||||
|
|
||||||
|
var trackers = new List<UserDataItemTracker>();
|
||||||
|
|
||||||
|
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
|
||||||
|
Balances = balanceTracker;
|
||||||
|
trackers.Add(balanceTracker);
|
||||||
|
|
||||||
|
var orderTracker = new SpotOrderTracker(logger, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
|
Orders = orderTracker;
|
||||||
|
trackers.Add(orderTracker);
|
||||||
|
|
||||||
|
if (config.TrackTrades)
|
||||||
|
{
|
||||||
|
var tradeTracker = new SpotUserTradeTracker(logger, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
|
Trades = tradeTracker;
|
||||||
|
trackers.Add(tradeTracker);
|
||||||
|
|
||||||
|
orderTracker.OnTradeUpdate += tradeTracker.HandleUpdateAsync;
|
||||||
|
tradeTracker.GetTrackedOrderIds = () => orderTracker.Values.Select(x => x.OrderId).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
DataTrackers = trackers.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<CallResult> DoStartAsync()
|
||||||
|
{
|
||||||
|
var symbolResult = await _symbolClient.GetSpotSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!symbolResult)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to start UserSpotDataTracker; symbols request failed: {Error}", symbolResult.Error);
|
||||||
|
return symbolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_listenKeyClient != null)
|
||||||
|
{
|
||||||
|
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
|
if (!lkResult)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to start UserSpotDataTracker; listen key request failed: {Error}", lkResult.Error);
|
||||||
|
return lkResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
_listenKey = lkResult.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ For more information on what CryptoExchange.Net and it's client libraries offers
|
|||||||
### CryptoExchange.Net Ecosystem
|
### CryptoExchange.Net Ecosystem
|
||||||
Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider using a referral link to support development, as well as potentially get some trading fee discount!
|
Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider using a referral link to support development, as well as potentially get some trading fee discount!
|
||||||
|
|
||||||
||Exchange|Type|Repository|Nuget|Referral Link|Referral Fee Discount|
|
||API|Type|Repository|Nuget|Referral Link|Referral Fee Discount|
|
||||||
|--|--|--|--|--|--|--|
|
|--|--|--|--|--|--|--|
|
||||||
||Aster|DEX|[JKorf/Aster.Net](https://github.com/JKorf/Aster.Net)|[](https://www.nuget.org/packages/JKorf.Aster.Net)|[Link](https://www.asterdex.com/en/referral/FD2E11)|4%|
|
||Aster|DEX|[JKorf/Aster.Net](https://github.com/JKorf/Aster.Net)|[](https://www.nuget.org/packages/JKorf.Aster.Net)|[Link](https://www.asterdex.com/en/referral/FD2E11)|4%|
|
||||||
||Binance|CEX|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[](https://www.nuget.org/packages/Binance.Net)|[Link](https://accounts.binance.com/register?ref=X5K3F2ZG)|20%|
|
||Binance|CEX|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[](https://www.nuget.org/packages/Binance.Net)|[Link](https://accounts.binance.com/register?ref=X5K3F2ZG)|20%|
|
||||||
@@ -34,6 +34,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|
|||||||
||Kucoin|CEX|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|[Link](https://www.kucoin.com/r/rf/QBS4FPED)|-|
|
||Kucoin|CEX|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|[Link](https://www.kucoin.com/r/rf/QBS4FPED)|-|
|
||||||
||Mexc|CEX|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[](https://www.nuget.org/packages/JK.Mexc.Net)|-|-|
|
||Mexc|CEX|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[](https://www.nuget.org/packages/JK.Mexc.Net)|-|-|
|
||||||
||OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%|
|
||OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%|
|
||||||
|
||Polymarket|DEX|[JKorf/Polymarket.Net](https://github.com/JKorf/Polymarket.Net)|[](https://www.nuget.org/packages/Polymarket.Net)|-|-|
|
||||||
||Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|
||Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|
||||||
||Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-|
|
||Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-|
|
||||||
||WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-|
|
||WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-|
|
||||||
@@ -66,6 +67,86 @@ 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.4.0 - 06 Feb 2026
|
||||||
|
* Added user data tracking logic
|
||||||
|
* Added LastReceiveTime, SocketStatus and SubscriptionStatus properties to UpdateSubscription
|
||||||
|
* Added SharedTransferStatus Enum and property to SharedDeposit
|
||||||
|
* Added PositionMode property to SharedPosition model
|
||||||
|
* Added IsZero property to SharedQuantity
|
||||||
|
* Added additional methods for requesting supported symbols to Shared ISpotSymbolRestClient/IFuturesSymbolRestClient interfaces
|
||||||
|
* Added Disposed property on BaseClient and IRestClient/ISocketClient interfaces
|
||||||
|
* Added AutoTimestamp option for socket client
|
||||||
|
* Renamed IWebSocket LastActionTime to LastReceiveTime
|
||||||
|
* Refactored AsyncResetEvent implementation
|
||||||
|
* Updated CryptoExchangeWebsocketClient LastReceiveTime logic
|
||||||
|
* Updated Subscription status change event handler to run sync instead of separate task
|
||||||
|
* Updated Interval property access on KlineTracker to public
|
||||||
|
* Fixed socket client timestamp offset bug
|
||||||
|
|
||||||
|
* Version 10.3.1 - 27 Jan 2026
|
||||||
|
* Fixed potential collection modified exception upon logging message not handled in websocket message handling
|
||||||
|
|
||||||
|
* Version 10.3.0 - 22 Jan 2026
|
||||||
|
* Added PlatformInfo class for specifying platform metadata
|
||||||
|
* Added better handling for enabling AutoTimestamp in client options when not implemented in the API
|
||||||
|
* Fixed state handling for subscriptions where queries do not get a response
|
||||||
|
* Fixed HandleSubQueryResponse not getting called
|
||||||
|
* Removed legacy websocket message handling and the corresponding UseUpdatedDeserialization client option
|
||||||
|
|
||||||
|
* Version 10.2.5 - 19 Jan 2026
|
||||||
|
* Updated SymbolOrderBook.WaitUntilFirstUpdateBufferedAsync
|
||||||
|
* Added GetRestOffsets and GetWebsocketOffsets to TimeOffsetManager
|
||||||
|
|
||||||
|
* Version 10.2.4 - 17 Jan 2026
|
||||||
|
* Added WaitUntilFirstUpdateBufferedAsync method on SymbolOrderBook
|
||||||
|
* Added some util methods
|
||||||
|
* Added CommaSplitStringConverter
|
||||||
|
* Fixed sequence validation bug SymbolOrderBook
|
||||||
|
|
||||||
|
* Version 10.2.3 - 14 Jan 2026
|
||||||
|
* Added HandleUnhandledMessage virtual method to SocketApiClient to allow some processing for messages which couldn't be mapped via the normal way
|
||||||
|
* Fixed semaphore exception when creating a new REST client while time sync is in progress on another client
|
||||||
|
|
||||||
|
* Version 10.2.2 - 13 Jan 2026
|
||||||
|
* Allow the same websocket connection sequence number to be recorded multiple times
|
||||||
|
|
||||||
|
* Version 10.2.1 - 13 Jan 2026
|
||||||
|
* Removed duplicate logging for rest responses in Trace verbosity
|
||||||
|
* Fixed parameter URL creation for array values with ArrayParametersSerialization.MultipleValues
|
||||||
|
|
||||||
|
* 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