mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62c9769c72 | |||
| 92d7bc1e2e | |||
| 99e4f96f63 | |||
| 94d8afe149 | |||
| 90ad59c63a | |||
| c2273edfaa | |||
| 236283f4dd | |||
| b66f12ff75 | |||
| 0403384beb | |||
| 7d7bc35869 | |||
| 48797038be | |||
| d21792d04c | |||
| 8414e9d94f | |||
| ab0243445d | |||
| f2cf70b02f | |||
| 9ff417bba8 | |||
| 6b43d08a4d | |||
| 39bf7fe9b9 | |||
| b5893c3b60 | |||
| 15657ba683 | |||
| 1aed9f0c67 | |||
| 17f1560310 | |||
| 41de0a3150 | |||
| 3e410be611 | |||
| be75449e4a | |||
| b1b05c8f6b |
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
@@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||
{
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
var options = new TestClientOptions();
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -24,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
||||
{
|
||||
}
|
||||
Initialize(options.Value);
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
|
||||
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using CryptoExchange.Net.Testing.Implementations;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -22,25 +23,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
Initialize(options.Value);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||
}
|
||||
@@ -70,7 +66,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestSocketOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
|
||||
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
||||
{
|
||||
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
||||
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
|
||||
@@ -13,39 +13,30 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// The api key / label to authenticate requests
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret or private key to authenticate requests
|
||||
/// </summary>
|
||||
public string Secret { get; }
|
||||
public string Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
public ApiCredentialsType CredentialType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key / label used for identification</param>
|
||||
/// <param name="secret">The api secret or private key used for signing</param>
|
||||
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key / label used for identification</param>
|
||||
/// <param name="secret">The api secret or private key used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
|
||||
/// <param name="credentialType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
CredentialType = credentialType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
@@ -65,7 +56,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="inputStream">The stream containing the json data</param>
|
||||
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
||||
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
||||
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
{
|
||||
var accessor = new SystemTextJsonStreamMessageAccessor();
|
||||
if (!accessor.Read(inputStream, false).Result)
|
||||
@@ -75,11 +66,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
|
||||
|
||||
inputStream.Seek(0, SeekOrigin.Begin);
|
||||
return new ApiCredentials(key, secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Get the API key of the current credentials
|
||||
/// </summary>
|
||||
public string ApiKey => _credentials.Key;
|
||||
public string ApiKey => _credentials.Key!;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
if (credentials.Secret == null)
|
||||
if (credentials.Key == null || credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
_credentials = credentials;
|
||||
|
||||
@@ -109,6 +109,16 @@ namespace CryptoExchange.Net.Clients
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the options delegate to a new options instance
|
||||
/// </summary>
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||
{
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow multiple subscriptions with the same topic on the same connection
|
||||
/// </summary>
|
||||
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
@@ -211,7 +216,7 @@ namespace CryptoExchange.Net.Clients
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
@@ -403,7 +408,7 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult(new NoApiCredentialsError());
|
||||
|
||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||
var authRequest = GetAuthenticationRequest(socket);
|
||||
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
||||
if (authRequest != null)
|
||||
{
|
||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||
@@ -428,7 +433,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Should return the request which can be used to authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system subscription. Used for example to reply to ping requests
|
||||
@@ -478,23 +483,28 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
|
||||
{
|
||||
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& s.Value.ApiClient.GetType() == GetType()
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
|
||||
&& s.Value.Connected);
|
||||
|
||||
SocketConnection connection;
|
||||
if (!dedicatedRequestConnection)
|
||||
{
|
||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
|
||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||
}
|
||||
|
||||
if (connection != null)
|
||||
@@ -519,7 +529,14 @@ namespace CryptoExchange.Net.Clients
|
||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
||||
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||
if (dedicatedRequestConnection)
|
||||
{
|
||||
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
|
||||
{
|
||||
IsDedicatedRequestConnection = dedicatedRequestConnection,
|
||||
Authenticated = authenticated
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var ptg in PeriodicTaskRegistrations)
|
||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
||||
@@ -652,7 +669,7 @@ namespace CryptoExchange.Net.Clients
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
|
||||
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection))
|
||||
tasks.Add(connection.CloseAsync());
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
@@ -108,7 +108,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return default;
|
||||
|
||||
var result = Activator.CreateInstance(typeToConvert);
|
||||
return (T)ParseObject(ref reader, result, typeToConvert);
|
||||
return (T)ParseObject(ref reader, result, typeToConvert, options);
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
@@ -148,7 +148,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("Not an array");
|
||||
@@ -175,15 +175,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
object? value = null;
|
||||
if (attribute.JsonConverterType != null)
|
||||
{
|
||||
// Has JsonConverter attribute
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add((JsonConverter)Activator.CreateInstance(attribute.JsonConverterType));
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
|
||||
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
|
||||
{
|
||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType);
|
||||
newOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
|
||||
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
|
||||
Converters = { converter },
|
||||
};
|
||||
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
|
||||
}
|
||||
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, newOptions);
|
||||
}
|
||||
else if (attribute.DefaultDeserialization)
|
||||
{
|
||||
// Use default deserialization
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType);
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -194,12 +203,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetDecimal(),
|
||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType),
|
||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||
};
|
||||
}
|
||||
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : value);
|
||||
else
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
index++;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for comma seperated enum values
|
||||
/// </summary>
|
||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -121,7 +126,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value.Value.Deserialize<T>(_serializerOptions);
|
||||
}
|
||||
catch { }
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>8.1.0</PackageVersion>
|
||||
<AssemblyVersion>8.1.0</AssemblyVersion>
|
||||
<FileVersion>8.1.0</FileVersion>
|
||||
<PackageVersion>8.4.0</PackageVersion>
|
||||
<AssemblyVersion>8.4.0</AssemblyVersion>
|
||||
<FileVersion>8.4.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@@ -48,16 +48,17 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -8,30 +8,21 @@
|
||||
/// <summary>
|
||||
/// The host address of the proxy
|
||||
/// </summary>
|
||||
public string Host { get; }
|
||||
public string Host { get; set; }
|
||||
/// <summary>
|
||||
/// The port of the proxy
|
||||
/// </summary>
|
||||
public int Port { get; }
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The login of the proxy
|
||||
/// </summary>
|
||||
public string? Login { get; }
|
||||
public string? Login { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The password of the proxy
|
||||
/// </summary>
|
||||
public string? Password { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
/// </summary>
|
||||
/// <param name="host">The proxy hostname/ip</param>
|
||||
/// <param name="port">The proxy port</param>
|
||||
public ApiProxy(string host, int port): this(host, port, null, null)
|
||||
{
|
||||
}
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
@@ -40,7 +31,7 @@
|
||||
/// <param name="port">The proxy port</param>
|
||||
/// <param name="login">The proxy login</param>
|
||||
/// <param name="password">The proxy password</param>
|
||||
public ApiProxy(string host, int port, string? login, string? password)
|
||||
public ApiProxy(string host, int port, string? login = null, string? password = null)
|
||||
{
|
||||
Host = host;
|
||||
Port = port;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Library options
|
||||
/// </summary>
|
||||
/// <typeparam name="TRestOptions"></typeparam>
|
||||
/// <typeparam name="TSocketOptions"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
where TRestOptions: RestExchangeOptions, new()
|
||||
where TSocketOptions: SocketExchangeOptions, new()
|
||||
where TApiCredentials: ApiCredentials
|
||||
where TEnvironment: TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Rest client options
|
||||
/// </summary>
|
||||
public TRestOptions Rest { get; set; } = new TRestOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Socket client options
|
||||
/// </summary>
|
||||
public TSocketOptions Socket { get; set; } = new TSocketOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API.
|
||||
/// </summary>
|
||||
public TEnvironment? Environment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests.
|
||||
/// </summary>
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The DI service lifetime for the socket client
|
||||
/// </summary>
|
||||
public ServiceLifetime? SocketClientLifeTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copy values from these options to the target options
|
||||
/// </summary>
|
||||
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
{
|
||||
targetOptions.ApiCredentials = ApiCredentials;
|
||||
targetOptions.Environment = Environment;
|
||||
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||
targetOptions.Socket = Socket.Set(targetOptions.Socket);
|
||||
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public TimeSpan? TimestampRecalculationInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public virtual T Copy<T>() where T : RestApiOptions, new()
|
||||
public T Set<T>(T item) where T : RestApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,25 +29,21 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestExchangeOptions, new()
|
||||
public T Set<T>(T item) where T : RestExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout,
|
||||
RateLimiterEnabled = RateLimiterEnabled,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
CachingEnabled = CachingEnabled,
|
||||
CachingMaxAge = CachingMaxAge,
|
||||
};
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.Proxy = Proxy;
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.CachingEnabled = CachingEnabled;
|
||||
item.CachingMaxAge = CachingMaxAge;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +62,13 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
|
||||
public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
base.Set(target);
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,19 +20,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketApiOptions, new()
|
||||
public T Set<T>(T item) where T : SocketApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,24 +57,22 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketExchangeOptions, new()
|
||||
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
ReconnectPolicy = ReconnectPolicy,
|
||||
DelayAfterConnect = DelayAfterConnect,
|
||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
RateLimiterEnabled = RateLimiterEnabled,
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.ReconnectPolicy = ReconnectPolicy;
|
||||
item.DelayAfterConnect = DelayAfterConnect;
|
||||
item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket;
|
||||
item.ReconnectInterval = ReconnectInterval;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
item.Proxy = Proxy;
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,15 +91,13 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
base.Set(target);
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
/// <summary>
|
||||
/// Name of the environment
|
||||
/// </summary>
|
||||
public string EnvironmentName { get; init; }
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
protected TradeEnvironment(string name)
|
||||
{
|
||||
EnvironmentName = name;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded;
|
||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded;
|
||||
return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// </summary>
|
||||
event Action<RateLimitEvent> RateLimitTriggered;
|
||||
|
||||
/// <summary>
|
||||
/// Event when the rate limit is updated. Note that it's only updated when a request is send, so there are no specific updates when the current usage is decaying.
|
||||
/// </summary>
|
||||
event Action<RateLimitUpdateEvent>? RateLimitUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit guard
|
||||
/// </summary>
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
/// <summary>
|
||||
/// No wait needed
|
||||
/// </summary>
|
||||
public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
|
||||
public static LimitCheck NotNeeded(int limit, TimeSpan period, int current) => new(true, default, limit, period, current);
|
||||
|
||||
/// <summary>
|
||||
/// Wait needed
|
||||
|
||||
@@ -4,10 +4,14 @@ using System;
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit event
|
||||
/// Rate limit triggered event
|
||||
/// </summary>
|
||||
public record RateLimitEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the item the limit was checked for
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
/// <summary>
|
||||
/// Name of the API limit that is reached
|
||||
/// </summary>
|
||||
@@ -52,18 +56,9 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="apiLimit"></param>
|
||||
/// <param name="limitDescription"></param>
|
||||
/// <param name="definition"></param>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="current"></param>
|
||||
/// <param name="requestWeight"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="timePeriod"></param>
|
||||
/// <param name="delayTime"></param>
|
||||
/// <param name="behaviour"></param>
|
||||
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||
{
|
||||
ItemId = itemId;
|
||||
ApiLimit = apiLimit;
|
||||
LimitDescription = limitDescription;
|
||||
RequestDefinition = definition;
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<RateLimitEvent>? RateLimitTriggered;
|
||||
/// <inheritdoc />
|
||||
public event Action<RateLimitUpdateEvent>? RateLimitUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -105,7 +107,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
else
|
||||
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
|
||||
}
|
||||
|
||||
@@ -120,7 +122,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
else
|
||||
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
@@ -133,6 +135,8 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
|
||||
if (result.IsApplied)
|
||||
{
|
||||
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
|
||||
|
||||
if (type == RateLimitItemType.Connection)
|
||||
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
|
||||
else
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit update event
|
||||
/// </summary>
|
||||
public record RateLimitUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the item the limit was checked for
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
/// <summary>
|
||||
/// Name of the API limit that is reached
|
||||
/// </summary>
|
||||
public string ApiLimit { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Description of the limit that is reached
|
||||
/// </summary>
|
||||
public string LimitDescription { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The current counter value
|
||||
/// </summary>
|
||||
public int Current { get; set; }
|
||||
/// <summary>
|
||||
/// The limit per time period
|
||||
/// </summary>
|
||||
public int? Limit { get; set; }
|
||||
/// <summary>
|
||||
/// The time period the limit is for
|
||||
/// </summary>
|
||||
public TimeSpan? TimePeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public RateLimitUpdateEvent(int itemId, string apiLimit, string limitDescription, int current, int? limit, TimeSpan? timePeriod)
|
||||
{
|
||||
ItemId = itemId;
|
||||
ApiLimit = apiLimit;
|
||||
LimitDescription = limitDescription;
|
||||
Current = current;
|
||||
Limit = limit;
|
||||
TimePeriod = timePeriod;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
public class RequestFactory : IRequestFactory
|
||||
{
|
||||
private HttpClient? _httpClient;
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
||||
@@ -19,7 +19,12 @@ namespace CryptoExchange.Net.Requests
|
||||
if (client == null)
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
try
|
||||
{
|
||||
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
}
|
||||
catch (PlatformNotSupportedException) { }
|
||||
|
||||
if (proxy != null)
|
||||
{
|
||||
handler.Proxy = new WebProxy
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
/// <summary>
|
||||
/// Leverage is configured for the symbol
|
||||
/// </summary>
|
||||
PerSymbol
|
||||
PerSymbol,
|
||||
/// <summary>
|
||||
/// Leverage is configured for the entire account
|
||||
/// </summary>
|
||||
PerAccount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for requesting user trading fees
|
||||
/// </summary>
|
||||
public interface IFeeRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Fee request options
|
||||
/// </summary>
|
||||
EndpointOptions<GetFeeRequest> GetFeeOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get trading fees for a symbol
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFee>> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
|
||||
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
|
||||
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
|
||||
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,6 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public int? MaxTotalDataPoints { get; set; }
|
||||
/// <summary>
|
||||
/// Max number of data points which can be requested in a single request
|
||||
/// </summary>
|
||||
public int? MaxRequestDataPoints { get; set; }
|
||||
/// <summary>
|
||||
/// The max age of the data that can be requested
|
||||
/// </summary>
|
||||
public TimeSpan? MaxAge { get; set; }
|
||||
@@ -31,14 +27,13 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
SupportIntervals = new[]
|
||||
{
|
||||
SharedKlineInterval.FiveMinutes,
|
||||
SharedKlineInterval.FifteenMinutes,
|
||||
SharedKlineInterval.OneHour,
|
||||
SharedKlineInterval.FifteenMinutes,
|
||||
SharedKlineInterval.OneDay,
|
||||
SharedKlineInterval.OneWeek,
|
||||
SharedKlineInterval.OneMonth
|
||||
@@ -48,7 +43,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, needsAuthentication)
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
SupportIntervals = intervals;
|
||||
}
|
||||
@@ -69,8 +64,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return new ArgumentError($"Only the most recent {MaxAge} klines are available");
|
||||
|
||||
if (MaxRequestDataPoints.HasValue && request.Limit > MaxRequestDataPoints.Value)
|
||||
return new ArgumentError($"Only {MaxRequestDataPoints} klines can be retrieved per request");
|
||||
if (request.Limit > MaxLimit)
|
||||
return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request");
|
||||
|
||||
if (MaxTotalDataPoints.HasValue)
|
||||
{
|
||||
@@ -96,8 +91,6 @@ namespace CryptoExchange.Net.SharedApis
|
||||
sb.AppendLine($"Max age of data: {MaxAge}");
|
||||
if (MaxTotalDataPoints != null)
|
||||
sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}");
|
||||
if (MaxRequestDataPoints != null)
|
||||
sb.AppendLine($"Max data points per request: {MaxRequestDataPoints}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetPositionHistoryOptions(SharedPaginationSupport paginationType) : base(paginationType, true)
|
||||
public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
|
||||
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
|
||||
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
@@ -13,12 +14,24 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public SharedPaginationSupport PaginationSupport { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether filtering based on start/end time is supported
|
||||
/// </summary>
|
||||
public bool TimePeriodFilterSupport { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Max amount of results that can be requested
|
||||
/// </summary>
|
||||
public int MaxLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(needsAuthentication)
|
||||
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
|
||||
{
|
||||
PaginationSupport = paginationType;
|
||||
TimePeriodFilterSupport = timePeriodSupport;
|
||||
MaxLimit = maxLimit;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -26,6 +39,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Pagination type: {PaginationSupport}");
|
||||
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
|
||||
sb.AppendLine($"Max limit: {MaxLimit}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to retrieve trading fees
|
||||
/// </summary>
|
||||
public record GetFeeRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to retrieve fees for</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetFeeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Trading fee info
|
||||
/// </summary>
|
||||
public record SharedFee
|
||||
{
|
||||
/// <summary>
|
||||
/// Taker fee percentage
|
||||
/// </summary>
|
||||
public decimal TakerFee { get; set; }
|
||||
/// <summary>
|
||||
/// Maker fee percentage
|
||||
/// </summary>
|
||||
public decimal MakerFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedFee(decimal makerFee, decimal takerFee)
|
||||
{
|
||||
MakerFee = makerFee;
|
||||
TakerFee = takerFee;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Side of the trade
|
||||
/// </summary>
|
||||
public SharedOrderSide Side { get; set; }
|
||||
public SharedOrderSide? Side { get; set; }
|
||||
/// <summary>
|
||||
/// Fee paid for the trade
|
||||
/// </summary>
|
||||
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedUserTrade(string symbol, string orderId, string id, SharedOrderSide side, decimal quantity, decimal price, DateTime timestamp)
|
||||
public SharedUserTrade(string symbol, string orderId, string id, SharedOrderSide? side, decimal quantity, decimal price, DateTime timestamp)
|
||||
{
|
||||
Symbol = symbol;
|
||||
OrderId = orderId;
|
||||
|
||||
@@ -14,4 +14,19 @@
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated connection state
|
||||
/// </summary>
|
||||
public class DedicatedConnectionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the connection is a dedicated request connection
|
||||
/// </summary>
|
||||
public bool IsDedicatedRequestConnection { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the dedication request connection should be authenticated
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +186,21 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this connection should be kept alive even when there is no subscription
|
||||
/// Info on whether this connection is a dedicated request connection
|
||||
/// </summary>
|
||||
public bool DedicatedRequestConnection { get; internal set; }
|
||||
public DedicatedConnectionState DedicatedRequestConnection { get; internal set; } = new DedicatedConnectionState();
|
||||
|
||||
/// <summary>
|
||||
/// Current subscription topics on this connection
|
||||
/// </summary>
|
||||
public IEnumerable<string> Topics
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_listenersLock)
|
||||
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToList()!;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _pausedActivity;
|
||||
private readonly object _listenersLock;
|
||||
@@ -603,6 +615,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public async Task CloseAsync(Subscription subscription)
|
||||
{
|
||||
// If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues
|
||||
while (subscription.IsResubscribing)
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
|
||||
subscription.Closed = true;
|
||||
|
||||
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||
@@ -618,7 +634,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (_listenersLock)
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
|
||||
if (!anyDuplicateSubscription)
|
||||
{
|
||||
@@ -841,7 +857,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new WebError("Socket not connected"));
|
||||
|
||||
if (!DedicatedRequestConnection)
|
||||
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||
{
|
||||
bool anySubscriptions;
|
||||
lock (_listenersLock)
|
||||
@@ -859,7 +875,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
lock (_listenersLock)
|
||||
{
|
||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||
|| (DedicatedRequestConnection && ApiClient.AuthenticationProvider != null);
|
||||
|| (DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated);
|
||||
}
|
||||
|
||||
if (anyAuthenticated)
|
||||
@@ -886,7 +902,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
List<Subscription> subList;
|
||||
lock (_listenersLock)
|
||||
subList = _listeners.OfType<Subscription>().Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
subList = _listeners.OfType<Subscription>().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
|
||||
if (subList.Count == 0)
|
||||
break;
|
||||
@@ -895,20 +911,30 @@ namespace CryptoExchange.Net.Sockets
|
||||
foreach (var subscription in subList)
|
||||
{
|
||||
subscription.ConnectionInvocations = 0;
|
||||
if (subscription.Closed)
|
||||
// Can be closed during resubscribing
|
||||
continue;
|
||||
|
||||
subscription.IsResubscribing = true;
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||
subscription.IsResubscribing = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
var subQuery = subscription.GetSubQuery(this);
|
||||
if (subQuery == null)
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
waitEvent.Set();
|
||||
if (r.Result.Success)
|
||||
|
||||
@@ -44,6 +44,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public bool Closed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the subscription currently resubscribing
|
||||
/// </summary>
|
||||
public bool IsResubscribing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
@@ -76,6 +81,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public abstract Type? GetMessageType(IMessageAccessor message);
|
||||
|
||||
/// <summary>
|
||||
/// Subscription topic
|
||||
/// </summary>
|
||||
public string? Topic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -224,6 +224,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||
&& propertyValue.GetType() != typeof(string))
|
||||
{
|
||||
if (propValue.Type != JTokenType.Array)
|
||||
return;
|
||||
|
||||
var jObjs = (JArray)propValue;
|
||||
var list = (IEnumerable)propertyValue;
|
||||
var enumerator = list.GetEnumerator();
|
||||
@@ -372,7 +375,8 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
}
|
||||
else if (objectValue is DateTime time)
|
||||
{
|
||||
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||
var jsonStr = jsonValue.Value<string>()!;
|
||||
if (!string.IsNullOrEmpty(jsonStr) && time != DateTimeConverter.ParseFromString(jsonStr))
|
||||
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
|
||||
}
|
||||
else if (objectValue is bool bl)
|
||||
|
||||
@@ -173,17 +173,20 @@ namespace CryptoExchange.Net.Testing
|
||||
|
||||
foreach (var clientInterface in clientInterfaces)
|
||||
{
|
||||
var implementation = assembly.GetTypes().Single(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
|
||||
int methods = 0;
|
||||
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
|
||||
var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray());
|
||||
if (interfaceMethod == null)
|
||||
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||
methods++;
|
||||
}
|
||||
int methods = 0;
|
||||
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
|
||||
{
|
||||
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray());
|
||||
if (interfaceMethod == null)
|
||||
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||
methods++;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"{clientInterface.Name} {methods} methods validated");
|
||||
Debug.WriteLine($"{clientInterface.Name} {methods} methods validated");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime)
|
||||
startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value);
|
||||
|
||||
var limit = Math.Min(_restClient.GetKlinesOptions.MaxRequestDataPoints ?? _restClient.GetKlinesOptions.MaxTotalDataPoints ?? 100, Limit ?? 100);
|
||||
var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100);
|
||||
|
||||
var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit);
|
||||
var data = new List<SharedKline>();
|
||||
|
||||
@@ -5,22 +5,23 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="10.7.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.8.2" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.4.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.14.3" />
|
||||
<PackageReference Include="CoinEx.Net" Version="7.7.2" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="1.0.1" />
|
||||
<PackageReference Include="GateIo.Net" Version="1.9.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="1.11.2" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.10.4" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="1.9.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.6.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="1.1.2" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="6.2.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="5.0.2" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.16.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||
<PackageReference Include="Binance.Net" Version="10.9.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.10.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.7.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.16.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="7.9.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="1.2.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="1.12.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="1.14.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.18.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
@inject IKucoinRestClient kucoinClient
|
||||
@inject IMexcRestClient mexcClient
|
||||
@inject IOKXRestClient okxClient
|
||||
@inject IWhiteBitRestClient whitebitClient
|
||||
|
||||
<h3>BTC-USD prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -41,12 +42,13 @@
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||
|
||||
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
|
||||
|
||||
if (binanceTask.Result.Success)
|
||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||
|
||||
|
||||
if (bingXTask.Result.Success)
|
||||
_prices.Add("BingX", bingXTask.Result.Data.First().LastPrice);
|
||||
|
||||
@@ -88,6 +90,12 @@
|
||||
|
||||
if (okxTask.Result.Success)
|
||||
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (whitebitTask.Result.Success){
|
||||
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side
|
||||
var tickers = whitebitTask.Result.Data;
|
||||
_prices.Add("WhiteBit", tickers.Single(x => x.Symbol == "BTC_USDT").LastPrice);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
@inject IKucoinSocketClient kucoinSocketClient
|
||||
@inject IMexcSocketClient mexcSocketClient
|
||||
@inject IOKXSocketClient okxSocketClient
|
||||
@inject IWhiteBitSocketClient whitebitSocketClient
|
||||
@using System.Collections.Concurrent
|
||||
@using CryptoExchange.Net.Objects
|
||||
@using CryptoExchange.Net.Objects.Sockets;
|
||||
@@ -49,6 +50,7 @@
|
||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
|
||||
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
|
||||
whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@inject IBinanceOrderBookFactory binanceFactory
|
||||
@inject IBingXOrderBookFactory bingXFactory
|
||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||
@@ -33,6 +34,7 @@
|
||||
@inject IKucoinOrderBookFactory kucoinFactory
|
||||
@inject IMexcOrderBookFactory mexcFactory
|
||||
@inject IOKXOrderBookFactory okxFactory
|
||||
@inject IWhiteBitOrderBookFactory whitebitFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC books, live updates:</h3>
|
||||
@@ -74,13 +76,14 @@
|
||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "CryptoCom", cryptocomFactory.CreateExchange("ETH_BTC") },
|
||||
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
||||
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
||||
{ "HTX", htxFactory.CreateSpot("ethbtc") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
|
||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
|
||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
@page "/Trackers"
|
||||
@using System.Collections.Concurrent
|
||||
@using System.Timers
|
||||
@using Binance.Net.Interfaces
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@using CryptoCom.Net.Interfaces
|
||||
@using CryptoExchange.Net.SharedApis
|
||||
@using CryptoExchange.Net.Trackers.Trades
|
||||
@using GateIo.Net.Interfaces
|
||||
@using HTX.Net.Interfaces
|
||||
@using Kraken.Net.Interfaces
|
||||
@using Kucoin.Net.Clients
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@inject IBinanceTrackerFactory binanceFactory
|
||||
@inject IBingXTrackerFactory bingXFactory
|
||||
@inject IBitfinexTrackerFactory bitfinexFactory
|
||||
@inject IBitgetTrackerFactory bitgetFactory
|
||||
@inject IBitMartTrackerFactory bitmartFactory
|
||||
@inject IBybitTrackerFactory bybitFactory
|
||||
@inject ICoinbaseTrackerFactory coinbaseFactory
|
||||
@inject ICoinExTrackerFactory coinExFactory
|
||||
@inject ICryptoComTrackerFactory cryptocomFactory
|
||||
@inject IGateIoTrackerFactory gateioFactory
|
||||
@inject IHTXTrackerFactory htxFactory
|
||||
@inject IKrakenTrackerFactory krakenFactory
|
||||
@inject IKucoinTrackerFactory kucoinFactory
|
||||
@inject IMexcTrackerFactory mexcFactory
|
||||
@inject IOKXTrackerFactory okxFactory
|
||||
@inject IWhiteBitTrackerFactory whitebitFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC trade Trackers, live updates:</h3>
|
||||
<div style="display:flex; flex-wrap: wrap;">
|
||||
@foreach (var tracker in _trackers.OrderBy(p => p.Exchange))
|
||||
{
|
||||
<div style="margin-bottom: 20px; flex: 1; min-width: 700px;">
|
||||
<h4>@tracker.Exchange</h4>
|
||||
@foreach(var line in GetInfo(tracker))
|
||||
{
|
||||
<div>@line</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code{
|
||||
private List<ITradeTracker> _trackers = new List<ITradeTracker>();
|
||||
private Timer _timer;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
_trackers = new List<ITradeTracker>
|
||||
{
|
||||
{ binanceFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bingXFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitfinexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitgetFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmartFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bybitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinbaseFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinExFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ cryptocomFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ gateioFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ htxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ krakenFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ whitebitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
|
||||
|
||||
// Use a manual update timer so the page isn't refreshed too often
|
||||
_timer = new Timer(500);
|
||||
_timer.Start();
|
||||
_timer.Elapsed += (o, e) => InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private string[] GetInfo(ITradeTracker tracker)
|
||||
{
|
||||
var secondLastMinute = tracker.GetStats(DateTime.UtcNow.AddMinutes(-2), DateTime.UtcNow.AddMinutes(-1));
|
||||
var lastMinute = tracker.GetStats(DateTime.UtcNow.AddMinutes(-1));
|
||||
var compare = lastMinute.CompareTo(secondLastMinute);
|
||||
|
||||
return [
|
||||
$"{tracker.SymbolName} | {tracker.Status} - Synced from {tracker.SyncedFrom}",
|
||||
$"Total trades: {tracker.Count}",
|
||||
$"Trades last minute: {lastMinute.TradeCount}, minute before: {secondLastMinute.TradeCount}",
|
||||
$"Average weighted price: {lastMinute.VolumeWeightedAveragePrice}, minute before: {secondLastMinute.VolumeWeightedAveragePrice}, dif: {compare.VolumeWeightedAveragePriceDif.PercentageDifference}%"
|
||||
];
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
foreach (var tracker in _trackers.Where(b => b.Status != CryptoExchange.Net.Objects.SyncStatus.Disconnected))
|
||||
// It's not necessary to wait for this
|
||||
_ = tracker.StopAsync();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,11 @@
|
||||
Order books
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Trackers">
|
||||
Trackers
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace BlazorClient
|
||||
services.AddKucoin();
|
||||
services.AddMexc();
|
||||
services.AddOKX();
|
||||
services.AddWhiteBit();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
||||
@@ -23,4 +23,5 @@
|
||||
@using Kucoin.Net.Interfaces.Clients;
|
||||
@using Mexc.Net.Interfaces.Clients;
|
||||
@using OKX.Net.Interfaces.Clients;
|
||||
@using WhiteBit.Net.Interfaces.Clients
|
||||
@using CryptoExchange.Net.Interfaces;
|
||||
@@ -6,20 +6,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="10.7.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.8.2" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.4.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.14.3" />
|
||||
<PackageReference Include="CoinEx.Net" Version="7.7.2" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="1.0.1" />
|
||||
<PackageReference Include="GateIo.Net" Version="1.9.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.10.4" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="1.9.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.6.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="1.1.2" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="6.2.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="5.0.2" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.16.0" />
|
||||
<PackageReference Include="Binance.Net" Version="10.9.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.10.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.7.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.16.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="7.9.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="1.2.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="1.12.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.18.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="10.7.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.4.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.6.0" />
|
||||
<PackageReference Include="Binance.Net" Version="10.9.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.7.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
// Options section, select this section during DI registration using Configuration.GetSection("ExchangeApiOptions")
|
||||
"ExchangeApiOptions": {
|
||||
// API credentials for both REST and Websocket client
|
||||
"ApiCredentials": {
|
||||
"Key": "APIKEY",
|
||||
"Secret": "SECRET",
|
||||
"PassPhrase": "Phrase" // Optional passphrase for exchanges which need it
|
||||
},
|
||||
// Set the environment by name
|
||||
"Environment": {
|
||||
"name": "live"
|
||||
},
|
||||
// REST client options
|
||||
"Rest": {
|
||||
"RequestTimeout": "00:00:20",
|
||||
"CachingEnabled": true,
|
||||
"OutputOriginalData": true,
|
||||
"Proxy": {
|
||||
"Host": "https://127.0.0.1",
|
||||
"Port": 8080,
|
||||
"Login": "User",
|
||||
"Password": "Pass"
|
||||
}
|
||||
},
|
||||
// Socket client options
|
||||
"Socket": {
|
||||
"RequestTimeout": "00:00:05",
|
||||
"SocketSubscriptionsCombineTarget": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ The following API's are directly supported. Note that there are 3rd party implem
|
||||
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|
|
||||
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[](https://www.nuget.org/packages/JK.Mexc.Net)|
|
||||
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[](https://www.nuget.org/packages/JK.OKX.Net)|
|
||||
|WhiteBit|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[](https://www.nuget.org/packages/WhiteBit.Net)|
|
||||
|
||||
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
|
||||
|
||||
@@ -49,6 +50,31 @@ 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).
|
||||
|
||||
## Release notes
|
||||
* Version 8.4.0 - 28 Nov 2024
|
||||
* Added GetFeesAsync Shared REST client support
|
||||
* Added LibraryOptions base class
|
||||
* Added CommaSplitEnumConverter System.Text.Json converter
|
||||
* Added TimePeriodFilterSupport and MaxLimit properties to PaginatedEndpointOptions
|
||||
* Updated package dependency versions
|
||||
|
||||
* Version 8.3.0 - 19 Nov 2024
|
||||
* Added support for IOptions injection, allowing options to be read from IConfiguration
|
||||
* Added handling of Infinity values in decimal converter
|
||||
* Added rate limit update event
|
||||
* Small refactor on client options internals
|
||||
* Fixed concurrency issue when unsubscribing websocket subscription during reconnection
|
||||
|
||||
* Version 8.2.0 - 06 Nov 2024
|
||||
* Added support for not allowing duplicate subscription topics on the same websocket connection
|
||||
* Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable
|
||||
* Added support for object deserialization in SystemTextJsonMessageAccessor.GetValue<T>
|
||||
* Changed SocketApiClient GetAuthenticationRequest to GetAuthenticationRequestAsync to allow for requesting token
|
||||
|
||||
* Version 8.1.1 - 01 Nov 2024
|
||||
* Fixed socket connections trying to authenticated connection when it's marked as dedicated request connection even when no authentication is needed
|
||||
* Fixed System.Text.Json ArrayConverter not passing serializer options to nested deserialization
|
||||
* Fixed System.Text.Json ArrayConverter creating new serializer options each time a JsonConverter attribute is encountered
|
||||
|
||||
* Version 8.1.0 - 28 Oct 2024
|
||||
* Added KlineTracker and TradeTracker implementation
|
||||
* Added Side to SharedTrade model
|
||||
|
||||
+509
-125
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user