1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

25 Commits

Author SHA1 Message Date
Jkorf 0b7107401f Updated to version 8.4.3 2024-12-03 09:59:36 +01:00
JKorf 06add65354 Fixed KlineTracker update handling 2024-12-02 21:46:30 +01:00
Jkorf 773d288497 Updated to version 8.4.2 2024-12-02 14:39:27 +01:00
Jkorf fd4e8da938 Removed special characters in ClientOrderIdSeperator to adhere to field content rules 2024-12-02 14:38:22 +01:00
Jkorf 271743b669 Updated to version 8.4.1 2024-12-02 13:15:31 +01:00
Jkorf f4797caf37 Added replace converter, added library helpers class 2024-12-02 13:13:56 +01:00
Jkorf 62c9769c72 Updated to version 8.4.0 2024-11-28 14:24:00 +01:00
Jkorf 92d7bc1e2e Added GetFeesAsync Shared REST client support, Added TimePeriodFilterSupport and MaxLimit properties to PaginatedEndpointOptions 2024-11-28 14:18:09 +01:00
Jkorf 99e4f96f63 Updated some testing code 2024-11-27 13:07:26 +01:00
Jkorf 94d8afe149 Updated package dependency versions 2024-11-27 13:07:05 +01:00
Jkorf 90ad59c63a Added comma split enum string json converter 2024-11-22 16:20:09 +01:00
Jkorf c2273edfaa Added library options class 2024-11-20 09:52:38 +01:00
Jkorf 236283f4dd Update example-config.json 2024-11-19 14:53:58 +01:00
Jkorf b66f12ff75 Updated to version 8.3.0 2024-11-19 11:52:46 +01:00
Jkorf 0403384beb Fixed warnings 2024-11-19 11:50:55 +01:00
Jan Korf 7d7bc35869 Client Configuration (#219)
Added support for IOptions injection, allowing options to be read from IConfiguration
Small refactor on client options internals
Updated HttpClient to be static field to be
2024-11-19 11:44:30 +01:00
Jkorf 48797038be Added rate limit update event 2024-11-13 14:29:43 +01:00
Jkorf d21792d04c Added handling of Infinity values in decimal converter 2024-11-13 11:39:55 +01:00
Jkorf 8414e9d94f Fixed concurrency issue when unsubscribing websocket subscription during reconnection 2024-11-12 16:21:15 +01:00
Jkorf ab0243445d Updated docs and examples, added WhiteBit reference 2024-11-07 11:39:44 +01:00
Jkorf f2cf70b02f Updated to version 8.2.0 2024-11-06 14:00:23 +01:00
Jkorf 9ff417bba8 Changed SocketApiClient GetAuthenticationRequest to GetAuthenticationRequestAsync to allow for requesting token 2024-11-06 13:56:33 +01:00
Jkorf 6b43d08a4d Added support for not allowing duplicate subscription topics on the same websocket connection 2024-11-06 11:39:11 +01:00
Jkorf 39bf7fe9b9 Added support for object deserialization in SystemTextJsonMessageAccessor.GetValue<T> 2024-11-06 11:20:37 +01:00
Jkorf b5893c3b60 Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable 2024-11-06 11:20:11 +01:00
62 changed files with 1241 additions and 361 deletions
@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"></PackageReference> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference> <PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -70,5 +71,20 @@ namespace CryptoExchange.Net.UnitTests
var result = ExchangeHelpers.Normalize(input); var result = ExchangeHelpers.Normalize(input);
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture)); Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
} }
[Test]
[TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
if (expected != null)
Assert.That(result, Is.EqualTo(expected));
}
} }
} }
+21 -5
View File
@@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider1.GetSecret() == "222"); Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123"); Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
} }
[Test] [Test]
@@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider2.GetKey() == "123"); Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123"); 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") Environment = new TestEnvironment("test", "https://test.com")
}; };
/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
/// <summary> /// <summary>
/// The default receive window for requests /// The default receive window for requests
/// </summary> /// </summary>
@@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
public RestApiOptions Api2Options { get; set; } = new RestApiOptions(); public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Copy() internal TestClientOptions Set(TestClientOptions targetOptions)
{ {
var options = Copy<TestClientOptions>(); targetOptions = base.Set<TestClientOptions>(targetOptions);
options.Api1Options = Api1Options.Copy<RestApiOptions>(); targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
options.Api2Options = Api2Options.Copy<RestApiOptions>(); targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return options; return targetOptions;
} }
} }
} }
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
public TestBaseClient(): base(null, "Test") public TestBaseClient(): base(null, "Test")
{ {
var options = TestClientOptions.Default.Copy(); var options = new TestClientOptions();
Initialize(options); Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions())); SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
} }
@@ -16,6 +16,7 @@ using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -24,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; } public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { 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") Api1 = new TestRestApi1Client(options.Value);
{ Api2 = new TestRestApi2Client(options.Value);
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
} }
public void SetResponse(string responseData, out IRequest requestObj) public void SetResponse(string responseData, out IRequest requestObj)
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
using Moq; using Moq;
using CryptoExchange.Net.Testing.Implementations; using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -22,25 +23,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
public TestSubSocketClient SubClient { get; } public TestSubSocketClient SubClient { get; }
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}
/// <summary> /// <summary>
/// Create a new instance of KucoinSocketClient /// Create a new instance of KucoinSocketClient
/// </summary> /// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param> /// <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>(); Initialize(options.Value);
optionsFunc(options);
Initialize(options);
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions)); SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object; 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")); 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") Environment = new TestEnvironment("Live", "https://test.test")
}; };
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions(); 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 public class TestSubSocketClient : SocketApiClient
@@ -13,39 +13,30 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// The api key / label to authenticate requests /// The api key / label to authenticate requests
/// </summary> /// </summary>
public string Key { get; } public string Key { get; set; }
/// <summary> /// <summary>
/// The api secret or private key to authenticate requests /// The api secret or private key to authenticate requests
/// </summary> /// </summary>
public string Secret { get; } public string Secret { get; set; }
/// <summary> /// <summary>
/// Type of the credentials /// Type of the credentials
/// </summary> /// </summary>
public ApiCredentialsType CredentialType { get; } public ApiCredentialsType CredentialType { get; set; }
/// <summary> /// <summary>
/// Create Api credentials providing an api key and secret for authentication /// Create Api credentials providing an api key and secret for authentication
/// </summary> /// </summary>
/// <param name="key">The api key / label used for identification</param> /// <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="secret">The api secret or private key used for signing</param>
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac) /// <param name="credentialType">The type of credentials</param>
{ public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = 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)
{ {
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty"); throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType; CredentialType = credentialType;
Key = key; Key = key;
Secret = secret; Secret = secret;
} }
@@ -65,7 +56,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="inputStream">The stream containing the json data</param> /// <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="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> /// <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(); var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result) if (!accessor.Read(inputStream, false).Result)
@@ -75,11 +66,9 @@ namespace CryptoExchange.Net.Authentication
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret")); var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
if (key == null || secret == null) if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key;
Secret = secret;
inputStream.Seek(0, SeekOrigin.Begin); inputStream.Seek(0, SeekOrigin.Begin);
return new ApiCredentials(key, secret);
} }
} }
} }
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// Get the API key of the current credentials /// Get the API key of the current credentials
/// </summary> /// </summary>
public string ApiKey => _credentials.Key; public string ApiKey => _credentials.Key!;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="credentials"></param> /// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials) protected AuthenticationProvider(ApiCredentials credentials)
{ {
if (credentials.Secret == null) if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed"); throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials; _credentials = credentials;
+10
View File
@@ -109,6 +109,16 @@ namespace CryptoExchange.Net.Clients
return apiClient; 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> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+11 -4
View File
@@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>(); 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 /> /// <inheritdoc />
public double IncomingKbps public double IncomingKbps
{ {
@@ -211,7 +216,7 @@ namespace CryptoExchange.Net.Clients
while (true) while (true)
{ {
// Get a new or existing socket connection // 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) if (!socketResult)
return socketResult.As<UpdateSubscription>(null); return socketResult.As<UpdateSubscription>(null);
@@ -403,7 +408,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult(new NoApiCredentialsError()); return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId); _logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = GetAuthenticationRequest(socket); var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
if (authRequest != null) if (authRequest != null)
{ {
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); 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 /// Should return the request which can be used to authenticate a socket connection
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException(); protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
/// <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
@@ -478,13 +483,15 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param> /// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</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="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> /// <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) 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.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType() && s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated) && (s.Value.Authenticated == authenticated || !authenticated)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
&& s.Value.Connected); && s.Value.Connected);
SocketConnection connection; SocketConnection connection;
@@ -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) if (reader.TokenType == JsonTokenType.String)
{ {
var value = reader.GetString(); var value = reader.GetString();
if (string.IsNullOrEmpty(value) || string.Equals("null", value)) if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
return null; return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal))
// Infinity returned by the server, default to max value
return decimal.MaxValue;
try try
{ {
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
@@ -0,0 +1,27 @@
using System;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Attribute for allowing specifying a JsonConverter with constructor parameters
/// </summary>
/// <typeparam name="T"></typeparam>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConverterCtorAttribute<T> : JsonConverterAttribute where T : JsonConverter
{
private readonly object[] _parameters;
/// <summary>
/// ctor
/// </summary>
public JsonConverterCtorAttribute(params object[] parameters) => _parameters = parameters;
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert)
{
return (T)Activator.CreateInstance(typeof(T), _parameters);
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Replace a value on a string property
/// </summary>
public class ReplaceConverter : JsonConverter<string>
{
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
/// <summary>
/// ctor
/// </summary>
public ReplaceConverter(params string[] replaceSets)
{
_replacementSets = replaceSets.Select(x =>
{
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
if (split.Length != 2)
throw new ArgumentException("Invalid replacement config");
return (split[0], split[1]);
}).ToArray();
}
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}
}
@@ -126,7 +126,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default; return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array) if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_serializerOptions);
}
catch { }
return default; return default;
}
if (typeof(T) == typeof(string)) if (typeof(T) == typeof(string))
{ {
+10 -9
View File
@@ -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>8.1.1</PackageVersion> <PackageVersion>8.4.3</PackageVersion>
<AssemblyVersion>8.1.1</AssemblyVersion> <AssemblyVersion>8.4.3</AssemblyVersion>
<FileVersion>8.1.1</FileVersion> <FileVersion>8.4.3</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</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</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -20,7 +20,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion> <LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -48,16 +48,17 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0"> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </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" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" /> <PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net
{
/// <summary>
/// Helpers for client libraries
/// </summary>
public static class LibraryHelpers
{
/// <summary>
/// Client order id seperator
/// </summary>
public const string ClientOrderIdSeperator = "JK";
/// <summary>
/// Apply broker id to a client order id
/// </summary>
/// <param name="clientOrderId"></param>
/// <param name="brokerId"></param>
/// <param name="maxLength"></param>
/// <param name="allowValueAdjustement"></param>
/// <returns></returns>
public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement)
{
var reservedLength = brokerId.Length + ClientOrderIdSeperator.Length;
if ((clientOrderId?.Length + reservedLength) > maxLength)
return clientOrderId!;
if (!string.IsNullOrEmpty(clientOrderId))
{
if (allowValueAdjustement)
clientOrderId = brokerId + ClientOrderIdSeperator + clientOrderId;
return clientOrderId!;
}
else
{
clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeperator, maxLength);
}
return clientOrderId;
}
}
}
+5 -14
View File
@@ -8,30 +8,21 @@
/// <summary> /// <summary>
/// The host address of the proxy /// The host address of the proxy
/// </summary> /// </summary>
public string Host { get; } public string Host { get; set; }
/// <summary> /// <summary>
/// The port of the proxy /// The port of the proxy
/// </summary> /// </summary>
public int Port { get; } public int Port { get; set; }
/// <summary> /// <summary>
/// The login of the proxy /// The login of the proxy
/// </summary> /// </summary>
public string? Login { get; } public string? Login { get; set; }
/// <summary> /// <summary>
/// The password of the proxy /// The password of the proxy
/// </summary> /// </summary>
public string? Password { get; } public string? Password { get; set; }
/// <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)
{
}
/// <summary> /// <summary>
/// Create new settings for a proxy /// Create new settings for a proxy
@@ -40,7 +31,7 @@
/// <param name="port">The proxy port</param> /// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param> /// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</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; Host = host;
Port = port; 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; } public TimeSpan? TimestampRecalculationInterval { get; set; }
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public T Set<T>(T item) where T : RestApiOptions, new()
/// <returns></returns>
public virtual T Copy<T>() where T : RestApiOptions, new()
{ {
return new T item.ApiCredentials = ApiCredentials?.Copy();
{ item.OutputOriginalData = OutputOriginalData;
ApiCredentials = ApiCredentials?.Copy(), item.AutoTimestamp = AutoTimestamp;
OutputOriginalData = OutputOriginalData, item.TimestampRecalculationInterval = TimestampRecalculationInterval;
AutoTimestamp = AutoTimestamp, return item;
TimestampRecalculationInterval = TimestampRecalculationInterval
};
} }
} }
@@ -29,25 +29,21 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public T Set<T>(T item) where T : RestExchangeOptions, new()
/// <returns></returns>
public T Copy<T>() where T : RestExchangeOptions, new()
{ {
return new T item.OutputOriginalData = OutputOriginalData;
{ item.AutoTimestamp = AutoTimestamp;
OutputOriginalData = OutputOriginalData, item.TimestampRecalculationInterval = TimestampRecalculationInterval;
AutoTimestamp = AutoTimestamp, item.ApiCredentials = ApiCredentials?.Copy();
TimestampRecalculationInterval = TimestampRecalculationInterval, item.Proxy = Proxy;
ApiCredentials = ApiCredentials?.Copy(), item.RequestTimeout = RequestTimeout;
Proxy = Proxy, item.RateLimiterEnabled = RateLimiterEnabled;
RequestTimeout = RequestTimeout, item.RateLimitingBehaviour = RateLimitingBehaviour;
RateLimiterEnabled = RateLimiterEnabled, item.CachingEnabled = CachingEnabled;
RateLimitingBehaviour = RateLimitingBehaviour, item.CachingMaxAge = CachingMaxAge;
CachingEnabled = CachingEnabled, return item;
CachingMaxAge = CachingMaxAge,
};
} }
} }
@@ -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. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new()
/// <returns></returns>
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
{ {
var result = base.Copy<T>(); base.Set(target);
result.Environment = Environment; target.Environment = Environment;
return result; return target;
} }
} }
@@ -20,19 +20,15 @@ namespace CryptoExchange.Net.Objects.Options
public int? MaxSocketConnections { get; set; } public int? MaxSocketConnections { get; set; }
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public T Set<T>(T item) where T : SocketApiOptions, new()
/// <returns></returns>
public T Copy<T>() where T : SocketApiOptions, new()
{ {
return new T item.ApiCredentials = ApiCredentials?.Copy();
{ item.OutputOriginalData = OutputOriginalData;
ApiCredentials = ApiCredentials?.Copy(), item.SocketNoDataTimeout = SocketNoDataTimeout;
OutputOriginalData = OutputOriginalData, item.MaxSocketConnections = MaxSocketConnections;
SocketNoDataTimeout = SocketNoDataTimeout, return item;
MaxSocketConnections = MaxSocketConnections,
};
} }
} }
@@ -57,24 +57,22 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <returns></returns> /// <returns></returns>
public T Copy<T>() where T : SocketExchangeOptions, new() public T Set<T>(T item) where T : SocketExchangeOptions, new()
{ {
return new T item.ApiCredentials = ApiCredentials?.Copy();
{ item.OutputOriginalData = OutputOriginalData;
ApiCredentials = ApiCredentials?.Copy(), item.ReconnectPolicy = ReconnectPolicy;
OutputOriginalData = OutputOriginalData, item.DelayAfterConnect = DelayAfterConnect;
ReconnectPolicy = ReconnectPolicy, item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket;
DelayAfterConnect = DelayAfterConnect, item.ReconnectInterval = ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket, item.SocketNoDataTimeout = SocketNoDataTimeout;
ReconnectInterval = ReconnectInterval, item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget;
SocketNoDataTimeout = SocketNoDataTimeout, item.MaxSocketConnections = MaxSocketConnections;
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget, item.Proxy = Proxy;
MaxSocketConnections = MaxSocketConnections, item.RequestTimeout = RequestTimeout;
Proxy = Proxy, item.RateLimitingBehaviour = RateLimitingBehaviour;
RequestTimeout = RequestTimeout, item.RateLimiterEnabled = RateLimiterEnabled;
RateLimitingBehaviour = RateLimitingBehaviour, return item;
RateLimiterEnabled = RateLimiterEnabled,
};
} }
} }
@@ -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. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new()
/// <returns></returns>
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
{ {
var result = base.Copy<T>(); base.Set(target);
result.Environment = Environment; target.Environment = Environment;
return result; return target;
} }
} }
@@ -24,14 +24,14 @@
/// <summary> /// <summary>
/// Name of the environment /// Name of the environment
/// </summary> /// </summary>
public string EnvironmentName { get; init; } public string Name { get; set; }
/// <summary> /// <summary>
/// </summary> /// </summary>
/// <param name="name"></param> /// <param name="name"></param>
protected TradeEnvironment(string name) protected TradeEnvironment(string name)
{ {
EnvironmentName = name; Name = name;
} }
} }
} }
@@ -110,7 +110,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight); var delay = tracker.GetWaitTime(requestWeight);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded; return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
return LimitCheck.Needed(delay, 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); var delay = tracker.GetWaitTime(requestWeight);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded; return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
return LimitCheck.Needed(delay, _limit, _period, tracker.Current); return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
} }
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary> /// </summary>
event Action<RateLimitEvent> RateLimitTriggered; 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> /// <summary>
/// Add a rate limit guard /// Add a rate limit guard
/// </summary> /// </summary>
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary> /// <summary>
/// No wait needed /// No wait needed
/// </summary> /// </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> /// <summary>
/// Wait needed /// Wait needed
@@ -4,10 +4,14 @@ using System;
namespace CryptoExchange.Net.RateLimiting namespace CryptoExchange.Net.RateLimiting
{ {
/// <summary> /// <summary>
/// Rate limit event /// Rate limit triggered event
/// </summary> /// </summary>
public record RateLimitEvent public record RateLimitEvent
{ {
/// <summary>
/// Id of the item the limit was checked for
/// </summary>
public int ItemId { get; set; }
/// <summary> /// <summary>
/// Name of the API limit that is reached /// Name of the API limit that is reached
/// </summary> /// </summary>
@@ -52,18 +56,9 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="apiLimit"></param> public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
/// <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)
{ {
ItemId = itemId;
ApiLimit = apiLimit; ApiLimit = apiLimit;
LimitDescription = limitDescription; LimitDescription = limitDescription;
RequestDefinition = definition; RequestDefinition = definition;
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.RateLimiting
/// <inheritdoc /> /// <inheritdoc />
public event Action<RateLimitEvent>? RateLimitTriggered; public event Action<RateLimitEvent>? RateLimitTriggered;
/// <inheritdoc />
public event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -105,7 +107,7 @@ namespace CryptoExchange.Net.RateLimiting
else else
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description); 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}")); return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
} }
@@ -120,7 +122,7 @@ namespace CryptoExchange.Net.RateLimiting
else else
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description); 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 Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, 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); var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
if (result.IsApplied) if (result.IsApplied)
{ {
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
if (type == RateLimitItemType.Connection) if (type == RateLimitItemType.Connection)
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current); logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
else 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> /// </summary>
public class RequestFactory : IRequestFactory public class RequestFactory : IRequestFactory
{ {
private HttpClient? _httpClient; private HttpClient? _httpClient;
/// <inheritdoc /> /// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
@@ -12,6 +12,10 @@
/// <summary> /// <summary>
/// Leverage is configured for the symbol /// Leverage is configured for the symbol
/// </summary> /// </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> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; TimeFilterSupported = timeFilterSupported;
} }
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; TimeFilterSupported = timeFilterSupported;
} }
@@ -8,7 +8,7 @@
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// </summary>
public int? MaxTotalDataPoints { get; set; } public int? MaxTotalDataPoints { get; set; }
/// <summary> /// <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 /// The max age of the data that can be requested
/// </summary> /// </summary>
public TimeSpan? MaxAge { get; set; } public TimeSpan? MaxAge { get; set; }
@@ -31,14 +27,13 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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[] SupportIntervals = new[]
{ {
SharedKlineInterval.FiveMinutes, SharedKlineInterval.FiveMinutes,
SharedKlineInterval.FifteenMinutes, SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.OneHour, SharedKlineInterval.OneHour,
SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.OneDay, SharedKlineInterval.OneDay,
SharedKlineInterval.OneWeek, SharedKlineInterval.OneWeek,
SharedKlineInterval.OneMonth SharedKlineInterval.OneMonth
@@ -48,7 +43,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; SupportIntervals = intervals;
} }
@@ -69,8 +64,8 @@ namespace CryptoExchange.Net.SharedApis
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
return new ArgumentError($"Only the most recent {MaxAge} klines are available"); return new ArgumentError($"Only the most recent {MaxAge} klines are available");
if (MaxRequestDataPoints.HasValue && request.Limit > MaxRequestDataPoints.Value) if (request.Limit > MaxLimit)
return new ArgumentError($"Only {MaxRequestDataPoints} klines can be retrieved per request"); return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request");
if (MaxTotalDataPoints.HasValue) if (MaxTotalDataPoints.HasValue)
{ {
@@ -96,8 +91,6 @@ namespace CryptoExchange.Net.SharedApis
sb.AppendLine($"Max age of data: {MaxAge}"); sb.AppendLine($"Max age of data: {MaxAge}");
if (MaxTotalDataPoints != null) if (MaxTotalDataPoints != null)
sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}"); sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}");
if (MaxRequestDataPoints != null)
sb.AppendLine($"Max data points per request: {MaxRequestDataPoints}");
return sb.ToString(); return sb.ToString();
} }
} }
@@ -8,7 +8,7 @@
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; TimeFilterSupported = timeFilterSupported;
} }
@@ -1,4 +1,5 @@
using System.Text; using CryptoExchange.Net.Objects;
using System.Text;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
@@ -13,12 +14,24 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public SharedPaginationSupport PaginationSupport { get; } 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> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(needsAuthentication) public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
{ {
PaginationSupport = paginationType; PaginationSupport = paginationType;
TimePeriodFilterSupport = timePeriodSupport;
MaxLimit = maxLimit;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -26,6 +39,8 @@ namespace CryptoExchange.Net.SharedApis
{ {
var sb = new StringBuilder(base.ToString(exchange)); var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Pagination type: {PaginationSupport}"); sb.AppendLine($"Pagination type: {PaginationSupport}");
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
sb.AppendLine($"Max limit: {MaxLimit}");
return sb.ToString(); 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> /// <summary>
/// Side of the trade /// Side of the trade
/// </summary> /// </summary>
public SharedOrderSide Side { get; set; } public SharedOrderSide? Side { get; set; }
/// <summary> /// <summary>
/// Fee paid for the trade /// Fee paid for the trade
/// </summary> /// </summary>
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; Symbol = symbol;
OrderId = orderId; OrderId = orderId;
+27 -1
View File
@@ -190,6 +190,18 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public DedicatedConnectionState DedicatedRequestConnection { get; internal set; } = new DedicatedConnectionState(); 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 bool _pausedActivity;
private readonly object _listenersLock; private readonly object _listenersLock;
private readonly List<IMessageProcessor> _listeners; private readonly List<IMessageProcessor> _listeners;
@@ -603,6 +615,10 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public async Task CloseAsync(Subscription subscription) 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; subscription.Closed = true;
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
@@ -886,7 +902,7 @@ namespace CryptoExchange.Net.Sockets
List<Subscription> subList; List<Subscription> subList;
lock (_listenersLock) 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) if (subList.Count == 0)
break; break;
@@ -895,20 +911,30 @@ namespace CryptoExchange.Net.Sockets
foreach (var subscription in subList) foreach (var subscription in subList)
{ {
subscription.ConnectionInvocations = 0; subscription.ConnectionInvocations = 0;
if (subscription.Closed)
// Can be closed during resubscribing
continue;
subscription.IsResubscribing = true;
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false); var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
if (!result) if (!result)
{ {
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString()); _logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
subscription.IsResubscribing = false;
return result; return result;
} }
var subQuery = subscription.GetSubQuery(this); var subQuery = subscription.GetSubQuery(this);
if (subQuery == null) if (subQuery == null)
{
subscription.IsResubscribing = false;
continue; continue;
}
var waitEvent = new AsyncResetEvent(false); var waitEvent = new AsyncResetEvent(false);
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
{ {
subscription.IsResubscribing = false;
subscription.HandleSubQueryResponse(subQuery.Response!); subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set(); waitEvent.Set();
if (r.Result.Success) if (r.Result.Success)
@@ -44,6 +44,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool Closed { get; set; } public bool Closed { get; set; }
/// <summary>
/// Is the subscription currently resubscribing
/// </summary>
public bool IsResubscribing { get; set; }
/// <summary> /// <summary>
/// Logger /// Logger
/// </summary> /// </summary>
@@ -76,6 +81,11 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message); public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary>
/// Subscription topic
/// </summary>
public string? Topic { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -224,6 +224,9 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable)) else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
&& propertyValue.GetType() != typeof(string)) && propertyValue.GetType() != typeof(string))
{ {
if (propValue.Type != JTokenType.Array)
return;
var jObjs = (JArray)propValue; var jObjs = (JArray)propValue;
var list = (IEnumerable)propertyValue; var list = (IEnumerable)propertyValue;
var enumerator = list.GetEnumerator(); var enumerator = list.GetEnumerator();
@@ -372,7 +375,8 @@ namespace CryptoExchange.Net.Testing.Comparers
} }
else if (objectValue is DateTime time) 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}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
} }
else if (objectValue is bool bl) else if (objectValue is bool bl)
+12 -9
View File
@@ -173,17 +173,20 @@ namespace CryptoExchange.Net.Testing
foreach (var clientInterface in clientInterfaces) foreach (var clientInterface in clientInterfaces)
{ {
var implementation = assembly.GetTypes().Single(t => clientInterface.IsAssignableFrom(t) && t != clientInterface); var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
int methods = 0; foreach (var implementation in implementations)
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()); int methods = 0;
if (interfaceMethod == null) foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}"); {
methods++; 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");
}
} }
} }
} }
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Trackers.Klines
/// <summary> /// <summary>
/// The internal data structure /// The internal data structure
/// </summary> /// </summary>
protected readonly Dictionary<DateTime, SharedKline> _data = new Dictionary<DateTime, SharedKline>(); protected readonly SortedDictionary<DateTime, SharedKline> _data = new SortedDictionary<DateTime, SharedKline>();
/// <summary> /// <summary>
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
/// </summary> /// </summary>
@@ -229,7 +229,7 @@ namespace CryptoExchange.Net.Trackers.Klines
if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime) if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime)
startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value); 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 request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit);
var data = new List<SharedKline>(); var data = new List<SharedKline>();
+16 -15
View File
@@ -5,22 +5,23 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" /> <PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="Bitfinex.Net" Version="7.9.0" /> <PackageReference Include="Bitfinex.Net" Version="7.10.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" /> <PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="Bybit.Net" Version="3.15.0" /> <PackageReference Include="Bybit.Net" Version="3.16.0" />
<PackageReference Include="CoinEx.Net" Version="7.8.0" /> <PackageReference Include="CoinEx.Net" Version="7.9.0" />
<PackageReference Include="CryptoCom.Net" Version="1.1.0" /> <PackageReference Include="CryptoCom.Net" Version="1.2.0" />
<PackageReference Include="GateIo.Net" Version="1.10.0" /> <PackageReference Include="GateIo.Net" Version="1.12.0" />
<PackageReference Include="JK.BingX.Net" Version="1.12.0" /> <PackageReference Include="JK.BingX.Net" Version="1.14.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.11.0" /> <PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.10.0" /> <PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" /> <PackageReference Include="JK.OKX.Net" Version="2.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.2.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.3.0" /> <PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.1.0" /> <PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
<PackageReference Include="Kucoin.Net" Version="5.17.0" /> <PackageReference Include="Kucoin.Net" Version="5.18.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="WhiteBit.Net" Version="1.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+9 -1
View File
@@ -14,6 +14,7 @@
@inject IKucoinRestClient kucoinClient @inject IKucoinRestClient kucoinClient
@inject IMexcRestClient mexcClient @inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient @inject IOKXRestClient okxClient
@inject IWhiteBitRestClient whitebitClient
<h3>BTC-USD prices:</h3> <h3>BTC-USD prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key)) @foreach(var price in _prices.OrderBy(p => p.Key))
@@ -41,12 +42,13 @@
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var okxTask = okxClient.UnifiedApi.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); await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
if (binanceTask.Result.Success) if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice); _prices.Add("Binance", binanceTask.Result.Data.LastPrice);
if (bingXTask.Result.Success) if (bingXTask.Result.Success)
_prices.Add("BingX", bingXTask.Result.Data.First().LastPrice); _prices.Add("BingX", bingXTask.Result.Data.First().LastPrice);
@@ -88,6 +90,12 @@
if (okxTask.Result.Success) if (okxTask.Result.Success)
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0); _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 IKucoinSocketClient kucoinSocketClient
@inject IMexcSocketClient mexcSocketClient @inject IMexcSocketClient mexcSocketClient
@inject IOKXSocketClient okxSocketClient @inject IOKXSocketClient okxSocketClient
@inject IWhiteBitSocketClient whitebitSocketClient
@using System.Collections.Concurrent @using System.Collections.Concurrent
@using CryptoExchange.Net.Objects @using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Objects.Sockets; @using CryptoExchange.Net.Objects.Sockets;
@@ -49,6 +50,7 @@
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)), kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)), mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)), 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); await Task.WhenAll(tasks);
+4 -1
View File
@@ -18,6 +18,7 @@
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@inject IBinanceOrderBookFactory binanceFactory @inject IBinanceOrderBookFactory binanceFactory
@inject IBingXOrderBookFactory bingXFactory @inject IBingXOrderBookFactory bingXFactory
@inject IBitfinexOrderBookFactory bitfinexFactory @inject IBitfinexOrderBookFactory bitfinexFactory
@@ -33,6 +34,7 @@
@inject IKucoinOrderBookFactory kucoinFactory @inject IKucoinOrderBookFactory kucoinFactory
@inject IMexcOrderBookFactory mexcFactory @inject IMexcOrderBookFactory mexcFactory
@inject IOKXOrderBookFactory okxFactory @inject IOKXOrderBookFactory okxFactory
@inject IWhiteBitOrderBookFactory whitebitFactory
@implements IDisposable @implements IDisposable
<h3>ETH-BTC books, live updates:</h3> <h3>ETH-BTC books, live updates:</h3>
@@ -77,10 +79,11 @@
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") }, { "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") }, { "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
{ "HTX", htxFactory.CreateSpot("ethbtc") }, { "HTX", htxFactory.CreateSpot("ethbtc") },
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") }, { "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") }, { "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") }, { "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") }, { "OKX", okxFactory.Create("ETH-BTC") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
}; };
await Task.WhenAll(_books.Select(b => b.Value.StartAsync())); await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
@@ -20,6 +20,7 @@
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@inject IBinanceTrackerFactory binanceFactory @inject IBinanceTrackerFactory binanceFactory
@inject IBingXTrackerFactory bingXFactory @inject IBingXTrackerFactory bingXFactory
@inject IBitfinexTrackerFactory bitfinexFactory @inject IBitfinexTrackerFactory bitfinexFactory
@@ -35,6 +36,7 @@
@inject IKucoinTrackerFactory kucoinFactory @inject IKucoinTrackerFactory kucoinFactory
@inject IMexcTrackerFactory mexcFactory @inject IMexcTrackerFactory mexcFactory
@inject IOKXTrackerFactory okxFactory @inject IOKXTrackerFactory okxFactory
@inject IWhiteBitTrackerFactory whitebitFactory
@implements IDisposable @implements IDisposable
<h3>ETH-BTC trade Trackers, live updates:</h3> <h3>ETH-BTC trade Trackers, live updates:</h3>
@@ -76,6 +78,7 @@
{ kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.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())); await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
+1
View File
@@ -50,6 +50,7 @@ namespace BlazorClient
services.AddKucoin(); services.AddKucoin();
services.AddMexc(); services.AddMexc();
services.AddOKX(); services.AddOKX();
services.AddWhiteBit();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+1
View File
@@ -23,4 +23,5 @@
@using Kucoin.Net.Interfaces.Clients; @using Kucoin.Net.Interfaces.Clients;
@using Mexc.Net.Interfaces.Clients; @using Mexc.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients; @using OKX.Net.Interfaces.Clients;
@using WhiteBit.Net.Interfaces.Clients
@using CryptoExchange.Net.Interfaces; @using CryptoExchange.Net.Interfaces;
+14 -14
View File
@@ -6,20 +6,20 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" /> <PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="Bitfinex.Net" Version="7.9.0" /> <PackageReference Include="Bitfinex.Net" Version="7.10.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" /> <PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="Bybit.Net" Version="3.15.0" /> <PackageReference Include="Bybit.Net" Version="3.16.0" />
<PackageReference Include="CoinEx.Net" Version="7.8.0" /> <PackageReference Include="CoinEx.Net" Version="7.9.0" />
<PackageReference Include="CryptoCom.Net" Version="1.1.0" /> <PackageReference Include="CryptoCom.Net" Version="1.2.0" />
<PackageReference Include="GateIo.Net" Version="1.10.0" /> <PackageReference Include="GateIo.Net" Version="1.12.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.11.0" /> <PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.10.0" /> <PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" /> <PackageReference Include="JK.OKX.Net" Version="2.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.2.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.3.0" /> <PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.1.0" /> <PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
<PackageReference Include="Kucoin.Net" Version="5.17.0" /> <PackageReference Include="Kucoin.Net" Version="5.18.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+3 -3
View File
@@ -8,9 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" /> <PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" /> <PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" /> <PackageReference Include="JK.OKX.Net" Version="2.8.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+32
View File
@@ -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
}
}
}
+32
View File
@@ -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)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)| |Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)|
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)| |Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)| |OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|
|WhiteBit|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](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. 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,37 @@ 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 8.4.3 - 03 Dec 2024
* Fixed KlineTracker update handling
* Version 8.4.2 - 02 Dec 2024
* Removed special characters in ClientOrderIdSeperator to adhere to field content rules
* Version 8.4.1 - 02 Dec 2024
* Added JsonConverterCtorAttribute to allow specifying a custom JsonConverter with constructor parameters on properties
* Added ReplaceConverter System.Text.Json converter
* Added LibraryHelpers class for internal helper methods
* 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 * 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 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 not passing serializer options to nested deserialization
+505 -124
View File
File diff suppressed because it is too large Load Diff